Files
EstiMaker/src/components/SubScopeCard.tsx
T

202 lines
10 KiB
TypeScript

import type { Component } from 'solid-js';
import { For, createMemo, Show, createSignal } from 'solid-js';
import { Trash2, ChevronDown, Copy, ListTree } from 'lucide-solid';
import type { EstimateItem, SubScope, Scope } from '../types';
import ItemRow from './ItemRow';
import LazyItem from './LazyItem';
interface SubScopeCardProps {
subScope: SubScope;
items: EstimateItem[];
onUpdateSubScope: (id: string, updates: Partial<SubScope>) => void;
onUpdateItem: (id: string, updates: Partial<EstimateItem>) => void;
onDeleteItem: (id: string) => void;
onDeleteSubScope: (id: string) => void;
onDragStart: (e: DragEvent, id: string) => void;
onDrop: (e: DragEvent, scopeId: string, subScopeId: string) => void;
onDuplicateItem: (id: string) => void;
allScopes: Scope[];
allSubScopes: SubScope[];
onCopySubScopeItems: (sourceSubScopeId: string, targetSubScopeId: string) => void;
isSidebarCollapsed: () => boolean;
selectedIds: string[];
onToggleSelection: (id: string, isMulti: boolean, isShift: boolean) => void;
onClearSelection: () => void;
hideColumns?: boolean;
}
const SubScopeCard: Component<SubScopeCardProps> = (props) => {
const [isCollapsed, setIsCollapsed] = createSignal(false);
const [isOver, setIsOver] = createSignal(false);
const [showCopyMenu, setShowCopyMenu] = createSignal(false);
const totals = createMemo(() => {
return props.items.reduce((acc, item) => {
const base = item.quantity * item.unitPrice;
const markup = item.markupType === 'percent' ? base * (item.markup / 100) : item.markup;
const contingency = item.contingencyType === 'percent' ? base * (item.contingency / 100) : item.contingency;
return {
amount: acc.amount + base + markup + contingency,
quantity: acc.quantity + item.quantity
};
}, { amount: 0, quantity: 0 });
});
const formatNumber = (num: number) => {
return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
const formatQuantity = (num: number) => {
return num.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
};
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsOver(true);
};
const handleDragLeave = () => {
setIsOver(false);
};
const handleDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsOver(false);
props.onDrop(e, props.subScope.scopeId, props.subScope.id);
};
return (
<div
id={`subscope-${props.subScope.id}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
class={`border rounded-xl transition-all duration-200 ml-8
${isOver()
? 'bg-primary/5 border-primary ring-4 ring-primary/10 z-10'
: 'bg-card border-border hover:border-border/50 shadow-sm'
}`}
>
<div class="px-4 py-3 flex items-center justify-between border-b border-border/40">
<div class="flex items-center gap-3">
<button
onClick={() => setIsCollapsed(!isCollapsed())}
class="p-1 hover:bg-muted rounded transition-colors text-muted-foreground"
>
<ChevronDown class={`w-4 h-4 transition-transform duration-200 ${isCollapsed() ? '-rotate-90' : ''}`} />
</button>
<div class="p-1.5 bg-muted rounded-lg text-muted-foreground">
<ListTree class="w-3.5 h-3.5" />
</div>
<input
type="text"
value={props.subScope.name}
onInput={(e) => props.onUpdateSubScope(props.subScope.id, { name: e.currentTarget.value })}
class="bg-transparent border-none outline-none font-black text-foreground text-sm focus:ring-0 w-48"
/>
<div class="flex items-center gap-2 text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
<span>{props.items.length} Items</span>
<span></span>
<span class="text-muted-foreground/60">{formatQuantity(totals().quantity)} Qty</span>
<span></span>
<span class="text-primary font-black">${formatNumber(totals().amount)}</span>
</div>
</div>
<div class="flex items-center gap-1">
<div class="relative">
<button
onClick={() => setShowCopyMenu(!showCopyMenu())}
class="flex items-center gap-1.5 px-2 py-1 bg-background border border-border rounded-lg text-[10px] font-black uppercase tracking-wider text-muted-foreground hover:text-primary hover:border-primary/30 transition-all"
title="Copy items to another sub-scope"
>
<span class="p-0.5 bg-muted rounded">
<Copy class="w-2.5 h-2.5" />
</span>
Copy To
</button>
<Show when={showCopyMenu()}>
<div
class="absolute right-0 mt-1 w-64 bg-popover border border-border rounded-xl shadow-elevated z-[100] py-2 animate-in fade-in zoom-in-95 duration-200"
>
<div class="px-4 py-2 border-b border-border/40 mb-1">
<span class="text-[9px] font-black text-muted-foreground uppercase tracking-[0.2em]">Select Target Sub-scope</span>
</div>
<div class="max-h-64 overflow-y-auto">
<For each={props.allScopes}>
{(scope) => (
<div class="px-2">
<div class="px-2 py-1 text-[9px] font-black text-primary/50 uppercase tracking-widest">{scope.name}</div>
<For each={props.allSubScopes.filter(ss => ss.scopeId === scope.id && ss.id !== props.subScope.id)}>
{(targetSS) => (
<button
onClick={() => {
props.onCopySubScopeItems(props.subScope.id, targetSS.id);
setShowCopyMenu(false);
}}
class="w-full text-left px-3 py-2 rounded-lg text-xs font-bold text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors flex items-center justify-between group"
>
<span>{targetSS.name}</span>
<ChevronDown class="w-3 h-3 opacity-0 group-hover:opacity-40 -rotate-90" />
</button>
)}
</For>
</div>
)}
</For>
</div>
</div>
</Show>
</div>
<button
onClick={() => props.onDeleteSubScope(props.subScope.id)}
class="p-1.5 text-muted-foreground/30 hover:text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
>
<Trash2 class="w-3.5 h-3.5" />
</button>
</div>
</div>
<Show when={!isCollapsed()}>
<div
onClick={(e) => { if (e.target === e.currentTarget) props.onClearSelection(); }}
class="p-2 space-y-1.5"
>
<For each={props.items}>
{(item) => (
<LazyItem estimatedHeight={60}>
<ItemRow
item={item}
onUpdate={props.onUpdateItem}
onDelete={props.onDeleteItem}
onDragStart={props.onDragStart}
onDuplicateItem={props.onDuplicateItem}
isSidebarCollapsed={props.isSidebarCollapsed}
isSelected={props.selectedIds.includes(item.id)}
anySelected={props.selectedIds.length > 0}
onToggleSelection={props.onToggleSelection}
hideColumns={props.hideColumns}
/>
</LazyItem>
)}
</For>
<Show when={props.items.length === 0}>
<div class="text-center py-4 text-[10px] font-bold text-muted-foreground/40 uppercase tracking-widest italic">
Empty Sub-scope
</div>
</Show>
</div>
</Show>
</div>
);
};
export default SubScopeCard;