mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat: Implement snapshot and export management, including API endpoints, database schema, and UI for listing, saving, loading, and deleting.
This commit is contained in:
1 parent
7288b3da72
commit
a93b1b36fc
7 files changed
+48
-20
No files matched your search
@@ -18,7 +18,7 @@ export default function ExportsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SnapshotList />
|
||||
<SnapshotList type="export" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export default function SnapshotsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SnapshotList />
|
||||
<SnapshotList type="snapshot" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
@@ -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", {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="flex flex-col items-center justify-center p-20 space-y-4">
|
||||
<Loader2 className="w-8 h-8 text-indigo-500 animate-spin" />
|
||||
<p className="text-slate-400 text-sm">Récupération de vos sessions...</p>
|
||||
<p className="text-slate-400 text-sm">Récupération de l'historique...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -103,9 +107,13 @@ export function SnapshotList() {
|
||||
<div className="w-16 h-16 rounded-full bg-slate-800 flex items-center justify-center mx-auto mb-4 border border-slate-700">
|
||||
<History className="w-8 h-8 text-slate-500" />
|
||||
</div>
|
||||
<h3 className="text-slate-200 font-bold mb-1">Aucun snapshot pour le moment</h3>
|
||||
<h3 className="text-slate-200 font-bold mb-1">
|
||||
{type === "export" ? "Aucun export pour le moment" : "Aucun snapshot pour le moment"}
|
||||
</h3>
|
||||
<p className="text-slate-500 text-sm max-w-xs mx-auto">
|
||||
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."}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user