history working
This commit is contained in:
@@ -37,72 +37,144 @@ 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 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 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" />
|
||||
</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>
|
||||
<p class="text-[10px] text-muted-foreground font-medium uppercase tracking-wider mt-1">
|
||||
The current tip of the project
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 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="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 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" />
|
||||
</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>
|
||||
<p class="text-[10px] text-muted-foreground font-medium uppercase tracking-wider mt-1">
|
||||
{new Date(snapshot.timestamp || Date.now()).toLocaleString()}
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 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();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user