From c842c2447ad3e146dd3488e28c83ddd59e6dc67d Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 22 Feb 2026 13:54:24 +0100 Subject: [PATCH] feat: Implement snapshot and export listing, restoration, and deletion with a new success modal and floating summary bar. --- src/components/shared/success-modal.tsx | 105 ++++++++++++++++++ .../grid/components/floating-summary-bar.tsx | 27 ++++- .../snapshots/components/snapshot-list.tsx | 34 +++++- 3 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 src/components/shared/success-modal.tsx diff --git a/src/components/shared/success-modal.tsx b/src/components/shared/success-modal.tsx new file mode 100644 index 0000000..b340792 --- /dev/null +++ b/src/components/shared/success-modal.tsx @@ -0,0 +1,105 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { CheckCircle2, X } from "lucide-react"; +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +interface SuccessModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + message: string; + duration?: number; +} + +export function SuccessModal({ + isOpen, + onClose, + title, + message, + duration = 3000 +}: SuccessModalProps) { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (isOpen) { + setIsVisible(true); + const timer = setTimeout(() => { + handleClose(); + }, duration); + return () => clearTimeout(timer); + } + }, [isOpen, duration]); + + const handleClose = () => { + setIsVisible(false); + setTimeout(onClose, 300); // Wait for fade out animation + }; + + if (!isOpen && !isVisible) return null; + + return ( +
+
+ +
+
+ + + +
+
+ +
+ +

+ {title} +

+

+ {message} +

+ + +
+
+ + +
+ ); +} diff --git a/src/features/grid/components/floating-summary-bar.tsx b/src/features/grid/components/floating-summary-bar.tsx index 792cb8e..ca9fa0a 100644 --- a/src/features/grid/components/floating-summary-bar.tsx +++ b/src/features/grid/components/floating-summary-bar.tsx @@ -6,6 +6,7 @@ import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts"; import { Loader2, CheckCircle, AlertCircle, RotateCcw, Camera } from "lucide-react"; import { useState, useTransition } from "react"; import { saveSnapshot } from "@/features/snapshots/api/save-snapshot"; +import { SuccessModal } from "@/components/shared/success-modal"; function Stat({ label, value, sub }: { label: string; value: string; sub?: string }) { // Determine color based on label to match the prototype @@ -25,6 +26,11 @@ export function FloatingSummaryBar() { const [isPending, startTransition] = useTransition(); const [isSavingSnapshot, setIsSavingSnapshot] = useState(false); const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle"); + const [modal, setModal] = useState<{ isOpen: boolean, title: string, message: string }>({ + isOpen: false, + title: "", + message: "" + }); const visibleCodeins = rows.map(r => r.codein); const { save, hasDrafts, count } = useSaveDrafts(filters.magasin || "TOTAL", visibleCodeins); @@ -75,7 +81,13 @@ export function FloatingSummaryBar() { }); if (res.success) { - if (!labelOverride) alert("Snapshot enregistré avec succès ! Retrouvez-le dans la page Snapshots."); + if (!labelOverride) { + setModal({ + isOpen: true, + title: "Snapshot Enregistré", + message: "Votre session de travail a été sauvegardée avec succès." + }); + } return res.snapshotId; } else { throw new Error(res.error); @@ -84,7 +96,11 @@ export function FloatingSummaryBar() { console.error(err); if (!labelOverride) { const msg = err instanceof Error ? err.message : String(err); - alert(`Erreur lors de la création du snapshot : ${msg}`); + setModal({ + isOpen: true, + title: "Erreur Snapshot", + message: `Impossible de créer le snapshot : ${msg}` + }); } } finally { setIsSavingSnapshot(false); @@ -219,6 +235,13 @@ export function FloatingSummaryBar() { {isPending ? "Validation..." : "Valider"}
+ + setModal(prev => ({ ...prev, isOpen: false }))} + title={modal.title} + message={modal.message} + />
); } diff --git a/src/features/snapshots/components/snapshot-list.tsx b/src/features/snapshots/components/snapshot-list.tsx index a79957a..c6fe7b5 100644 --- a/src/features/snapshots/components/snapshot-list.tsx +++ b/src/features/snapshots/components/snapshot-list.tsx @@ -13,8 +13,8 @@ import { History, ChevronRight, Loader2, - AlertCircle } from "lucide-react"; +import { SuccessModal } from "@/components/shared/success-modal"; const formatDate = (date: Date) => { return new Intl.DateTimeFormat("fr-FR", { @@ -35,6 +35,11 @@ export function SnapshotList({ type }: SnapshotListProps) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isDeleting, setIsDeleting] = useState(null); + const [modal, setModal] = useState<{ isOpen: boolean, title: string, message: string }>({ + isOpen: false, + title: "", + message: "" + }); const restoreSnapshot = useGridStore(state => state.restoreSnapshot); const router = useRouter(); @@ -62,11 +67,24 @@ export function SnapshotList({ type }: SnapshotListProps) { const res = await deleteSnapshot(id); if (res.success) { setSnapshots(prev => prev.filter(s => s.id !== id)); + setModal({ + isOpen: true, + title: "Action Réussie", + message: "L'élément a été supprimé de votre historique." + }); } else { - alert("Erreur lors de la suppression."); + setModal({ + isOpen: true, + title: "Erreur", + message: "Impossible de supprimer l'élément." + }); } } catch (err) { - alert("Erreur technique lors de la suppression."); + setModal({ + isOpen: true, + title: "Erreur Technique", + message: "Une erreur est survenue lors de la suppression." + }); } finally { setIsDeleting(null); } @@ -75,8 +93,6 @@ export function SnapshotList({ type }: SnapshotListProps) { 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; @@ -84,7 +100,6 @@ export function SnapshotList({ type }: SnapshotListProps) { 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); @@ -187,6 +202,13 @@ export function SnapshotList({ type }: SnapshotListProps) { ))} + + setModal(prev => ({ ...prev, isOpen: false }))} + title={modal.title} + message={modal.message} + /> ); }