mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-16 11:49:18 +02:00
feat(grille): tendance réseau (sparkline) + modal ventes réseau par mois
Le sync Qlik calculait déjà les ventes réseau mois par mois mais les
sommait en un total. On conserve désormais le détail mensuel.
- Sync : accumulation additive qteByMonth { code: { 'YYYY-MM': qté } }
in-page (chemins rapide + par lots), rattachée au métrique réseau.
Reset propre en cas de repli code 15. Total inchangé.
- Stockage : colonne jsonb qte_by_month sur qlik_network_metrics
(schema + db-init ALTER), upsert + lecture.
- ProductRow.qteReseauByMonth propagé jusqu'à la grille.
- UI grille :
· colonne '% Présence réseau' remplacée par 'Tendance réseau' :
sparkline 12 mois teintée + flèche + variation % (moyenne 4 derniers
mois vs 4 précédents), triable ;
· cellule 'Nb magasins' rendue cliquable → modal des ventes réseau
mois par mois (barres, 12 derniers mois).
Nécessite un nouveau Sync Qlik pour peupler le détail mensuel (les
données déjà en cache n'ont que le total → modal 'pas encore de détail').
This commit is contained in:
8 files changed
+185
-21
No files matched your search
@@ -89,6 +89,8 @@ export interface NetworkMetric {
|
||||
/** Taux de marge réseau (ratio brut Qlik, ex 0.32 = 32%) */
|
||||
margePctReseau: number;
|
||||
periode?: string;
|
||||
/** Quantité vendue réseau par mois : { "YYYY-MM": qté }. Rempli si sync avec filtre Date. */
|
||||
qteByMonth?: Record<string, number>;
|
||||
}
|
||||
|
||||
function makeXrfkey(): string {
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface NetworkMetricCached {
|
||||
nbMagasinsReseau: number;
|
||||
caParMagasinReseau: number;
|
||||
margePctReseau: number;
|
||||
/** Quantité réseau par mois : { "YYYY-MM": qté } — null si non synchronisé avec dates. */
|
||||
qteByMonth: Record<string, number> | null;
|
||||
fetchedAt: string | null;
|
||||
}
|
||||
|
||||
@@ -40,6 +42,7 @@ export async function getNetworkMetricsByCodeCentrale(
|
||||
nbMagasinsReseau: Number(r.nbMagasinsReseau ?? 0) || 0,
|
||||
caParMagasinReseau: Number(r.caParMagasinReseau ?? 0) || 0,
|
||||
margePctReseau: Number(r.margePctReseau ?? 0) || 0,
|
||||
qteByMonth: (r.qteByMonth as Record<string, number> | null) ?? null,
|
||||
fetchedAt: r.fetchedAt ? new Date(r.fetchedAt).toISOString() : null,
|
||||
});
|
||||
}
|
||||
@@ -58,6 +61,7 @@ export async function upsertNetworkMetrics(metrics: NetworkMetric[]): Promise<nu
|
||||
caParMagasinReseau: String(m.caParMagasinReseau),
|
||||
margePctReseau: String(m.margePctReseau),
|
||||
periode: m.periode ?? null,
|
||||
qteByMonth: m.qteByMonth ?? null,
|
||||
fetchedAt: now,
|
||||
}));
|
||||
|
||||
@@ -78,6 +82,7 @@ export async function upsertNetworkMetrics(metrics: NetworkMetric[]): Promise<nu
|
||||
caParMagasinReseau: sql`excluded.ca_par_magasin_reseau`,
|
||||
margePctReseau: sql`excluded.marge_pct_reseau`,
|
||||
periode: sql`excluded.periode`,
|
||||
qteByMonth: sql`excluded.qte_by_month`,
|
||||
fetchedAt: sql`excluded.fetched_at`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,6 +34,8 @@ 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>>;
|
||||
size?: number;
|
||||
error?: string;
|
||||
diag?: Record<string, unknown>;
|
||||
@@ -253,6 +255,18 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
// causé par la réutilisation du même objet sur des centaines de recalculs.
|
||||
|
||||
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 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;
|
||||
}
|
||||
};
|
||||
// Compteurs agrégés pour le résumé timing final (loggué côté Node après la sync).
|
||||
let aggMonths = 0;
|
||||
let aggBatches = 0;
|
||||
@@ -366,6 +380,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
const batchRows: Array<Array<string | number>> = [];
|
||||
const { value: got, ms } = await timed("cube", "batch", () => fetchCubeForSelection(batchRows, monthTimings, () => { code15Retries++; }), monthTimings);
|
||||
out.push(...batchRows);
|
||||
accMonthly(batchRows, m.label);
|
||||
aggBatches++;
|
||||
aggCode15Retries += code15Retries;
|
||||
console.log("[qlik-pw][timing] " + prefix + " — rows=" + got + " cumul=" + out.length + " ms=" + ms + " retries15=" + code15Retries + " timings=" + timingSummary(monthTimings));
|
||||
@@ -381,6 +396,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
if (isEngineCode15(e)) {
|
||||
console.log("[qlik-pw] chemin rapide échoué (code 15) → repli sur le chemin par lots");
|
||||
out.length = savedLen;
|
||||
for (const k of Object.keys(monthlyByCode)) delete monthlyByCode[k];
|
||||
aggMonths = 0; aggBatches = 0; aggCode15Retries = 0;
|
||||
try { await rpc("Clear", {}, dfh); if (fh !== -1) await rpc("Clear", {}, fh); } catch { /* noop */ }
|
||||
await sleep(qlikSettleMs);
|
||||
@@ -469,6 +485,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
const batchRows: Array<Array<string | number>> = [];
|
||||
const { value: got, ms } = await timed("cube", "batch", () => fetchCubeForSelection(batchRows, timings, () => { code15Retries++; }), timings);
|
||||
out.push(...batchRows);
|
||||
accMonthly(batchRows, m.label);
|
||||
aggBatches++;
|
||||
aggCode15Retries += code15Retries;
|
||||
console.log("[qlik-pw][timing] " + batchPrefix + " — rows=" + got + " cumul=" + out.length + " ms=" + ms + " retries15=" + code15Retries + " timings=" + timingSummary(timings));
|
||||
@@ -508,7 +525,7 @@ export async function fetchNetworkMetricsPlaywright(
|
||||
}
|
||||
|
||||
ws.close();
|
||||
resolve({ ok: true, rows: out, size: out.length, months: aggMonths, batches: aggBatches, code15Retries: aggCode15Retries, fallbackCount: aggFallbackCount });
|
||||
resolve({ ok: true, rows: out, monthly: monthlyByCode, size: out.length, months: aggMonths, batches: aggBatches, code15Retries: aggCode15Retries, fallbackCount: aggFallbackCount });
|
||||
} catch (e) { try { ws.close(); } catch { /* noop */ } fail(String((e as Error)?.message || e)); }
|
||||
})();
|
||||
}),
|
||||
@@ -577,6 +594,12 @@ 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é.
|
||||
const monthly = result.monthly ?? {};
|
||||
for (const [code, metric] of out) {
|
||||
const mm = monthly[code];
|
||||
if (mm && Object.keys(mm).length) metric.qteByMonth = mm;
|
||||
}
|
||||
console.log(`[qlik-pw] ${out.size} produits réseau (cube ${result.size})`);
|
||||
// Échantillon brut pour vérifier que les 4 nouvelles mesures (cols 4-7) renvoient des valeurs
|
||||
const sample = (result.rows ?? []).slice(0, 3).map((r) => ({
|
||||
|
||||
Reference in new issue
Block a user