mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-13 18:27:40 +02:00
feat(api): inventaire des nomenclatures, pour découper un gros fournisseur
Les filtres code1/code2/code3 existaient déjà sur /grid, mais un appelant ne pouvait pas savoir quelles valeurs existent chez un fournisseur : il aurait dû tout télécharger pour les découvrir, ce qui annulait l'intérêt du découpage. GET /api/v1/nomenclatures?fournisseur=…&niveau=1|2|3&parent=… renvoie les postes avec leur nombre d'articles et leur CA, agrégés en SQL — quelques dizaines de lignes, même pour un fournisseur de 130 000 articles. `parent` permet de descendre l'arborescence sans tout charger, et meta.filtreGrid indique le paramètre de /grid correspondant au niveau demandé. Le parcours devient : lister les postes, puis /grid?fournisseur=…&code1=… poste par poste. Bien plus praticable pour une IA que 132 000 lignes d'un bloc, et plus pertinent métier que le découpage par gamme. Les libellés sont lus dans le payload jsonb (ils n'ont pas de colonne dédiée). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
This commit is contained in:
5 files changed
+191
No files matched your search
@@ -88,6 +88,19 @@ export const fournisseursQuerySchema = z.object({
|
||||
withData: z.enum(["0", "1"]).optional(),
|
||||
});
|
||||
|
||||
/** `/api/v1/nomenclatures` — inventaire des postes d'un fournisseur. */
|
||||
export const nomenclaturesQuerySchema = z.object({
|
||||
fournisseur: z.string().min(1, "Paramètre 'fournisseur' requis"),
|
||||
/**
|
||||
* 1 = univers, 2 = famille, 3 = sous-famille (selon la nomenclature FF).
|
||||
* Le `transform` ne fait que redonner le type littéral : les bornes ont déjà
|
||||
* été validées juste avant, la valeur ne peut être que 1, 2 ou 3.
|
||||
*/
|
||||
niveau: z.coerce.number().int().min(1).max(3).default(1).transform((n) => n as 1 | 2 | 3),
|
||||
/** Restreint à un poste parent : un code1 pour niveau=2, un code2 pour niveau=3. */
|
||||
parent: z.string().min(1).optional(),
|
||||
});
|
||||
|
||||
export const productDetailSchema = z.object({
|
||||
fournisseur: z.string().min(1).optional(),
|
||||
/**
|
||||
|
||||
@@ -277,3 +277,66 @@ export async function listGridSuppliers(): Promise<
|
||||
computedAt: r.computedAt ? new Date(r.computedAt).toISOString() : null,
|
||||
}));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inventaire des nomenclatures d'un fournisseur
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Un poste de nomenclature, avec son poids — de quoi décider par où découper. */
|
||||
export interface GridNomenclature {
|
||||
code: string;
|
||||
/** Libellé du poste, lu dans le payload (il n'a pas de colonne dédiée). */
|
||||
libelle: string | null;
|
||||
nbArticles: number;
|
||||
totalCa: number;
|
||||
totalQuantite: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Répartition des articles d'un fournisseur par niveau de nomenclature.
|
||||
*
|
||||
* Sans cela, un appelant qui veut découper un gros fournisseur par nomenclature
|
||||
* est coincé : les filtres `code1..3` existent sur `/grid`, mais rien ne lui dit
|
||||
* **quelles** valeurs existent — il devrait tout télécharger pour les découvrir,
|
||||
* ce qui annule le bénéfice du découpage.
|
||||
*
|
||||
* Tout est agrégé en SQL : la réponse fait quelques dizaines de lignes, même pour
|
||||
* un fournisseur de 130 000 articles.
|
||||
*/
|
||||
export async function listGridNomenclatures(
|
||||
codeFournisseur: string,
|
||||
niveau: 1 | 2 | 3,
|
||||
parent?: string,
|
||||
): Promise<GridNomenclature[]> {
|
||||
const codeCol = niveau === 1 ? gridRows.code1 : niveau === 2 ? gridRows.code2 : gridRows.code3;
|
||||
// Les libellés ne sont pas dénormalisés en colonnes : on les lit dans le payload.
|
||||
const libelleKey = niveau === 1 ? "libelleNiveau1" : niveau === 2 ? "libelleNiveau2" : "libelle3";
|
||||
const parentCol = niveau === 2 ? gridRows.code1 : niveau === 3 ? gridRows.code2 : null;
|
||||
|
||||
const clauses: SQL[] = [
|
||||
eq(gridRows.codeFournisseur, codeFournisseur),
|
||||
sql`${codeCol} is not null and ${codeCol} <> ''`,
|
||||
];
|
||||
if (parent && parentCol) clauses.push(eq(parentCol, parent));
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
code: codeCol,
|
||||
libelle: sql<string | null>`max(${gridRows.payload} ->> ${libelleKey})`,
|
||||
nbArticles: sql<number>`count(*)::int`,
|
||||
totalCa: sql<number>`coalesce(sum(${gridRows.totalCa}), 0)::float`,
|
||||
totalQuantite: sql<number>`coalesce(sum(${gridRows.totalQuantite}), 0)::float`,
|
||||
})
|
||||
.from(gridRows)
|
||||
.where(and(...clauses))
|
||||
.groupBy(codeCol)
|
||||
.orderBy(desc(sql`coalesce(sum(${gridRows.totalCa}), 0)`));
|
||||
|
||||
return rows.map((r) => ({
|
||||
code: String(r.code ?? ""),
|
||||
libelle: r.libelle ?? null,
|
||||
nbArticles: Number(r.nbArticles) || 0,
|
||||
totalCa: Number(r.totalCa) || 0,
|
||||
totalQuantite: Number(r.totalQuantite) || 0,
|
||||
}));
|
||||
}
|
||||
Reference in new issue
Block a user