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">
{/* Current Live Version */}
<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()
? '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" />
@@ -74,10 +75,46 @@ const HistoryModal = (props: HistoryModalProps) => {
</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 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="p-2 bg-muted text-muted-foreground rounded-lg">
<Clock class="w-4 h-4" />
@@ -104,6 +141,41 @@ const HistoryModal = (props: HistoryModalProps) => {
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>
@@ -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) => {
setIsCloudLoading(true);
try {
const data = getFullEstimateData();
const record = await pb.collection(COLLECTIONS.ESTIMATES).create({
name,
items: data,
history: [] // New estimate start with empty history
items: { ...data, changeSummary: null },
history: []
});
setShowSaveModal(false);
setEstimateName(name);
setEstimateId(record.id);
setHistory([]);
setLatestSnapshot(data);
setLatestSnapshot({ ...data, changeSummary: null });
setIsLatestVersion(true);
alert('New estimate saved successfully!');
} catch (err) {
@@ -109,24 +171,23 @@ export function useEstimateSync(
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);
const changeSummary = calculateChangeSummary(record.items, currentData);
const newTip = {
...currentData,
changeSummary
};
// 3. Update record with new current items and expanded history
await pb.collection(COLLECTIONS.ESTIMATES).update(id, {
items: currentData,
history: updatedHistory
items: newTip,
history: [...updatedHistory, record.items]
});
setHistory(updatedHistory);
setLatestSnapshot(currentData);
setHistory([...updatedHistory, record.items]);
setLatestSnapshot(newTip);
setIsLatestVersion(true);
alert('Changes saved successfully!');
} catch (err) {
@@ -138,8 +199,6 @@ export function useEstimateSync(
};
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();
};