mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-22 14:38:10 +02:00
feat: Implement snapshot and export listing, restoration, and deletion with a new success modal and floating summary bar.
This commit is contained in:
1 parent
a93b1b36fc
commit
c842c2447a
3 files changed
+158
-8
No files matched your search
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-[100] flex items-center justify-center p-4 transition-all duration-300",
|
||||||
|
isVisible ? "opacity-100 backdrop-blur-sm" : "opacity-0 backdrop-blur-0"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-slate-950/40"
|
||||||
|
onClick={handleClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-full max-w-sm overflow-hidden rounded-3xl border border-slate-200/50 dark:border-slate-800/50 bg-white dark:bg-slate-900 shadow-2xl transition-all duration-300 transform",
|
||||||
|
isVisible ? "scale-100 translate-y-0" : "scale-95 translate-y-4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-400 to-teal-500" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="absolute top-4 right-4 p-1 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-400 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="p-8 flex flex-col items-center text-center">
|
||||||
|
<div className="w-16 h-16 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center mb-6 animate-bounce-slow">
|
||||||
|
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-xl font-black text-slate-900 dark:text-slate-100 mb-2 tracking-tight">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400 font-medium leading-relaxed">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="mt-8 w-full py-3 rounded-2xl bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 font-bold text-sm transition-all active:scale-95 hover:brightness-110 shadow-lg shadow-slate-900/10"
|
||||||
|
>
|
||||||
|
Continuer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style jsx global>{`
|
||||||
|
@keyframes bounce-slow {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-4px); }
|
||||||
|
}
|
||||||
|
.animate-bounce-slow {
|
||||||
|
animation: bounce-slow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
|
|||||||
import { Loader2, CheckCircle, AlertCircle, RotateCcw, Camera } from "lucide-react";
|
import { Loader2, CheckCircle, AlertCircle, RotateCcw, Camera } from "lucide-react";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { saveSnapshot } from "@/features/snapshots/api/save-snapshot";
|
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 }) {
|
function Stat({ label, value, sub }: { label: string; value: string; sub?: string }) {
|
||||||
// Determine color based on label to match the prototype
|
// Determine color based on label to match the prototype
|
||||||
@@ -25,6 +26,11 @@ export function FloatingSummaryBar() {
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [isSavingSnapshot, setIsSavingSnapshot] = useState(false);
|
const [isSavingSnapshot, setIsSavingSnapshot] = useState(false);
|
||||||
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
|
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 visibleCodeins = rows.map(r => r.codein);
|
||||||
const { save, hasDrafts, count } = useSaveDrafts(filters.magasin || "TOTAL", visibleCodeins);
|
const { save, hasDrafts, count } = useSaveDrafts(filters.magasin || "TOTAL", visibleCodeins);
|
||||||
@@ -75,7 +81,13 @@ export function FloatingSummaryBar() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.success) {
|
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;
|
return res.snapshotId;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
@@ -84,7 +96,11 @@ export function FloatingSummaryBar() {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
if (!labelOverride) {
|
if (!labelOverride) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
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 {
|
} finally {
|
||||||
setIsSavingSnapshot(false);
|
setIsSavingSnapshot(false);
|
||||||
@@ -219,6 +235,13 @@ export function FloatingSummaryBar() {
|
|||||||
{isPending ? "Validation..." : "Valider"}
|
{isPending ? "Validation..." : "Valider"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SuccessModal
|
||||||
|
isOpen={modal.isOpen}
|
||||||
|
onClose={() => setModal(prev => ({ ...prev, isOpen: false }))}
|
||||||
|
title={modal.title}
|
||||||
|
message={modal.message}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
History,
|
History,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Loader2,
|
Loader2,
|
||||||
AlertCircle
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { SuccessModal } from "@/components/shared/success-modal";
|
||||||
|
|
||||||
const formatDate = (date: Date) => {
|
const formatDate = (date: Date) => {
|
||||||
return new Intl.DateTimeFormat("fr-FR", {
|
return new Intl.DateTimeFormat("fr-FR", {
|
||||||
@@ -35,6 +35,11 @@ export function SnapshotList({ type }: SnapshotListProps) {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||||
|
const [modal, setModal] = useState<{ isOpen: boolean, title: string, message: string }>({
|
||||||
|
isOpen: false,
|
||||||
|
title: "",
|
||||||
|
message: ""
|
||||||
|
});
|
||||||
|
|
||||||
const restoreSnapshot = useGridStore(state => state.restoreSnapshot);
|
const restoreSnapshot = useGridStore(state => state.restoreSnapshot);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -62,11 +67,24 @@ export function SnapshotList({ type }: SnapshotListProps) {
|
|||||||
const res = await deleteSnapshot(id);
|
const res = await deleteSnapshot(id);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setSnapshots(prev => prev.filter(s => s.id !== id));
|
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 {
|
} else {
|
||||||
alert("Erreur lors de la suppression.");
|
setModal({
|
||||||
|
isOpen: true,
|
||||||
|
title: "Erreur",
|
||||||
|
message: "Impossible de supprimer l'élément."
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
setIsDeleting(null);
|
setIsDeleting(null);
|
||||||
}
|
}
|
||||||
@@ -75,8 +93,6 @@ export function SnapshotList({ type }: SnapshotListProps) {
|
|||||||
const handleLoad = (snapshot: any) => {
|
const handleLoad = (snapshot: any) => {
|
||||||
if (!window.confirm(`Charger la session "${snapshot.label}" ? Cela remplacera vos brouillons actuels.`)) return;
|
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<string, GammeCode>
|
|
||||||
const changes: Record<string, string> = {};
|
const changes: Record<string, string> = {};
|
||||||
Object.entries(snapshot.changes as any).forEach(([codein, delta]: [string, any]) => {
|
Object.entries(snapshot.changes as any).forEach(([codein, delta]: [string, any]) => {
|
||||||
changes[codein] = delta.after;
|
changes[codein] = delta.after;
|
||||||
@@ -84,7 +100,6 @@ export function SnapshotList({ type }: SnapshotListProps) {
|
|||||||
|
|
||||||
restoreSnapshot(changes);
|
restoreSnapshot(changes);
|
||||||
|
|
||||||
// Redirection vers la grille avec les filtres du snapshot
|
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
if (snapshot.codeFournisseur) query.set("fournisseur", snapshot.codeFournisseur);
|
if (snapshot.codeFournisseur) query.set("fournisseur", snapshot.codeFournisseur);
|
||||||
if (snapshot.magasin) query.set("magasin", snapshot.magasin);
|
if (snapshot.magasin) query.set("magasin", snapshot.magasin);
|
||||||
@@ -187,6 +202,13 @@ export function SnapshotList({ type }: SnapshotListProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<SuccessModal
|
||||||
|
isOpen={modal.isOpen}
|
||||||
|
onClose={() => setModal(prev => ({ ...prev, isOpen: false }))}
|
||||||
|
title={modal.title}
|
||||||
|
message={modal.message}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user