502 lines
30 KiB
TypeScript
502 lines
30 KiB
TypeScript
import { For, Show, createMemo, createSignal, type Component } from 'solid-js';
|
||
import { Trash2, Plus, Settings2, Percent, DollarSign, ChevronDown, FileText, ClipboardList, FileBox } from 'lucide-solid';
|
||
import NumericInput from './ui/NumericInput';
|
||
import type { EstimateItem, Scope, SubScope, PricingType } from '../types';
|
||
import ItemRow from './ItemRow';
|
||
import SubScopeCard from './SubScopeCard';
|
||
import NoteEditor from './NoteEditor';
|
||
import LazyItem from './LazyItem';
|
||
import ScopeScaleWrapper from './ui/ScopeScaleWrapper';
|
||
import { ModifierRegistry } from '../utils/modifier-registry';
|
||
|
||
interface ScopeCardProps {
|
||
scope: Scope;
|
||
subScopes: SubScope[];
|
||
items: EstimateItem[];
|
||
onUpdateScope: (id: string, updates: Partial<Scope>) => void;
|
||
onDeleteScope: (id: string) => void;
|
||
onUpdateItem: (id: string, updates: Partial<EstimateItem>) => void;
|
||
onDeleteItem: (id: string) => void;
|
||
onAddSubScope: (scopeId: string) => void;
|
||
onUpdateSubScope: (id: string, updates: Partial<SubScope>) => void;
|
||
onDeleteSubScope: (id: string) => void;
|
||
onDragStart: (e: DragEvent, id: string) => void;
|
||
onDrop: (e: DragEvent, scopeId: string, subScopeId?: string) => void;
|
||
onBulkUpdate: (scopeId: string, subScopeId: string | undefined, updates: { markup?: number, markupType?: PricingType, contingency?: number, contingencyType?: PricingType }) => void;
|
||
onDuplicateItem: (id: string) => void;
|
||
allScopes: Scope[];
|
||
allSubScopes: SubScope[];
|
||
onCopySubScopeItems: (sourceSubScopeId: string, targetSubScopeId: string) => void;
|
||
onOpenApplyTemplate: (scopeId: string) => void;
|
||
isSidebarCollapsed: () => boolean;
|
||
selectedIds: string[];
|
||
onToggleSelection: (id: string, isMulti: boolean, isShift: boolean) => void;
|
||
onClearSelection: () => void;
|
||
hideColumns?: boolean;
|
||
}
|
||
|
||
|
||
const ScopeCard: Component<ScopeCardProps> = (props) => {
|
||
const [showBulkActions, setShowBulkActions] = createSignal(false);
|
||
const [isCollapsed, setIsCollapsed] = createSignal(false);
|
||
const [bulkMarkup, setBulkMarkup] = createSignal(0);
|
||
const [bulkContingency, setBulkContingency] = createSignal(0);
|
||
const [isOver, setIsOver] = createSignal(false);
|
||
|
||
const scopeItems = createMemo(() => props.items.filter(i => !i.subScopeId));
|
||
|
||
const totals = createMemo(() => {
|
||
let subtotal = 0;
|
||
let markup = 0;
|
||
let contingency = 0;
|
||
let totalQuantity = 0;
|
||
|
||
props.items.forEach(item => {
|
||
const base = item.quantity * item.unitPrice;
|
||
subtotal += base;
|
||
totalQuantity += item.quantity;
|
||
markup += item.markupType === 'percent' ? base * (item.markup / 100) : item.markup;
|
||
contingency += item.contingencyType === 'percent' ? base * (item.contingency / 100) : item.contingency;
|
||
});
|
||
|
||
// Add Management and Delivery
|
||
const mgmtTotal = props.scope.useManagementLogic === 'percent'
|
||
? subtotal * (props.scope.managementFee / 100)
|
||
: props.scope.managementFee;
|
||
|
||
const deliveryTotal = props.scope.useDeliveryLogic === 'percent'
|
||
? subtotal * (props.scope.deliveryFee / 100)
|
||
: props.scope.deliveryFee;
|
||
|
||
let modifiersTotal = 0;
|
||
props.subScopes.forEach(ss => {
|
||
const ssItems = props.items.filter(i => i.subScopeId === ss.id);
|
||
const ssTotalWithMarkup = ssItems.reduce((sum, 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 sum + base + markup + contingency;
|
||
}, 0);
|
||
|
||
(ss.modifiers || []).forEach(m => {
|
||
if (!m.includeInTotal) return;
|
||
modifiersTotal += ModifierRegistry.calculate(m, ssTotalWithMarkup, ssItems);
|
||
});
|
||
});
|
||
|
||
return {
|
||
subtotal,
|
||
markup,
|
||
contingency,
|
||
mgmtTotal,
|
||
deliveryTotal,
|
||
modifiersTotal,
|
||
totalQuantity,
|
||
total: subtotal + markup + contingency + mgmtTotal + deliveryTotal + modifiersTotal
|
||
};
|
||
});
|
||
|
||
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.dataTransfer!.dropEffect = 'move';
|
||
setIsOver(true);
|
||
};
|
||
|
||
const handleDragLeave = () => {
|
||
setIsOver(false);
|
||
};
|
||
|
||
return (
|
||
<div
|
||
onDragOver={handleDragOver}
|
||
onDrop={(e) => {
|
||
setIsOver(false);
|
||
props.onDrop(e, props.scope.id);
|
||
}}
|
||
onDragLeave={handleDragLeave}
|
||
class={`bg-muted/30 border rounded-3xl overflow-visible transition-all ring-1 ring-border/50 shadow-premium
|
||
${isOver() ? 'bg-primary/5 ring-4 ring-primary/10 border-primary' : 'border-border'}
|
||
`}
|
||
>
|
||
{/* Scope Header - Rounded Top */}
|
||
<div class="p-6 bg-card border-b border-border flex items-center justify-between rounded-t-3xl">
|
||
<div class="flex items-center gap-4">
|
||
<button
|
||
onClick={() => setIsCollapsed(!isCollapsed())}
|
||
class="p-2 hover:bg-muted rounded-lg transition-colors text-muted-foreground group"
|
||
>
|
||
<ChevronDown class={`w-4 h-4 transition-transform duration-200 ${isCollapsed() ? '-rotate-90' : ''}`} />
|
||
</button>
|
||
<div class="flex flex-col">
|
||
<input
|
||
type="text"
|
||
value={props.scope.name}
|
||
onInput={(e) => props.onUpdateScope(props.scope.id, { name: e.currentTarget.value })}
|
||
class="text-xl font-black text-foreground bg-transparent border-none outline-none focus:ring-0 w-64"
|
||
/>
|
||
<div class="flex items-center gap-2 text-xs text-muted-foreground font-bold uppercase tracking-wider">
|
||
<span>{props.items.length} Items</span>
|
||
<span>•</span>
|
||
<span>{formatQuantity(totals().totalQuantity)} Qty</span>
|
||
<span>•</span>
|
||
<span class="text-primary">${formatNumber(totals().total)} Total</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2">
|
||
<div class="flex items-center gap-1.5">
|
||
<button
|
||
onClick={() => props.onAddSubScope(props.scope.id)}
|
||
class="flex items-center gap-2 px-3 py-2 bg-primary/10 text-primary rounded-xl text-sm font-bold hover:bg-primary/20 transition-colors border border-primary/20"
|
||
>
|
||
<Plus class="h-3.5 w-3.5" /> Sub-scope
|
||
</button>
|
||
<button
|
||
onClick={() => props.onOpenApplyTemplate(props.scope.id)}
|
||
class="flex items-center gap-2 px-3 py-2 bg-indigo-50 text-indigo-600 rounded-xl text-sm font-bold hover:bg-indigo-100 transition-colors border border-indigo-100"
|
||
>
|
||
<FileBox class="h-3.5 w-3.5" /> Template
|
||
</button>
|
||
</div>
|
||
<div class="h-8 w-px bg-border mx-2"></div>
|
||
<button
|
||
onClick={() => setShowBulkActions(!showBulkActions())}
|
||
class="p-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-all"
|
||
>
|
||
<Settings2 class="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => props.onDeleteScope(props.scope.id)}
|
||
class="p-2 text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10 rounded-lg transition-all"
|
||
>
|
||
<Trash2 class="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<Show when={!isCollapsed()}>
|
||
<Show when={showBulkActions()}>
|
||
<div class="bg-primary/5 border-b border-primary/10 p-4 flex flex-wrap gap-4 items-center animate-in slide-in-from-top duration-300">
|
||
<span class="text-sm font-semibold text-primary">Bulk Adjust Scope:</span>
|
||
<div class="flex items-center gap-2">
|
||
<label class="text-xs text-primary/70">Markup %:</label>
|
||
<NumericInput
|
||
class="w-16 px-2 py-1 border border-primary/20 bg-background rounded text-sm focus:ring-2 focus:ring-primary outline-none"
|
||
value={bulkMarkup()}
|
||
onUpdate={(val) => setBulkMarkup(val)}
|
||
/>
|
||
<button
|
||
onClick={() => props.onBulkUpdate(props.scope.id, undefined, { markup: bulkMarkup(), markupType: 'percent' })}
|
||
class="px-2 py-1 bg-primary text-primary-foreground text-xs rounded-lg font-bold hover:bg-primary/90"
|
||
>
|
||
Apply
|
||
</button>
|
||
</div>
|
||
<div class="flex items-center gap-2 border-l border-primary/20 pl-4">
|
||
<label class="text-xs text-primary/70">Contingency %:</label>
|
||
<NumericInput
|
||
class="w-16 px-2 py-1 border border-primary/20 bg-background rounded text-sm focus:ring-2 focus:ring-primary outline-none"
|
||
value={bulkContingency()}
|
||
onUpdate={(val) => setBulkContingency(val)}
|
||
/>
|
||
<button
|
||
onClick={() => props.onBulkUpdate(props.scope.id, undefined, { contingency: bulkContingency(), contingencyType: 'percent' })}
|
||
class="px-2 py-1 bg-primary text-primary-foreground text-xs rounded-lg font-bold hover:bg-primary/90"
|
||
>
|
||
Apply
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<ScopeScaleWrapper>
|
||
{/* Sticky Column Headers */}
|
||
<div class="scope-sticky-header py-2 mb-2 hidden md:flex items-center gap-4 text-[10px] font-bold text-muted-foreground uppercase tracking-wider px-3">
|
||
<div class="w-4 shrink-0"></div> {/* Grip space */}
|
||
<div class="flex-1 min-w-[200px]">Description</div>
|
||
<div class="w-20 shrink-0 text-right">Qty</div>
|
||
<div class="w-24 shrink-0 text-right">Price</div>
|
||
<div class="w-24 shrink-0 text-right">Subtotal</div>
|
||
<Show when={props.isSidebarCollapsed() || !props.hideColumns}>
|
||
<div class="w-32 shrink-0 text-center">Markup</div>
|
||
<div class="w-32 shrink-0 text-center">Contingency</div>
|
||
</Show>
|
||
<div class="w-28 shrink-0 text-right">Total</div>
|
||
<div class="w-16 shrink-0"></div> {/* Action space */}
|
||
</div>
|
||
|
||
{/* Scope-level items */}
|
||
<div
|
||
onClick={(e) => { if (e.target === e.currentTarget) props.onClearSelection(); }}
|
||
class="px-3 pb-3 space-y-2"
|
||
>
|
||
<Show when={scopeItems().length > 0}>
|
||
<div class="grid gap-2">
|
||
<For each={props.items.filter(i => !i.subScopeId)}>
|
||
{(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>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={scopeItems().length === 0 && props.subScopes.length === 0}>
|
||
<div class="text-center py-8 text-muted-foreground/50 italic text-sm">
|
||
Empty scope. Drag items here or add sub-scopes.
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
|
||
{/* Sub-scopes — rendered inside the same scale wrapper so they zoom together */}
|
||
<div class="px-3 pb-3 space-y-4">
|
||
<For each={props.subScopes}>
|
||
{(subScope) => (
|
||
<SubScopeCard
|
||
subScope={subScope}
|
||
items={props.items.filter(i => i.subScopeId === subScope.id)}
|
||
onUpdateSubScope={props.onUpdateSubScope}
|
||
onUpdateItem={props.onUpdateItem}
|
||
onDeleteItem={props.onDeleteItem}
|
||
onDeleteSubScope={props.onDeleteSubScope}
|
||
onDragStart={props.onDragStart}
|
||
onDrop={(e, sid, ssid) => props.onDrop(e, sid, ssid)}
|
||
onDuplicateItem={props.onDuplicateItem}
|
||
allScopes={props.allScopes}
|
||
allSubScopes={props.allSubScopes}
|
||
onCopySubScopeItems={props.onCopySubScopeItems}
|
||
isSidebarCollapsed={props.isSidebarCollapsed}
|
||
selectedIds={props.selectedIds}
|
||
onToggleSelection={props.onToggleSelection}
|
||
onClearSelection={props.onClearSelection}
|
||
hideColumns={props.hideColumns}
|
||
/>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</ScopeScaleWrapper>
|
||
|
||
<div class="p-6 pt-4 space-y-4">
|
||
{/* Scope Extras & Totals */}
|
||
<div class="mt-4 pt-6 border-t border-border space-y-3">
|
||
{/* Management Line */}
|
||
<div class="flex items-center justify-between group">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm font-medium text-muted-foreground">Management Fee</span>
|
||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||
<button
|
||
onClick={() => props.onUpdateScope(props.scope.id, { useManagementLogic: 'percent' })}
|
||
class={`p-1 rounded-md transition-colors ${props.scope.useManagementLogic === 'percent' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}`}
|
||
>
|
||
<Percent class="w-3 h-3" />
|
||
</button>
|
||
<button
|
||
onClick={() => props.onUpdateScope(props.scope.id, {
|
||
useManagementLogic: 'simple',
|
||
managementFee: (props.scope.managementFeeQuantity || 0) * (props.scope.managementFeeRate || 0)
|
||
})}
|
||
class={`p-1 rounded-md transition-colors ${props.scope.useManagementLogic === 'simple' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}`}
|
||
>
|
||
<DollarSign class="w-3 h-3" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-4">
|
||
{props.scope.useManagementLogic === 'percent' ? (
|
||
<NumericInput
|
||
value={props.scope.managementFee}
|
||
onUpdate={(val) => props.onUpdateScope(props.scope.id, { managementFee: val })}
|
||
class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
|
||
/>
|
||
) : (
|
||
<div class="flex items-center gap-3">
|
||
<div class="flex flex-col items-end gap-0.5 text-[9px] font-black tracking-[0.3em] text-muted-foreground uppercase">
|
||
<span>Qty</span>
|
||
<NumericInput
|
||
value={props.scope.managementFeeQuantity}
|
||
onUpdate={(val) => {
|
||
const qty = val;
|
||
const rate = props.scope.managementFeeRate || 0;
|
||
props.onUpdateScope(props.scope.id, {
|
||
managementFeeQuantity: qty,
|
||
managementFee: qty * rate
|
||
});
|
||
}}
|
||
class="w-16 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
|
||
/>
|
||
</div>
|
||
<span class="text-xs font-black text-muted-foreground/70">×</span>
|
||
<div class="flex flex-col items-end gap-0.5 text-[9px] font-black tracking-[0.3em] text-muted-foreground uppercase">
|
||
<span>Rate</span>
|
||
<div class="flex items-center gap-1">
|
||
<span class="text-[10px] font-black text-muted-foreground">$</span>
|
||
<NumericInput
|
||
value={props.scope.managementFeeRate}
|
||
onUpdate={(val) => {
|
||
const rate = val;
|
||
const qty = props.scope.managementFeeQuantity || 0;
|
||
props.onUpdateScope(props.scope.id, {
|
||
managementFeeRate: rate,
|
||
managementFee: qty * rate
|
||
});
|
||
}}
|
||
class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<span class="w-28 text-right text-foreground font-medium">${formatNumber(totals().mgmtTotal)}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Delivery Fee */}
|
||
<div class="flex items-center justify-between group">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm font-medium text-muted-foreground">Delivery Fee</span>
|
||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||
<button
|
||
onClick={() => props.onUpdateScope(props.scope.id, { useDeliveryLogic: 'percent' })}
|
||
class={`p-1 rounded-md transition-colors ${props.scope.useDeliveryLogic === 'percent' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}`}
|
||
>
|
||
<Percent class="w-3 h-3" />
|
||
</button>
|
||
<button
|
||
onClick={() => props.onUpdateScope(props.scope.id, {
|
||
useDeliveryLogic: 'simple',
|
||
deliveryFee: (props.scope.deliveryFeeQuantity || 0) * (props.scope.deliveryFeeRate || 0)
|
||
})}
|
||
class={`p-1 rounded-md transition-colors ${props.scope.useDeliveryLogic === 'simple' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}`}
|
||
>
|
||
<DollarSign class="w-3 h-3" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-4">
|
||
{props.scope.useDeliveryLogic === 'percent' ? (
|
||
<NumericInput
|
||
value={props.scope.deliveryFee}
|
||
onUpdate={(val) => props.onUpdateScope(props.scope.id, { deliveryFee: val })}
|
||
class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
|
||
/>
|
||
) : (
|
||
<div class="flex items-center gap-3">
|
||
<div class="flex flex-col items-end gap-0.5 text-[9px] font-black tracking-[0.3em] text-muted-foreground uppercase">
|
||
<span>Qty</span>
|
||
<NumericInput
|
||
value={props.scope.deliveryFeeQuantity}
|
||
onUpdate={(val) => {
|
||
const qty = val;
|
||
const rate = props.scope.deliveryFeeRate || 0;
|
||
props.onUpdateScope(props.scope.id, {
|
||
deliveryFeeQuantity: qty,
|
||
deliveryFee: qty * rate
|
||
});
|
||
}}
|
||
class="w-16 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
|
||
/>
|
||
</div>
|
||
<span class="text-xs font-black text-muted-foreground/70">×</span>
|
||
<div class="flex flex-col items-end gap-0.5 text-[9px] font-black tracking-[0.3em] text-muted-foreground uppercase">
|
||
<span>Rate</span>
|
||
<div class="flex items-center gap-1">
|
||
<span class="text-[10px] font-black text-muted-foreground">$</span>
|
||
<NumericInput
|
||
value={props.scope.deliveryFeeRate}
|
||
onUpdate={(val) => {
|
||
const rate = val;
|
||
const qty = props.scope.deliveryFeeQuantity || 0;
|
||
props.onUpdateScope(props.scope.id, {
|
||
deliveryFeeRate: rate,
|
||
deliveryFee: qty * rate
|
||
});
|
||
}}
|
||
class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<span class="w-28 text-right text-foreground font-medium">${formatNumber(totals().deliveryTotal)}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Scope Notes Section */}
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-8 border-t border-border">
|
||
<NoteEditor
|
||
label="Estimator's Notes"
|
||
value={props.scope.estimatorNotes || ''}
|
||
onUpdate={(val) => props.onUpdateScope(props.scope.id, { estimatorNotes: val })}
|
||
icon={ClipboardList}
|
||
highlight={props.scope.estimatorNotes?.includes('#')}
|
||
/>
|
||
<NoteEditor
|
||
label="Scope Description (Appears on Finale Estimate)"
|
||
value={props.scope.bidDescription || ''}
|
||
onUpdate={(val) => props.onUpdateScope(props.scope.id, { bidDescription: val })}
|
||
icon={FileText}
|
||
highlight={props.scope.bidDescription?.includes('#')}
|
||
/>
|
||
</div>
|
||
|
||
{/* Scope Totals Breakdown */}
|
||
<div class="space-y-1 text-sm pt-4">
|
||
<div class="flex justify-end gap-8 text-muted-foreground italic">
|
||
<span>Scope Subtotal:</span>
|
||
<span class="w-28 text-right">${formatNumber(totals().subtotal)}</span>
|
||
</div>
|
||
<div class="flex justify-end gap-8 text-muted-foreground italic">
|
||
<span>Scope Total Quantity:</span>
|
||
<span class="w-28 text-right">{formatQuantity(totals().totalQuantity)}</span>
|
||
</div>
|
||
<div class="flex justify-end gap-8 text-primary">
|
||
<span>Scope Markup:</span>
|
||
<span class="w-28 text-right">${formatNumber(totals().markup)}</span>
|
||
</div>
|
||
<div class="flex justify-end gap-8 text-orange-600">
|
||
<span>Scope Contingency:</span>
|
||
<span class="w-28 text-right">${formatNumber(totals().contingency)}</span>
|
||
</div>
|
||
<Show when={totals().modifiersTotal !== 0}>
|
||
<div class="flex justify-end gap-8 text-indigo-600">
|
||
<span>Scope Modifiers:</span>
|
||
<span class="w-28 text-right">${formatNumber(totals().modifiersTotal)}</span>
|
||
</div>
|
||
</Show>
|
||
<div class="flex justify-end gap-8 font-black text-foreground text-xl mt-4">
|
||
<span>Scope Grand Total:</span>
|
||
<span class="w-32 text-right border-t-2 border-foreground pt-1">
|
||
${formatNumber(totals().total)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ScopeCard;
|