mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-23 15:08:12 +02:00
feat: integrate /referentiel endpoint for nomenclature, gamme, stock, PA
Replaces ventesProduits DB query with API call to /articles/:no_id/referentiel. Called in parallel with mensuel and commandes. Extracts: - article.nom_code/nom_libelle → code3/libelle3 (nomenclature) - gammes[0].gamme_code → codeGamme + codeGammeInit (frozen initial) - prix.achat + fournisseurs[0].pcb → pa/prixAchat/pcb - stock[] → stockActuel/stockTotal/stockValeur (real-time) - performance.derniere_vente/derniere_entree → derniereVente/derniereLivraison Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca467c73f6
commit
8986f7d001
2 files changed
+122
-34
No files matched your search
@@ -5,12 +5,13 @@ import { computeProductScores } from "@/lib/score-engine";
|
|||||||
import {
|
import {
|
||||||
getArticlesByFournisseur,
|
getArticlesByFournisseur,
|
||||||
getMensuelByArticles,
|
getMensuelByArticles,
|
||||||
|
getReferentielByArticles,
|
||||||
getCommandesByFournisseur,
|
getCommandesByFournisseur,
|
||||||
buildLast12MonthsRange,
|
buildLast12MonthsRange,
|
||||||
} from "@/lib/api-ff-client";
|
} from "@/lib/api-ff-client";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { sessionSnapshots, ventesProduits } from "@/db/schema";
|
import { sessionSnapshots } from "@/db/schema";
|
||||||
import { eq, desc, inArray } from "drizzle-orm";
|
import { eq, desc } from "drizzle-orm";
|
||||||
|
|
||||||
interface GetProductRowsInput {
|
interface GetProductRowsInput {
|
||||||
codeFournisseur: string;
|
codeFournisseur: string;
|
||||||
@@ -29,8 +30,9 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
|
|||||||
|
|
||||||
// ─── Phase 2 : Mensuel + Commandes (en parallèle) ────────────────────
|
// ─── Phase 2 : Mensuel + Commandes (en parallèle) ────────────────────
|
||||||
const { dateDebut, dateFin } = buildLast12MonthsRange();
|
const { dateDebut, dateFin } = buildLast12MonthsRange();
|
||||||
const [mensuelMap, commandesMap] = await Promise.all([
|
const [mensuelMap, referentielMap, commandesMap] = await Promise.all([
|
||||||
getMensuelByArticles(articles, dateDebut, dateFin),
|
getMensuelByArticles(articles, dateDebut, dateFin),
|
||||||
|
getReferentielByArticles(articles),
|
||||||
getCommandesByFournisseur(codeFournisseur),
|
getCommandesByFournisseur(codeFournisseur),
|
||||||
]);
|
]);
|
||||||
console.log(`[getProductRows] mensuel data for ${mensuelMap.size}/${articles.length} articles`);
|
console.log(`[getProductRows] mensuel data for ${mensuelMap.size}/${articles.length} articles`);
|
||||||
@@ -151,40 +153,43 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
|
|||||||
if (cmdQty) product.commandesEnCours = cmdQty;
|
if (cmdQty) product.commandesEnCours = cmdQty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Phase 6 : Classification depuis ventesProduits (lecture seule) ──
|
// ─── Phase 6 : Classification depuis referentiel API (nomenclature + gamme) ─
|
||||||
// Requête par codein (pas par code_fournisseur, car l'ancien code peut différer)
|
let refCount = 0;
|
||||||
try {
|
for (const [codein, ref] of referentielMap.entries()) {
|
||||||
const codeins = Array.from(productMap.keys());
|
const product = productMap.get(codein);
|
||||||
if (codeins.length > 0) {
|
if (!product) continue;
|
||||||
const classifRows = await db
|
|
||||||
.select({
|
|
||||||
codein: ventesProduits.codein,
|
|
||||||
codeGamme: ventesProduits.codeGamme,
|
|
||||||
codeGammeInit: ventesProduits.codeGammeInit,
|
|
||||||
code3: ventesProduits.code3,
|
|
||||||
libelle3: ventesProduits.libelle3,
|
|
||||||
})
|
|
||||||
.from(ventesProduits)
|
|
||||||
.where(inArray(ventesProduits.codein, codeins));
|
|
||||||
|
|
||||||
// Déduplique en JS : premier occurrence par codein
|
// Nomenclature
|
||||||
const seen = new Set<string>();
|
product.code3 = ref.article.nom_code ?? "";
|
||||||
for (const row of classifRows) {
|
product.libelle3 = ref.article.nom_libelle ?? "";
|
||||||
if (seen.has(row.codein)) continue;
|
|
||||||
seen.add(row.codein);
|
// Gamme initiale (figée) + gamme courante (sera overridée par snapshot si besoin)
|
||||||
const product = productMap.get(row.codein);
|
const gammeCode = ref.gammes?.[0]?.gamme_code ?? null;
|
||||||
if (product) {
|
product.codeGammeInit = gammeCode;
|
||||||
product.codeGamme = row.codeGamme ?? null;
|
product.codeGamme = gammeCode;
|
||||||
product.codeGammeInit = row.codeGammeInit ?? null;
|
|
||||||
product.code3 = row.code3 ?? "";
|
// PA et PCB depuis referentiel (plus précis que articles endpoint)
|
||||||
product.libelle3 = row.libelle3 ?? "";
|
const pa = parseFloat(ref.prix?.achat ?? "0") || 0;
|
||||||
}
|
if (pa > 0) { product.pa = pa; product.prixAchat = pa; }
|
||||||
}
|
const pcb = parseFloat(ref.fournisseurs?.[0]?.pcb ?? "0") || 0;
|
||||||
console.log(`[getProductRows] Classification: ${seen.size} codeins trouvés sur ${codeins.length} articles`);
|
if (pcb > 0) product.pcb = pcb;
|
||||||
|
|
||||||
|
// Stock actuel agrégé tous sites (valeur temps réel)
|
||||||
|
if (ref.stock?.length) {
|
||||||
|
product.stockActuel = ref.stock.reduce((sum, s) => sum + (parseFloat(s.stockdispo ?? "0") || 0), 0);
|
||||||
|
product.stockTotal = ref.stock.reduce((sum, s) => sum + (parseFloat(s.qte ?? "0") || 0), 0);
|
||||||
|
product.stockValeur = ref.stock.reduce((sum, s) => sum + (parseFloat(s.valstock ?? "0") || 0), 0);
|
||||||
|
const firstSite = ref.stock.find(s => parseFloat(s.prmp ?? "0") > 0);
|
||||||
|
if (firstSite && !product.pa) { product.pa = parseFloat(firstSite.prmp); product.prixAchat = product.pa; }
|
||||||
}
|
}
|
||||||
} catch (classifErr) {
|
|
||||||
console.warn("[getProductRows] ventesProduits classification non disponible:", classifErr);
|
// Dernière vente depuis performance
|
||||||
|
if (ref.performance?.derniere_vente) product.derniereVente = ref.performance.derniere_vente;
|
||||||
|
if (ref.performance?.derniere_entree) product.derniereLivraison = ref.performance.derniere_entree;
|
||||||
|
|
||||||
|
refCount++;
|
||||||
}
|
}
|
||||||
|
console.log(`[getProductRows] Referentiel: ${refCount}/${articles.length} articles enrichis`);
|
||||||
|
|
||||||
// ─── Phase 7 : Restaurer gammes depuis dernier snapshot ──────────────
|
// ─── Phase 7 : Restaurer gammes depuis dernier snapshot ──────────────
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -311,6 +311,89 @@ export async function getMensuelByArticles(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Referentiel — article complet : nomenclature, gammes, stock, prix, fournisseurs
|
||||||
|
// GET /api/articles/:no_id/referentiel
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface FfReferentiel {
|
||||||
|
article: {
|
||||||
|
no_id: string;
|
||||||
|
codein: string;
|
||||||
|
nom_code: string | null; // code nomenclature (ex: "360504")
|
||||||
|
nom_libelle: string | null; // libellé nomenclature
|
||||||
|
nom_niveau: number | null;
|
||||||
|
nom_chemin_pere: string | null;
|
||||||
|
};
|
||||||
|
gammes: {
|
||||||
|
gamme_code: string;
|
||||||
|
gamme_libelle: string;
|
||||||
|
saison_code: string;
|
||||||
|
saison_libelle: string;
|
||||||
|
}[];
|
||||||
|
stock: {
|
||||||
|
site: string;
|
||||||
|
stockdispo: string;
|
||||||
|
qte: string;
|
||||||
|
prmp: string;
|
||||||
|
valstock: string;
|
||||||
|
pv: string;
|
||||||
|
dernierevente: string;
|
||||||
|
dernierereception: string;
|
||||||
|
stockmort: boolean;
|
||||||
|
}[];
|
||||||
|
prix: {
|
||||||
|
achat: string | null;
|
||||||
|
vente_par_site: { site: string; pv: string }[];
|
||||||
|
} | null;
|
||||||
|
fournisseurs: {
|
||||||
|
codefou: string;
|
||||||
|
ref_fou: string;
|
||||||
|
pcb: string;
|
||||||
|
prixachat: string;
|
||||||
|
}[];
|
||||||
|
performance: {
|
||||||
|
derniere_vente: string | null;
|
||||||
|
derniere_entree: string | null;
|
||||||
|
qte_totale_vendue: string | null;
|
||||||
|
ca_ttc_total: string | null;
|
||||||
|
marge_totale: string | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le référentiel complet (nomenclature, gamme, stock, prix) pour un lot d'articles.
|
||||||
|
* Utilise no_id comme identifiant.
|
||||||
|
* Retourne Map<codein, FfReferentiel>
|
||||||
|
*/
|
||||||
|
export async function getReferentielByArticles(
|
||||||
|
articles: FfArticle[],
|
||||||
|
batchSize = 20
|
||||||
|
): Promise<Map<string, FfReferentiel>> {
|
||||||
|
const result = new Map<string, FfReferentiel>();
|
||||||
|
|
||||||
|
for (let i = 0; i < articles.length; i += batchSize) {
|
||||||
|
const batch = articles.slice(i, i + batchSize);
|
||||||
|
await Promise.all(
|
||||||
|
batch.map(async (art) => {
|
||||||
|
const noId = art.noid;
|
||||||
|
if (!noId) return;
|
||||||
|
try {
|
||||||
|
const url = `${FF_API_BASE}/api/articles/${encodeURIComponent(String(noId))}/referentiel`;
|
||||||
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data: FfReferentiel = await res.json();
|
||||||
|
if (data?.article) result.set(art.codein, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[api-ff] getReferentiel error for ${art.codein}:`, err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Commandes en cours
|
// Commandes en cours
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in new issue
Block a user