mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-19 21:18:16 +02:00
feat(grid): replace ranking/AI/score with Qlik network data
Remove ranking, AI analysis (routes + ai-copilot module), and score-engine from the grid. Add Qlik Sense network metrics (CA, Qte, nb magasins per product) joined by code centrale (articles.artcentrale). - qlik-client.ts: NTLM + ticket SSO auth, QIX hypercube extraction (master items) - qlik-network-cache.ts + qlik_network_metrics table (db-init): cache layer - POST /api/qlik/sync?fournisseur=: per-supplier sync into cache - SyncQlikButton: per-supplier sync, grid reload, last-update date display - get-product-rows Phase 8: enrich rows with cached network metrics - heatmap-grid: CA reseau / Qte reseau / Magasins (/270) / % presence columns - discovery: app 9872ee6e, master items CA N / Quantite N / Magasin Ventes Nb N - deps: httpntlm, ws Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
02a10caa4b
commit
aee968d30f
34 files changed
+1043
-2925
No files matched your search
@@ -496,102 +496,6 @@ export async function getCommandesByFournisseur(
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ranking — classement réseau et magasin
|
||||
// GET /api/ranking?codein=<codein>
|
||||
// Réponse : { count: number, ranking: RankingEntry[] }
|
||||
// Les valeurs ranking sont des string|null, à parser en number.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface FfRanking {
|
||||
ranking_ca?: number;
|
||||
ranking_qte?: number;
|
||||
ranking_mag_ca?: number;
|
||||
ranking_mag_qte?: number;
|
||||
ranking_mag_marge?: number;
|
||||
pv_calcule?: number;
|
||||
pv_mag?: number;
|
||||
pv_cen?: number;
|
||||
}
|
||||
|
||||
/** Parse une valeur string|null en number|undefined */
|
||||
function parseRankNum(val: string | null | undefined): number | undefined {
|
||||
if (val == null || val === "") return undefined;
|
||||
const n = Number(val);
|
||||
return isNaN(n) ? undefined : n;
|
||||
}
|
||||
|
||||
export interface RankingResult {
|
||||
rankings: Map<string, FfRanking>;
|
||||
/** Nombre total de produits dans le classement (produits avec ventes sur la période) */
|
||||
totalRankedProducts: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le ranking réseau pour les articles en 1 seul appel :
|
||||
* GET /api/ranking?limit=<N> — récupère tout le classement réseau, puis filtre par codein.
|
||||
*
|
||||
* Fallback par article individuel si le bulk échoue.
|
||||
*/
|
||||
/**
|
||||
* Récupère le ranking par article individuel (GET /api/ranking?codein=<codein>).
|
||||
* Appelé seulement pour les articles AVEC ventes — taille raisonnable même pour grands fournisseurs.
|
||||
* Le bulk endpoint est capé à 500 résultats par l'API → inutilisable pour matching complet.
|
||||
*/
|
||||
export async function getRankingByArticles(
|
||||
articles: { codein: string; gtin?: string }[],
|
||||
_codeFournisseur?: string,
|
||||
): Promise<RankingResult> {
|
||||
const rankings = new Map<string, FfRanking>();
|
||||
if (articles.length === 0) return { rankings, totalRankedProducts: 0 };
|
||||
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < articles.length; i += batchSize) {
|
||||
const batch = articles.slice(i, i + batchSize);
|
||||
await Promise.all(
|
||||
batch.map(async (art) => {
|
||||
try {
|
||||
const url = `${FF_API_BASE}/api/ranking?codein=${encodeURIComponent(art.codein)}`;
|
||||
const res = await fetch(url, { cache: "no-store" });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const rankingList = data?.ranking;
|
||||
if (!Array.isArray(rankingList) || rankingList.length === 0) return;
|
||||
const entry = rankingList[0] as Record<string, string | null>;
|
||||
rankings.set(art.codein, {
|
||||
ranking_ca: parseRankNum(entry.ranking_ca),
|
||||
ranking_qte: parseRankNum(entry.ranking_qte),
|
||||
ranking_mag_ca: parseRankNum(entry.ranking_mag_ca),
|
||||
ranking_mag_qte: parseRankNum(entry.ranking_mag_qte),
|
||||
ranking_mag_marge: parseRankNum(entry.ranking_mag_marge),
|
||||
pv_calcule: parseRankNum(entry.pv_calcule),
|
||||
pv_mag: parseRankNum(entry.pv_mag),
|
||||
pv_cen: parseRankNum(entry.pv_cen),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[api-ff] getRanking error for ${art.codein}:`, err);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// totalRankedProducts = nombre d'articles ayant un classement réseau
|
||||
const totalRankedProducts = rankings.size > 0
|
||||
? await fetchTotalRankedProducts()
|
||||
: 0;
|
||||
console.log(`[api-ff] Ranking: ${rankings.size}/${articles.length} articles classés (réseau: ${totalRankedProducts})`);
|
||||
return { rankings, totalRankedProducts };
|
||||
}
|
||||
|
||||
async function fetchTotalRankedProducts(): Promise<number> {
|
||||
try {
|
||||
const res = await fetch(`${FF_API_BASE}/api/ranking?limit=500`, { cache: "no-store" });
|
||||
if (!res.ok) return 0;
|
||||
const data = await res.json();
|
||||
return (data?.ranking as unknown[])?.length ?? 0;
|
||||
} catch { return 0; }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Statut de synchronisation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user