diff --git a/src/app/(dashboard)/hit-parade/client.tsx b/src/app/(dashboard)/hit-parade/client.tsx index 7c75c66..6159892 100644 --- a/src/app/(dashboard)/hit-parade/client.tsx +++ b/src/app/(dashboard)/hit-parade/client.tsx @@ -4,28 +4,21 @@ import { useRouter } from "next/navigation"; import { useState, useMemo } from "react"; import type { HitParadePivotRow } from "./page"; -type SortKey = "caTotal" | "ca292" | "ca579" | "qteTotal" | "qte292" | "qte579" | "margeTotal"; +type SortKey = "caTotal" | "ca292" | "ca579" | "qteTotal" | "qte292" | "qte579"; type SortDir = "desc" | "asc"; +const DEFAULT_SORT: SortKey = "caTotal"; +const DEFAULT_DIR: SortDir = "desc"; + function formatCA(v: number) { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(v); } - function formatQte(v: number) { return new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 0 }).format(v); } - function formatPct(ca: number, marge: number) { - if (ca === 0) return null; - return ((marge / ca) * 100).toFixed(1) + "%"; -} - -function SortBtn({ active, dir, onClick }: { active: boolean; dir: SortDir; onClick: () => void }) { - return ( - - ); + if (ca === 0) return "—"; + return ((marge / ca) * 100).toFixed(1) + " %"; } interface Props { @@ -34,10 +27,10 @@ interface Props { pivotted: HitParadePivotRow[]; } -export function HitParadeClient({ mois, currentMois, pivotted }: Props) { +export function HitParadeClient({ mois, pivotted }: Props) { const router = useRouter(); - const [sortKey, setSortKey] = useState("caTotal"); - const [sortDir, setSortDir] = useState("desc"); + const [sortKey, setSortKey] = useState(DEFAULT_SORT); + const [sortDir, setSortDir] = useState(DEFAULT_DIR); const months: { value: string; label: string }[] = []; const now = new Date(); @@ -48,13 +41,6 @@ export function HitParadeClient({ mois, currentMois, pivotted }: Props) { months.push({ value, label: label.charAt(0).toUpperCase() + label.slice(1) }); } - const sorted = useMemo(() => { - return [...pivotted].sort((a, b) => { - const diff = a[sortKey] - b[sortKey]; - return sortDir === "desc" ? -diff : diff; - }); - }, [pivotted, sortKey, sortDir]); - function handleSort(key: SortKey) { if (sortKey === key) { setSortDir(d => d === "desc" ? "asc" : "desc"); @@ -64,101 +50,176 @@ export function HitParadeClient({ mois, currentMois, pivotted }: Props) { } } + function resetSort() { + setSortKey(DEFAULT_SORT); + setSortDir(DEFAULT_DIR); + } + + const isSorted = sortKey !== DEFAULT_SORT || sortDir !== DEFAULT_DIR; + + const sorted = useMemo(() => { + return [...pivotted].sort((a, b) => { + const diff = a[sortKey] - b[sortKey]; + return sortDir === "desc" ? -diff : diff; + }); + }, [pivotted, sortKey, sortDir]); + const totals = useMemo(() => pivotted.reduce( (acc, r) => ({ - qte292: acc.qte292 + r.qte292, - ca292: acc.ca292 + r.ca292, - marge292: acc.marge292 + r.marge292, - qte579: acc.qte579 + r.qte579, - ca579: acc.ca579 + r.ca579, - marge579: acc.marge579 + r.marge579, - qteTotal: acc.qteTotal + r.qteTotal, - caTotal: acc.caTotal + r.caTotal, - margeTotal: acc.margeTotal + r.margeTotal, + qte292: acc.qte292 + r.qte292, ca292: acc.ca292 + r.ca292, marge292: acc.marge292 + r.marge292, + qte579: acc.qte579 + r.qte579, ca579: acc.ca579 + r.ca579, marge579: acc.marge579 + r.marge579, + qteTotal: acc.qteTotal + r.qteTotal, caTotal: acc.caTotal + r.caTotal, margeTotal: acc.margeTotal + r.margeTotal, }), { qte292: 0, ca292: 0, marge292: 0, qte579: 0, ca579: 0, marge579: 0, qteTotal: 0, caTotal: 0, margeTotal: 0 } ), [pivotted]); - function th(label: string, key: SortKey) { + function ColHeader({ label, sortable, k }: { label: string; sortable?: SortKey; k?: string }) { + const isActive = sortable && sortKey === sortable; return ( - + handleSort(sortable) : undefined} + className={[ + "px-4 py-2 text-right text-xs font-semibold whitespace-nowrap select-none", + sortable ? "cursor-pointer hover:bg-blue-50" : "", + isActive ? "bg-blue-50 text-blue-700" : "text-gray-600", + ].join(" ")} + > {label} - handleSort(key)} /> + {sortable && ( + + {isActive ? (sortDir === "desc" ? "↓" : "↑") : ↕} + + )} ); } return (
- {/* Controls */} -
- - - {sorted.length} produits + {/* Barre de contrôles */} +
+
+ Mois + +
+ +
+ + + {sorted.length} produits + + + {isSorted && ( + + )} + + {!isSorted && ( + Trié par CA Total ↓ — cliquez sur une colonne pour trier + )}
- {/* Table */} -
- + {/* Tableau */} +
+
- - - - - - - + {/* Ligne 1 — groupes */} + + + + + + + - - {th("Qté", "qte292")} - {th("CA TTC", "ca292")} - - {th("Qté", "qte579")} - {th("CA TTC", "ca579")} - - {th("Qté", "qteTotal")} - {th("CA TTC", "caTotal")} - + {/* Ligne 2 — sous-colonnes */} + + + + + + + + + + - {sorted.map((row, idx) => ( - - - - - - - - - - - - - - - ))} + {sorted.map((row, idx) => { + const highlight = sortKey.includes("292") ? "bg-blue-50/30" : sortKey.includes("579") ? "bg-violet-50/30" : ""; + return ( + + + + + {/* 292 */} + + + + {/* 579 */} + + + + {/* Total */} + + + + + ); + })} - - - - - - - - - - + + + + + + + + + +
CodeDésignationFournisseurFrouard/Nancy (292)Houdemont (579)Total Réseau
CodeDésignationFournisseur + Frouard / Nancy — 292 + + Houdemont — 579 + + Total Réseau +
MargeMargeMarge
% Marge% Marge% Marge
{row.codein}{row.libelle.trim()}{row.fournisseur}{row.qte292 > 0 ? formatQte(row.qte292) : ""}{row.ca292 > 0 ? formatCA(row.ca292) : ""}{row.ca292 > 0 ? formatPct(row.ca292, row.marge292) : ""}{row.qte579 > 0 ? formatQte(row.qte579) : ""}{row.ca579 > 0 ? formatCA(row.ca579) : ""}{row.ca579 > 0 ? formatPct(row.ca579, row.marge579) : ""}{row.qteTotal > 0 ? formatQte(row.qteTotal) : ""}{row.caTotal > 0 ? formatCA(row.caTotal) : ""}{row.caTotal > 0 ? formatPct(row.caTotal, row.margeTotal) : ""}
{row.codein}{row.libelle.trim()}{row.fournisseur} + {row.qte292 > 0 ? formatQte(row.qte292) : —} + + {row.ca292 > 0 ? formatCA(row.ca292) : —} + + {row.ca292 > 0 ? formatPct(row.ca292, row.marge292) : ""} + + {row.qte579 > 0 ? formatQte(row.qte579) : —} + + {row.ca579 > 0 ? formatCA(row.ca579) : —} + + {row.ca579 > 0 ? formatPct(row.ca579, row.marge579) : ""} + + {row.qteTotal > 0 ? formatQte(row.qteTotal) : —} + + {row.caTotal > 0 ? formatCA(row.caTotal) : —} + + {row.caTotal > 0 ? formatPct(row.caTotal, row.margeTotal) : ""} +
TOTAL{formatQte(totals.qte292)}{formatCA(totals.ca292)}{formatPct(totals.ca292, totals.marge292)}{formatQte(totals.qte579)}{formatCA(totals.ca579)}{formatPct(totals.ca579, totals.marge579)}{formatQte(totals.qteTotal)}{formatCA(totals.caTotal)}{formatPct(totals.caTotal, totals.margeTotal)} + TOTAL — {sorted.length} articles + {formatQte(totals.qte292)}{formatCA(totals.ca292)}{formatPct(totals.ca292, totals.marge292)}{formatQte(totals.qte579)}{formatCA(totals.ca579)}{formatPct(totals.ca579, totals.marge579)}{formatQte(totals.qteTotal)}{formatCA(totals.caTotal)}{formatPct(totals.caTotal, totals.margeTotal)}