history working

This commit is contained in:
2026-03-13 15:04:37 -05:00
parent e637e0903d
commit 4b2585a59f
2 changed files with 196 additions and 65 deletions
@@ -37,12 +37,13 @@ const HistoryModal = (props: HistoryModalProps) => {
<div class="space-y-3 overflow-y-auto pr-2"> <div class="space-y-3 overflow-y-auto pr-2">
{/* Current Live Version */} {/* Current Live Version */}
<div <div
class={`group flex items-center justify-between p-4 rounded-2xl border transition-all ${ class={`group flex flex-col p-4 rounded-2xl border transition-all ${
appStore.isLatestVersion() appStore.isLatestVersion()
? 'border-primary bg-primary/5 shadow-premium' ? 'border-primary bg-primary/5 shadow-premium'
: 'border-border/60 bg-muted/10 hover:border-primary/30' : '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="flex items-center gap-4">
<div class={`p-2 rounded-lg ${appStore.isLatestVersion() ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}> <div class={`p-2 rounded-lg ${appStore.isLatestVersion() ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}>
<CheckCircle2 class="w-4 h-4" /> <CheckCircle2 class="w-4 h-4" />
@@ -74,10 +75,46 @@ const HistoryModal = (props: HistoryModalProps) => {
</Show> </Show>
</div> </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 */} {/* Historical Snapshots */}
<For each={historyEntries()}> <For each={historyEntries()}>
{(snapshot) => ( {(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="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="flex items-center gap-4">
<div class="p-2 bg-muted text-muted-foreground rounded-lg"> <div class="p-2 bg-muted text-muted-foreground rounded-lg">
<Clock class="w-4 h-4" /> <Clock class="w-4 h-4" />
@@ -104,6 +141,41 @@ const HistoryModal = (props: HistoryModalProps) => {
Checkout Checkout
</button> </button>
</div> </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> </For>
@@ -77,21 +77,83 @@ export function useEstimateSync(
}; };
}; };
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();
const record = await pb.collection(COLLECTIONS.ESTIMATES).create({ const record = await pb.collection(COLLECTIONS.ESTIMATES).create({
name, name,
items: data, items: { ...data, changeSummary: null },
history: [] // New estimate start with empty history history: []
}); });
setShowSaveModal(false); setShowSaveModal(false);
setEstimateName(name); setEstimateName(name);
setEstimateId(record.id); setEstimateId(record.id);
setHistory([]); setHistory([]);
setLatestSnapshot(data); setLatestSnapshot({ ...data, changeSummary: null });
setIsLatestVersion(true); setIsLatestVersion(true);
alert('New estimate saved successfully!'); alert('New estimate saved successfully!');
} catch (err) { } catch (err) {
@@ -109,24 +171,23 @@ export function useEstimateSync(
try { try {
const currentData = getFullEstimateData(); 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 record = await pb.collection(COLLECTIONS.ESTIMATES).getOne(id);
const updatedHistory = [...(record.history || [])]; const updatedHistory = [...(record.history || [])];
// 2. Push current state (before this save) to history if it's new const changeSummary = calculateChangeSummary(record.items, currentData);
// Actually, the user wants to save "as diffs" or "history".
// In a single-record approach, 'items' is the LATEST state. const newTip = {
// When we "Save Changes", we move the PREVIOUS 'items' to history. ...currentData,
updatedHistory.push(record.items); changeSummary
};
// 3. Update record with new current items and expanded history
await pb.collection(COLLECTIONS.ESTIMATES).update(id, { await pb.collection(COLLECTIONS.ESTIMATES).update(id, {
items: currentData, items: newTip,
history: updatedHistory history: [...updatedHistory, record.items]
}); });
setHistory(updatedHistory); setHistory([...updatedHistory, record.items]);
setLatestSnapshot(currentData); setLatestSnapshot(newTip);
setIsLatestVersion(true); setIsLatestVersion(true);
alert('Changes saved successfully!'); alert('Changes saved successfully!');
} catch (err) { } catch (err) {
@@ -138,8 +199,6 @@ export function useEstimateSync(
}; };
const saveAsNewestToCloud = async () => { 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(); await saveChangesToCloud();
}; };