From 8b7d81b20c031064bc96cbcd8a21b3830f115acf Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Thu, 19 Mar 2026 12:41:03 +0100 Subject: [PATCH] feat: implement product grid with comprehensive product data fetching, processing, and heatmap display. --- src/features/grid/api/get-product-rows.ts | 14 ++++- src/features/grid/components/heatmap-grid.tsx | 52 +++++++++++++++++ src/lib/api-ff-client.ts | 56 +++++++++++++++++++ src/types/grid.ts | 6 ++ 4 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/features/grid/api/get-product-rows.ts b/src/features/grid/api/get-product-rows.ts index 001d55b..e68aa00 100644 --- a/src/features/grid/api/get-product-rows.ts +++ b/src/features/grid/api/get-product-rows.ts @@ -7,6 +7,7 @@ import { getMensuelByArticles, getReferentielByArticles, getCommandesByFournisseur, + getRankingByArticles, buildLast12MonthsRange, } from "@/lib/api-ff-client"; import { db } from "@/db"; @@ -30,10 +31,11 @@ export async function getProductRows(input: GetProductRowsInput): Promise
Rk CA
Réseau
, + size: 70, + cell: ({ getValue }) => { + const val = getValue(); + return ( +
+ {val != null ? `#${val}` : "-"} +
+ ); + }, + }, + { + accessorKey: "rankingQte", + header: () =>
Rk Qté
Réseau
, + size: 70, + cell: ({ getValue }) => { + const val = getValue(); + return ( +
+ {val != null ? `#${val}` : "-"} +
+ ); + }, + }, + { + accessorKey: "rankingMagCa", + header: () =>
Rk CA
Mag.
, + size: 70, + cell: ({ getValue }) => { + const val = getValue(); + return ( +
+ {val != null ? `#${val}` : "-"} +
+ ); + }, + }, + { + accessorKey: "rankingMagQte", + header: () =>
Rk Qté
Mag.
, + size: 70, + cell: ({ getValue }) => { + const val = getValue(); + return ( +
+ {val != null ? `#${val}` : "-"} +
+ ); + }, + }, ...MONTHS_12.map((monthKey) => ({ id: `month_${monthKey}`, header: () =>
{formatMonthLabel(monthKey)}
, diff --git a/src/lib/api-ff-client.ts b/src/lib/api-ff-client.ts index 107dcfc..b7a9273 100644 --- a/src/lib/api-ff-client.ts +++ b/src/lib/api-ff-client.ts @@ -419,6 +419,62 @@ export async function getCommandesByFournisseur( return result; } +// --------------------------------------------------------------------------- +// Ranking — classement réseau et magasin +// GET /api/ranking?gencod= +// --------------------------------------------------------------------------- + +export interface FfRanking { + ranking_ca?: number; + ranking_qte?: number; + ranking_mag_ca?: number; + ranking_mag_qte?: number; +} + +/** + * Récupère le ranking (réseau + magasin) pour un lot d'articles via leur GTIN (EAN). + * Retourne Map + */ +export async function getRankingByArticles( + articles: { codein: string; gtin?: string }[], + batchSize = 20 +): Promise> { + const result = new Map(); + + const withGtin = articles.filter(a => a.gtin && a.gtin.trim().length > 0); + if (withGtin.length === 0) return result; + + for (let i = 0; i < withGtin.length; i += batchSize) { + const batch = withGtin.slice(i, i + batchSize); + await Promise.all( + batch.map(async (art) => { + try { + const url = `${FF_API_BASE}/api/ranking?gencod=${encodeURIComponent(art.gtin!.trim())}`; + const res = await fetch(url, { cache: "no-store" }); + if (!res.ok) return; + const data = await res.json(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = Array.isArray(data) ? data : (data?.data ?? data?.items ?? [data]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const entry: any = Array.isArray(items) ? items[0] : items; + if (!entry) return; + result.set(art.codein, { + ranking_ca: entry.ranking_ca != null ? Number(entry.ranking_ca) : undefined, + ranking_qte: entry.ranking_qte != null ? Number(entry.ranking_qte) : undefined, + ranking_mag_ca: entry.ranking_mag_ca != null ? Number(entry.ranking_mag_ca) : undefined, + ranking_mag_qte: entry.ranking_mag_qte != null ? Number(entry.ranking_mag_qte) : undefined, + }); + } catch (err) { + console.error(`[api-ff] getRanking error for ${art.codein} (gtin: ${art.gtin}):`, err); + } + }) + ); + } + + console.log(`[api-ff] Ranking: ${result.size}/${withGtin.length} articles enrichis`); + return result; +} + // --------------------------------------------------------------------------- // Statut de synchronisation // --------------------------------------------------------------------------- diff --git a/src/types/grid.ts b/src/types/grid.ts index 96b0d62..f0e9582 100644 --- a/src/types/grid.ts +++ b/src/types/grid.ts @@ -93,6 +93,12 @@ export interface ProductRow { shareQty?: number; /** Référentiels globaux */ totalFournisseurCa?: number; + /** Ranking réseau (classement global) */ + rankingCa?: number; + rankingQte?: number; + /** Ranking magasin */ + rankingMagCa?: number; + rankingMagQte?: number; } /** Summary bar totals for the currently visible/filtered rows */