mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-21 05:58:10 +02:00
feat(produits): page de recherche produit + fiche 360° local/réseau
Nouvelle page /produits : recherche par libellé ou par code centrale, puis fiche complète d'un produit — identité, prix, stock par magasin, ventes sur 12 mois glissants, et l'intégralité des métriques réseau Qlik. Jusqu'ici les données produit n'étaient accessibles que par fournisseur (la Grille) ou par classement (Hit Parade) : aucune route n'acceptait un codein. Les données réseau Qlik n'étaient visibles que dans six colonnes de la grille d'arbitrage. Deux analyses ajoutées : - comparatif local vs réseau, ramené au magasin moyen (indice de performance sur les quantités, seule base commune fiable — le CA local est TTC et la base du CA Qlik n'est pas documentée, donc indicatif) ; - opportunités de la même famille : références que le réseau vend bien et que nous vendons peu, écart calculé par magasin. Détail mensuel réseau enrichi : le cube Qlik renvoyait déjà CA, nb de magasins, CA/magasin et marge % par (article, mois) mais seule la quantité était conservée. Ces mesures sont désormais stockées dans une nouvelle colonne metrics_by_month, sans requête Qlik supplémentaire. qte_by_month reste inchangée — la colonne « Tendance / Réseau » de la Grille en dépend. L'extraction Qlik peut désormais cibler un seul code centrale (/api/qlik/sync?codeCentrale=…), ouverte à tout utilisateur connecté et plafonnée à 3 extractions simultanées ; le mode fournisseur reste réservé aux admins. Refactor sans changement de comportement : les helpers de mois, le calcul de tendance et les graphes SVG quittent heatmap-grid.tsx pour des modules partagés, et le polling des jobs Qlik devient le hook useQlikSyncJob, réutilisé par la Grille et par la fiche. Les calculs de ventes reprennent le SQL canonique de la Grille (genremvt=3 avec quantités niées pour déduire les retours, stock de fin de mois pris sur le dernier mouvement, report sur les mois sans mouvement), afin que les chiffres des deux pages soient identiques par construction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8cMbsdjXWChX8seL9NCP
This commit is contained in:
25 files changed
+2694
-465
No files matched your search
+40
-10
@@ -34,8 +34,16 @@ async function getBrowser(): Promise<Browser> {
|
||||
interface InPageResult {
|
||||
ok: boolean;
|
||||
rows?: Array<Array<string | number>>;
|
||||
/** Quantité réseau par code central et par mois : { code: { "YYYY-MM": qté } }. */
|
||||
monthly?: Record<string, Record<string, number>>;
|
||||
/**
|
||||
* Métriques réseau par code central et par mois : { code: { "YYYY-MM": {…} } }.
|
||||
*
|
||||
* Le cube mensuel renvoie déjà les 6 mesures par (Article Code, Mois) ; on les
|
||||
* conserve toutes. Seule `qte` est systématiquement présente : les mois N-1
|
||||
* dérivés de « Quantité COMP » n'ont que la quantité (COMP ne porte pas les
|
||||
* autres mesures), et la passe complémentaire N-1 les complète quand le champ
|
||||
* `Année` existe.
|
||||
*/
|
||||
monthly?: Record<string, Record<string, { qte: number; ca?: number; nbMag?: number; caMag?: number; margePct?: number }>>;
|
||||
size?: number;
|
||||
error?: string;
|
||||
diag?: Record<string, unknown>;
|
||||
@@ -296,14 +304,21 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
const out: Array<Array<string | number>> = [];
|
||||
// Quantité réseau par code central et par mois (label "YYYY-MM").
|
||||
// Accumulée en parallèle du total, sans toucher l'agrégation existante.
|
||||
const monthlyByCode: Record<string, Record<string, number>> = {};
|
||||
const monthlyByCode: Record<string, Record<string, { qte: number; ca?: number; nbMag?: number; caMag?: number; margePct?: number }>> = {};
|
||||
const accMonthly = (batchRows: Array<Array<string | number>>, monthLabel: string) => {
|
||||
for (const r of batchRows) {
|
||||
const c = String(r[0] ?? "").trim();
|
||||
if (!c || c === "-") continue;
|
||||
const q = Number(r[2]) || 0;
|
||||
(monthlyByCode[c] ??= {});
|
||||
monthlyByCode[c][monthLabel] = (monthlyByCode[c][monthLabel] ?? 0) + q;
|
||||
const cur = monthlyByCode[c][monthLabel];
|
||||
monthlyByCode[c][monthLabel] = {
|
||||
qte: (cur?.qte ?? 0) + q,
|
||||
ca: (cur?.ca ?? 0) + (Number(r[1]) || 0),
|
||||
nbMag: Math.max(cur?.nbMag ?? 0, Number(r[3]) || 0),
|
||||
caMag: Number(r[4]) || 0,
|
||||
margePct: Number(r[5]) || 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
// Compteurs agrégés pour le résumé timing final (loggué côté Node après la sync).
|
||||
@@ -485,14 +500,19 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
}));
|
||||
}
|
||||
(monthlyByCode[code] ??= {});
|
||||
// Les 5 mesures du mois courant de la ligne. Le cube les renvoie
|
||||
// déjà toutes : les conserver ne coûte aucune requête Qlik en plus.
|
||||
const full = { qte, ca, nbMag, caMag, margePct: marge };
|
||||
if (isMainPass && y === yearN) {
|
||||
// Mois de l'année N + son comparable N-1 (COMP = même mois, N-1).
|
||||
monthlyByCode[code][mm] = qte;
|
||||
monthlyByCode[code][mm] = full;
|
||||
const mmPrev = String(y - 1) + mm.slice(4); // "2026-03" → "2025-03"
|
||||
if (monthlyByCode[code][mmPrev] === undefined) monthlyByCode[code][mmPrev] = qteComp;
|
||||
// COMP ne porte QUE la quantité : les autres mesures restent
|
||||
// absentes tant que la passe N-1 ne les a pas complétées.
|
||||
if (monthlyByCode[code][mmPrev] === undefined) monthlyByCode[code][mmPrev] = { qte: qteComp };
|
||||
} else {
|
||||
// Passe année sélectionnée : valeur directe, prioritaire sur COMP.
|
||||
monthlyByCode[code][mm] = qte;
|
||||
// Passe année sélectionnée : valeurs directes, prioritaires sur COMP.
|
||||
monthlyByCode[code][mm] = full;
|
||||
}
|
||||
}, timings, () => { code15Retries++; }), timings);
|
||||
aggBatches++;
|
||||
@@ -826,11 +846,21 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
prev.caParMagasinReseau = prev.nbMagasinsReseau > 0 ? prev.caReseau / prev.nbMagasinsReseau : 0;
|
||||
margeWeight.set(code, nextWeight);
|
||||
}
|
||||
// Rattache la quantité mensuelle (par code central) au métrique agrégé.
|
||||
// Rattache le détail mensuel (par code central) au métrique agrégé.
|
||||
// On produit DEUX représentations :
|
||||
// - `qteByMonth` : { "YYYY-MM": qté } — format historique, dont dépend la
|
||||
// colonne « Tendance / Réseau » de la Grille. Inchangé.
|
||||
// - `metricsByMonth` : toutes les mesures du mois, pour la fiche produit.
|
||||
const monthly = result.monthly ?? {};
|
||||
for (const [code, metric] of out) {
|
||||
const mm = monthly[code];
|
||||
if (mm && Object.keys(mm).length) metric.qteByMonth = mm;
|
||||
if (!mm || Object.keys(mm).length === 0) continue;
|
||||
const qteByMonth: Record<string, number> = {};
|
||||
for (const [mois, v] of Object.entries(mm)) {
|
||||
qteByMonth[mois] = Number(v?.qte) || 0;
|
||||
}
|
||||
metric.qteByMonth = qteByMonth;
|
||||
metric.metricsByMonth = mm;
|
||||
}
|
||||
// Diagnostic CONCLUSIF : combien de codes ont des mois qui VARIENT vs identiques.
|
||||
// Si presque tous varient → la mesure respecte le mois (feature OK).
|
||||
|
||||
Reference in new issue
Block a user