import type { Component } from 'solid-js'; import { createSignal, createMemo, onMount, Show } from 'solid-js'; import { FileText, ChevronDown, CheckCircle2, X } from 'lucide-solid'; import { Portal } from 'solid-js/web'; import { appStore } from '../store/appStore'; import type { Scope, SubScope, EstimateItem } from '../types'; import type { ExtractedItem } from '../utils/csv-parser'; import { GENERAL_NOTES_DEFAULT, GENERAL_PRE_NOTES_DEFAULT } from '../company-info'; import PrintEstimate from './PrintEstimate'; import NoteEditor from './NoteEditor'; import CalculatorPopover from './CalculatorPopover'; import CloudLoadModal from './CloudLoadModal'; import ExportTableModal from './ExportTableModal'; import ApplyTemplateModal from './ApplyTemplateModal'; import HistoryModal from './estimate-builder/HistoryModal'; import { NewEstimateModal } from './estimate-builder/NewEstimateModal'; // Import extracted components & hooks import { useEstimateCalculations } from './estimate-builder/useEstimateCalculations'; import { useSelectionManager } from './estimate-builder/useSelectionManager'; import { useDragAndDrop } from './estimate-builder/useDragAndDrop'; import { useEstimateSync } from './estimate-builder/useEstimateSync'; import { BuilderHeader } from './estimate-builder/BuilderHeader'; import { BuilderSidebar } from './estimate-builder/BuilderSidebar'; import { ProjectDetailsForm } from './estimate-builder/ProjectDetailsForm'; import { UnassignedItemsSection } from './estimate-builder/UnassignedItemsSection'; import { ExecutiveSummary } from './estimate-builder/ExecutiveSummary'; import { SelectionToolbar } from './estimate-builder/SelectionToolbar'; import { ValidationStatusBar } from './estimate-builder/ValidationStatusBar'; import { ScopesList } from './estimate-builder/ScopesList'; interface EstimateBuilderProps { initialItems: ExtractedItem[]; } const EstimateBuilder: Component = (props) => { // 1. Initialize State const { scopes, setScopes, subScopes, setSubScopes, estimateItems: items, setEstimateItems: setItems, clientInfo, jobInfo, generalEstimateNotes, setGeneralEstimateNotes, generalPreNotes, setGeneralPreNotes } = appStore; const [isSidebarCollapsed, setIsSidebarCollapsed] = createSignal(false); const [isNotesExpanded, setIsNotesExpanded] = createSignal(true); const [isPreNotesExpanded, setIsPreNotesExpanded] = createSignal(true); // Cloud Modals const [showSaveModal, setShowSaveModal] = createSignal(false); const [showLoadModal, setShowLoadModal] = createSignal(false); const [showHistoryModal, setShowHistoryModal] = createSignal(false); const [showNewEstimateModal, setShowNewEstimateModal] = createSignal(false); const [showExportTableModal, setShowExportTableModal] = createSignal(false); const [applyTemplateScopeId, setApplyTemplateScopeId] = createSignal(null); // 2. Initialize custom hooks/primitives const { selectedIds, setSelectedIds, lastSelectedId, setLastSelectedId, toggleSelection, clearSelection, copyColumnSum } = useSelectionManager(); const { grandTotals, validationStats, selectionTotals } = useEstimateCalculations(selectedIds); const updateItem = (id: string, updates: Partial) => { setItems(item => item.id === id, updates); }; const { hoveredScopeId, setHoveredScopeId, handleDragStart, handleDrop } = useDragAndDrop(selectedIds, setSelectedIds, updateItem); const { exportEstimate, importEstimate, saveToCloud, saveChangesToCloud, saveAsNewestToCloud, checkoutVersion, isCloudLoading } = useEstimateSync(setShowSaveModal); onMount(() => { if (!generalEstimateNotes()) setGeneralEstimateNotes(GENERAL_NOTES_DEFAULT); if (!generalPreNotes()) setGeneralPreNotes(GENERAL_PRE_NOTES_DEFAULT); if (items.length === 0 && props.initialItems.length > 0) { const transformed: EstimateItem[] = props.initialItems.map(item => ({ id: item.id, description: item.description, quantity: parseFloat(item.quantity.replace(/,/g, '')) || 0, unitPrice: 0, markup: 0, markupType: 'percent', contingency: 0, contingencyType: 'percent' })); setItems(transformed); } }); const unassignedItems = createMemo(() => items.filter(i => !i.scopeId)); const anyDescriptionIsLong = createMemo(() => items.some(item => (item.description || '').length > 120)); const finalizeBid = () => { window.print(); }; const addScope = () => { const scopeId = crypto.randomUUID(); const newScope: Scope = { id: scopeId, name: 'New Scope', managementFee: 0, managementFeeType: 'percent', managementFeeQuantity: 1, managementFeeRate: 0, deliveryFee: 0, deliveryFeeType: 'percent', deliveryFeeQuantity: 1, deliveryFeeRate: 0, useManagementLogic: 'percent', useDeliveryLogic: 'percent', estimatorNotes: '', bidDescription: '**Material and labor to install #**\n**Includes:**\n- #\n**Does Not Include:**\n- #' }; setScopes(prev => [...prev, newScope]); addSubScopeWithName(scopeId, 'Materials'); addSubScopeWithName(scopeId, 'Labor'); return scopeId; }; const addSubScopeWithName = (scopeId: string, name: string) => { const newSubScope: SubScope = { id: crypto.randomUUID(), name, scopeId }; setSubScopes(prev => [...prev, newSubScope]); return newSubScope.id; }; const addSubScope = (scopeId: string) => { addSubScopeWithName(scopeId, 'New Sub-scope'); }; const deleteItem = (id: string) => { setItems(items.filter(i => i.id !== id)); setSelectedIds(prev => prev.filter(selId => selId !== id)); if (lastSelectedId() === id) setLastSelectedId(null); }; const duplicateItem = (id: string) => { const itemIndex = items.findIndex(i => i.id === id); if (itemIndex !== -1) { const itemToClone = items[itemIndex]; const newItem: EstimateItem = { ...itemToClone, id: crypto.randomUUID() }; const newItems = [...items]; newItems.splice(itemIndex + 1, 0, newItem); setItems(newItems); } }; const copySubScopeItems = (sourceSubScopeId: string, targetSubScopeId: string) => { const targetSubScope = subScopes.find(ss => ss.id === targetSubScopeId); if (!targetSubScope) return; const itemsToClone = items.filter(i => i.subScopeId === sourceSubScopeId); const newClonedItems = itemsToClone.map(item => ({ ...item, id: crypto.randomUUID(), subScopeId: targetSubScopeId, scopeId: targetSubScope.scopeId })); setItems(prev => [...prev, ...newClonedItems]); }; const updateScope = (id: string, updates: Partial) => { setScopes(s => s.id === id, updates); }; const updateSubScope = (id: string, updates: Partial) => { setSubScopes(ss => ss.id === id, updates); }; const deleteScope = (id: string) => { setScopes(scopes.filter(s => s.id !== id)); setItems(i => i.scopeId === id, { scopeId: undefined, subScopeId: undefined }); setSubScopes(subScopes.filter(ss => ss.scopeId !== id)); }; const deleteSubScope = (id: string) => { setSubScopes(subScopes.filter(ss => ss.id !== id)); setItems(i => i.subScopeId === id, { subScopeId: undefined }); }; const handleBulkUpdate = (scopeId: string, subScopeId: string | undefined, updates: any) => { setItems(item => item.scopeId === scopeId && (subScopeId === undefined || item.subScopeId === subScopeId), updates); }; const formatNumber = (num: number) => { return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; const [copyToast, setCopyToast] = createSignal(null); return (
setIsSidebarCollapsed(!isSidebarCollapsed())} onAddScope={addScope} onShowNewEstimateModal={() => setShowNewEstimateModal(true)} onImportEstimate={importEstimate} onShowExportTableModal={() => setShowExportTableModal(true)} onExportEstimate={exportEstimate} onShowLoadModal={() => setShowLoadModal(true)} onShowSaveModal={() => setShowSaveModal(true)} onShowHistoryModal={() => setShowHistoryModal(true)} onSaveChanges={() => appStore.isLatestVersion() ? saveChangesToCloud() : saveAsNewestToCloud()} />
setIsSidebarCollapsed(!isSidebarCollapsed())} unassignedItems={unassignedItems()} grandTotals={grandTotals()} hoveredScopeId={hoveredScopeId()} setHoveredScopeId={setHoveredScopeId} onDrop={handleDrop} onAddScope={addScope} />

General Pre-Notes

Project-wide preamble & terms

{ setSelectedIds([]); setLastSelectedId(null); }} onCopyColumnSum={(col) => copyColumnSum(col, selectionTotals()[col], setCopyToast)} />
{copyToast()}

Save to Cloud

setShowLoadModal(false)} /> setShowExportTableModal(false)} /> setApplyTemplateScopeId(null)} /> setShowHistoryModal(false)} onCheckout={checkoutVersion} /> setShowNewEstimateModal(false)} onResetCSV={() => { appStore.resetEstimate(); setShowNewEstimateModal(false); }} onManualEstimate={() => { appStore.resetEstimate(); setItems([{ id: crypto.randomUUID(), description: 'Item', quantity: 1, unitPrice: 0, markup: 0, markupType: 'percent', contingency: 0, contingencyType: 'percent' }]); setShowNewEstimateModal(false); }} />
); }; export default EstimateBuilder;