mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-19 05:06:56 +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
130 lines
5.0 KiB
Rust
130 lines
5.0 KiB
Rust
//! 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(())
|
|
}
|