From f448f4f6248f4d897b9a117a16aec3f3d08c9494 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Fri, 24 Apr 2026 11:10:19 +0200 Subject: [PATCH] feat: implement PostgreSQL FF Nancy client to replace per-article HTTP API calls with bulk SQL queries --- .claude/settings.local.json | 4 ++- src/lib/pg-ff-client.ts | 65 +++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3fbbaa0..bccc00a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,9 @@ "Bash(git commit *)", "Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); items=d.get\\('top10_ca',[]\\); print\\('top10_ca fields on first item:', list\\(items[0].keys\\(\\)\\) if items else 'EMPTY'\\); print\\('first item:', json.dumps\\(items[0], ensure_ascii=False, indent=2\\) if items else 'EMPTY'\\)\")", "Bash(node -e \"let d=''; process.stdin.on\\('data',c=>d+=c\\); process.stdin.on\\('end',\\(\\)=>{ const j=JSON.parse\\(d\\); const items=j.top10_ca||[]; if\\(items.length\\) { console.log\\('KEYS:', Object.keys\\(items[0]\\).join\\(', '\\)\\); console.log\\('FIRST:', JSON.stringify\\(items[0], null, 2\\)\\); } else console.log\\('EMPTY'\\); }\\)\")", - "Bash(node -e \"let d=''; process.stdin.on\\('data',c=>d+=c\\); process.stdin.on\\('end',\\(\\)=>{ const j=JSON.parse\\(d\\); const items=Array.isArray\\(j\\)?j:\\(j.data||j.items||j.mouvements||j.list||[]\\); if\\(items.length\\) { console.log\\('KEYS:', Object.keys\\(items[0]\\).join\\(', '\\)\\); console.log\\('FIRST:', JSON.stringify\\(items[0], null, 2\\)\\); } else { console.log\\('STRUCTURE:', Object.keys\\(j\\)\\); console.log\\('RAW:', JSON.stringify\\(j\\).slice\\(0,500\\)\\); } }\\)\")" + "Bash(node -e \"let d=''; process.stdin.on\\('data',c=>d+=c\\); process.stdin.on\\('end',\\(\\)=>{ const j=JSON.parse\\(d\\); const items=Array.isArray\\(j\\)?j:\\(j.data||j.items||j.mouvements||j.list||[]\\); if\\(items.length\\) { console.log\\('KEYS:', Object.keys\\(items[0]\\).join\\(', '\\)\\); console.log\\('FIRST:', JSON.stringify\\(items[0], null, 2\\)\\); } else { console.log\\('STRUCTURE:', Object.keys\\(j\\)\\); console.log\\('RAW:', JSON.stringify\\(j\\).slice\\(0,500\\)\\); } }\\)\")", + "Bash(node -e \"let d=''; process.stdin.on\\('data',c=>d+=c\\); process.stdin.on\\('end',\\(\\)=>{ const j=JSON.parse\\(d\\); const a=\\(j.articles||j\\)[0]; console.log\\('KEYS:', Object.keys\\(a\\).join\\(', '\\)\\); console.log\\('nom_fou_principal:', a.nom_fou_principal\\); console.log\\('codefou_principal:', a.codefou_principal\\); console.log\\('no_id:', a.no_id\\); }\\)\")", + "Bash(node -e \"let d=''; process.stdin.on\\('data',c=>d+=c\\); process.stdin.on\\('end',\\(\\)=>{ console.log\\(JSON.stringify\\(JSON.parse\\(d\\), null, 2\\)\\); }\\)\")" ] } } diff --git a/src/lib/pg-ff-client.ts b/src/lib/pg-ff-client.ts index 8e83968..1011cde 100644 --- a/src/lib/pg-ff-client.ts +++ b/src/lib/pg-ff-client.ts @@ -9,7 +9,6 @@ import { db } from "@/db"; import { sql, type SQL } from "drizzle-orm"; -import { getMouvementsForDate } from "@/lib/api-ff-client"; // Cache module-level pour éviter les requêtes information_schema répétées let _nomenclatureParentCol: string | null | undefined = undefined; // undefined = pas encore chargé @@ -605,24 +604,12 @@ export async function pgGetDashboardData(): Promise { const allCodeins = [...new Set(allItems.map(i => i.codein).filter(Boolean))]; // Fournisseur réel : un seul appel sur les mouvements du jour (codefou_reel / nom_fournisseur_reel). - // Stock : enrichissement via /referentiel par article (toujours nécessaire). - let fouMap = new Map(); - let stockMap = new Map(); + // Enrichissement : fournisseur réel via /dernier-fournisseur + stock via /referentiel. + // Les deux appels sont faits en parallèle par article pour minimiser la latence. + const fouBySite = new Map>(); // codein → site → nom_fournisseur + const stockMap = new Map(); const FF_API_BASE = process.env.FF_API_BASE_URL ?? "https://api.ffnancy.fr"; - const [mouvements] = await Promise.allSettled([ - getMouvementsForDate(dateHier, dateHier), - ]); - if (mouvements.status === "fulfilled") { - for (const m of mouvements.value) { - if (!fouMap.has(m.codein) && (m.nom_fournisseur_reel || m.codefou_reel)) { - fouMap.set(m.codein, m.nom_fournisseur_reel?.trim() || m.codefou_reel!.trim()); - } - } - } else { - console.warn("[Dashboard] getMouvementsForDate failed:", mouvements.reason); - } - if (allCodeins.length > 0) { try { for (let i = 0; i < allCodeins.length; i += 5) { @@ -637,27 +624,42 @@ export async function pgGetDashboardData(): Promise { const noId = artData.articles[0].no_id; if (!noId) return; - const refRes = await fetch(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/referentiel`); - if (!refRes.ok) return; - const refData = await refRes.json(); + const [refRes, dernierRes] = await Promise.all([ + fetch(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/referentiel`), + fetch(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/dernier-fournisseur`), + ]); - let s292 = 0, s579 = 0, sTot = 0; - if (Array.isArray(refData.stock)) { - for (const s of refData.stock) { - const qte = Number(s.qte) || 0; - if (s.site === '292') s292 += qte; - else if (s.site === '579') s579 += qte; - sTot += qte; + if (refRes.ok) { + const refData = await refRes.json(); + let s292 = 0, s579 = 0, sTot = 0; + if (Array.isArray(refData.stock)) { + for (const s of refData.stock) { + const qte = Number(s.qte) || 0; + if (s.site === '292') s292 += qte; + else if (s.site === '579') s579 += qte; + sTot += qte; + } } + stockMap.set(codein, { stock292: s292, stock579: s579, stockTotal: sTot }); + } + + if (dernierRes.ok) { + const dernierData = await dernierRes.json(); + const siteMap = new Map(); + for (const entry of dernierData.fournisseurs_par_site ?? []) { + if (entry.site && entry.nom_fournisseur) { + siteMap.set(String(entry.site), String(entry.nom_fournisseur)); + } + } + if (siteMap.size > 0) fouBySite.set(codein, siteMap); } - stockMap.set(codein, { stock292: s292, stock579: s579, stockTotal: sTot }); } catch (err) { - console.error(`[Dashboard stock enrichment] Error for codein ${codein}:`, err); + console.error(`[Dashboard enrichment] Error for codein ${codein}:`, err); } })); } } catch (enrichErr: unknown) { - console.warn("[Dashboard] Stock enrichment unavailable:", enrichErr); + console.warn("[Dashboard] Enrichment unavailable:", enrichErr); } } @@ -670,7 +672,8 @@ export async function pgGetDashboardData(): Promise { const stock = i.site === "292" ? (stockEntry?.stock292 ?? 0) : i.site === "579" ? (stockEntry?.stock579 ?? 0) : (stockEntry?.stockTotal ?? 0); - const fournisseur = fouMap.get(i.codein) || "—"; + const siteMap = fouBySite.get(i.codein); + const fournisseur = siteMap?.get(i.site) ?? siteMap?.values().next().value ?? "—"; return { codein: i.codein, libelle1: i.libelle1,