history working
This commit is contained in:
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user