"use client"; import { useRouter } from "next/navigation"; import { useMemo, useTransition } from "react"; import { Loader2 } from "lucide-react"; import { DataTable, type DataColumn } from "@/components/ui/data-table"; import { Segmented, type TabItem } from "@/components/ui/tabs"; import { Select } from "@/components/ui/form-controls"; import { DeltaBadge } from "@/components/ui/badge"; import { Terme } from "@/components/ui/tooltip"; import { MAGASINS, nomMagasin } from "@/lib/magasins"; import { fmtEur2 } from "@/lib/format"; import { telechargerExcel } from "@/lib/export-excel"; import { cn } from "@/lib/utils"; export type ModeAnalytics = "fournisseur" | "nomenclature"; export interface OptionMois { value: string; label: string; } export interface AnalyticsRow { key: string; label: string; ca292: number; caN1_292: number; ca579: number; caN1_579: number; caTotal: number; caN1Total: number; evolutionTotal: number | null; } interface AnalyticsClientProps { mode: ModeAnalytics; /** Mois affiché, « AAAA-MM ». */ mois: string; /** Mois en clair : « octobre 2026 ». */ libelleMois: string; libelleMoisN1: string; moisDisponibles: OptionMois[]; pivotted: AnalyticsRow[]; } /** Champs numériques de la ligne pivotée. */ type Champ = { [K in keyof AnalyticsRow]: AnalyticsRow[K] extends number ? K : never }[keyof AnalyticsRow]; interface Groupe { id: string; titre: string; ca: Champ; caN1: Champ; } /** Champs de la ligne pivotée pour chaque magasin (la requête en fournit un jeu par magasin). */ const CHAMPS_MAGASIN: Record> = { "292": { ca: "ca292", caN1: "caN1_292" }, "579": { ca: "ca579", caN1: "caN1_579" }, }; const GROUPES: Groupe[] = [ ...MAGASINS.filter((m) => CHAMPS_MAGASIN[m.code]).map((m) => ({ id: m.code, titre: nomMagasin(m.code), ...CHAMPS_MAGASIN[m.code], })), { id: "total", titre: "Total nos magasins", ca: "caTotal", caN1: "caN1Total" }, ]; const MODES: TabItem[] = [ { value: "fournisseur", label: "Fournisseur" }, { value: "nomenclature", label: "Famille" }, ]; const somme = (rows: readonly AnalyticsRow[], champ: Champ) => rows.reduce((acc, r) => acc + r[champ], 0); /** Évolution en % par rapport à N-1 ; `null` sans chiffre d'affaires N-1. */ const evolution = (ca: number, caN1: number) => (caN1 > 0 ? ((ca - caN1) / caN1) * 100 : null); const recherche = (r: AnalyticsRow) => [r.label, r.key]; function Montant({ v }: { v: number }) { return {fmtEur2(v)}; } export function AnalyticsClient({ mode, mois, libelleMois, libelleMoisN1, moisDisponibles, pivotted, }: AnalyticsClientProps) { const router = useRouter(); const [navigation, demarrerNavigation] = useTransition(); const handleModeChange = (newMode: string) => { demarrerNavigation(() => { router.push(`/analytics?mode=${newMode}&mois=${mois}`); }); }; const handleMonthChange = (newMois: string) => { demarrerNavigation(() => { router.push(`/analytics?mode=${mode}&mois=${newMois}`); }); }; const libelleColonne = mode === "fournisseur" ? "Fournisseur" : "Famille"; const colonnes = useMemo[]>(() => [ { id: "label", header: libelleColonne, sortValue: (r) => r.label, cell: (r) => {r.label}, footer: () => "Total", className: "min-w-[140px] sm:min-w-[220px]", sticky: true, grow: true, }, ...GROUPES.flatMap((g): DataColumn[] => { const total = g.id === "total"; // En-tête commun, séparation et teinte du total : gérés par DataTable. const commun = { group: g.titre, highlight: total, align: "right" as const }; return [ { ...commun, id: `${g.id}-ca`, header: libelleMois, hint: `Chiffre d'affaires TTC (${g.titre}) en ${libelleMois}, retours déduits`, sortValue: (r) => r[g.ca], cell: (r) => , footer: (rows) => , className: cn(total && "font-semibold"), }, { ...commun, id: `${g.id}-n1`, header: {libelleMoisN1} · , hint: `Chiffre d'affaires TTC (${g.titre}) en ${libelleMoisN1}, même mois l'année précédente`, sortValue: (r) => r[g.caN1], cell: (r) => , footer: (rows) => , className: cn(total && "font-semibold"), }, { ...commun, id: `${g.id}-evolution`, header: "Évolution", hint: `Évolution du chiffre d'affaires (${g.titre}) entre ${libelleMoisN1} et ${libelleMois}`, sortValue: (r) => evolution(r[g.ca], r[g.caN1]), cell: (r) => , footer: (rows) => , }, ]; }), ], [libelleColonne, libelleMois, libelleMoisN1]); async function exporter(rows: readonly AnalyticsRow[]) { const arrondi = (v: number) => Math.round(v * 100) / 100; const evol = (ca: number, caN1: number) => { const e = evolution(ca, caN1); return e === null ? null : Math.round(e * 10) / 10; }; await telechargerExcel({ feuille: "Ventes par mois", fichier: `ventes-par-mois_${mode === "fournisseur" ? "fournisseurs" : "familles"}_${mois}`, entetes: [ libelleColonne, ...GROUPES.flatMap((g) => [ `${g.titre} — CA ${libelleMois}`, `${g.titre} — CA ${libelleMoisN1} (N-1)`, `${g.titre} — Évolution (%)`, ]), ], largeurs: [40, ...GROUPES.flatMap(() => [22, 22, 16])], lignes: rows.map((r) => [ r.label, ...GROUPES.flatMap((g) => [arrondi(r[g.ca]), arrondi(r[g.caN1]), evol(r[g.ca], r[g.caN1])]), ]), totaux: [ "Total", ...GROUPES.flatMap((g) => { const ca = somme(rows, g.ca); const caN1 = somme(rows, g.caN1); return [arrondi(ca), arrondi(caN1), evol(ca, caN1)]; }), ], }); } return (
{/* Contrôles (relancent le calcul côté serveur) */}
Afficher par