"use client"; import React from "react"; import { useGridStore } from "@/features/grid/store/use-grid-store"; import { SyncQlikButton } from "./sync-qlik-button"; import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts"; import { ChevronDown, Download, FileSpreadsheet, FileText, History, Loader2, RotateCcw, Save, Table2 } from "lucide-react"; import { useMemo, useState, useTransition } from "react"; import { saveSnapshot } from "@/features/snapshots/api/save-snapshot"; import { isStaleServerActionError, STALE_ACTION_MESSAGE } from "@/lib/stale-action"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { confirmer, toast } from "@/components/ui/feedback"; import { fmtDecimal1, fmtEntier, fmtEur0 } from "@/lib/format"; import { couleurMarge } from "@/lib/marge"; import { exporterFichierGammes, exporterGammeA, exporterTousProduits, imprimerPdf, lignesModifiees, } from "@/features/grid/lib/exports"; function Stat({ label, value, sub, subColor }: { label: string; value: string; sub?: string; subColor?: string }) { return (
{label} {value} {sub && {sub}}
); } function messageErreur(err: unknown): string { if (isStaleServerActionError(err)) return STALE_ACTION_MESSAGE; return err instanceof Error ? err.message : String(err); } /** * Barre du bas de la Grille : totaux du périmètre affiché et TOUTES les actions * sur les gammes (annuler, enregistrer, exporter). Il y avait des doublons dans * l'en-tête, avec des droits différents. */ function FloatingSummaryBarInner({ isAdmin, nomFournisseur }: { isAdmin: boolean; nomFournisseur: string }) { const summary = useGridStore((s) => s.summary); const resetDrafts = useGridStore((s) => s.resetDrafts); const rows = useGridStore((s) => s.rows); const filterFournisseur = useGridStore((s) => s.filters.codeFournisseur); // Magasin affiché (bascule de la Grille). `filters.magasin` n'est jamais // renseigné : les sessions et validations étaient toutes notées « TOTAL ». const activeMagasin = useGridStore((s) => s.activeMagasin); const [isPending, startTransition] = useTransition(); const [enCours, setEnCours] = useState(null); const { save, hasDrafts, count } = useSaveDrafts(activeMagasin || "TOTAL"); const supplierCode = filterFournisseur || rows[0]?.codeFournisseur; const lastQlikUpdate = useMemo(() => { let max: string | null = null; for (const r of rows) { if (r.networkFetchedAt && (!max || r.networkFetchedAt > max)) max = r.networkFetchedAt; } return max; }, [rows]); const handleSave = () => { startTransition(async () => { try { const result = await save(); if (result.success) { toast.succes(`${result.saved} gamme${result.saved > 1 ? "s" : ""} enregistrée${result.saved > 1 ? "s" : ""}.`); } else { toast.erreur(`Enregistrement impossible : ${result.error ?? "erreur inconnue"}`); } } catch (err) { toast.erreur(`Enregistrement impossible : ${messageErreur(err)}`); } }); }; const handleReset = async () => { const ok = await confirmer({ titre: "Annuler les modifications ?", message: `Les ${count} gamme${count > 1 ? "s" : ""} modifiée${count > 1 ? "s" : ""} et non enregistrée${count > 1 ? "s" : ""} reviendront à leur valeur précédente.`, libelleConfirmer: "Annuler les modifications", danger: true, }); if (ok) { resetDrafts(); toast.info("Modifications annulées."); } }; /** * Enregistre une copie de la session dans l'Historique. * Renvoie l'identifiant créé, ou `null` en cas d'échec (déjà signalé). */ const handleSnapshot = async (labelOverride?: string, type: "snapshot" | "export" = "snapshot") => { if (rows.length === 0) return null; const { draftChanges } = useGridStore.getState(); // Tous les changements : brouillons + gammes déjà enregistrées (≠ FF) const changes = Object.fromEntries( lignesModifiees().map(r => [ r.codein, { before: r.codeGammeInit, after: (draftChanges[r.codein] ?? r.codeGamme) as string }, ]) ); try { const res = await saveSnapshot({ codeFournisseur: filterFournisseur || rows[0].codeFournisseur, nomFournisseur: rows[0].nomFournisseur, magasin: activeMagasin || "TOTAL", label: labelOverride || `Session ${rows[0].nomFournisseur} — ${new Date().toLocaleString("fr-FR", { dateStyle: "short", timeStyle: "short" })}`, changes, type, summary: { totalRows: summary.totalRows, totalCa: summary.totalCa, totalMarge: summary.totalMarge, tauxMargeGlobal: summary.tauxMargeGlobal, }, }); if (!res.success) throw new Error(res.error); if (!labelOverride) toast.succes("Session enregistrée dans l'Historique."); return res.snapshotId; } catch (err) { if (!labelOverride) toast.erreur(`La session n'a pas pu être enregistrée : ${messageErreur(err)}`); return null; } }; /** Lance un export avec indicateur et messages d'erreur communs. */ const lancer = async (cle: string, action: () => Promise, vide: string) => { if (enCours) return; setEnCours(cle); try { const ok = await action(); if (!ok) toast.info(vide); } catch (err) { toast.erreur(`L'export a échoué : ${messageErreur(err)}`); } finally { setEnCours(null); } }; return (
{isAdmin && } Fichier d'import des gammes lancer("modifs", async () => { if (lignesModifiees().length === 0) return false; const nom = lignesModifiees()[0].nomFournisseur; // Copie dans l'Historique, onglet « Exports » await handleSnapshot(`Export ${nom} — ${new Date().toLocaleString("fr-FR", { dateStyle: "short", timeStyle: "short" })}`, "export"); return exporterFichierGammes(true); }, "Aucune gamme n'a été modifiée par rapport à FF.")} >
Gammes modifiées seulement Les produits dont la gamme a changé
lancer("complet", () => exporterFichierGammes(false), "Aucun produit à exporter.")} >
Toutes les gammes Tous les produits du fournisseur
Documents de travail lancer("tous", () => exporterTousProduits(nomFournisseur), "Aucun produit à exporter.")} >
Tableau complet (Excel) Gamme avant / après et chiffres sur 12 mois
lancer("gammeA", exporterGammeA, "Aucun produit en gamme A.")} >
Liste des produits en gamme A (Excel) Code-barres, référence, libellé, magasins
lancer("pdf", () => imprimerPdf(nomFournisseur), "Aucun produit à imprimer.")} >
Tableau imprimable (PDF) Mise en page paysage
{ void handleSnapshot(); }}>
Garder une copie dans l'Historique Pour reprendre cette session plus tard
{hasDrafts && ( )}
); } /** * Mémoïsé : ses props sont stables, il n'a donc pas à se redessiner quand le * parent change d'état (sélection, progression du chargement…). */ export const FloatingSummaryBar = React.memo(FloatingSummaryBarInner);