mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-21 22:18:08 +02:00
feat: create API client for FF Nancy REST API to synchronize supplier, article, and movement data
This commit is contained in:
1 parent
5a3ac3ed7f
commit
a06d7cd7e8
2 files changed
+30
-75
No files matched your search
@@ -43,6 +43,8 @@ export interface FfMouvement {
|
|||||||
marge?: number; // marge (si disponible)
|
marge?: number; // marge (si disponible)
|
||||||
qtestock: number; // stock APRÈS ce mouvement — clé pour stock12m
|
qtestock: number; // stock APRÈS ce mouvement — clé pour stock12m
|
||||||
site: string; // code magasin / site
|
site: string; // code magasin / site
|
||||||
|
codefou_reel?: string; // code fournisseur réel (dernière entrée avant cette vente, ou code de l'entrée)
|
||||||
|
nom_fournisseur_reel?: string; // nom résolu du fournisseur réel
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FfStock {
|
export interface FfStock {
|
||||||
@@ -286,6 +288,8 @@ export async function getMouvementsByFournisseur(
|
|||||||
marge: Number(r.margemvt ?? r.marge ?? r.Marge ?? r.marge_mvt ?? 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),
|
qtestock: Number(r.qtestock ?? r.QteStock ?? r.qte_stock ?? r.stock ?? 0),
|
||||||
site: r.site ?? r.Site ?? r.magasin ?? r.code_magasin ?? r.codesite ?? "",
|
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);
|
})).filter(m => m.codein && m.datemvt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-75
@@ -530,6 +530,8 @@ interface ApiPerfTopItem {
|
|||||||
codein: string;
|
codein: string;
|
||||||
libelle1: string;
|
libelle1: string;
|
||||||
site: string;
|
site: string;
|
||||||
|
codefou_reel?: string;
|
||||||
|
nom_fournisseur_reel?: string;
|
||||||
ca_ttc: number;
|
ca_ttc: number;
|
||||||
qte_vendue: number;
|
qte_vendue: number;
|
||||||
marge: number;
|
marge: number;
|
||||||
@@ -601,100 +603,46 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
|||||||
];
|
];
|
||||||
const allCodeins = [...new Set(allItems.map(i => i.codein).filter(Boolean))];
|
const allCodeins = [...new Set(allItems.map(i => i.codein).filter(Boolean))];
|
||||||
|
|
||||||
// Enrichissement : stock et fournisseur via PostgreSQL (optionnel — le dashboard
|
// Enrichissement stock uniquement — le fournisseur réel vient maintenant directement
|
||||||
// s'affiche même si la DB n'est pas configurée).
|
// des champs codefou_reel / nom_fournisseur_reel retournés par l'API mouvements.
|
||||||
let stockMap = new Map<string, { stock292: number; stock579: number; stockTotal: number }>();
|
let stockMap = new Map<string, { stock292: number; stock579: number; stockTotal: number }>();
|
||||||
let fouMap = new Map<string, string>();
|
|
||||||
const FF_API_BASE = process.env.FF_API_BASE_URL ?? "https://api.ffnancy.fr";
|
const FF_API_BASE = process.env.FF_API_BASE_URL ?? "https://api.ffnancy.fr";
|
||||||
|
|
||||||
if (allCodeins.length > 0) {
|
if (allCodeins.length > 0) {
|
||||||
try {
|
try {
|
||||||
// Batch the API requests to avoid overwhelming the server (e.g., batch of 5)
|
|
||||||
for (let i = 0; i < allCodeins.length; i += 5) {
|
for (let i = 0; i < allCodeins.length; i += 5) {
|
||||||
const batch = allCodeins.slice(i, i + 5);
|
const batch = allCodeins.slice(i, i + 5);
|
||||||
await Promise.all(batch.map(async (codein) => {
|
await Promise.all(batch.map(async (codein) => {
|
||||||
try {
|
try {
|
||||||
// 1. Fetch article search by codein to get no_id
|
|
||||||
const artRes = await fetch(`${FF_API_BASE}/api/articles?codein=${encodeURIComponent(codein)}`);
|
const artRes = await fetch(`${FF_API_BASE}/api/articles?codein=${encodeURIComponent(codein)}`);
|
||||||
if (!artRes.ok) return;
|
if (!artRes.ok) return;
|
||||||
const artData = await artRes.json();
|
const artData = await artRes.json();
|
||||||
if (!artData.articles || artData.articles.length === 0) return;
|
if (!artData.articles || artData.articles.length === 0) return;
|
||||||
|
|
||||||
const article = artData.articles[0];
|
|
||||||
const noId = article.no_id;
|
|
||||||
|
|
||||||
// 2. Fetch referentiel using no_id — provides stock + real supplier (preference=1)
|
const noId = artData.articles[0].no_id;
|
||||||
if (noId) {
|
if (!noId) return;
|
||||||
const refRes = await fetch(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/referentiel`);
|
|
||||||
if (!refRes.ok) {
|
const refRes = await fetch(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/referentiel`);
|
||||||
// Fallback: use nom_fou_principal from article list
|
if (!refRes.ok) return;
|
||||||
fouMap.set(codein, article.nom_fou_principal || article.codefou_principal || 'Sans fournisseur');
|
const refData = await refRes.json();
|
||||||
return;
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
const refData = await refRes.json();
|
|
||||||
|
|
||||||
// --- Supplier: pick the last fournisseur who entered stock ---
|
|
||||||
// Business rule: attribute the article to the most recently active supplier
|
|
||||||
// = non-depot fournisseur with suspendu=null (currently active delivery relationship)
|
|
||||||
type FouEntry = { codefou: string; nom_fou: string | null; preference: number | null; suspendu: string | null };
|
|
||||||
let fou = '—';
|
|
||||||
|
|
||||||
// Known depot/platform codes that are NOT real commercial suppliers
|
|
||||||
const DEPOT_CODES = new Set(['D005', 'B0071', 'D005 ', 'B0071 ']);
|
|
||||||
|
|
||||||
if (refData.fournisseurs && Array.isArray(refData.fournisseurs)) {
|
|
||||||
const fous = refData.fournisseurs as FouEntry[];
|
|
||||||
const pickLabel = (f: FouEntry): string => f.nom_fou?.trim() || f.codefou.trim();
|
|
||||||
|
|
||||||
// Truly active = suspendu is null or the 1901 epoch sentinel (never suspended)
|
|
||||||
const isTrulyActive = (f: FouEntry): boolean =>
|
|
||||||
!f.suspendu || f.suspendu.startsWith('1901');
|
|
||||||
|
|
||||||
// Non-depot fournisseurs only (exclude known platform/depot codes)
|
|
||||||
const realFous = fous.filter(f => !DEPOT_CODES.has(f.codefou.trim()));
|
|
||||||
|
|
||||||
if (realFous.length > 0) {
|
|
||||||
// Priority 1: currently active (suspendu=null) non-depot — most likely to have made last reception
|
|
||||||
const activeNow = realFous.find(f => isTrulyActive(f));
|
|
||||||
// Priority 2: preference=1 (declared main supplier, even if suspended)
|
|
||||||
const p1 = realFous.find(f => f.preference === 1);
|
|
||||||
// Priority 3: first non-depot available (last resort)
|
|
||||||
const first = realFous[0];
|
|
||||||
|
|
||||||
const best = activeNow ?? p1 ?? first;
|
|
||||||
fou = pickLabel(best);
|
|
||||||
} else {
|
|
||||||
// Only depot codes found → article sourced exclusively through depot
|
|
||||||
fou = 'Dépôt';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fouMap.set(codein, fou);
|
|
||||||
|
|
||||||
// --- Stock: sum qte per site ---
|
|
||||||
let s292 = 0;
|
|
||||||
let s579 = 0;
|
|
||||||
let sTot = 0;
|
|
||||||
|
|
||||||
if (refData.stock && 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 });
|
|
||||||
} else {
|
|
||||||
// no_id missing: fallback supplier from article list
|
|
||||||
fouMap.set(codein, article.nom_fou_principal || article.codefou_principal || 'Sans fournisseur');
|
|
||||||
}
|
}
|
||||||
|
stockMap.set(codein, { stock292: s292, stock579: s579, stockTotal: sTot });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Dashboard API enrichment] Error for codein ${codein}:`, err);
|
console.error(`[Dashboard stock enrichment] Error for codein ${codein}:`, err);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (enrichErr: any) {
|
} catch (enrichErr: unknown) {
|
||||||
console.warn("[Dashboard] API enrichment unavailable:", enrichErr);
|
console.warn("[Dashboard] Stock enrichment unavailable:", enrichErr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,6 +655,9 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
|||||||
const stock = i.site === "292" ? (stockEntry?.stock292 ?? 0)
|
const stock = i.site === "292" ? (stockEntry?.stock292 ?? 0)
|
||||||
: i.site === "579" ? (stockEntry?.stock579 ?? 0)
|
: i.site === "579" ? (stockEntry?.stock579 ?? 0)
|
||||||
: (stockEntry?.stockTotal ?? 0);
|
: (stockEntry?.stockTotal ?? 0);
|
||||||
|
const fournisseur = i.nom_fournisseur_reel?.trim()
|
||||||
|
|| i.codefou_reel?.trim()
|
||||||
|
|| "—";
|
||||||
return {
|
return {
|
||||||
codein: i.codein,
|
codein: i.codein,
|
||||||
libelle1: i.libelle1,
|
libelle1: i.libelle1,
|
||||||
@@ -714,7 +665,7 @@ export async function pgGetDashboardData(): Promise<DashboardData> {
|
|||||||
qte: Number(i.qte_vendue) || 0,
|
qte: Number(i.qte_vendue) || 0,
|
||||||
marge: Number(i.marge) || 0,
|
marge: Number(i.marge) || 0,
|
||||||
stock,
|
stock,
|
||||||
fournisseur: fouMap.get(i.codein) ?? "—",
|
fournisseur,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => b[sortKey] - a[sortKey])
|
.sort((a, b) => b[sortKey] - a[sortKey])
|
||||||
|
|||||||
Reference in new issue
Block a user