diff --git a/src/app/(dashboard)/exports/page.tsx b/src/app/(dashboard)/exports/page.tsx
index a62ce29..3017ba1 100644
--- a/src/app/(dashboard)/exports/page.tsx
+++ b/src/app/(dashboard)/exports/page.tsx
@@ -1,14 +1,24 @@
+import { SnapshotList } from "@/features/snapshots/components/snapshot-list";
+import { FileDown } from "lucide-react";
+
export default function ExportsPage() {
return (
-
-
-
Exports
-
Génération des fichiers Excel différentiels pour l'ERP.
-
-
-
Sélectionnez un fournisseur dans la grille, puis cliquez sur « Exporter ».
-
L'export génère un fichier .xlsx avec le delta avant/après des gammes révisées.
+
+
+
+
+
+
+
+
Historique d'Arbitrage
+
+
+ Retrouvez ici toutes vos sessions sauvegardées et exports réalisés. Vous pouvez restaurer une session pour reprendre votre travail ou supprimer les anciens enregistrements.
+
+
+
+
);
}
diff --git a/src/app/(dashboard)/snapshots/page.tsx b/src/app/(dashboard)/snapshots/page.tsx
index ec96772..880768d 100644
--- a/src/app/(dashboard)/snapshots/page.tsx
+++ b/src/app/(dashboard)/snapshots/page.tsx
@@ -1,14 +1,24 @@
+import { SnapshotList } from "@/features/snapshots/components/snapshot-list";
+import { Camera } from "lucide-react";
+
export default function SnapshotsPage() {
return (
-
-
-
Snapshots
-
Historique de vos sessions d'arbitrage sauvegardées.
-
-
-
Aucun snapshot enregistré.
-
Utilisez le bouton « Valider » dans la grille pour créer votre premier snapshot.
+
+
+
+
+
+
+
+
Snapshots
+
+
+ Historique de vos sessions d'arbitrage. Restaurez une sauvegarde pour retrouver vos modifications en cours sur une gamme spécifique.
+
+
+
+
);
}
diff --git a/src/features/grid/components/floating-summary-bar.tsx b/src/features/grid/components/floating-summary-bar.tsx
index 9bcf919..58b9469 100644
--- a/src/features/grid/components/floating-summary-bar.tsx
+++ b/src/features/grid/components/floating-summary-bar.tsx
@@ -1,12 +1,11 @@
-"use client";
-
import { useGridStore } from "@/features/grid/store/use-grid-store";
import { useAiCopilotStore } from "@/features/ai-copilot/store/use-ai-copilot-store";
import { BulkAiAnalyzer } from "./bulk-ai-analyzer";
import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
-import { Loader2, CheckCircle, AlertCircle, RotateCcw } from "lucide-react";
+import { Loader2, CheckCircle, AlertCircle, RotateCcw, Camera } from "lucide-react";
import { useState, useTransition } from "react";
+import { saveSnapshot } from "@/features/snapshots/api/save-snapshot";
function Stat({ label, value, sub }: { label: string; value: string; sub?: string }) {
// Determine color based on label to match the prototype
@@ -21,9 +20,10 @@ function Stat({ label, value, sub }: { label: string; value: string; sub?: strin
}
export function FloatingSummaryBar() {
- const { summary, resetDrafts, rows, filters } = useGridStore();
+ const { summary, resetDrafts, rows, filters, draftChanges } = useGridStore();
const { resetInsights } = useAiCopilotStore();
const [isPending, startTransition] = useTransition();
+ const [isSavingSnapshot, setIsSavingSnapshot] = useState(false);
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
const visibleCodeins = rows.map(r => r.codein);
@@ -44,6 +44,49 @@ export function FloatingSummaryBar() {
}
};
+ const handleSnapshot = async (labelOverride?: string) => {
+ if (rows.length === 0) return;
+ setIsSavingSnapshot(true);
+ try {
+ const modifiedRows = rows.filter(r => draftChanges[r.codein]);
+ const changes = Object.fromEntries(
+ modifiedRows.map(r => [
+ r.codein,
+ {
+ before: r.codeGamme,
+ after: draftChanges[r.codein] as string
+ }
+ ])
+ );
+
+ const res = await saveSnapshot({
+ codeFournisseur: filters.codeFournisseur || rows[0].codeFournisseur,
+ nomFournisseur: rows[0].nomFournisseur,
+ magasin: filters.magasin || "TOTAL",
+ label: labelOverride || `Session ${rows[0].nomFournisseur} — ${new Date().toLocaleTimeString()}`,
+ changes,
+ summary: {
+ totalRows: summary.totalRows,
+ totalCa: summary.totalCa,
+ totalMarge: summary.totalMarge,
+ tauxMargeGlobal: summary.tauxMargeGlobal,
+ }
+ });
+
+ if (res.success) {
+ if (!labelOverride) alert("Snapshot enregistré avec succès ! Retrouvez-le dans la page Snapshots.");
+ return res.snapshotId;
+ } else {
+ throw new Error(res.error);
+ }
+ } catch (err) {
+ console.error(err);
+ if (!labelOverride) alert("Erreur lors de la création du snapshot.");
+ } finally {
+ setIsSavingSnapshot(false);
+ }
+ };
+
return (
-
+
-
+
+
+
{hasDrafts && (
{count} modif.
@@ -111,6 +163,9 @@ export function FloatingSummaryBar() {
const nomFournisseur = modifiedRows[0].nomFournisseur;
+ // Auto-save snapshot for history
+ await handleSnapshot(`Export ${nomFournisseur} — ${new Date().toLocaleTimeString()}`);
+
try {
const res = await fetch("/api/export/modified-gammes", {
method: "POST",
diff --git a/src/features/grid/store/use-grid-store.ts b/src/features/grid/store/use-grid-store.ts
index 625699c..4b7ef49 100644
--- a/src/features/grid/store/use-grid-store.ts
+++ b/src/features/grid/store/use-grid-store.ts
@@ -24,6 +24,7 @@ interface GridState {
setFilter: (key: keyof GridFilters, value: string | null) => void;
setDisplayDensity: (density: "compact" | "normal" | "comfortable") => void;
setActiveGridQuery: (query: string) => void;
+ restoreSnapshot: (changes: Record) => void;
}
function computeSummary(rows: ProductRow[], drafts: Record): GridSummary {
@@ -101,6 +102,9 @@ export const useGridStore = create()(
},
setDisplayDensity: (density) => set({ displayDensity: density }),
setActiveGridQuery: (query) => set({ activeGridQuery: query }),
+ restoreSnapshot: (changes) => {
+ set({ draftChanges: changes, summary: computeSummary(get().rows, changes) });
+ },
}),
{
name: "collectflow-grid-storage",
diff --git a/src/features/snapshots/api/delete-snapshot.ts b/src/features/snapshots/api/delete-snapshot.ts
new file mode 100644
index 0000000..de84817
--- /dev/null
+++ b/src/features/snapshots/api/delete-snapshot.ts
@@ -0,0 +1,15 @@
+"use server";
+
+import { db } from "@/db";
+import { sessionSnapshots } from "@/db/schema";
+import { eq } from "drizzle-orm";
+
+export async function deleteSnapshot(id: number) {
+ try {
+ await db.delete(sessionSnapshots).where(eq(sessionSnapshots.id, id));
+ return { success: true };
+ } catch (err) {
+ console.error(err);
+ return { success: false, error: "Delete failed" };
+ }
+}
diff --git a/src/features/snapshots/api/get-snapshots.ts b/src/features/snapshots/api/get-snapshots.ts
new file mode 100644
index 0000000..fda2126
--- /dev/null
+++ b/src/features/snapshots/api/get-snapshots.ts
@@ -0,0 +1,9 @@
+"use server";
+
+import { db } from "@/db";
+import { sessionSnapshots } from "@/db/schema";
+import { desc } from "drizzle-orm";
+
+export async function getSnapshots() {
+ return db.select().from(sessionSnapshots).orderBy(desc(sessionSnapshots.createdAt));
+}
diff --git a/src/features/snapshots/components/snapshot-list.tsx b/src/features/snapshots/components/snapshot-list.tsx
new file mode 100644
index 0000000..24c8e3b
--- /dev/null
+++ b/src/features/snapshots/components/snapshot-list.tsx
@@ -0,0 +1,184 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { getSnapshots } from "../api/get-snapshots";
+import { deleteSnapshot } from "../api/delete-snapshot";
+import { useGridStore } from "@/features/grid/store/use-grid-store";
+import { useRouter } from "next/navigation";
+import {
+ Trash2,
+ UploadCloud,
+ Calendar,
+ Store,
+ History,
+ ChevronRight,
+ Loader2,
+ AlertCircle
+} from "lucide-react";
+
+const formatDate = (date: Date) => {
+ return new Intl.DateTimeFormat("fr-FR", {
+ day: "numeric",
+ month: "long",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit"
+ }).format(date);
+};
+
+export function SnapshotList() {
+ const [snapshots, setSnapshots] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [isDeleting, setIsDeleting] = useState(null);
+
+ const restoreSnapshot = useGridStore(state => state.restoreSnapshot);
+ const router = useRouter();
+
+ const fetchSnapshots = async () => {
+ setLoading(true);
+ try {
+ const data = await getSnapshots();
+ setSnapshots(data);
+ } catch (err) {
+ setError("Impossible de charger les snapshots.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchSnapshots();
+ }, []);
+
+ const handleDelete = async (id: number) => {
+ if (!window.confirm("Supprimer ce snapshot définitivement ?")) return;
+ setIsDeleting(id);
+ try {
+ const res = await deleteSnapshot(id);
+ if (res.success) {
+ setSnapshots(prev => prev.filter(s => s.id !== id));
+ } else {
+ alert("Erreur lors de la suppression.");
+ }
+ } catch (err) {
+ alert("Erreur technique lors de la suppression.");
+ } finally {
+ setIsDeleting(null);
+ }
+ };
+
+ const handleLoad = (snapshot: any) => {
+ if (!window.confirm(`Charger la session "${snapshot.label}" ? Cela remplacera vos brouillons actuels.`)) return;
+
+ // Les changements sont stockés sous forme d'objet { codein: { before, after } }
+ // On doit re-transformer en Record
+ const changes: Record = {};
+ Object.entries(snapshot.changes as any).forEach(([codein, delta]: [string, any]) => {
+ changes[codein] = delta.after;
+ });
+
+ restoreSnapshot(changes);
+
+ // Redirection vers la grille avec les filtres du snapshot
+ const query = new URLSearchParams();
+ if (snapshot.codeFournisseur) query.set("fournisseur", snapshot.codeFournisseur);
+ if (snapshot.magasin) query.set("magasin", snapshot.magasin);
+
+ router.push(`/grid?${query.toString()}`);
+ };
+
+ if (loading) {
+ return (
+