mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-24 07:28:10 +02:00
Serveur Rust : module grid, phase 1 (instantané, snapshots, gammes, exports, API)
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
This commit is contained in:
19 files changed
+6139
-2
No files matched your search
@@ -0,0 +1,129 @@
|
||||
//! CollectFlow — Gammes proposées par l'API, en attente de validation
|
||||
//! (port de `src/lib/gammes-a-valider.ts`).
|
||||
//!
|
||||
//! L'API `/api/v1` ne change plus une gamme directement : elle dépose une
|
||||
//! proposition, que la Grille affiche comme une modification non enregistrée.
|
||||
//! L'utilisateur voit ainsi quels produits changent de gamme, puis valide
|
||||
//! (« Enregistrer ») ou rejette (« Annuler les modifications »).
|
||||
|
||||
use crate::db;
|
||||
use crate::error::AppResult;
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder, Row};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PropositionGamme {
|
||||
pub codein: String,
|
||||
/// Gamme proposée.
|
||||
pub gamme: String,
|
||||
/// Gamme enregistrée au moment de la proposition.
|
||||
pub gamme_precedente: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GammeAValider {
|
||||
pub codein: String,
|
||||
/// Gamme proposée.
|
||||
pub gamme: String,
|
||||
/// Gamme enregistrée au moment de la proposition.
|
||||
pub gamme_precedente: Option<String>,
|
||||
pub auteur: Option<String>,
|
||||
/// Date ISO de la proposition.
|
||||
pub proposee_le: String,
|
||||
}
|
||||
|
||||
/// Dépose des propositions (la dernière l'emporte pour un même article) et retire
|
||||
/// celles des articles `annules` — redemander la gamme déjà enregistrée annule la
|
||||
/// proposition en attente.
|
||||
pub async fn proposer_gammes(code_fournisseur: &str, propositions: &[PropositionGamme], annules: &[String], auteur: &str) -> AppResult<()> {
|
||||
let pool = db::pool()?;
|
||||
let mut tx = pool.begin().await?;
|
||||
if !annules.is_empty() {
|
||||
sqlx::query("delete from gammes_a_valider where code_fournisseur = $1 and codein = any($2)")
|
||||
.bind(code_fournisseur)
|
||||
.bind(annules)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
if !propositions.is_empty() {
|
||||
let maintenant = chrono::Utc::now().naive_utc();
|
||||
let auteur: String = auteur.chars().take(100).collect();
|
||||
let mut qb: QueryBuilder<Postgres> =
|
||||
QueryBuilder::new("insert into gammes_a_valider (code_fournisseur, codein, gamme, gamme_precedente, auteur, created_at) ");
|
||||
qb.push_values(propositions, |mut b, p| {
|
||||
b.push_bind(code_fournisseur.to_string())
|
||||
.push_bind(p.codein.clone())
|
||||
.push_bind(p.gamme.clone())
|
||||
.push_bind(p.gamme_precedente.clone())
|
||||
.push_bind(auteur.clone())
|
||||
.push_bind(maintenant);
|
||||
});
|
||||
qb.push(
|
||||
" on conflict (code_fournisseur, codein) do update set gamme = excluded.gamme, \
|
||||
gamme_precedente = excluded.gamme_precedente, auteur = excluded.auteur, created_at = excluded.created_at",
|
||||
);
|
||||
qb.build().execute(&mut *tx).await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Propositions en attente pour un fournisseur.
|
||||
pub async fn lister_gammes_a_valider(code_fournisseur: &str) -> AppResult<Vec<GammeAValider>> {
|
||||
let pool = db::pool()?;
|
||||
let rows = sqlx::query(
|
||||
"select code_fournisseur, codein, gamme, gamme_precedente, auteur, created_at from gammes_a_valider \
|
||||
where code_fournisseur = $1",
|
||||
)
|
||||
.bind(code_fournisseur)
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| GammeAValider {
|
||||
codein: r.try_get("codein").unwrap_or_default(),
|
||||
gamme: r.try_get("gamme").unwrap_or_default(),
|
||||
gamme_precedente: r.try_get("gamme_precedente").ok().flatten(),
|
||||
auteur: r.try_get("auteur").ok().flatten(),
|
||||
proposee_le: r.try_get::<NaiveDateTime, _>("created_at").map(db::iso).unwrap_or_default(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Propositions en attente pour une liste d'articles, indexées par `fournisseur|codein`.
|
||||
pub async fn gammes_a_valider_par_article(codeins: &[String]) -> AppResult<HashMap<String, String>> {
|
||||
if codeins.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
let pool = db::pool()?;
|
||||
let rows = sqlx::query("select code_fournisseur, codein, gamme from gammes_a_valider where codein = any($1)")
|
||||
.bind(codeins)
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let f: String = r.try_get("code_fournisseur").unwrap_or_default();
|
||||
let c: String = r.try_get("codein").unwrap_or_default();
|
||||
(format!("{f}|{c}"), r.try_get("gamme").unwrap_or_default())
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Retire des propositions, une fois validées ou rejetées dans la Grille.
|
||||
pub async fn supprimer_gammes_a_valider(code_fournisseur: &str, codeins: &[String]) -> AppResult<()> {
|
||||
if codeins.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let pool = db::pool()?;
|
||||
sqlx::query("delete from gammes_a_valider where code_fournisseur = $1 and codein = any($2)")
|
||||
.bind(code_fournisseur)
|
||||
.bind(codeins)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user