diff --git a/index.html b/index.html index 9dd3c18..196cb66 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,9 @@ + + + item-extractor-solid @@ -13,4 +16,4 @@ - \ No newline at end of file + diff --git a/src/components/EstimateBuilder.tsx b/src/components/EstimateBuilder.tsx index d80ffe2..a92a878 100644 --- a/src/components/EstimateBuilder.tsx +++ b/src/components/EstimateBuilder.tsx @@ -38,6 +38,8 @@ interface EstimateBuilderProps { } const EstimateBuilder: Component = (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 = (props) => { const [pendingCsvImportText, setPendingCsvImportText] = createSignal(null); const [excludedScopeIds, setExcludedScopeIds] = createSignal([]); const [excludedSubScopeIds, setExcludedSubScopeIds] = createSignal([]); + const [printMode, setPrintMode] = createSignal('finalized'); // 2. Initialize custom hooks/primitives const { selectedIds, setSelectedIds, lastSelectedId, setLastSelectedId, toggleSelection, clearSelection, copyColumnSum } = useSelectionManager(); @@ -92,7 +95,19 @@ const EstimateBuilder: Component = (props) => { const anyDescriptionIsLong = createMemo(() => items.some(item => (item.description || '').length > 120)); const finalizeBid = () => { - window.print(); + 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 = (props) => { generalPreNotes={generalPreNotes()} formatNumber={formatNumber} hideColumns={anyDescriptionIsLong()} + variant={printMode()} />
@@ -465,7 +481,7 @@ const EstimateBuilder: Component = (props) => { - +
diff --git a/src/components/PrintEstimate.tsx b/src/components/PrintEstimate.tsx index 0e776bb..a004de5 100644 --- a/src/components/PrintEstimate.tsx +++ b/src/components/PrintEstimate.tsx @@ -28,9 +28,13 @@ interface PrintEstimateProps { generalEstimateNotes: string; formatNumber: (num: number) => string; hideColumns?: boolean; + variant?: 'finalized' | 'manager'; } const PrintEstimate: Component = (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 = (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 (
-

Estimate

+

+ {isManagerVariant() ? 'Manager Info' : 'Estimate'} +

*** NOT A BILL ***

Date: {new Date().toLocaleDateString()}

@@ -110,6 +138,10 @@ const PrintEstimate: Component = (props) => {

Project Details

+
+ Job No. + {props.jobInfo.number || '---'} +
Project {props.jobInfo.name || '---'} @@ -129,7 +161,7 @@ const PrintEstimate: Component = (props) => {
{/* General Pre-Notes */} - +
{props.generalPreNotes} @@ -138,47 +170,154 @@ const PrintEstimate: Component = (props) => { {/* Scope Details Table */} -
-
-
Scope
-
Description
-
Subtotal
-
-
- - {(scope) => ( -
-
-

{scope.name}

-
-
-
- - {(line) => { - let processed = line; - processed = processed.replace(/\*\*(.*?)\*\*/g, '$1'); - processed = processed.replace(/(\*|_)(.*?)\1/g, '$2'); - if (processed.trim().startsWith('- ')) { - return
  • ; - } - // Handle empty lines more gracefully in compact mode - if (!processed.trim()) return
    ; - return

    ; - }} - + +

    +
    Scope
    +
    Description
    +
    Subtotal
    +
    +
    + + {(scope) => ( +
    +
    +

    {scope.name}

    +
    +
    +
    + + {(line) => { + let processed = line; + processed = processed.replace(/\*\*(.*?)\*\*/g, '$1'); + processed = processed.replace(/(\*|_)(.*?)\1/g, '$2'); + if (processed.trim().startsWith('- ')) { + return
  • ; + } + if (!processed.trim()) return
    ; + return

    ; + }} + +

  • +
    +
    + ${props.formatNumber(getScopeTotal(scope.id))} +
    -
    -
    - ${props.formatNumber(getScopeTotal(scope.id))} -
    -
    - )} + )} + +
    +
    + } + > +
    + + {(scope) => { + const scopeItems = () => getScopeItems(scope.id); + + return ( +
    +
    +
    +

    {scope.name}

    +

    Scope Total

    +
    +
    +
    ${props.formatNumber(getManagerScopeVisibleTotal(scope.id))}
    +
    +
    + +
    +
    +
    +
    Scope Description
    +
    + + {(line) => { + let processed = line; + processed = processed.replace(/\*\*(.*?)\*\*/g, '$1'); + processed = processed.replace(/(\*|_)(.*?)\1/g, '$2'); + if (processed.trim().startsWith('- ')) { + return
  • ; + } + if (!processed.trim()) return
    ; + return

    ; + }} + +

  • +
    + +
    +
    Estimator Notes
    +
    + {scope.estimatorNotes?.trim() || 'No estimator notes provided.'} +
    +
    +
    + +
    +
    +
    Section
    +
    Line Item
    +
    Qty
    +
    Unit
    +
    Subtotal
    +
    + + 0}> +
    +
    Scope Level
    + + {(item) => ( +
    +
    Scope Level
    +
    {item.description}
    +
    {item.quantity}
    +
    ${props.formatNumber(item.unitPrice)}
    +
    ${props.formatNumber(getItemSubtotal(item))}
    +
    + )} +
    +
    +
    + + + {(entry) => ( +
    +
    + {entry.subScope.name} +
    + + {(item) => ( +
    +
    {entry.subScope.name}
    +
    {item.description}
    +
    {item.quantity}
    +
    ${props.formatNumber(item.unitPrice)}
    +
    ${props.formatNumber(getItemSubtotal(item))}
    +
    + )} +
    +
    + )} +
    + + +
    No line items assigned to this scope.
    +
    +
    +
    +
    + ); + }}
    -
    + {/* General Estimate Notes */} - +
    General Estimate Notes
    @@ -188,51 +327,53 @@ const PrintEstimate: Component = (props) => { {/* Final Section: Totals & Signature aligned */} -
    -
    -
    -
    - X -
    + +
    +
    +
    +
    + X +
    +
    +
    +

    + A Work Order is then established and Cardoza Construction, LLC will proceed with work. + Valid for 30 days from original date. +

    + Authorized Signature +
    -
    -

    - A Work Order is then established and Cardoza Construction, LLC will proceed with work. - Valid for 30 days from original date. -

    - Authorized Signature +
    + +
    +
    +
    Grand Total
    +
    ${props.formatNumber(props.grandTotals().grandTotal)}
    -
    -
    -
    Grand Total
    -
    ${props.formatNumber(props.grandTotals().grandTotal)}
    + {/* Terms & Legal Notices */} +
    +
    +
    Terms & Conditions
    +

    + Unless otherwise agreed, credit approval and 50% down is required prior to scheduling/starting work. + (Credit card authorization may be substituted for jobs under $2,000) +

    +
    + +
    +
    Notice to Owner
    +
    + {NOTICE_TO_OWNERS.replace('NOTICE TO OWNER', '').trim()} +
    -
    - {/* Terms & Legal Notices */} -
    -
    -
    Terms & Conditions
    -

    - Unless otherwise agreed, credit approval and 50% down is required prior to scheduling/starting work. - (Credit card authorization may be substituted for jobs under $2,000) -

    -
    - -
    -
    Notice to Owner
    -
    - {NOTICE_TO_OWNERS.replace('NOTICE TO OWNER', '').trim()} -
    -
    -
    - - {/* Print Page Counter */} - + {/* Print Page Counter */} + +
    ); }; diff --git a/src/components/estimate-builder/ExecutiveSummary.tsx b/src/components/estimate-builder/ExecutiveSummary.tsx index d7ca9a3..4ce5cfa 100644 --- a/src/components/estimate-builder/ExecutiveSummary.tsx +++ b/src/components/estimate-builder/ExecutiveSummary.tsx @@ -11,6 +11,7 @@ interface ExecutiveSummaryProps { grandTotal: number; }; onFinalizeBid: () => void; + onExportManagerInfo: () => void; } export const ExecutiveSummary: Component = (props) => { @@ -68,12 +69,20 @@ export const ExecutiveSummary: Component = (props) => { {formatNumber(props.totals.grandTotal)}
    - +
    + + +
    diff --git a/src/index.css b/src/index.css index 3a8114b..6f119c8 100644 --- a/src/index.css +++ b/src/index.css @@ -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; @@ -130,4 +128,4 @@ .shadow-premium-hover:hover { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.07), 0 8px 10px -6px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.04); -} \ No newline at end of file +}