Estimate History management added
This commit is contained in:
@@ -49,6 +49,9 @@ const CloudLoadModal: Component<CloudLoadModalProps> = (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([]);
|
||||
|
||||
@@ -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<EstimateBuilderProps> = (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<string | null>(null);
|
||||
|
||||
@@ -58,7 +60,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (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<EstimateBuilderProps> = (props) => {
|
||||
onExportEstimate={exportEstimate}
|
||||
onShowLoadModal={() => setShowLoadModal(true)}
|
||||
onShowSaveModal={() => setShowSaveModal(true)}
|
||||
onShowHistoryModal={() => setShowHistoryModal(true)}
|
||||
onSaveChanges={saveChangesToCloud}
|
||||
/>
|
||||
|
||||
<div class="flex flex-1 relative items-start">
|
||||
@@ -353,6 +357,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
<CloudLoadModal show={showLoadModal()} onClose={() => setShowLoadModal(false)} />
|
||||
<ExportTableModal show={showExportTableModal()} onClose={() => setShowExportTableModal(false)} />
|
||||
<ApplyTemplateModal show={!!applyTemplateScopeId()} scopeId={applyTemplateScopeId()} onClose={() => setApplyTemplateScopeId(null)} />
|
||||
<HistoryModal show={showHistoryModal()} onClose={() => setShowHistoryModal(false)} onCheckout={checkoutVersion} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<BuilderHeaderProps> = (props) => {
|
||||
@@ -78,11 +81,29 @@ export const BuilderHeader: Component<BuilderHeaderProps> = (props) => {
|
||||
>
|
||||
<Download class="w-4 h-4" /> Load
|
||||
</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
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<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 items-center justify-between 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 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>
|
||||
|
||||
{/* Historical Snapshots */}
|
||||
<For each={historyEntries()}>
|
||||
{(snapshot) => (
|
||||
<div class="group flex items-center justify-between p-4 rounded-2xl border border-border/60 bg-muted/5 hover:border-primary/30 transition-all">
|
||||
<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>
|
||||
)}
|
||||
</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;
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ const [activeIgnoredLines, setActiveIgnoredLines] = createSignal<string[][]>([])
|
||||
// Estimate Metadata
|
||||
const [activeEstimateId, setActiveEstimateId] = createSignal<string | null>(null);
|
||||
const [activeEstimateName, setActiveEstimateName] = createSignal('');
|
||||
const [activeHistory, setActiveHistory] = createSignal<any[]>([]);
|
||||
const [activeLatestSnapshot, setActiveLatestSnapshot] = createSignal<any | null>(null);
|
||||
const [isLatestVersion, setIsLatestVersion] = createSignal(true);
|
||||
|
||||
// Estimate Builder State
|
||||
const [scopes, setScopes] = createStore<Scope[]>([]);
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user