True clean start with src folder
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user