mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-20 13:38:08 +02:00
feat: implement FF Nancy API client including data models and functions for suppliers, articles, movements, and stock.
This commit is contained in:
1 parent
7b209292b9
commit
c056750ea7
2 files changed
+136
-71
No files matched your search
+55
-59
@@ -1,69 +1,65 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { ventesProduits } from "@/db/schema";
|
import { sql } from "drizzle-orm";
|
||||||
import { sql, eq } from "drizzle-orm";
|
|
||||||
|
const FF_API_BASE = process.env.FF_API_BASE_URL ?? "https://api.ffnancy.fr";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
const codeFournisseur = searchParams.get("fournisseur");
|
const target = searchParams.get("target") ?? "all";
|
||||||
|
|
||||||
if (!codeFournisseur) {
|
const result: Record<string, unknown> = {
|
||||||
return NextResponse.json({
|
timestamp: new Date().toISOString(),
|
||||||
error: "Paramètre 'fournisseur' manquant.",
|
env: {
|
||||||
usage: "/api/diag?fournisseur=VOTRE_CODE"
|
FF_API_BASE_URL: process.env.FF_API_BASE_URL ?? "(not set — using default)",
|
||||||
}, { status: 400 });
|
FF_API_BASE_resolved: FF_API_BASE,
|
||||||
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Test FF Nancy API ────────────────────────────────────────────────────
|
||||||
|
if (target === "all" || target === "ff") {
|
||||||
|
const ffTests: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
// Test fournisseurs
|
||||||
|
try {
|
||||||
|
const url = `${FF_API_BASE}/api/fournisseurs?limit=5`;
|
||||||
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
|
const text = await res.text();
|
||||||
|
let parsed: unknown = null;
|
||||||
|
try { parsed = JSON.parse(text); } catch { parsed = text.slice(0, 500); }
|
||||||
|
ffTests.fournisseurs = { status: res.status, url, raw: parsed };
|
||||||
|
} catch (e) {
|
||||||
|
ffTests.fournisseurs = { error: String(e) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test articles (avec un code fournisseur de test)
|
||||||
|
const codefou = searchParams.get("codefou");
|
||||||
|
if (codefou) {
|
||||||
|
try {
|
||||||
|
const url = `${FF_API_BASE}/api/articles?codefou=${codefou}&limit=3`;
|
||||||
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
|
const text = await res.text();
|
||||||
|
let parsed: unknown = null;
|
||||||
|
try { parsed = JSON.parse(text); } catch { parsed = text.slice(0, 500); }
|
||||||
|
ffTests.articles = { status: res.status, url, raw: parsed };
|
||||||
|
} catch (e) {
|
||||||
|
ffTests.articles = { error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ffNancyApi = ffTests;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// ── Test PostgreSQL ──────────────────────────────────────────────────────
|
||||||
console.log(`\n--- [DIAG API] Starting check for supplier: ${codeFournisseur} ---`);
|
if (target === "all" || target === "db") {
|
||||||
|
try {
|
||||||
// 1. Raw Count
|
const countResult = await db.execute(sql`SELECT COUNT(*) as count FROM session_snapshots`);
|
||||||
const countAll = await db.execute(sql`
|
result.postgresql = { ok: true, snapshots: Number(countResult.rows[0]?.count ?? 0) };
|
||||||
SELECT COUNT(*) as count FROM ventes_produits
|
} catch (e) {
|
||||||
WHERE code_fournisseur = ${codeFournisseur}
|
result.postgresql = { ok: false, error: String(e) };
|
||||||
`);
|
}
|
||||||
const totalRows = Number(countAll.rows[0].count);
|
|
||||||
|
|
||||||
// 2. Distinct Products
|
|
||||||
const countDistinctTotal = await db.execute(sql`
|
|
||||||
SELECT COUNT(DISTINCT codein) as count FROM ventes_produits
|
|
||||||
WHERE code_fournisseur = ${codeFournisseur}
|
|
||||||
`);
|
|
||||||
const totalDistinct = Number(countDistinctTotal.rows[0].count);
|
|
||||||
|
|
||||||
// 3. Sample of data (to see if it's truncated by DB query)
|
|
||||||
const sample = await db.execute(sql`
|
|
||||||
SELECT codein, libelle1, magasin, periode FROM ventes_produits
|
|
||||||
WHERE code_fournisseur = ${codeFournisseur}
|
|
||||||
LIMIT 5
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log(`- DB sees ${totalRows} total rows.`);
|
|
||||||
console.log(`- DB sees ${totalDistinct} distinct products.`);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
supplier: codeFournisseur,
|
|
||||||
database: {
|
|
||||||
totalRowsInTable: totalRows,
|
|
||||||
distinctProducts: totalDistinct,
|
|
||||||
sampleData: sample.rows
|
|
||||||
},
|
|
||||||
serverConfig: {
|
|
||||||
nextVersion: "16.1.6",
|
|
||||||
nodeEnv: process.env.NODE_ENV,
|
|
||||||
bodySizeLimit: "50mb"
|
|
||||||
},
|
|
||||||
message: totalDistinct > 1000
|
|
||||||
? "La base de données contient plus de 1000 produits. Si la grille en affiche 999, le problème est dans le transport (Application/RSC)."
|
|
||||||
: "La base de données renvoie moins de 1000 produits uniques. Le problème vient de l'import ou du filtrage SQL."
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("[DIAG API] Error:", error);
|
|
||||||
return NextResponse.json({
|
|
||||||
error: "Failed to fetch diagnostic data",
|
|
||||||
details: error.message
|
|
||||||
}, { status: 500 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(result, { status: 200 });
|
||||||
}
|
}
|
||||||
+81
-12
@@ -80,9 +80,24 @@ export interface AggregatedStock {
|
|||||||
// Pagination générique
|
// Pagination générique
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function extractList(data: unknown): any[] {
|
||||||
|
if (Array.isArray(data)) return data;
|
||||||
|
const d = data as Record<string, unknown>;
|
||||||
|
// Essaie les wrappers courants
|
||||||
|
for (const key of ["items", "data", "result", "results", "records", "list"]) {
|
||||||
|
if (Array.isArray(d[key])) return d[key] as unknown[];
|
||||||
|
}
|
||||||
|
// Cherche la première propriété qui est un tableau
|
||||||
|
for (const val of Object.values(d)) {
|
||||||
|
if (Array.isArray(val)) return val as unknown[];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchAllPages<T>(
|
async function fetchAllPages<T>(
|
||||||
buildUrl: (page: number) => string,
|
buildUrl: (page: number) => string,
|
||||||
extractItems: (data: unknown) => T[],
|
_extractItems: (data: unknown) => T[], // conservé pour compatibilité, mais on utilise extractList
|
||||||
pageSize = 500
|
pageSize = 500
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const all: T[] = [];
|
const all: T[] = [];
|
||||||
@@ -90,13 +105,17 @@ async function fetchAllPages<T>(
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const url = buildUrl(page);
|
const url = buildUrl(page);
|
||||||
|
console.log(`[api-ff] GET ${url}`);
|
||||||
const res = await fetch(url, { cache: "no-store" });
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error(`[api-ff] HTTP ${res.status} — ${url}`);
|
console.error(`[api-ff] HTTP ${res.status} — ${url}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const items = extractItems(data);
|
const items = extractList(data) as T[];
|
||||||
|
if (page === 1) {
|
||||||
|
console.log(`[api-ff] page 1: ${items.length} items, keys: ${items[0] ? Object.keys(items[0] as object).join(",") : "empty"}`);
|
||||||
|
}
|
||||||
all.push(...items);
|
all.push(...items);
|
||||||
if (items.length < pageSize) break;
|
if (items.length < pageSize) break;
|
||||||
page++;
|
page++;
|
||||||
@@ -134,6 +153,16 @@ export function dateToYYYYMM(isoDate: string): string {
|
|||||||
// Fournisseurs
|
// Fournisseurs
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Extrait le code fournisseur depuis n'importe quelle forme de réponse API */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function extractFouCode(f: any): string {
|
||||||
|
return f.codefou ?? f.CodeFou ?? f.code_fournisseur ?? f.codeFournisseur ?? f.code ?? String(f.id ?? "");
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function extractFouNom(f: any): string {
|
||||||
|
return f.nomfou ?? f.NomFou ?? f.nom_fournisseur ?? f.nomFournisseur ?? f.nom ?? f.libelle ?? f.name ?? "Inconnu";
|
||||||
|
}
|
||||||
|
|
||||||
export async function getFournisseursFromApi(
|
export async function getFournisseursFromApi(
|
||||||
search?: string
|
search?: string
|
||||||
): Promise<{ code: string; nom: string }[]> {
|
): Promise<{ code: string; nom: string }[]> {
|
||||||
@@ -142,13 +171,18 @@ export async function getFournisseursFromApi(
|
|||||||
? `${FF_API_BASE}/api/fournisseurs?search=${encodeURIComponent(search)}&limit=500`
|
? `${FF_API_BASE}/api/fournisseurs?search=${encodeURIComponent(search)}&limit=500`
|
||||||
: `${FF_API_BASE}/api/fournisseurs?limit=500`;
|
: `${FF_API_BASE}/api/fournisseurs?limit=500`;
|
||||||
|
|
||||||
|
console.log(`[api-ff] GET ${url}`);
|
||||||
const res = await fetch(url, { cache: "no-store" });
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status} on ${url}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
// L'API peut renvoyer { items: [...] } ou directement [...]
|
// Log premier item pour diagnostic
|
||||||
const list: FfFournisseur[] = Array.isArray(data) ? data : (data.items ?? data.data ?? []);
|
const rawList = extractList(data);
|
||||||
return list.map(f => ({ code: f.codefou, nom: f.nomfou }));
|
console.log(`[api-ff] fournisseurs: ${rawList.length} items, sample keys:`, rawList[0] ? Object.keys(rawList[0] as object) : "empty");
|
||||||
|
|
||||||
|
return rawList
|
||||||
|
.map((f: unknown) => ({ code: extractFouCode(f), nom: extractFouNom(f) }))
|
||||||
|
.filter((f: { code: string; nom: string }) => f.code);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[api-ff] getFournisseursFromApi error:", err);
|
console.error("[api-ff] getFournisseursFromApi error:", err);
|
||||||
return [];
|
return [];
|
||||||
@@ -162,11 +196,21 @@ export async function getFournisseursFromApi(
|
|||||||
export async function getArticlesByFournisseur(
|
export async function getArticlesByFournisseur(
|
||||||
codefou: string
|
codefou: string
|
||||||
): Promise<FfArticle[]> {
|
): Promise<FfArticle[]> {
|
||||||
return fetchAllPages<FfArticle>(
|
const raw = await fetchAllPages<unknown>(
|
||||||
(page) => `${FF_API_BASE}/api/articles?codefou=${encodeURIComponent(codefou)}&page=${page}&limit=500`,
|
(page) => `${FF_API_BASE}/api/articles?codefou=${encodeURIComponent(codefou)}&page=${page}&limit=500`,
|
||||||
(data) => Array.isArray(data) ? data : ((data as any).items ?? (data as any).data ?? []),
|
extractList,
|
||||||
500
|
500
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return raw.map((r: any): FfArticle => ({
|
||||||
|
codein: r.codein ?? r.Codein ?? r.code_article ?? r.codeArticle ?? String(r.id ?? ""),
|
||||||
|
libelle1: r.libelle1 ?? r.Libelle1 ?? r.libelle ?? r.designation ?? r.nom ?? "",
|
||||||
|
codefou: r.codefou ?? r.Codefou ?? r.code_fournisseur ?? r.codeFournisseur ?? codefou,
|
||||||
|
nomfou: r.nomfou ?? r.Nomfou ?? r.nom_fournisseur ?? r.nomFournisseur ?? "",
|
||||||
|
pcb: r.pcb ?? r.Pcb ?? r.conditionnement ?? r.colisage,
|
||||||
|
pv_central: r.pv_central ?? r.PvCentral ?? r.pv ?? r.prixVente,
|
||||||
|
noid: r.noid ?? r.NoId ?? r.no_id,
|
||||||
|
})).filter(a => a.codein);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -178,12 +222,23 @@ export async function getMouvementsByFournisseur(
|
|||||||
dateDebut: string,
|
dateDebut: string,
|
||||||
dateFin: string
|
dateFin: string
|
||||||
): Promise<FfMouvement[]> {
|
): Promise<FfMouvement[]> {
|
||||||
return fetchAllPages<FfMouvement>(
|
const raw = await fetchAllPages<unknown>(
|
||||||
(page) =>
|
(page) =>
|
||||||
`${FF_API_BASE}/api/mouvements/articles?codefou=${encodeURIComponent(codefou)}&datedeb=${dateDebut}&datefin=${dateFin}&page=${page}&limit=1000`,
|
`${FF_API_BASE}/api/mouvements/articles?codefou=${encodeURIComponent(codefou)}&datedeb=${dateDebut}&datefin=${dateFin}&page=${page}&limit=1000`,
|
||||||
(data) => Array.isArray(data) ? data : ((data as any).items ?? (data as any).data ?? []),
|
extractList,
|
||||||
1000
|
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.datemvt ?? r.DateMvt ?? r.date_mvt ?? r.date ?? "",
|
||||||
|
qte: Number(r.qte ?? r.Qte ?? r.quantite ?? 0),
|
||||||
|
montant: Number(r.montant ?? r.Montant ?? r.montant_mvt ?? r.ca ?? 0),
|
||||||
|
marge: Number(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 ?? "",
|
||||||
|
})).filter(m => m.codein && m.datemvt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -207,8 +262,22 @@ export async function getStockByFournisseur(
|
|||||||
);
|
);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const sites: FfStock[] = Array.isArray(data) ? data : ((data as any).items ?? (data as any).data ?? []);
|
const rawSites = extractList(data);
|
||||||
if (sites.length === 0) return;
|
if (rawSites.length === 0) return;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const sites: FfStock[] = rawSites.map((s: any) => ({
|
||||||
|
codein: s.codein ?? s.Codein ?? art.codein,
|
||||||
|
site: s.site ?? s.Site ?? s.codesite ?? s.magasin ?? "",
|
||||||
|
stockdispo: Number(s.stockdispo ?? s.StockDispo ?? s.stock_dispo ?? s.stock ?? 0),
|
||||||
|
qte: Number(s.qte ?? s.Qte ?? s.quantite ?? 0),
|
||||||
|
valstock: Number(s.valstock ?? s.ValStock ?? s.val_stock ?? s.valeur ?? 0),
|
||||||
|
prmp: Number(s.prmp ?? s.Prmp ?? s.pa ?? s.prix_achat ?? 0) || undefined,
|
||||||
|
pv: Number(s.pv ?? s.Pv ?? s.prix_vente ?? s.pv_central ?? 0) || undefined,
|
||||||
|
dernieresventes: s.dernieresventes ?? s.DernVentes ?? s.derniere_vente ?? s.last_sale ?? "",
|
||||||
|
dernierereception: s.dernierereception ?? s.DernReception ?? s.derniere_reception ?? s.last_reception ?? "",
|
||||||
|
nbjoursdernsventes: Number(s.nbjoursdernsventes ?? s.NbJoursDernVentes ?? s.nb_jours ?? 0) || undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
const agg: AggregatedStock = {
|
const agg: AggregatedStock = {
|
||||||
stockActuel: 0,
|
stockActuel: 0,
|
||||||
|
|||||||
Reference in new issue
Block a user