mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-14 18:58:17 +02:00
feat(qlik): extract 4 decision metrics (CA/mag, couverture, marge%, rupture%)
Adds CA par Magasin, Couverture stock Qte, Marge % and Stock Rupture % network measures to the Qlik hypercube extraction (in-page Playwright ws + legacy server ws), persists them in qlik_network_metrics, joins them in getProductRows and renders 4 new grid columns with buy-signal coloring (low coverage / high margin = green; high rupture = rose). - qlik-client: config IDs (env-overridable, master GUIDs of app 9872ee6e), NetworkMetric fields, hypercube width 4->8, page height 2500->1250 - qlik-playwright: same 4 measures in the in-page hypercube + row mapping - schema + db-init: 4 numeric columns (ALTER ADD COLUMN IF NOT EXISTS for existing DBs) - cache + grid types + enrich: plumb the new fields end to end Note: percent measures stored as raw Qlik ratio; UI renders x100 when |v|<=1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b9846f55d
commit
bd9bc78f4c
8 files changed
+148
-12
No files matched your search
@@ -481,6 +481,10 @@ async function enrichWithNetworkMetrics(productMap: Map<string, ProductRow>): Pr
|
||||
product.caReseau = m.caReseau;
|
||||
product.qteReseau = m.qteReseau;
|
||||
product.nbMagasinsReseau = m.nbMagasinsReseau;
|
||||
product.caParMagasinReseau = m.caParMagasinReseau;
|
||||
product.couvertureStockReseau = m.couvertureStockReseau;
|
||||
product.margePctReseau = m.margePctReseau;
|
||||
product.rupturePctReseau = m.rupturePctReseau;
|
||||
product.tauxPresenceReseau = m.nbMagasinsReseau / NB_MAGASINS_RESEAU;
|
||||
product.networkFetchedAt = m.fetchedAt ?? undefined;
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ const GridRow = React.memo(({ virtualRow, row, rowHeight, isSelected, columnVisi
|
||||
>
|
||||
{row.getVisibleCells().map((cell: Cell<ProductRow, unknown>) => {
|
||||
const isFlexible = cell.column.id === "libelle1" || cell.column.id === "libelle3";
|
||||
const isCenter = cell.column.id === "totalQuantite" || cell.column.id === "totalCa" || cell.column.id === "totalMarge" || cell.column.id.startsWith("month_") || cell.column.id === "gammeInitial" || cell.column.id === "caReseau" || cell.column.id === "qteReseau" || cell.column.id === "nbMagasinsReseau" || cell.column.id === "tauxPresenceReseau" || cell.column.id === "gamme";
|
||||
const isCenter = cell.column.id === "totalQuantite" || cell.column.id === "totalCa" || cell.column.id === "totalMarge" || cell.column.id.startsWith("month_") || cell.column.id === "gammeInitial" || cell.column.id === "caReseau" || cell.column.id === "qteReseau" || cell.column.id === "nbMagasinsReseau" || cell.column.id === "caParMagasinReseau" || cell.column.id === "couvertureStockReseau" || cell.column.id === "margePctReseau" || cell.column.id === "rupturePctReseau" || cell.column.id === "tauxPresenceReseau" || cell.column.id === "gamme";
|
||||
const size = cell.column.getSize();
|
||||
return (
|
||||
<td
|
||||
@@ -560,6 +560,68 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "caParMagasinReseau",
|
||||
header: () => <div className="text-center w-full">CA / Mag<br/><span className="text-[9px] opacity-60">Réseau</span></div>,
|
||||
size: 85,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<number | undefined>();
|
||||
return (
|
||||
<div className="text-center tabular-nums text-[12px] font-bold" style={{ color: "var(--text-secondary)" }}>
|
||||
{val != null ? val.toLocaleString("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }) : "-"}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "couvertureStockReseau",
|
||||
header: () => <div className="text-center w-full">Couv.<br/><span className="text-[9px] opacity-60">Stock</span></div>,
|
||||
size: 70,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<number | undefined>();
|
||||
if (val == null) return <div className="text-center text-[12px]" style={{ color: "var(--text-secondary)" }}>-</div>;
|
||||
// Couverture basse = rotation rapide = bon signal d'achat
|
||||
const color = val <= 2 ? "text-emerald-500" : val <= 4 ? "text-amber-500" : "text-rose-500";
|
||||
return (
|
||||
<div className={cn("text-center font-bold text-[12px] tabular-nums", color)}>
|
||||
{val.toLocaleString("fr-FR", { maximumFractionDigits: 1 })}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "margePctReseau",
|
||||
header: () => <div className="text-center w-full">Marge %<br/><span className="text-[9px] opacity-60">Réseau</span></div>,
|
||||
size: 75,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<number | undefined>();
|
||||
if (val == null) return <div className="text-center text-[12px]" style={{ color: "var(--text-secondary)" }}>-</div>;
|
||||
const pct = Math.abs(val) <= 1 ? val * 100 : val;
|
||||
const color = pct >= 30 ? "text-emerald-500" : pct >= 15 ? "text-amber-500" : "text-rose-500";
|
||||
return (
|
||||
<div className={cn("text-center font-bold text-[12px] tabular-nums", color)}>
|
||||
{pct.toLocaleString("fr-FR", { maximumFractionDigits: 1 })}%
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "rupturePctReseau",
|
||||
header: () => <div className="text-center w-full">Rupt. %<br/><span className="text-[9px] opacity-60">Réseau</span></div>,
|
||||
size: 75,
|
||||
cell: ({ getValue }) => {
|
||||
const val = getValue<number | undefined>();
|
||||
if (val == null) return <div className="text-center text-[12px]" style={{ color: "var(--text-secondary)" }}>-</div>;
|
||||
const pct = Math.abs(val) <= 1 ? val * 100 : val;
|
||||
// Rupture haute = demande non servie = signal, mais aussi tension stock
|
||||
const color = pct >= 15 ? "text-rose-500" : pct >= 5 ? "text-amber-500" : "text-emerald-500";
|
||||
return (
|
||||
<div className={cn("text-center font-bold text-[12px] tabular-nums", color)}>
|
||||
{pct.toLocaleString("fr-FR", { maximumFractionDigits: 1 })}%
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
...MONTHS_12.map((monthKey) => ({
|
||||
id: `month_${monthKey}`,
|
||||
header: () => <div className="text-center w-full">{formatMonthLabel(monthKey)}</div>,
|
||||
@@ -778,7 +840,7 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
|
||||
<tr key={headerGroup.id} className="flex w-full">
|
||||
{headerGroup.headers.map((header) => {
|
||||
const isFlexible = header.column.id === "libelle1" || header.column.id === "libelle3";
|
||||
const isCenter = header.column.id === "totalQuantite" || header.column.id === "totalCa" || header.column.id === "totalMarge" || header.column.id.startsWith("month_") || header.column.id === "gammeInitial" || header.column.id === "caReseau" || header.column.id === "qteReseau" || header.column.id === "nbMagasinsReseau" || header.column.id === "tauxPresenceReseau" || header.column.id === "gamme";
|
||||
const isCenter = header.column.id === "totalQuantite" || header.column.id === "totalCa" || header.column.id === "totalMarge" || header.column.id.startsWith("month_") || header.column.id === "gammeInitial" || header.column.id === "caReseau" || header.column.id === "qteReseau" || header.column.id === "nbMagasinsReseau" || header.column.id === "caParMagasinReseau" || header.column.id === "couvertureStockReseau" || header.column.id === "margePctReseau" || header.column.id === "rupturePctReseau" || header.column.id === "tauxPresenceReseau" || header.column.id === "gamme";
|
||||
const size = header.getSize();
|
||||
return (
|
||||
<th
|
||||
|
||||
Reference in new issue
Block a user