From 2c02807493e3617e7577d34607ec5b609ab73958 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 7 Apr 2026 08:38:25 +0200 Subject: [PATCH] feat: implement Hit Parade dashboard with PostgreSQL-backed data aggregation and pivot logic --- src/app/(dashboard)/hit-parade/client.tsx | 33 ++++++++++++++++++++--- src/app/(dashboard)/hit-parade/page.tsx | 2 ++ src/lib/pg-ff-client.ts | 6 ++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/app/(dashboard)/hit-parade/client.tsx b/src/app/(dashboard)/hit-parade/client.tsx index 5e5bd03..c83208e 100644 --- a/src/app/(dashboard)/hit-parade/client.tsx +++ b/src/app/(dashboard)/hit-parade/client.tsx @@ -32,6 +32,7 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) { const [sortKey, setSortKey] = useState(DEFAULT_SORT); const [sortDir, setSortDir] = useState(DEFAULT_DIR); const [filterFournisseur, setFilterFournisseur] = useState("Tous"); + const [filterNomenclature, setFilterNomenclature] = useState("Tous"); // Dates locales pour les inputs (évite rechargement à chaque frappe) const [localDebut, setLocalDebut] = useState(dateDebut); @@ -42,6 +43,11 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) { return ["Tous", ...Array.from(set).sort((a, b) => a.localeCompare(b, "fr"))]; }, [pivotted]); + const nomenclatures = useMemo(() => { + const set = new Set(pivotted.map(r => r.nomenclature)); + return ["Tous", ...Array.from(set).sort((a, b) => a.localeCompare(b, "fr"))]; + }, [pivotted]); + function handleSort(key: SortKey) { if (sortKey === key) { setSortDir(d => d === "desc" ? "asc" : "desc"); @@ -65,8 +71,11 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) { const isSorted = sortKey !== DEFAULT_SORT || sortDir !== DEFAULT_DIR; const filtered = useMemo(() => - filterFournisseur === "Tous" ? pivotted : pivotted.filter(r => r.fournisseur === filterFournisseur), - [pivotted, filterFournisseur] + pivotted.filter(r => + (filterFournisseur === "Tous" || r.fournisseur === filterFournisseur) && + (filterNomenclature === "Tous" || r.nomenclature === filterNomenclature) + ), + [pivotted, filterFournisseur, filterNomenclature] ); const sorted = useMemo(() => @@ -161,6 +170,22 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) {
+ {/* Filtre nomenclature */} +
+ + +
+ +
+ {sorted.length} produits @@ -183,6 +208,7 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) { Code Désignation Fournisseur + Nomenclature Frouard / Nancy — 292 @@ -214,6 +240,7 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) { {row.codein} {row.libelle.trim()} {row.fournisseur} + {row.nomenclature} {row.qte292 > 0 ? formatQte(row.qte292) : —} @@ -255,7 +282,7 @@ export function HitParadeClient({ dateDebut, dateFin, pivotted }: Props) { - + TOTAL — {sorted.length} articles {formatQte(totals.qte292)} diff --git a/src/app/(dashboard)/hit-parade/page.tsx b/src/app/(dashboard)/hit-parade/page.tsx index 65b9c6b..95de543 100644 --- a/src/app/(dashboard)/hit-parade/page.tsx +++ b/src/app/(dashboard)/hit-parade/page.tsx @@ -5,6 +5,7 @@ export interface HitParadePivotRow { codein: string; libelle: string; fournisseur: string; + nomenclature: string; qte292: number; ca292: number; marge292: number; @@ -28,6 +29,7 @@ function pivotHitParade(rows: HitParadeRow[]): HitParadePivotRow[] { codein: row.codein, libelle: row.libelle, fournisseur: row.fournisseur, + nomenclature: row.nomenclature, qte292: 0, ca292: 0, marge292: 0, qte579: 0, ca579: 0, marge579: 0, qteTotal: 0, caTotal: 0, margeTotal: 0, diff --git a/src/lib/pg-ff-client.ts b/src/lib/pg-ff-client.ts index 51bbf60..e523910 100644 --- a/src/lib/pg-ff-client.ts +++ b/src/lib/pg-ff-client.ts @@ -717,6 +717,7 @@ export interface HitParadeRow { codein: string; libelle: string; fournisseur: string; + nomenclature: string; site: string; qte_vendue: number; ca_ttc: number; @@ -738,6 +739,7 @@ export async function pgGetHitParade(dateDebut: string, dateFin: string): Promis a.codein::text AS codein, a.libelle1::text AS libelle, COALESCE(fi.nom, af.code, 'Sans fournisseur')::text AS fournisseur, + COALESCE(n.libelle, 'Sans nomenclature')::text AS nomenclature, m.site, SUM(ABS(m.qtemvt))::float AS qte_vendue, ABS(SUM(m.mntmvtttc))::float AS ca_ttc, @@ -746,10 +748,11 @@ export async function pgGetHitParade(dateDebut: string, dateFin: string): Promis JOIN articles a ON a.no_id = m.artnoid LEFT JOIN artfou1 af ON af.art_no_id = a.no_id AND af.preference = 1 LEFT JOIN fouident fi ON fi.code = af.code + LEFT JOIN nomenclature n ON n.no_id = a.nom_no_id WHERE m.datmvt BETWEEN ${dateDebut}::date AND ${dateFin}::date AND m.site IN ('292', '579') AND m.genremvt = 3 - GROUP BY a.no_id, a.codein, a.libelle1, fi.nom, af.code, m.site + GROUP BY a.no_id, a.codein, a.libelle1, fi.nom, af.code, n.libelle, m.site ), stock_agg AS ( SELECT @@ -765,6 +768,7 @@ export async function pgGetHitParade(dateDebut: string, dateFin: string): Promis v.codein, v.libelle, v.fournisseur, + v.nomenclature, v.site, v.qte_vendue, v.ca_ttc,