Added Manager Info
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet" />
|
||||
<title>item-extractor-solid</title>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ interface EstimateBuilderProps {
|
||||
}
|
||||
|
||||
const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
type PrintMode = 'finalized' | 'manager';
|
||||
|
||||
// 1. Initialize State
|
||||
const { scopes, setScopes, subScopes, setSubScopes, estimateItems: items, setEstimateItems: setItems, clientInfo, jobInfo, generalEstimateNotes, setGeneralEstimateNotes, generalPreNotes, setGeneralPreNotes } = appStore;
|
||||
|
||||
@@ -56,6 +58,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
const [pendingCsvImportText, setPendingCsvImportText] = createSignal<string | null>(null);
|
||||
const [excludedScopeIds, setExcludedScopeIds] = createSignal<string[]>([]);
|
||||
const [excludedSubScopeIds, setExcludedSubScopeIds] = createSignal<string[]>([]);
|
||||
const [printMode, setPrintMode] = createSignal<PrintMode>('finalized');
|
||||
|
||||
// 2. Initialize custom hooks/primitives
|
||||
const { selectedIds, setSelectedIds, lastSelectedId, setLastSelectedId, toggleSelection, clearSelection, copyColumnSum } = useSelectionManager();
|
||||
@@ -92,7 +95,19 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
const anyDescriptionIsLong = createMemo(() => items.some(item => (item.description || '').length > 120));
|
||||
|
||||
const finalizeBid = () => {
|
||||
runPrint('finalized');
|
||||
};
|
||||
|
||||
const exportManagerInfo = () => {
|
||||
runPrint('manager');
|
||||
};
|
||||
|
||||
const runPrint = (mode: PrintMode) => {
|
||||
setPrintMode(mode);
|
||||
window.requestAnimationFrame(() => {
|
||||
window.print();
|
||||
window.setTimeout(() => setPrintMode('finalized'), 0);
|
||||
});
|
||||
};
|
||||
|
||||
const addScope = () => {
|
||||
@@ -347,6 +362,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
generalPreNotes={generalPreNotes()}
|
||||
formatNumber={formatNumber}
|
||||
hideColumns={anyDescriptionIsLong()}
|
||||
variant={printMode()}
|
||||
/>
|
||||
|
||||
<div class="no-print flex flex-col min-h-dvh">
|
||||
@@ -465,7 +481,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
|
||||
<ValidationStatusBar validationStats={validationStats()} />
|
||||
|
||||
<ExecutiveSummary totals={grandTotals()} onFinalizeBid={finalizeBid} />
|
||||
<ExecutiveSummary totals={grandTotals()} onFinalizeBid={finalizeBid} onExportManagerInfo={exportManagerInfo} />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,9 +28,13 @@ interface PrintEstimateProps {
|
||||
generalEstimateNotes: string;
|
||||
formatNumber: (num: number) => string;
|
||||
hideColumns?: boolean;
|
||||
variant?: 'finalized' | 'manager';
|
||||
}
|
||||
|
||||
const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
const variant = () => props.variant ?? 'finalized';
|
||||
const isManagerVariant = () => variant() === 'manager';
|
||||
|
||||
const getScopeTotal = (scopeId: string) => {
|
||||
let scopeSubtotal = 0;
|
||||
let scopeMarkup = 0;
|
||||
@@ -75,6 +79,28 @@ const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
return scopeSubtotal + scopeMarkup + scopeContingency + mgmtFee + deliveryFee + modifiersTotal;
|
||||
};
|
||||
|
||||
const getScopeItems = (scopeId: string) => {
|
||||
const scopeLevelItems = props.items.filter((item: EstimateItem) => item.scopeId === scopeId && !item.subScopeId);
|
||||
const scopeSubScopes = props.subScopes
|
||||
.filter((subScope) => subScope.scopeId === scopeId)
|
||||
.map((subScope) => ({
|
||||
subScope,
|
||||
items: props.items.filter((item: EstimateItem) => item.subScopeId === subScope.id)
|
||||
}))
|
||||
.filter((entry) => entry.items.length > 0);
|
||||
|
||||
return {
|
||||
scopeLevelItems,
|
||||
scopeSubScopes
|
||||
};
|
||||
};
|
||||
|
||||
const getItemSubtotal = (item: EstimateItem) => item.quantity * item.unitPrice;
|
||||
const getManagerScopeVisibleTotal = (scopeId: string) =>
|
||||
props.items
|
||||
.filter((item: EstimateItem) => item.scopeId === scopeId)
|
||||
.reduce((sum, item) => sum + getItemSubtotal(item), 0);
|
||||
|
||||
return (
|
||||
<div class="print-only p-6 bg-white font-sans max-w-[8.5in] mx-auto text-sm leading-normal text-foreground">
|
||||
{/* Company Header */}
|
||||
@@ -88,7 +114,9 @@ const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<h2 class="text-2xl font-black text-foreground uppercase tracking-tighter leading-none">Estimate</h2>
|
||||
<h2 class="text-2xl font-black text-foreground uppercase tracking-tighter leading-none">
|
||||
{isManagerVariant() ? 'Manager Info' : 'Estimate'}
|
||||
</h2>
|
||||
<p class="text-[10px] text-muted-foreground italic uppercase tracking-widest font-bold mb-1">*** NOT A BILL ***</p>
|
||||
<p class="text-foreground font-bold text-xs">Date: {new Date().toLocaleDateString()}</p>
|
||||
</div>
|
||||
@@ -110,6 +138,10 @@ const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
<div>
|
||||
<h3 class="text-[10px] font-bold text-muted-foreground uppercase tracking-wider border-b border-border pb-1 mb-2">Project Details</h3>
|
||||
<div class="pl-1 space-y-1">
|
||||
<div class="flex gap-2 text-xs">
|
||||
<span class="font-bold text-muted-foreground/60 w-16 uppercase text-[10px] pt-0.5">Job No.</span>
|
||||
<span class="font-bold text-foreground">{props.jobInfo.number || '---'}</span>
|
||||
</div>
|
||||
<div class="flex gap-2 text-xs">
|
||||
<span class="font-bold text-muted-foreground/60 w-16 uppercase text-[10px] pt-0.5">Project</span>
|
||||
<span class="font-bold text-foreground">{props.jobInfo.name || '---'}</span>
|
||||
@@ -129,7 +161,7 @@ const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
</div>
|
||||
|
||||
{/* General Pre-Notes */}
|
||||
<Show when={props.generalPreNotes}>
|
||||
<Show when={props.generalPreNotes && !isManagerVariant()}>
|
||||
<div class="mb-6 p-4 bg-muted/30 rounded-xl border border-border/40 break-inside-avoid shadow-sm outline outline-1 outline-border/20">
|
||||
<div class="text-[11px] text-foreground/80 leading-relaxed font-medium italic">
|
||||
{props.generalPreNotes}
|
||||
@@ -138,6 +170,9 @@ const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
</Show>
|
||||
|
||||
{/* Scope Details Table */}
|
||||
<Show
|
||||
when={isManagerVariant()}
|
||||
fallback={
|
||||
<div class="mb-8 border-t-2 border-foreground">
|
||||
<div class="grid grid-cols-12 gap-2 py-2 bg-muted/40 px-3 text-[10px] font-bold text-muted-foreground uppercase tracking-wider border-b border-border/60">
|
||||
<div class="col-span-2">Scope</div>
|
||||
@@ -161,7 +196,6 @@ const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
if (processed.trim().startsWith('- ')) {
|
||||
return <li class="ml-3 list-disc pl-1 text-foreground/90" innerHTML={processed.substring(2)} />;
|
||||
}
|
||||
// Handle empty lines more gracefully in compact mode
|
||||
if (!processed.trim()) return <div class="h-1"></div>;
|
||||
return <p class="mb-0.5 last:mb-0 text-foreground/90" innerHTML={processed} />;
|
||||
}}
|
||||
@@ -176,9 +210,114 @@ const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="mb-8 space-y-6">
|
||||
<For each={props.scopes}>
|
||||
{(scope) => {
|
||||
const scopeItems = () => getScopeItems(scope.id);
|
||||
|
||||
return (
|
||||
<section class={`border border-border/60 rounded-2xl overflow-hidden ${isManagerVariant() ? '' : 'break-inside-avoid'}`}>
|
||||
<div class="px-4 py-3 bg-muted/30 border-b border-border/60 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-black text-foreground uppercase">{scope.name}</h4>
|
||||
<p class="text-[10px] text-muted-foreground font-bold uppercase tracking-widest mt-1">Scope Total</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-black text-foreground">${props.formatNumber(getManagerScopeVisibleTotal(scope.id))}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="rounded-xl border border-border/50 p-3 bg-white">
|
||||
<div class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-2">Scope Description</div>
|
||||
<div class="prose prose-sm max-w-none prose-strong:text-foreground prose-em:text-foreground/70 text-xs text-foreground/90">
|
||||
<For each={(scope.bidDescription || 'See detailed specifications.').split('\n')}>
|
||||
{(line) => {
|
||||
let processed = line;
|
||||
processed = processed.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
processed = processed.replace(/(\*|_)(.*?)\1/g, '<em>$2</em>');
|
||||
if (processed.trim().startsWith('- ')) {
|
||||
return <li class="ml-3 list-disc pl-1 text-foreground/90" innerHTML={processed.substring(2)} />;
|
||||
}
|
||||
if (!processed.trim()) return <div class="h-1"></div>;
|
||||
return <p class="mb-0.5 last:mb-0 text-foreground/90" innerHTML={processed} />;
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border/50 p-3 bg-white">
|
||||
<div class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest mb-2">Estimator Notes</div>
|
||||
<div class="text-xs text-foreground/90 whitespace-pre-wrap leading-relaxed">
|
||||
{scope.estimatorNotes?.trim() || 'No estimator notes provided.'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-border/50 rounded-xl overflow-hidden">
|
||||
<div class="grid grid-cols-12 gap-3 px-3 py-2 bg-muted/20 text-[10px] font-bold text-muted-foreground uppercase tracking-wider border-b border-border/50">
|
||||
<div class="col-span-2">Section</div>
|
||||
<div class="col-span-6">Line Item</div>
|
||||
<div class="col-span-1 text-right">Qty</div>
|
||||
<div class="col-span-1 text-right">Unit</div>
|
||||
<div class="col-span-2 text-right">Subtotal</div>
|
||||
</div>
|
||||
|
||||
<Show when={scopeItems().scopeLevelItems.length > 0}>
|
||||
<div class="border-b border-border/40">
|
||||
<div class="px-3 py-2 bg-background text-[10px] font-black uppercase tracking-widest text-muted-foreground">Scope Level</div>
|
||||
<For each={scopeItems().scopeLevelItems}>
|
||||
{(item) => (
|
||||
<div class="grid grid-cols-12 gap-3 px-3 py-2 text-xs border-t border-border/20 items-start">
|
||||
<div class="col-span-2 font-bold text-foreground">Scope Level</div>
|
||||
<div class="col-span-6 text-foreground/90">{item.description}</div>
|
||||
<div class="col-span-1 text-right text-foreground">{item.quantity}</div>
|
||||
<div class="col-span-1 text-right text-foreground">${props.formatNumber(item.unitPrice)}</div>
|
||||
<div class="col-span-2 text-right font-bold text-foreground">${props.formatNumber(getItemSubtotal(item))}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<For each={scopeItems().scopeSubScopes}>
|
||||
{(entry) => (
|
||||
<div class="border-b last:border-b-0 border-border/40">
|
||||
<div class="px-3 py-2 bg-background text-[10px] font-black uppercase tracking-widest text-muted-foreground">
|
||||
{entry.subScope.name}
|
||||
</div>
|
||||
<For each={entry.items}>
|
||||
{(item) => (
|
||||
<div class="grid grid-cols-12 gap-3 px-3 py-2 text-xs border-t border-border/20 items-start">
|
||||
<div class="col-span-2 font-bold text-foreground">{entry.subScope.name}</div>
|
||||
<div class="col-span-6 text-foreground/90">{item.description}</div>
|
||||
<div class="col-span-1 text-right text-foreground">{item.quantity}</div>
|
||||
<div class="col-span-1 text-right text-foreground">${props.formatNumber(item.unitPrice)}</div>
|
||||
<div class="col-span-2 text-right font-bold text-foreground">${props.formatNumber(getItemSubtotal(item))}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={scopeItems().scopeLevelItems.length === 0 && scopeItems().scopeSubScopes.length === 0}>
|
||||
<div class="px-3 py-4 text-xs text-muted-foreground italic">No line items assigned to this scope.</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* General Estimate Notes */}
|
||||
<Show when={props.generalEstimateNotes}>
|
||||
<Show when={props.generalEstimateNotes && !isManagerVariant()}>
|
||||
<div class="mb-8">
|
||||
<h5 class="text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1 border-b border-border/40 pb-1">General Estimate Notes</h5>
|
||||
<div class="text-[11px] text-foreground/80 leading-relaxed whitespace-pre-wrap font-medium">
|
||||
@@ -188,6 +327,7 @@ const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
</Show>
|
||||
|
||||
{/* Final Section: Totals & Signature aligned */}
|
||||
<Show when={!isManagerVariant()}>
|
||||
<div class="grid grid-cols-12 gap-8 items-end mb-8 break-inside-avoid">
|
||||
<div class="col-span-7">
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -233,6 +373,7 @@ const PrintEstimate: Component<PrintEstimateProps> = (props) => {
|
||||
|
||||
{/* Print Page Counter */}
|
||||
<div class="print-page-number"></div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ interface ExecutiveSummaryProps {
|
||||
grandTotal: number;
|
||||
};
|
||||
onFinalizeBid: () => void;
|
||||
onExportManagerInfo: () => void;
|
||||
}
|
||||
|
||||
export const ExecutiveSummary: Component<ExecutiveSummaryProps> = (props) => {
|
||||
@@ -68,12 +69,20 @@ export const ExecutiveSummary: Component<ExecutiveSummaryProps> = (props) => {
|
||||
{formatNumber(props.totals.grandTotal)}
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
onClick={props.onFinalizeBid}
|
||||
class="w-full py-5 bg-primary hover:bg-primary/90 text-primary-foreground rounded-2xl font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-primary/20 active:scale-95 flex items-center justify-center gap-3"
|
||||
>
|
||||
<Plus class="w-5 h-5" /> Finalize Bid
|
||||
</button>
|
||||
<button
|
||||
onClick={props.onExportManagerInfo}
|
||||
class="w-full py-4 bg-muted hover:bg-muted/80 text-foreground rounded-2xl font-black text-xs uppercase tracking-[0.2em] transition-all border border-border active:scale-95 flex items-center justify-center gap-3"
|
||||
>
|
||||
<Info class="w-5 h-5" /> Manager Info
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
/* Import Standard Shadcn Font (Inter) */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
||||
|
||||
@theme {
|
||||
/* Clean, single-font family for a modern SaaS look */
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
|
||||
Reference in New Issue
Block a user