diff --git a/src/components/CloudLoadModal.tsx b/src/components/CloudLoadModal.tsx index 9e81318..b87f846 100644 --- a/src/components/CloudLoadModal.tsx +++ b/src/components/CloudLoadModal.tsx @@ -49,6 +49,9 @@ const CloudLoadModal: Component = (props) => { appStore.setEstimateName(record.name); 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 appStore.setItems([]); diff --git a/src/components/EstimateBuilder.tsx b/src/components/EstimateBuilder.tsx index 033a388..1f57754 100644 --- a/src/components/EstimateBuilder.tsx +++ b/src/components/EstimateBuilder.tsx @@ -14,6 +14,7 @@ import CalculatorPopover from './CalculatorPopover'; import CloudLoadModal from './CloudLoadModal'; import ExportTableModal from './ExportTableModal'; import ApplyTemplateModal from './ApplyTemplateModal'; +import HistoryModal from './estimate-builder/HistoryModal'; // Import extracted components & hooks import { useEstimateCalculations } from './estimate-builder/useEstimateCalculations'; @@ -46,6 +47,7 @@ const EstimateBuilder: Component = (props) => { // Cloud Modals const [showSaveModal, setShowSaveModal] = createSignal(false); const [showLoadModal, setShowLoadModal] = createSignal(false); + const [showHistoryModal, setShowHistoryModal] = createSignal(false); const [showExportTableModal, setShowExportTableModal] = createSignal(false); const [applyTemplateScopeId, setApplyTemplateScopeId] = createSignal(null); @@ -58,7 +60,7 @@ const EstimateBuilder: Component = (props) => { }; 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(() => { if (!generalEstimateNotes()) setGeneralEstimateNotes(GENERAL_NOTES_DEFAULT); @@ -199,6 +201,8 @@ const EstimateBuilder: Component = (props) => { onExportEstimate={exportEstimate} onShowLoadModal={() => setShowLoadModal(true)} onShowSaveModal={() => setShowSaveModal(true)} + onShowHistoryModal={() => setShowHistoryModal(true)} + onSaveChanges={saveChangesToCloud} />
@@ -353,6 +357,7 @@ const EstimateBuilder: Component = (props) => { setShowLoadModal(false)} /> setShowExportTableModal(false)} /> setApplyTemplateScopeId(null)} /> + setShowHistoryModal(false)} onCheckout={checkoutVersion} />
); }; diff --git a/src/components/estimate-builder/BuilderHeader.tsx b/src/components/estimate-builder/BuilderHeader.tsx index e646ef3..617f1bf 100644 --- a/src/components/estimate-builder/BuilderHeader.tsx +++ b/src/components/estimate-builder/BuilderHeader.tsx @@ -1,5 +1,6 @@ +import { Show } 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, FileUp, Upload, Table, Download, History, Save } from 'lucide-solid'; import { appStore } from '../../store/appStore'; interface BuilderHeaderProps { @@ -12,6 +13,8 @@ interface BuilderHeaderProps { onExportEstimate: () => void; onShowLoadModal: () => void; onShowSaveModal: () => void; + onShowHistoryModal: () => void; + onSaveChanges: () => void; } export const BuilderHeader: Component = (props) => { @@ -78,11 +81,29 @@ export const BuilderHeader: Component = (props) => { > Load + + + + + + + diff --git a/src/components/estimate-builder/HistoryModal.tsx b/src/components/estimate-builder/HistoryModal.tsx new file mode 100644 index 0000000..e160bb7 --- /dev/null +++ b/src/components/estimate-builder/HistoryModal.tsx @@ -0,0 +1,124 @@ +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 ( + + +
+
+
+
+
+ +
+
+

Estimate History

+

Version Control & Timeline

+
+
+ +
+ +
+
+ {/* Current Live Version */} +
+
+
+ +
+
+
+

Latest Version

+ Live + + Active + +
+

+ The current tip of the project +

+
+
+ + + + +
+ + {/* Historical Snapshots */} + + {(snapshot) => ( +
+
+
+ +
+
+
+

+ {snapshot.jobInfo?.name || snapshot.name || 'Version'} +

+
+

+ {new Date(snapshot.timestamp || Date.now()).toLocaleString()} +

+
+
+ + +
+ )} +
+ + +
+ No previous versions found. +
+
+
+
+
+
+
+
+ ); +}; + +export default HistoryModal; diff --git a/src/components/estimate-builder/useEstimateSync.ts b/src/components/estimate-builder/useEstimateSync.ts index 53a4fa4..543fca0 100644 --- a/src/components/estimate-builder/useEstimateSync.ts +++ b/src/components/estimate-builder/useEstimateSync.ts @@ -7,7 +7,19 @@ export function useEstimateSync( ) { 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 data = { @@ -18,7 +30,7 @@ export function useEstimateSync( jobInfo: { ...jobInfo }, generalEstimateNotes: generalEstimateNotes(), generalPreNotes: generalPreNotes(), - version: '1.0' + version: '1.2' }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -60,7 +72,8 @@ export function useEstimateSync( clientInfo: { ...clientInfo }, jobInfo: { ...jobInfo }, generalEstimateNotes: generalEstimateNotes(), - generalPreNotes: generalPreNotes() + generalPreNotes: generalPreNotes(), + timestamp: new Date().toISOString() }; }; @@ -68,20 +81,88 @@ export function useEstimateSync( setIsCloudLoading(true); try { const data = getFullEstimateData(); - await pb.collection(COLLECTIONS.ESTIMATES).create({ + const record = await pb.collection(COLLECTIONS.ESTIMATES).create({ name, - items: data + items: data, + history: [] // New estimate start with empty history }); + setShowSaveModal(false); - appStore.setEstimateName(name); - alert('Estimate saved to PocketBase successfully!'); + setEstimateName(name); + setEstimateId(record.id); + setHistory([]); + setLatestSnapshot(data); + setIsLatestVersion(true); + alert('New estimate saved successfully!'); } catch (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 { setIsCloudLoading(false); } }; - return { exportEstimate, importEstimate, saveToCloud, isCloudLoading }; + const saveChangesToCloud = async () => { + const id = appStore.estimateId(); + if (!id) return; + setIsCloudLoading(true); + try { + const currentData = getFullEstimateData(); + + // 1. Fetch current record to get latest history (to be safe against concurrency) + const record = await pb.collection(COLLECTIONS.ESTIMATES).getOne(id); + const updatedHistory = [...(record.history || [])]; + + // 2. Push current state (before this save) to history if it's new + // Actually, the user wants to save "as diffs" or "history". + // In a single-record approach, 'items' is the LATEST state. + // When we "Save Changes", we move the PREVIOUS 'items' to history. + updatedHistory.push(record.items); + + // 3. Update record with new current items and expanded history + await pb.collection(COLLECTIONS.ESTIMATES).update(id, { + items: currentData, + history: updatedHistory + }); + + setHistory(updatedHistory); + setLatestSnapshot(currentData); + setIsLatestVersion(true); + alert('Changes saved successfully!'); + } catch (err) { + console.error(err); + alert('Failed to save changes.'); + } finally { + setIsCloudLoading(false); + } + }; + + const saveAsNewestToCloud = async () => { + // "Save as Newest" is exactly same as saveChanges in this model + // It pushes the current editor state as the new 'items' and moves whatever was in 'items' to history. + 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 }; } diff --git a/src/store/appStore.ts b/src/store/appStore.ts index a0eaf7f..400e980 100644 --- a/src/store/appStore.ts +++ b/src/store/appStore.ts @@ -10,6 +10,9 @@ const [activeIgnoredLines, setActiveIgnoredLines] = createSignal([]) // Estimate Metadata const [activeEstimateId, setActiveEstimateId] = createSignal(null); const [activeEstimateName, setActiveEstimateName] = createSignal(''); +const [activeHistory, setActiveHistory] = createSignal([]); +const [activeLatestSnapshot, setActiveLatestSnapshot] = createSignal(null); +const [isLatestVersion, setIsLatestVersion] = createSignal(true); // Estimate Builder State const [scopes, setScopes] = createStore([]); @@ -50,6 +53,12 @@ export const appStore = { setEstimateId: setActiveEstimateId, estimateName: activeEstimateName, setEstimateName: setActiveEstimateName, + history: activeHistory, + setHistory: setActiveHistory, + latestSnapshot: activeLatestSnapshot, + setLatestSnapshot: setActiveLatestSnapshot, + isLatestVersion: isLatestVersion, + setIsLatestVersion: setIsLatestVersion, // Estimate Builder scopes, setScopes,