Compare commits
3 Commits
c090c18734
...
989baa9be7
| Author | SHA1 | Date | |
|---|---|---|---|
| 989baa9be7 | |||
| 4b2585a59f | |||
| e637e0903d |
@@ -23,7 +23,6 @@ const ItemExtractor: Component<ItemExtractorProps> = () => {
|
|||||||
const ignoredLines = appStore.ignoredLines;
|
const ignoredLines = appStore.ignoredLines;
|
||||||
const setIgnoredLines = appStore.setIgnoredLines;
|
const setIgnoredLines = appStore.setIgnoredLines;
|
||||||
const estimateItems = appStore.estimateItems;
|
const estimateItems = appStore.estimateItems;
|
||||||
const setEstimateName = appStore.setEstimateName;
|
|
||||||
|
|
||||||
const [showToast] = createSignal(false);
|
const [showToast] = createSignal(false);
|
||||||
const [toastMessage] = createSignal('');
|
const [toastMessage] = createSignal('');
|
||||||
@@ -80,11 +79,6 @@ const ItemExtractor: Component<ItemExtractorProps> = () => {
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<EstimateBuilder
|
<EstimateBuilder
|
||||||
initialItems={items()}
|
initialItems={items()}
|
||||||
onReset={() => {
|
|
||||||
setItems([]);
|
|
||||||
setIgnoredLines([]);
|
|
||||||
setEstimateName('');
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ const CloudLoadModal: Component<CloudLoadModalProps> = (props) => {
|
|||||||
|
|
||||||
appStore.setEstimateName(record.name);
|
appStore.setEstimateName(record.name);
|
||||||
appStore.setEstimateId(record.id);
|
appStore.setEstimateId(record.id);
|
||||||
|
appStore.setHistory(record.history || []);
|
||||||
|
appStore.setLatestSnapshot(record.items);
|
||||||
|
appStore.setIsLatestVersion(true);
|
||||||
|
|
||||||
// Also clear out the extracted CSV items so we don't accidentally switch back to the upload screen or cause conflicts
|
// Also clear out the extracted CSV items so we don't accidentally switch back to the upload screen or cause conflicts
|
||||||
appStore.setItems([]);
|
appStore.setItems([]);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import CalculatorPopover from './CalculatorPopover';
|
|||||||
import CloudLoadModal from './CloudLoadModal';
|
import CloudLoadModal from './CloudLoadModal';
|
||||||
import ExportTableModal from './ExportTableModal';
|
import ExportTableModal from './ExportTableModal';
|
||||||
import ApplyTemplateModal from './ApplyTemplateModal';
|
import ApplyTemplateModal from './ApplyTemplateModal';
|
||||||
|
import HistoryModal from './estimate-builder/HistoryModal';
|
||||||
|
import { NewEstimateModal } from './estimate-builder/NewEstimateModal';
|
||||||
|
|
||||||
// Import extracted components & hooks
|
// Import extracted components & hooks
|
||||||
import { useEstimateCalculations } from './estimate-builder/useEstimateCalculations';
|
import { useEstimateCalculations } from './estimate-builder/useEstimateCalculations';
|
||||||
@@ -32,7 +34,6 @@ import { ScopesList } from './estimate-builder/ScopesList';
|
|||||||
|
|
||||||
interface EstimateBuilderProps {
|
interface EstimateBuilderProps {
|
||||||
initialItems: ExtractedItem[];
|
initialItems: ExtractedItem[];
|
||||||
onReset: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||||
@@ -46,6 +47,8 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
|||||||
// Cloud Modals
|
// Cloud Modals
|
||||||
const [showSaveModal, setShowSaveModal] = createSignal(false);
|
const [showSaveModal, setShowSaveModal] = createSignal(false);
|
||||||
const [showLoadModal, setShowLoadModal] = createSignal(false);
|
const [showLoadModal, setShowLoadModal] = createSignal(false);
|
||||||
|
const [showHistoryModal, setShowHistoryModal] = createSignal(false);
|
||||||
|
const [showNewEstimateModal, setShowNewEstimateModal] = createSignal(false);
|
||||||
const [showExportTableModal, setShowExportTableModal] = createSignal(false);
|
const [showExportTableModal, setShowExportTableModal] = createSignal(false);
|
||||||
const [applyTemplateScopeId, setApplyTemplateScopeId] = createSignal<string | null>(null);
|
const [applyTemplateScopeId, setApplyTemplateScopeId] = createSignal<string | null>(null);
|
||||||
|
|
||||||
@@ -58,7 +61,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { hoveredScopeId, setHoveredScopeId, handleDragStart, handleDrop } = useDragAndDrop(selectedIds, setSelectedIds, updateItem);
|
const { hoveredScopeId, setHoveredScopeId, handleDragStart, handleDrop } = useDragAndDrop(selectedIds, setSelectedIds, updateItem);
|
||||||
const { exportEstimate, importEstimate, saveToCloud, isCloudLoading } = useEstimateSync(setShowSaveModal);
|
const { exportEstimate, importEstimate, saveToCloud, saveChangesToCloud, saveAsNewestToCloud, checkoutVersion, isCloudLoading } = useEstimateSync(setShowSaveModal);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!generalEstimateNotes()) setGeneralEstimateNotes(GENERAL_NOTES_DEFAULT);
|
if (!generalEstimateNotes()) setGeneralEstimateNotes(GENERAL_NOTES_DEFAULT);
|
||||||
@@ -103,6 +106,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
|||||||
setScopes(prev => [...prev, newScope]);
|
setScopes(prev => [...prev, newScope]);
|
||||||
addSubScopeWithName(scopeId, 'Materials');
|
addSubScopeWithName(scopeId, 'Materials');
|
||||||
addSubScopeWithName(scopeId, 'Labor');
|
addSubScopeWithName(scopeId, 'Labor');
|
||||||
|
return scopeId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSubScopeWithName = (scopeId: string, name: string) => {
|
const addSubScopeWithName = (scopeId: string, name: string) => {
|
||||||
@@ -112,6 +116,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
|||||||
scopeId
|
scopeId
|
||||||
};
|
};
|
||||||
setSubScopes(prev => [...prev, newSubScope]);
|
setSubScopes(prev => [...prev, newSubScope]);
|
||||||
|
return newSubScope.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSubScope = (scopeId: string) => {
|
const addSubScope = (scopeId: string) => {
|
||||||
@@ -193,12 +198,14 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
|||||||
isSidebarCollapsed={isSidebarCollapsed()}
|
isSidebarCollapsed={isSidebarCollapsed()}
|
||||||
onToggleSidebar={() => setIsSidebarCollapsed(!isSidebarCollapsed())}
|
onToggleSidebar={() => setIsSidebarCollapsed(!isSidebarCollapsed())}
|
||||||
onAddScope={addScope}
|
onAddScope={addScope}
|
||||||
onReset={props.onReset}
|
onShowNewEstimateModal={() => setShowNewEstimateModal(true)}
|
||||||
onImportEstimate={importEstimate}
|
onImportEstimate={importEstimate}
|
||||||
onShowExportTableModal={() => setShowExportTableModal(true)}
|
onShowExportTableModal={() => setShowExportTableModal(true)}
|
||||||
onExportEstimate={exportEstimate}
|
onExportEstimate={exportEstimate}
|
||||||
onShowLoadModal={() => setShowLoadModal(true)}
|
onShowLoadModal={() => setShowLoadModal(true)}
|
||||||
onShowSaveModal={() => setShowSaveModal(true)}
|
onShowSaveModal={() => setShowSaveModal(true)}
|
||||||
|
onShowHistoryModal={() => setShowHistoryModal(true)}
|
||||||
|
onSaveChanges={() => appStore.isLatestVersion() ? saveChangesToCloud() : saveAsNewestToCloud()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-1 relative items-start">
|
<div class="flex flex-1 relative items-start">
|
||||||
@@ -353,6 +360,30 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
|||||||
<CloudLoadModal show={showLoadModal()} onClose={() => setShowLoadModal(false)} />
|
<CloudLoadModal show={showLoadModal()} onClose={() => setShowLoadModal(false)} />
|
||||||
<ExportTableModal show={showExportTableModal()} onClose={() => setShowExportTableModal(false)} />
|
<ExportTableModal show={showExportTableModal()} onClose={() => setShowExportTableModal(false)} />
|
||||||
<ApplyTemplateModal show={!!applyTemplateScopeId()} scopeId={applyTemplateScopeId()} onClose={() => setApplyTemplateScopeId(null)} />
|
<ApplyTemplateModal show={!!applyTemplateScopeId()} scopeId={applyTemplateScopeId()} onClose={() => setApplyTemplateScopeId(null)} />
|
||||||
|
<HistoryModal show={showHistoryModal()} onClose={() => setShowHistoryModal(false)} onCheckout={checkoutVersion} />
|
||||||
|
|
||||||
|
<NewEstimateModal
|
||||||
|
show={showNewEstimateModal()}
|
||||||
|
onClose={() => 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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
|
import { createSignal, onCleanup, Show } from 'solid-js';
|
||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import { PanelLeftOpen, PanelLeftClose, Calculator, Plus, FileUp, Upload, Table, Download } from 'lucide-solid';
|
import { PanelLeftOpen, PanelLeftClose, Calculator, Plus, Upload, Table, Download, History, Save, ChevronDown, MoreHorizontal } from 'lucide-solid';
|
||||||
import { appStore } from '../../store/appStore';
|
import { appStore } from '../../store/appStore';
|
||||||
|
|
||||||
interface BuilderHeaderProps {
|
interface BuilderHeaderProps {
|
||||||
isSidebarCollapsed: boolean;
|
isSidebarCollapsed: boolean;
|
||||||
onToggleSidebar: () => void;
|
onToggleSidebar: () => void;
|
||||||
onAddScope: () => void;
|
onAddScope: () => void;
|
||||||
onReset: () => void;
|
onShowNewEstimateModal: () => void;
|
||||||
onImportEstimate: (e: Event) => void;
|
onImportEstimate: (e: Event) => void;
|
||||||
onShowExportTableModal: () => void;
|
onShowExportTableModal: () => void;
|
||||||
onExportEstimate: () => void;
|
onExportEstimate: () => void;
|
||||||
onShowLoadModal: () => void;
|
onShowLoadModal: () => void;
|
||||||
onShowSaveModal: () => void;
|
onShowSaveModal: () => void;
|
||||||
|
onShowHistoryModal: () => void;
|
||||||
|
onSaveChanges: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BuilderHeader: Component<BuilderHeaderProps> = (props) => {
|
export const BuilderHeader: Component<BuilderHeaderProps> = (props) => {
|
||||||
const { scopes, estimateItems: items } = appStore;
|
const { scopes, estimateItems: items } = appStore;
|
||||||
|
const [showActions, setShowActions] = createSignal(false);
|
||||||
|
let dropdownRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (dropdownRef && !dropdownRef.contains(e.target as Node)) {
|
||||||
|
setShowActions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('click', handleClickOutside);
|
||||||
|
onCleanup(() => window.removeEventListener('click', handleClickOutside));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="min-h-[4rem] py-3 px-6 border-b border-border bg-background/50 flex flex-wrap gap-y-4 items-center justify-between sticky top-0 z-50 backdrop-blur-md">
|
<div class="min-h-[4rem] py-3 px-6 border-b border-border bg-background/50 flex flex-wrap gap-y-4 items-center justify-between sticky top-0 z-50 backdrop-blur-md">
|
||||||
@@ -43,46 +57,79 @@ export const BuilderHeader: Component<BuilderHeaderProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={props.onAddScope}
|
onClick={props.onShowNewEstimateModal}
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-foreground text-background rounded-xl text-sm font-bold hover:bg-foreground/90 transition-all shadow-lg shadow-foreground/10 mr-2"
|
class="flex items-center gap-2 px-3 py-2 bg-muted/50 text-blue-600 rounded-xl text-xs font-black hover:bg-blue-100/50 transition-all border border-blue-200"
|
||||||
>
|
>
|
||||||
<Plus class="w-4 h-4" /> New Scope
|
<Plus class="w-4 h-4" /> New Estimate
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={props.onReset}
|
|
||||||
class="flex items-center gap-2 px-3 py-2 bg-muted/50 text-primary rounded-xl text-xs font-black hover:bg-primary/10 transition-all border border-border"
|
|
||||||
>
|
|
||||||
<FileUp class="w-4 h-4" /> New CSV
|
|
||||||
</button>
|
|
||||||
<div class="h-6 w-px bg-border mx-1"></div>
|
|
||||||
<label class="flex items-center gap-2 px-3 py-2 bg-muted text-muted-foreground rounded-xl text-xs font-bold hover:bg-accent hover:text-accent-foreground transition-all border border-border cursor-pointer">
|
|
||||||
<Upload class="w-4 h-4" /> Import JSON
|
|
||||||
<input type="file" accept=".json" class="hidden" onChange={props.onImportEstimate} />
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
onClick={props.onShowExportTableModal}
|
|
||||||
class="flex items-center gap-2 px-3 py-2 bg-muted text-muted-foreground rounded-xl text-xs font-bold hover:bg-accent hover:text-accent-foreground transition-all border border-border mr-2"
|
|
||||||
>
|
|
||||||
<Table class="w-4 h-4" /> Export Table
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={props.onExportEstimate}
|
|
||||||
class="flex items-center gap-2 px-3 py-2 bg-muted text-muted-foreground rounded-xl text-xs font-bold hover:bg-accent hover:text-accent-foreground transition-all border border-border mr-2"
|
|
||||||
>
|
|
||||||
<Download class="w-4 h-4" /> Export JSON
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="relative" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setShowActions(!showActions()); }}
|
||||||
|
class="flex items-center gap-2 px-3 py-2 bg-muted text-muted-foreground rounded-xl text-xs font-bold hover:bg-accent hover:text-accent-foreground transition-all border border-border"
|
||||||
|
>
|
||||||
|
<MoreHorizontal class="w-4 h-4" /> Actions <ChevronDown class={`w-3 h-3 transition-transform ${showActions() ? 'rotate-180' : ''}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={showActions()}>
|
||||||
|
<div class="absolute right-0 mt-2 w-56 bg-card border border-border rounded-2xl shadow-premium z-[60] overflow-hidden animate-in fade-in zoom-in-95 duration-150 origin-top-right">
|
||||||
|
<div class="p-2 space-y-1">
|
||||||
|
<p class="text-[9px] font-black uppercase tracking-widest text-muted-foreground/60 px-3 py-2">Data Exchange</p>
|
||||||
|
<label class="flex items-center gap-3 px-3 py-2.5 text-xs font-bold text-foreground hover:bg-muted rounded-xl cursor-pointer transition-colors w-full">
|
||||||
|
<Upload class="w-4 h-4 text-blue-500" /> Import JSON
|
||||||
|
<input type="file" accept=".json" class="hidden" onChange={(e) => { props.onImportEstimate(e); setShowActions(false); }} />
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={() => { props.onExportEstimate(); setShowActions(false); }}
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 text-xs font-bold text-foreground hover:bg-muted rounded-xl transition-colors w-full text-left"
|
||||||
|
>
|
||||||
|
<Download class="w-4 h-4 text-blue-500" /> Export JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-border/50 p-2 space-y-1 bg-muted/10">
|
||||||
|
<p class="text-[9px] font-black uppercase tracking-widest text-muted-foreground/60 px-3 py-2">Summaries</p>
|
||||||
|
<button
|
||||||
|
onClick={() => { props.onShowExportTableModal(); setShowActions(false); }}
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 text-xs font-bold text-foreground hover:bg-muted rounded-xl transition-colors w-full text-left"
|
||||||
|
>
|
||||||
|
<Table class="w-4 h-4 text-purple-500" /> Export Table
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="h-6 w-px bg-border mx-1"></div>
|
<div class="h-6 w-px bg-border mx-1"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={props.onShowLoadModal}
|
onClick={props.onShowLoadModal}
|
||||||
class="flex items-center gap-2 px-3 py-2 bg-blue-50 text-blue-600 rounded-xl text-xs font-bold hover:bg-blue-100 transition-all border border-blue-200"
|
class="flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-600 rounded-xl text-xs font-bold hover:bg-blue-100 transition-all border border-blue-200"
|
||||||
>
|
>
|
||||||
<Download class="w-4 h-4" /> Load
|
<Download class="w-4 h-4" /> Load
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<Show when={appStore.estimateId()}>
|
||||||
|
<button
|
||||||
|
onClick={props.onShowHistoryModal}
|
||||||
|
class="flex items-center gap-2 px-3 py-2 bg-purple-50 text-purple-600 rounded-xl text-xs font-bold hover:bg-purple-100 transition-all border border-purple-200"
|
||||||
|
>
|
||||||
|
<History class="w-4 h-4" /> History
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={props.onSaveChanges}
|
||||||
|
class="flex items-center gap-2 px-3 py-2 bg-green-50 text-green-600 rounded-xl text-xs font-bold hover:bg-green-100 transition-all border border-green-200"
|
||||||
|
>
|
||||||
|
<Save class="w-4 h-4" />
|
||||||
|
{appStore.isLatestVersion() ? 'Save Changes' : 'Save as Newest'}
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={props.onShowSaveModal}
|
onClick={props.onShowSaveModal}
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-xl text-sm font-bold hover:bg-primary/90 transition-all shadow-lg shadow-primary/10"
|
class="flex items-center gap-4 py-2 px-4 bg-primary text-primary-foreground rounded-xl text-sm font-bold hover:bg-primary/90 transition-all shadow-lg shadow-primary/10"
|
||||||
>
|
>
|
||||||
<Upload class="w-4 h-4" /> Save
|
<Upload class="w-4 h-4" /> Save New
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { Show, For } from 'solid-js';
|
||||||
|
import { Portal } from 'solid-js/web';
|
||||||
|
import { X, History, CheckCircle2, Clock } from 'lucide-solid';
|
||||||
|
import { appStore } from '../../store/appStore';
|
||||||
|
|
||||||
|
interface HistoryModalProps {
|
||||||
|
show: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onCheckout: (record: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryModal = (props: HistoryModalProps) => {
|
||||||
|
// History is an array of snapshots from the 'history' field of the estimate record
|
||||||
|
const historyEntries = () => [...appStore.history()].reverse();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={props.show}>
|
||||||
|
<Portal>
|
||||||
|
<div class="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||||
|
<div class="bg-card w-full max-w-2xl max-h-[85vh] flex flex-col rounded-3xl shadow-premium border border-border overflow-hidden animate-in zoom-in-95 duration-200">
|
||||||
|
<div class="flex items-center justify-between p-6 border-b border-border">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-purple-100 text-purple-600 rounded-xl">
|
||||||
|
<History class="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-black text-foreground">Estimate History</h3>
|
||||||
|
<p class="text-[10px] uppercase font-bold text-muted-foreground tracking-wider mt-0.5">Version Control & Timeline</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={props.onClose} class="p-2 hover:bg-muted rounded-xl transition-colors">
|
||||||
|
<X class="w-5 h-5 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 flex flex-col p-6 gap-4 overflow-hidden">
|
||||||
|
<div class="space-y-3 overflow-y-auto pr-2">
|
||||||
|
{/* Current Live Version */}
|
||||||
|
<div
|
||||||
|
class={`group flex flex-col p-4 rounded-2xl border transition-all ${
|
||||||
|
appStore.isLatestVersion()
|
||||||
|
? 'border-primary bg-primary/5 shadow-premium'
|
||||||
|
: 'border-border/60 bg-muted/10 hover:border-primary/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class={`p-2 rounded-lg ${appStore.isLatestVersion() ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}>
|
||||||
|
<CheckCircle2 class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h4 class="font-bold text-foreground">Latest Version</h4>
|
||||||
|
<span class="px-1.5 py-0.5 bg-green-100 text-green-700 text-[8px] font-black uppercase rounded tracking-widest">Live</span>
|
||||||
|
<Show when={appStore.isLatestVersion()}>
|
||||||
|
<span class="px-1.5 py-0.5 bg-primary/20 text-primary text-[8px] font-black uppercase rounded tracking-widest border border-primary/20">Active</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<p class="text-[10px] text-muted-foreground font-medium uppercase tracking-wider mt-1">
|
||||||
|
The current tip of the project
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={!appStore.isLatestVersion()}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
props.onCheckout(appStore.latestSnapshot());
|
||||||
|
props.onClose();
|
||||||
|
}}
|
||||||
|
class="flex items-center gap-2 px-4 py-2 bg-background border border-border rounded-xl text-xs font-bold shadow-sm hover:text-primary hover:border-primary/50 transition-all opacity-0 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
Return to Latest
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Change Summary Breakdown for Latest */}
|
||||||
|
<Show when={appStore.latestSnapshot()?.changeSummary && Object.keys(appStore.latestSnapshot().changeSummary).length > 0}>
|
||||||
|
<div class="mt-4 pt-4 border-t border-border/40">
|
||||||
|
<p class="text-[9px] font-black uppercase tracking-widest text-muted-foreground/60 mb-2">Changes in this Version:</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<For each={Object.values(appStore.latestSnapshot().changeSummary)}>
|
||||||
|
{(sum: any) => (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex items-center justify-between text-[11px]">
|
||||||
|
<span class="font-bold text-foreground/80">{sum.scopeName}</span>
|
||||||
|
<div class="flex gap-2 font-medium">
|
||||||
|
<Show when={sum.itemsAdded > 0}>
|
||||||
|
<span class="text-green-600">+{sum.itemsAdded} Add</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={sum.itemsRemoved > 0}>
|
||||||
|
<span class="text-red-600">-{sum.itemsRemoved} Rem</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={sum.itemsUpdated > 0}>
|
||||||
|
<span class="text-blue-600">{sum.itemsUpdated} Upd</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={sum.fieldChanges > 0}>
|
||||||
|
<p class="text-[9px] text-muted-foreground ml-1">
|
||||||
|
↳ {sum.fieldChanges} field adjustments
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Historical Snapshots */}
|
||||||
|
<For each={historyEntries()}>
|
||||||
|
{(snapshot) => (
|
||||||
|
<div class="group flex flex-col p-4 rounded-2xl border border-border/60 bg-muted/5 hover:border-primary/30 transition-all">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="p-2 bg-muted text-muted-foreground rounded-lg">
|
||||||
|
<Clock class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h4 class="font-bold text-foreground">
|
||||||
|
{snapshot.jobInfo?.name || snapshot.name || 'Version'}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<p class="text-[10px] text-muted-foreground font-medium uppercase tracking-wider mt-1">
|
||||||
|
{new Date(snapshot.timestamp || Date.now()).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
props.onCheckout(snapshot);
|
||||||
|
props.onClose();
|
||||||
|
}}
|
||||||
|
class="flex items-center gap-2 px-4 py-2 bg-background border border-border rounded-xl text-xs font-bold shadow-sm hover:text-primary hover:border-primary/50 transition-all opacity-0 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
Checkout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Change Summary Breakdown */}
|
||||||
|
<Show when={snapshot.changeSummary && Object.keys(snapshot.changeSummary).length > 0}>
|
||||||
|
<div class="mt-4 pt-4 border-t border-border/40">
|
||||||
|
<p class="text-[9px] font-black uppercase tracking-widest text-muted-foreground/60 mb-2">Scope Changes in this Version:</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<For each={Object.values(snapshot.changeSummary)}>
|
||||||
|
{(sum: any) => (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex items-center justify-between text-[11px]">
|
||||||
|
<span class="font-bold text-foreground/80">{sum.scopeName}</span>
|
||||||
|
<div class="flex gap-2 font-medium">
|
||||||
|
<Show when={sum.itemsAdded > 0}>
|
||||||
|
<span class="text-green-600">+{sum.itemsAdded} Add</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={sum.itemsRemoved > 0}>
|
||||||
|
<span class="text-red-600">-{sum.itemsRemoved} Rem</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={sum.itemsUpdated > 0}>
|
||||||
|
<span class="text-blue-600">{sum.itemsUpdated} Upd</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={sum.fieldChanges > 0}>
|
||||||
|
<p class="text-[9px] text-muted-foreground ml-1">
|
||||||
|
↳ {sum.fieldChanges} field adjustments
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<Show when={historyEntries().length === 0}>
|
||||||
|
<div class="py-12 text-center text-muted-foreground/60 border-2 border-dashed border-border/60 rounded-xl bg-muted/5 font-medium text-sm">
|
||||||
|
No previous versions found.
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryModal;
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Portal } from 'solid-js/web';
|
||||||
|
import { Show } from 'solid-js';
|
||||||
|
import { X, FileSpreadsheet, PlusCircle, AlertCircle } from 'lucide-solid';
|
||||||
|
|
||||||
|
interface NewEstimateModalProps {
|
||||||
|
show: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onResetCSV: () => void;
|
||||||
|
onManualEstimate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NewEstimateModal = (props: NewEstimateModalProps) => {
|
||||||
|
return (
|
||||||
|
<Show when={props.show}>
|
||||||
|
<Portal>
|
||||||
|
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-background/80 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||||
|
<div class="bg-card w-full max-w-lg rounded-3xl shadow-premium border border-border overflow-hidden animate-in zoom-in-95 duration-200">
|
||||||
|
<div class="p-6 border-b border-border/50 flex items-center justify-between bg-muted/30">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-xl text-primary">
|
||||||
|
<PlusCircle class="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-black text-foreground">New Estimate</h3>
|
||||||
|
<p class="text-[10px] text-muted-foreground font-black uppercase tracking-widest mt-0.5">Start a fresh project</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={props.onClose}
|
||||||
|
class="p-2 hover:bg-background rounded-xl transition-all group"
|
||||||
|
>
|
||||||
|
<X class="w-5 h-5 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-8 space-y-6">
|
||||||
|
<div class="p-4 bg-amber-50 border border-amber-200 rounded-2xl flex gap-3">
|
||||||
|
<AlertCircle class="w-5 h-5 text-amber-600 shrink-0" />
|
||||||
|
<div class="text-xs text-amber-900 font-medium">
|
||||||
|
Starting a new estimate will clear all unsaved progress in the current project. Make sure you've saved any changes to the cloud first.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
onClick={props.onResetCSV}
|
||||||
|
class="flex flex-col items-center text-center p-6 bg-muted/30 border border-border/60 rounded-3xl hover:border-primary/50 hover:bg-primary/5 transition-all group gap-4"
|
||||||
|
>
|
||||||
|
<div class="p-4 bg-background rounded-2xl shadow-sm group-hover:shadow-md transition-all group-hover:scale-110">
|
||||||
|
<FileSpreadsheet class="w-8 h-8 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-black text-foreground mb-1 text-sm">New from CSV</h4>
|
||||||
|
<p class="text-[10px] text-muted-foreground font-medium leading-relaxed">
|
||||||
|
Clear current state and upload a new estimate CSV file to begin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={props.onManualEstimate}
|
||||||
|
class="flex flex-col items-center text-center p-6 bg-muted/30 border border-border/60 rounded-3xl hover:border-primary/50 hover:bg-primary/5 transition-all group gap-4"
|
||||||
|
>
|
||||||
|
<div class="p-4 bg-background rounded-2xl shadow-sm group-hover:shadow-md transition-all group-hover:scale-110">
|
||||||
|
<PlusCircle class="w-8 h-8 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-black text-foreground mb-1 text-sm">Manual Estimate</h4>
|
||||||
|
<p class="text-[10px] text-muted-foreground font-medium leading-relaxed">
|
||||||
|
Start with a blank estimate and a single default item.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 bg-muted/30 border-t border-border/50 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={props.onClose}
|
||||||
|
class="px-6 py-2.5 rounded-xl font-bold text-sm hover:bg-background border border-transparent hover:border-border transition-all"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,7 +7,19 @@ export function useEstimateSync(
|
|||||||
) {
|
) {
|
||||||
const [isCloudLoading, setIsCloudLoading] = createSignal(false);
|
const [isCloudLoading, setIsCloudLoading] = createSignal(false);
|
||||||
|
|
||||||
const { scopes, setScopes, subScopes, setSubScopes, estimateItems: items, setEstimateItems: setItems, clientInfo, setClientInfo, jobInfo, setJobInfo, generalEstimateNotes, setGeneralEstimateNotes, generalPreNotes, setGeneralPreNotes } = appStore;
|
const {
|
||||||
|
scopes, setScopes,
|
||||||
|
subScopes, setSubScopes,
|
||||||
|
estimateItems: items, setEstimateItems: setItems,
|
||||||
|
clientInfo, setClientInfo,
|
||||||
|
jobInfo, setJobInfo,
|
||||||
|
generalEstimateNotes, setGeneralEstimateNotes,
|
||||||
|
generalPreNotes, setGeneralPreNotes,
|
||||||
|
setHistory,
|
||||||
|
setIsLatestVersion,
|
||||||
|
setLatestSnapshot,
|
||||||
|
setEstimateId, setEstimateName
|
||||||
|
} = appStore;
|
||||||
|
|
||||||
const exportEstimate = () => {
|
const exportEstimate = () => {
|
||||||
const data = {
|
const data = {
|
||||||
@@ -18,7 +30,7 @@ export function useEstimateSync(
|
|||||||
jobInfo: { ...jobInfo },
|
jobInfo: { ...jobInfo },
|
||||||
generalEstimateNotes: generalEstimateNotes(),
|
generalEstimateNotes: generalEstimateNotes(),
|
||||||
generalPreNotes: generalPreNotes(),
|
generalPreNotes: generalPreNotes(),
|
||||||
version: '1.0'
|
version: '1.2'
|
||||||
};
|
};
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -60,28 +72,156 @@ export function useEstimateSync(
|
|||||||
clientInfo: { ...clientInfo },
|
clientInfo: { ...clientInfo },
|
||||||
jobInfo: { ...jobInfo },
|
jobInfo: { ...jobInfo },
|
||||||
generalEstimateNotes: generalEstimateNotes(),
|
generalEstimateNotes: generalEstimateNotes(),
|
||||||
generalPreNotes: generalPreNotes()
|
generalPreNotes: generalPreNotes(),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const calculateChangeSummary = (prevData: any, newData: any) => {
|
||||||
|
const summary: Record<string, { scopeName: string, itemsAdded: number, itemsRemoved: number, itemsUpdated: number, fieldChanges: number }> = {};
|
||||||
|
|
||||||
|
const prevItems = prevData.items || [];
|
||||||
|
const newItems = newData.items || [];
|
||||||
|
|
||||||
|
const allScopeIds = new Set([
|
||||||
|
...prevItems.map((i: any) => i.scopeId),
|
||||||
|
...newItems.map((i: any) => i.scopeId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const scopesMap = new Map((newData.scopes || []).map((s: any) => [s.id, s.name]));
|
||||||
|
|
||||||
|
allScopeIds.forEach(rawId => {
|
||||||
|
const scopeId = String(rawId);
|
||||||
|
if (!scopeId || scopeId === 'undefined') return;
|
||||||
|
const scopeName = scopesMap.get(scopeId) || "Unknown Scope";
|
||||||
|
|
||||||
|
const scopePrevItems = prevItems.filter((i: any) => i.scopeId === scopeId);
|
||||||
|
const scopeNewItems = newItems.filter((i: any) => i.scopeId === scopeId);
|
||||||
|
|
||||||
|
let added = 0;
|
||||||
|
let removed = 0;
|
||||||
|
let updated = 0;
|
||||||
|
let fields = 0;
|
||||||
|
|
||||||
|
const scopePrevMap = new Map(scopePrevItems.map((i: any) => [i.id, i]));
|
||||||
|
const scopeNewMap = new Map(scopeNewItems.map((i: any) => [i.id, i]));
|
||||||
|
|
||||||
|
scopeNewItems.forEach((item: any) => {
|
||||||
|
const prev = scopePrevMap.get(item.id);
|
||||||
|
if (!prev) {
|
||||||
|
added++;
|
||||||
|
} else {
|
||||||
|
let changed = false;
|
||||||
|
['description', 'quantity', 'unit', 'price', 'type'].forEach(k => {
|
||||||
|
const v1 = (item as any)[k];
|
||||||
|
const v2 = (prev as any)[k];
|
||||||
|
if (String(v1) !== String(v2)) {
|
||||||
|
fields++;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (changed) updated++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scopePrevItems.forEach((item: any) => {
|
||||||
|
if (!scopeNewMap.has(item.id)) {
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (added > 0 || removed > 0 || updated > 0 || fields > 0) {
|
||||||
|
(summary as any)[scopeId] = { scopeName, itemsAdded: added, itemsRemoved: removed, itemsUpdated: updated, fieldChanges: fields };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
};
|
||||||
|
|
||||||
const saveToCloud = async (name: string) => {
|
const saveToCloud = async (name: string) => {
|
||||||
setIsCloudLoading(true);
|
setIsCloudLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = getFullEstimateData();
|
const data = getFullEstimateData();
|
||||||
await pb.collection(COLLECTIONS.ESTIMATES).create({
|
const record = await pb.collection(COLLECTIONS.ESTIMATES).create({
|
||||||
name,
|
name,
|
||||||
items: data
|
items: { ...data, changeSummary: null },
|
||||||
|
history: []
|
||||||
});
|
});
|
||||||
|
|
||||||
setShowSaveModal(false);
|
setShowSaveModal(false);
|
||||||
appStore.setEstimateName(name);
|
setEstimateName(name);
|
||||||
alert('Estimate saved to PocketBase successfully!');
|
setEstimateId(record.id);
|
||||||
|
setHistory([]);
|
||||||
|
setLatestSnapshot({ ...data, changeSummary: null });
|
||||||
|
setIsLatestVersion(true);
|
||||||
|
alert('New estimate saved successfully!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('Failed to save. Ensure EstiMaker_Estimates collection permissions are public.');
|
alert('Failed to save. Ensure collection permissions are public.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsCloudLoading(false);
|
setIsCloudLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { exportEstimate, importEstimate, saveToCloud, isCloudLoading };
|
const saveChangesToCloud = async () => {
|
||||||
|
const id = appStore.estimateId();
|
||||||
|
if (!id) return;
|
||||||
|
setIsCloudLoading(true);
|
||||||
|
try {
|
||||||
|
const currentData = getFullEstimateData();
|
||||||
|
|
||||||
|
const record = await pb.collection(COLLECTIONS.ESTIMATES).getOne(id);
|
||||||
|
const updatedHistory = [...(record.history || [])];
|
||||||
|
|
||||||
|
const changeSummary = calculateChangeSummary(record.items, currentData);
|
||||||
|
|
||||||
|
const newTip = {
|
||||||
|
...currentData,
|
||||||
|
changeSummary
|
||||||
|
};
|
||||||
|
|
||||||
|
await pb.collection(COLLECTIONS.ESTIMATES).update(id, {
|
||||||
|
items: newTip,
|
||||||
|
history: [...updatedHistory, record.items]
|
||||||
|
});
|
||||||
|
|
||||||
|
setHistory([...updatedHistory, record.items]);
|
||||||
|
setLatestSnapshot(newTip);
|
||||||
|
setIsLatestVersion(true);
|
||||||
|
alert('Changes saved successfully!');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert('Failed to save changes.');
|
||||||
|
} finally {
|
||||||
|
setIsCloudLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAsNewestToCloud = async () => {
|
||||||
|
await saveChangesToCloud();
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkoutVersion = async (snapshot: any) => {
|
||||||
|
// snapshot is an entry from the history array
|
||||||
|
if (!snapshot) return;
|
||||||
|
|
||||||
|
if (snapshot.scopes) setScopes(snapshot.scopes);
|
||||||
|
if (snapshot.subScopes) setSubScopes(snapshot.subScopes);
|
||||||
|
if (snapshot.items) setItems(snapshot.items);
|
||||||
|
if (snapshot.clientInfo) setClientInfo(snapshot.clientInfo);
|
||||||
|
if (snapshot.jobInfo) setJobInfo(snapshot.jobInfo);
|
||||||
|
if (snapshot.generalEstimateNotes !== undefined) setGeneralEstimateNotes(snapshot.generalEstimateNotes);
|
||||||
|
if (snapshot.generalPreNotes !== undefined) setGeneralPreNotes(snapshot.generalPreNotes);
|
||||||
|
|
||||||
|
// This checkout handles both historical items and the 'latestSnapshot'
|
||||||
|
// If we are checking out the latest snapshot, then isLatestVersion is true
|
||||||
|
const isTip = JSON.stringify(snapshot) === JSON.stringify(appStore.latestSnapshot());
|
||||||
|
setIsLatestVersion(isTip);
|
||||||
|
|
||||||
|
// Clear CSV items to avoid confusion
|
||||||
|
appStore.setItems([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { exportEstimate, importEstimate, saveToCloud, saveChangesToCloud, saveAsNewestToCloud, checkoutVersion, isCloudLoading };
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-1
@@ -10,6 +10,9 @@ const [activeIgnoredLines, setActiveIgnoredLines] = createSignal<string[][]>([])
|
|||||||
// Estimate Metadata
|
// Estimate Metadata
|
||||||
const [activeEstimateId, setActiveEstimateId] = createSignal<string | null>(null);
|
const [activeEstimateId, setActiveEstimateId] = createSignal<string | null>(null);
|
||||||
const [activeEstimateName, setActiveEstimateName] = createSignal('');
|
const [activeEstimateName, setActiveEstimateName] = createSignal('');
|
||||||
|
const [activeHistory, setActiveHistory] = createSignal<any[]>([]);
|
||||||
|
const [activeLatestSnapshot, setActiveLatestSnapshot] = createSignal<any | null>(null);
|
||||||
|
const [isLatestVersion, setIsLatestVersion] = createSignal(true);
|
||||||
|
|
||||||
// Estimate Builder State
|
// Estimate Builder State
|
||||||
const [scopes, setScopes] = createStore<Scope[]>([]);
|
const [scopes, setScopes] = createStore<Scope[]>([]);
|
||||||
@@ -50,6 +53,12 @@ export const appStore = {
|
|||||||
setEstimateId: setActiveEstimateId,
|
setEstimateId: setActiveEstimateId,
|
||||||
estimateName: activeEstimateName,
|
estimateName: activeEstimateName,
|
||||||
setEstimateName: setActiveEstimateName,
|
setEstimateName: setActiveEstimateName,
|
||||||
|
history: activeHistory,
|
||||||
|
setHistory: setActiveHistory,
|
||||||
|
latestSnapshot: activeLatestSnapshot,
|
||||||
|
setLatestSnapshot: setActiveLatestSnapshot,
|
||||||
|
isLatestVersion: isLatestVersion,
|
||||||
|
setIsLatestVersion: setIsLatestVersion,
|
||||||
|
|
||||||
// Estimate Builder
|
// Estimate Builder
|
||||||
scopes, setScopes,
|
scopes, setScopes,
|
||||||
@@ -66,5 +75,22 @@ export const appStore = {
|
|||||||
templateName: activeTemplateName,
|
templateName: activeTemplateName,
|
||||||
setTemplateName: setActiveTemplateName,
|
setTemplateName: setActiveTemplateName,
|
||||||
templateItems: activeTemplateItems,
|
templateItems: activeTemplateItems,
|
||||||
setTemplateItems: setActiveTemplateItems
|
setTemplateItems: setActiveTemplateItems,
|
||||||
|
|
||||||
|
resetEstimate: () => {
|
||||||
|
setActiveItems([]);
|
||||||
|
setActiveIgnoredLines([]);
|
||||||
|
setActiveEstimateId(null);
|
||||||
|
setActiveEstimateName('');
|
||||||
|
setActiveHistory([]);
|
||||||
|
setActiveLatestSnapshot(null);
|
||||||
|
setIsLatestVersion(true);
|
||||||
|
setScopes([]);
|
||||||
|
setSubScopes([]);
|
||||||
|
setEstimateItems([]);
|
||||||
|
setClientInfo({ name: '', address: '', phone: '', fax: '' });
|
||||||
|
setJobInfo({ number: '', name: '', address: '', workOrder: '' });
|
||||||
|
setGeneralEstimateNotes('');
|
||||||
|
setGeneralPreNotes('');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user