mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-14 18:58:17 +02:00
Portage de tout ce qui, dans la Grille, ne dépend ni de la base FF ni de Qlik : - types.rs : ProductRow et types associés, JSON identique à JSON.stringify (camelCase, champs facultatifs omis, entiers sans décimale, NaN → null) et relecture sans perte des payloads grid_rows écrits par la version Node ; - store.rs : grid_rows (upsert par lots de 200 + purge, instantané, report des gammes, requêtes filtrées/triées/paginées via QueryBuilder et liste blanche de tri, fiche, fournisseurs, fraîcheur, nomenclatures) ; - rows_cache.rs : cache mémoire 10 min des lignes calculées (clé, LRU, patchGridRowsCache, invalidation, calculs en cours, compléments par magasin) ; - network_trend.rs, months.rs, store_patch.rs : logique pure testée ; - gammes_a_valider.rs, enregistrer.rs, snapshots.rs : actions getSnapshots, deleteSnapshot, saveSnapshot, saveDraftChanges, getGammesAValider, rejeterGammesAValider (validation zod reproduite, mêmes messages) ; - exports.rs : /api/export/excel et /api/export/modified-gammes (rust_xlsxwriter, mêmes feuilles, styles, largeurs et formats) ; - api_schemas.rs, zod.rs : schémas /api/v1, issues zod 4 à l'identique ; - api_v1.rs, openapi.rs : /api/v1/nomenclatures et /api/v1/openapi.json (document identique octet pour octet à celui de la version Node). Écart volontaire : listGridNomenclatures groupe par position (GROUP BY 1) ; la requête drizzle répétait left(code3, $n) avec un autre paramètre et échouait systématiquement sous PostgreSQL. Test d'intégration (grid_rows, snapshots, gammes à valider) activé par GRID_TEST_DATABASE_URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LG2LLJuswrrTTPKJmunEVf
69 lines
3.3 KiB
Rust
69 lines
3.3 KiB
Rust
//! Routes `/api/v1` du module qui ne dépendent que de l'instantané `grid_rows`
|
|
//! (phase 1) : `GET /api/v1/nomenclatures`.
|
|
|
|
use super::api_schemas::{parse_nomenclatures_query, query_map};
|
|
use super::store::{get_grid_freshness, list_grid_nomenclatures};
|
|
use crate::api_v1_response::{fail, ok, ApiErrorCode};
|
|
use crate::auth::ApiAuth;
|
|
use axum::extract::Query;
|
|
use axum::response::Response;
|
|
use serde_json::{json, Map, Value};
|
|
|
|
/// `GET /api/v1/nomenclatures?fournisseur=…&niveau=1|2|3&parent=…`
|
|
///
|
|
/// Répartition des articles d'un fournisseur par poste de nomenclature, avec le
|
|
/// nombre d'articles et le CA de chacun.
|
|
///
|
|
/// C'est la porte d'entrée pour traiter un gros fournisseur : certains dépassent
|
|
/// 130 000 articles, et tout récupérer d'un coup n'a de sens ni pour une IA ni
|
|
/// pour une application. On liste d'abord les postes — quelques dizaines de
|
|
/// lignes — puis on interroge `/grid?fournisseur=…&nomenclature=…` poste par poste.
|
|
///
|
|
/// Les codes sont hiérarchiques par préfixe (6 chiffres, de 31xxxx à 40xxxx) :
|
|
/// niveau 1 = « 32 », niveau 2 = « 3202 », niveau 3 = « 320211 ». `parent` accepte
|
|
/// un préfixe pour descendre l'arborescence sans tout charger.
|
|
pub async fn get_nomenclatures(ApiAuth(_auth): ApiAuth, Query(params): Query<Vec<(String, String)>>) -> Response {
|
|
let params = query_map(params);
|
|
let parsed = match parse_nomenclatures_query(¶ms) {
|
|
Ok(p) => p,
|
|
Err(issues) => return fail(ApiErrorCode::BadRequest, "Paramètres invalides.", Some(issues.to_json())),
|
|
};
|
|
let fournisseur = parsed.fournisseur;
|
|
|
|
// Même distinction que /grid : « pas encore calculé » n'est pas « vide ».
|
|
let freshness = match get_grid_freshness(&fournisseur).await {
|
|
Ok(f) => f,
|
|
Err(e) => return fail(ApiErrorCode::InternalError, e.message, None),
|
|
};
|
|
let Some(freshness) = freshness else {
|
|
return fail(
|
|
ApiErrorCode::NotReady,
|
|
format!(
|
|
"Aucun instantané pour le fournisseur « {fournisseur} ». Appelez /api/v1/grid?fournisseur={fournisseur} une fois pour le calculer."
|
|
),
|
|
Some(json!({ "fournisseur": fournisseur })),
|
|
);
|
|
};
|
|
|
|
let postes = match list_grid_nomenclatures(&fournisseur, parsed.niveau, parsed.parent.as_deref()).await {
|
|
Ok(p) => p,
|
|
Err(e) => return fail(ApiErrorCode::InternalError, e.message, None),
|
|
};
|
|
let nb_articles: i64 = postes.iter().map(|p| p.nb_articles).sum();
|
|
|
|
let mut meta = Map::new();
|
|
meta.insert("fournisseur".into(), json!(fournisseur));
|
|
meta.insert("niveau".into(), json!(parsed.niveau));
|
|
meta.insert("parent".into(), parsed.parent.map(Value::String).unwrap_or(Value::Null));
|
|
meta.insert("nbPostes".into(), json!(postes.len()));
|
|
// Total couvert par les postes listés — à rapprocher de snapshotRowCount.
|
|
meta.insert("nbArticles".into(), json!(nb_articles));
|
|
meta.insert("snapshotRowCount".into(), json!(freshness.row_count));
|
|
meta.insert("snapshotComputedAt".into(), json!(freshness.computed_at));
|
|
// Le paramètre de /grid à utiliser avec les `code` renvoyés ci-dessus.
|
|
// `nomenclature` filtre par préfixe et fonctionne aux trois niveaux.
|
|
meta.insert("filtreGrid".into(), json!("nomenclature"));
|
|
|
|
ok(postes, None, Some(meta))
|
|
}
|