mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-19 05:06:56 +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
+3
-82
@@ -39,6 +39,8 @@ async function pgNoParallel(query: SQL): Promise<{ rows: unknown[] }> {
|
||||
export interface PgArticle {
|
||||
no_id: number;
|
||||
codein: string;
|
||||
/** Code centrale = articles.artcentrale (format 10000XXXXXX) — clé jointure Qlik "Article Code". Vide pour les articles non référencés centralement. */
|
||||
codeCentrale?: string;
|
||||
codefou: string;
|
||||
nomfou?: string;
|
||||
libelle1?: string;
|
||||
@@ -74,15 +76,6 @@ export interface PgStockRow {
|
||||
dernierereception?: string;
|
||||
}
|
||||
|
||||
export interface PgRankingRow {
|
||||
codein: string;
|
||||
site: string;
|
||||
ranking_ca?: number;
|
||||
ranking_qte?: number;
|
||||
ranking_mag_ca?: number;
|
||||
ranking_mag_qte?: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 0. Liste des fournisseurs
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -127,6 +120,7 @@ export async function pgGetArticlesByFournisseur(codefou: string): Promise<PgArt
|
||||
SELECT DISTINCT ON (a.no_id)
|
||||
a.no_id,
|
||||
a.codein,
|
||||
a.artcentrale AS "codeCentrale",
|
||||
af.code AS codefou,
|
||||
fi.nom AS nomfou,
|
||||
a.libelle1,
|
||||
@@ -378,79 +372,6 @@ export async function pgGetStockByFournisseur(codefou: string): Promise<Map<stri
|
||||
return map;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. Ranking réseau
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Retourne le classement réseau pour chaque article du fournisseur.
|
||||
* 1 requête SQL — remplace getRankingByArticles() (N appels HTTP per-article,
|
||||
* capés à 500 par l'API).
|
||||
*/
|
||||
export async function pgGetRankingByFournisseur(codefou: string): Promise<{
|
||||
rankings: Map<string, PgRankingRow>;
|
||||
totalRankedProducts: number;
|
||||
}> {
|
||||
// D'abord, découvrir quels sites existent dans la table ranking (réseau vs magasin)
|
||||
const [rankResult, totalResult, sitesSample] = await Promise.all([
|
||||
pgNoParallel(sql`
|
||||
SELECT DISTINCT ON (a.codein)
|
||||
a.codein,
|
||||
r.site,
|
||||
r.ranking_ca,
|
||||
r.ranking_qte,
|
||||
r.ranking_mag_ca,
|
||||
r.ranking_mag_qte
|
||||
FROM ranking r
|
||||
JOIN art_gtin ag
|
||||
ON ag.gtin = r.gencod
|
||||
JOIN articles a
|
||||
ON a.no_id = ag.idarticle
|
||||
JOIN artfou1 af
|
||||
ON af.art_no_id = a.no_id AND af.code = ${codefou}
|
||||
ORDER BY a.codein,
|
||||
CASE
|
||||
WHEN r.site IN ('000', 'ALL', 'TOTAL', 'NET', 'RES') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
r.site
|
||||
`),
|
||||
pgNoParallel(sql`
|
||||
SELECT COUNT(DISTINCT gencod)::int AS total FROM ranking
|
||||
`),
|
||||
pgNoParallel(sql`
|
||||
SELECT DISTINCT site FROM ranking ORDER BY site LIMIT 10
|
||||
`),
|
||||
]);
|
||||
|
||||
// Log sites disponibles pour diagnostic ranking
|
||||
const sitesDispos = (sitesSample.rows as { site: string }[]).map(r => r.site);
|
||||
console.log(`[pg-ff] Ranking sites disponibles:`, sitesDispos.join(", "));
|
||||
|
||||
const rankings = new Map<string, PgRankingRow>();
|
||||
for (const row of rankResult.rows as unknown as PgRankingRow[]) {
|
||||
if (row.codein) rankings.set(row.codein, row);
|
||||
}
|
||||
|
||||
// Log doublons de rank pour diagnostic
|
||||
const rankCount = new Map<number, number>();
|
||||
for (const r of rankings.values()) {
|
||||
if (r.ranking_ca) {
|
||||
const v = Number(r.ranking_ca);
|
||||
rankCount.set(v, (rankCount.get(v) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
const dupes = [...rankCount.entries()].filter(([, c]) => c > 1).slice(0, 5);
|
||||
if (dupes.length > 0) {
|
||||
console.log(`[pg-ff] Ranking doublons détectés (rank → nb articles):`, dupes.map(([r, c]) => `rank${r}×${c}`).join(", "));
|
||||
}
|
||||
|
||||
const totalRankedProducts = Number((totalResult.rows[0] as unknown as { total: number })?.total ?? 0);
|
||||
console.log(`[pg-ff] Ranking: ${rankings.size} articles classés, réseau total: ${totalRankedProducts}`);
|
||||
|
||||
return { rankings, totalRankedProducts };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 6. Commandes en cours
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user