From a93b1b36fcfd79ffa127eee1668b1fda5811c7ea Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 22 Feb 2026 13:49:51 +0100 Subject: [PATCH] feat: Implement snapshot and export management, including API endpoints, database schema, and UI for listing, saving, loading, and deleting. --- src/app/(dashboard)/exports/page.tsx | 2 +- src/app/(dashboard)/snapshots/page.tsx | 2 +- src/db/schema.ts | 2 ++ .../grid/components/floating-summary-bar.tsx | 7 +++--- src/features/snapshots/api/get-snapshots.ts | 13 +++++++--- src/features/snapshots/api/save-snapshot.ts | 18 ++++++++++---- .../snapshots/components/snapshot-list.tsx | 24 ++++++++++++------- 7 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/app/(dashboard)/exports/page.tsx b/src/app/(dashboard)/exports/page.tsx index 3017ba1..bfc32ce 100644 --- a/src/app/(dashboard)/exports/page.tsx +++ b/src/app/(dashboard)/exports/page.tsx @@ -18,7 +18,7 @@ export default function ExportsPage() { - + ); } diff --git a/src/app/(dashboard)/snapshots/page.tsx b/src/app/(dashboard)/snapshots/page.tsx index 880768d..32b5e4a 100644 --- a/src/app/(dashboard)/snapshots/page.tsx +++ b/src/app/(dashboard)/snapshots/page.tsx @@ -18,7 +18,7 @@ export default function SnapshotsPage() { - + ); } diff --git a/src/db/schema.ts b/src/db/schema.ts index fc21ee3..30e4f63 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -59,5 +59,7 @@ export const sessionSnapshots = pgTable("session_snapshots", { summaryJson: jsonb("summary_json"), /** Label for the snapshot */ label: text("label"), + /** snapshot vs export */ + type: varchar("type", { length: 20 }).default("snapshot"), createdAt: timestamp("created_at").defaultNow(), }); diff --git a/src/features/grid/components/floating-summary-bar.tsx b/src/features/grid/components/floating-summary-bar.tsx index 26f9e6a..792cb8e 100644 --- a/src/features/grid/components/floating-summary-bar.tsx +++ b/src/features/grid/components/floating-summary-bar.tsx @@ -44,7 +44,7 @@ export function FloatingSummaryBar() { } }; - const handleSnapshot = async (labelOverride?: string) => { + const handleSnapshot = async (labelOverride?: string, type: "snapshot" | "export" = "snapshot") => { if (rows.length === 0) return; setIsSavingSnapshot(true); try { @@ -63,8 +63,9 @@ export function FloatingSummaryBar() { codeFournisseur: filters.codeFournisseur || rows[0].codeFournisseur, nomFournisseur: rows[0].nomFournisseur, magasin: filters.magasin || "TOTAL", - label: labelOverride || `Session ${rows[0].nomFournisseur} — ${new Date().toLocaleTimeString()}`, + label: labelOverride || `${type === 'export' ? 'Export' : 'Session'} ${rows[0].nomFournisseur} — ${new Date().toLocaleTimeString()}`, changes, + type, summary: { totalRows: summary.totalRows, totalCa: summary.totalCa, @@ -167,7 +168,7 @@ export function FloatingSummaryBar() { const nomFournisseur = modifiedRows[0].nomFournisseur; // Auto-save snapshot for history - await handleSnapshot(`Export ${nomFournisseur} — ${new Date().toLocaleTimeString()}`); + await handleSnapshot(`Export ${nomFournisseur} — ${new Date().toLocaleTimeString()}`, "export"); try { const res = await fetch("/api/export/modified-gammes", { diff --git a/src/features/snapshots/api/get-snapshots.ts b/src/features/snapshots/api/get-snapshots.ts index fda2126..456fcd4 100644 --- a/src/features/snapshots/api/get-snapshots.ts +++ b/src/features/snapshots/api/get-snapshots.ts @@ -2,8 +2,15 @@ import { db } from "@/db"; import { sessionSnapshots } from "@/db/schema"; -import { desc } from "drizzle-orm"; +import { desc, eq } from "drizzle-orm"; -export async function getSnapshots() { - return db.select().from(sessionSnapshots).orderBy(desc(sessionSnapshots.createdAt)); +export async function getSnapshots(type?: "snapshot" | "export") { + let query = db.select().from(sessionSnapshots); + + if (type) { + // @ts-ignore - type column added dynamically + return query.where(eq(sessionSnapshots.type, type)).orderBy(desc(sessionSnapshots.createdAt)); + } + + return query.orderBy(desc(sessionSnapshots.createdAt)); } diff --git a/src/features/snapshots/api/save-snapshot.ts b/src/features/snapshots/api/save-snapshot.ts index bc764b6..e94892b 100644 --- a/src/features/snapshots/api/save-snapshot.ts +++ b/src/features/snapshots/api/save-snapshot.ts @@ -14,6 +14,7 @@ const SaveSnapshotSchema = z.object({ before: z.string().nullable(), after: z.string(), })), + type: z.enum(["snapshot", "export"]).optional(), summary: z.object({ totalRows: z.number(), totalCa: z.number(), @@ -29,7 +30,7 @@ export async function saveSnapshot(raw: unknown) { return { success: false, error: "Validation failed: " + parsed.error.issues.map((i: any) => i.message).join(", ") }; } - const { codeFournisseur, nomFournisseur, magasin, label, changes, summary } = parsed.data; + const { codeFournisseur, nomFournisseur, magasin, label, changes, summary, type } = parsed.data; try { const [created] = await db @@ -38,9 +39,10 @@ export async function saveSnapshot(raw: unknown) { codeFournisseur, nomFournisseur: nomFournisseur ?? null, magasin, - label: label ?? `Snapshot — ${new Date().toLocaleDateString("fr-FR")}`, + label: label ?? `${type === 'export' ? 'Export' : 'Snapshot'} — ${new Date().toLocaleDateString("fr-FR")}`, changes, summaryJson: summary ?? null, + type: type ?? "snapshot", }) .returning({ id: sessionSnapshots.id }); @@ -59,10 +61,18 @@ export async function saveSnapshot(raw: unknown) { changes JSONB NOT NULL, summary_json JSONB, label TEXT, + type VARCHAR(20) DEFAULT 'snapshot', created_at TIMESTAMP DEFAULT NOW() ); `); + // Si la table existait déjà mais sans la colonne 'type', on l'ajoute + try { + await db.execute(sql`ALTER TABLE session_snapshots ADD COLUMN IF NOT EXISTS type VARCHAR(20) DEFAULT 'snapshot'`); + } catch (e) { + // Ignore if column already exists or other alter errors + } + // Deuxième tentative d'insertion const [retryCreated] = await db .insert(sessionSnapshots) @@ -70,16 +80,16 @@ export async function saveSnapshot(raw: unknown) { codeFournisseur, nomFournisseur: nomFournisseur ?? null, magasin, - label: label ?? `Snapshot — ${new Date().toLocaleDateString("fr-FR")}`, + label: label ?? `${type === 'export' ? 'Export' : 'Snapshot'} — ${new Date().toLocaleDateString("fr-FR")}`, changes, summaryJson: summary ?? null, + type: type ?? "snapshot", }) .returning({ id: sessionSnapshots.id }); return { success: true, snapshotId: retryCreated?.id }; } catch (repairErr: any) { console.error("Snapshot auto-repair/retry failed:", repairErr); - // On renvoie les deux erreurs pour comprendre si c'est la création ou l'insertion qui échoue const origMsg = (err.message || String(err)).split('\n')[0]; const retryMsg = (repairErr.message || String(repairErr)).split('\n')[0]; return { diff --git a/src/features/snapshots/components/snapshot-list.tsx b/src/features/snapshots/components/snapshot-list.tsx index 24c8e3b..a79957a 100644 --- a/src/features/snapshots/components/snapshot-list.tsx +++ b/src/features/snapshots/components/snapshot-list.tsx @@ -26,7 +26,11 @@ const formatDate = (date: Date) => { }).format(date); }; -export function SnapshotList() { +interface SnapshotListProps { + type?: "snapshot" | "export"; +} + +export function SnapshotList({ type }: SnapshotListProps) { const [snapshots, setSnapshots] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -38,10 +42,10 @@ export function SnapshotList() { const fetchSnapshots = async () => { setLoading(true); try { - const data = await getSnapshots(); + const data = await getSnapshots(type); setSnapshots(data); } catch (err) { - setError("Impossible de charger les snapshots."); + setError("Impossible de charger les données."); } finally { setLoading(false); } @@ -49,10 +53,10 @@ export function SnapshotList() { useEffect(() => { fetchSnapshots(); - }, []); + }, [type]); const handleDelete = async (id: number) => { - if (!window.confirm("Supprimer ce snapshot définitivement ?")) return; + if (!window.confirm(`Supprimer cet ${type === 'export' ? 'export' : 'snapshot'} définitivement ?`)) return; setIsDeleting(id); try { const res = await deleteSnapshot(id); @@ -92,7 +96,7 @@ export function SnapshotList() { return (
-

Récupération de vos sessions...

+

Récupération de l'historique...

); } @@ -103,9 +107,13 @@ export function SnapshotList() {
-

Aucun snapshot pour le moment

+

+ {type === "export" ? "Aucun export pour le moment" : "Aucun snapshot pour le moment"} +

- Capturez l'état de votre travail depuis la grille pour le retrouver ici ultérieurement. + {type === "export" + ? "Vos exports Excel apparaîtront ici pour historique." + : "Capturez l'état de votre travail depuis la grille pour le retrouver ici ultérieurement."}

);