mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
Grille - Corrige le lot 4 : au changement de magasin, les chiffres et le tri affichés suivent tout de suite (nouvelle liste), la sélection est gardée. - Lignes toujours chargées « Nos 2 magasins » ; changement de magasin sans aller-retour serveur (URL mise à jour par l'historique du navigateur). - Complément de l'API FF par magasin servi à part (/api/grid/rows/store-patch), appliqué à son arrivée, calcul identique à l'ancien (lib/store-patch.ts). - La recherche ne reconstruit plus toutes les lignes à chaque frappe ; la sélection n'est remise à zéro qu'au changement de fournisseur. Sécurité - lib/authz.ts : session relue en base (cache 1 min), rôle de la base prioritaire ; variantes pour actions, routes et pages. - Paramètres réservés aux administrateurs ; session vérifiée dans les actions Grille, Commandes, Historique et les routes ; diagnostics réservés admin. - Mots de passe et clés d'IA plus envoyés au navigateur ; champ vide = mot de passe conservé ; plus de mot de passe dans le localStorage ; l'enregistrement de la base n'efface plus les clés d'IA. - admin/admin créé seulement si aucun utilisateur ; fichier de secours utilisé uniquement si la base est injoignable ; data/ ignoré par git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
115 lines
4.5 KiB
TypeScript
115 lines
4.5 KiB
TypeScript
"use server";
|
|
|
|
import { db } from "@/db";
|
|
import { sessionSnapshots } from "@/db/schema";
|
|
import { z } from "zod";
|
|
import { sql, and, eq } from "drizzle-orm";
|
|
import { verifierSession } from "@/lib/authz";
|
|
import { revalidatePath } from "next/cache";
|
|
|
|
const SaveSnapshotSchema = z.object({
|
|
codeFournisseur: z.string(),
|
|
nomFournisseur: z.string().optional(),
|
|
magasin: z.string(),
|
|
label: z.string().optional(),
|
|
changes: z.record(z.string(), z.object({
|
|
before: z.string().nullable(),
|
|
after: z.string(),
|
|
})),
|
|
type: z.enum(["snapshot", "export"]).optional(),
|
|
summary: z.object({
|
|
totalRows: z.number(),
|
|
totalCa: z.number(),
|
|
totalMarge: z.number(),
|
|
tauxMargeGlobal: z.number(),
|
|
}).optional(),
|
|
});
|
|
|
|
export async function saveSnapshot(raw: unknown) {
|
|
const acces = await verifierSession();
|
|
if (!acces.ok) return { success: false, error: acces.message };
|
|
|
|
const parsed = SaveSnapshotSchema.safeParse(raw);
|
|
if (!parsed.success) {
|
|
console.error("Snapshot validation failed:", parsed.error.format());
|
|
return { success: false, error: "Validation failed: " + parsed.error.issues.map((i: any) => i.message).join(", ") };
|
|
}
|
|
|
|
const { codeFournisseur, nomFournisseur, magasin, label, changes, summary, type } = parsed.data;
|
|
const rawUserId = acces.utilisateur.id;
|
|
const userId = rawUserId ? parseInt(rawUserId, 10) : null;
|
|
|
|
console.log(`[saveSnapshot] User: ${userId} (raw: ${rawUserId}), Supplier: ${codeFournisseur}, Type: ${type}`);
|
|
|
|
const finalUserId = (userId && !isNaN(userId)) ? userId : null;
|
|
|
|
try {
|
|
const [created] = await db
|
|
.insert(sessionSnapshots)
|
|
.values({
|
|
userId: finalUserId,
|
|
codeFournisseur,
|
|
nomFournisseur: nomFournisseur ?? null,
|
|
magasin,
|
|
label: label ?? `${type === 'export' ? 'Export' : 'Snapshot'} — ${new Date().toLocaleDateString("fr-FR")}`,
|
|
changes,
|
|
summaryJson: summary ?? null,
|
|
type: type ?? "snapshot",
|
|
})
|
|
.returning({ id: sessionSnapshots.id });
|
|
|
|
revalidatePath("/snapshots");
|
|
revalidatePath("/exports");
|
|
return { success: true, snapshotId: created?.id };
|
|
} catch (err) {
|
|
console.error("Initial snapshot save failed, attempting auto-repair...", err);
|
|
|
|
try {
|
|
// Tentative systématique de création de table (no-op si déjà là)
|
|
await db.execute(sql`
|
|
CREATE TABLE IF NOT EXISTS session_snapshots (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER,
|
|
code_fournisseur VARCHAR(20) NOT NULL,
|
|
nom_fournisseur VARCHAR(255),
|
|
magasin VARCHAR(20) NOT NULL,
|
|
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' ou 'user_id', on les ajoute
|
|
try {
|
|
await db.execute(sql`ALTER TABLE session_snapshots ADD COLUMN IF NOT EXISTS type VARCHAR(20) DEFAULT 'snapshot'`);
|
|
await db.execute(sql`ALTER TABLE session_snapshots ADD COLUMN IF NOT EXISTS user_id INTEGER`);
|
|
} catch (e) {
|
|
// Ignore
|
|
}
|
|
|
|
// Deuxième tentative d'insertion
|
|
const [retryCreated] = await db
|
|
.insert(sessionSnapshots)
|
|
.values({
|
|
userId: finalUserId,
|
|
codeFournisseur,
|
|
nomFournisseur: nomFournisseur ?? null,
|
|
magasin,
|
|
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) {
|
|
// Le détail technique (les deux erreurs) reste dans les journaux du serveur.
|
|
console.error("Snapshot auto-repair/retry failed:", repairErr);
|
|
return { success: false, error: "erreur technique sur le serveur" };
|
|
}
|
|
}
|
|
}
|