From df1d05995247a72adf9cfa78aa9eafb4da3d58a4 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Fri, 24 Apr 2026 10:43:32 +0200 Subject: [PATCH] feat: implement API clients for Nancy FF data sync and add local configuration settings --- .claude/settings.local.json | 5 ++++- src/lib/api-ff-client.ts | 29 +++++++++++++++++++++++++++++ src/lib/pg-ff-client.ts | 23 ++++++++++++++++++----- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6737ae1..3fbbaa0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,10 @@ "Bash(rtk git *)", "Bash(rtk npx *)", "Bash(git add *)", - "Bash(git commit *)" + "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\\)\\); } }\\)\")" ] } } diff --git a/src/lib/api-ff-client.ts b/src/lib/api-ff-client.ts index b3ec435..2c4e58f 100644 --- a/src/lib/api-ff-client.ts +++ b/src/lib/api-ff-client.ts @@ -293,6 +293,35 @@ export async function getMouvementsByFournisseur( })).filter(m => m.codein && m.datemvt); } +/** + * Retourne les mouvements pour une plage de dates (sans filtre fournisseur). + * Utilisé pour extraire codefou_reel / nom_fournisseur_reel par codein. + */ +export async function getMouvementsForDate( + dateDebut: string, + dateFin: string +): Promise { + const raw = await fetchAllPages( + (page) => + `${FF_API_BASE}/api/mouvements/articles?dateDebut=${dateDebut}&dateFin=${dateFin}&page=${page}&limit=1000`, + extractList, + 1000 + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return raw.map((r: any): FfMouvement => ({ + codein: r.codein ?? r.Codein ?? r.code_article ?? "", + genremvt: Number(r.genremvt ?? r.GenreMvt ?? r.genre_mvt ?? r.type ?? 0), + datemvt: r.datmvt ?? r.datemvt ?? r.DateMvt ?? r.date_mvt ?? r.date ?? "", + qte: Number(r.qtemvt ?? r.qte ?? r.Qte ?? r.quantite ?? 0), + montant: Number(r.mntmvtht ?? r.montant ?? r.Montant ?? r.montant_mvt ?? r.ca ?? 0), + marge: Number(r.margemvt ?? r.marge ?? r.Marge ?? r.marge_mvt ?? 0), + qtestock: Number(r.qtestock ?? r.QteStock ?? r.qte_stock ?? r.stock ?? 0), + site: r.site ?? r.Site ?? r.magasin ?? r.code_magasin ?? r.codesite ?? "", + codefou_reel: r.codefou_reel ?? undefined, + nom_fournisseur_reel: r.nom_fournisseur_reel ?? undefined, + })).filter(m => m.codein && m.datemvt); +} + // --------------------------------------------------------------------------- // Mensuel — stock fin de mois + ventes + réceptions par article et par site // GET /api/articles/:no_id/mensuel?dateDebut=&dateFin= diff --git a/src/lib/pg-ff-client.ts b/src/lib/pg-ff-client.ts index 31ddf4f..8e83968 100644 --- a/src/lib/pg-ff-client.ts +++ b/src/lib/pg-ff-client.ts @@ -9,6 +9,7 @@ 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é @@ -603,11 +604,25 @@ export async function pgGetDashboardData(): Promise { ]; const allCodeins = [...new Set(allItems.map(i => i.codein).filter(Boolean))]; - // Enrichissement stock uniquement — le fournisseur réel vient maintenant directement - // des champs codefou_reel / nom_fournisseur_reel retournés par l'API mouvements. + // 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(); 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) { @@ -655,9 +670,7 @@ export async function pgGetDashboardData(): Promise { const stock = i.site === "292" ? (stockEntry?.stock292 ?? 0) : i.site === "579" ? (stockEntry?.stock579 ?? 0) : (stockEntry?.stockTotal ?? 0); - const fournisseur = i.nom_fournisseur_reel?.trim() - || i.codefou_reel?.trim() - || "—"; + const fournisseur = fouMap.get(i.codein) || "—"; return { codein: i.codein, libelle1: i.libelle1,