mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-18 04:36:21 +02:00
Lot 4 : Grille servie depuis l'instantané et requêtes allégées
- Grille : lecture de l'instantané grid_rows (gamme INIT et dernières gammes enregistrées reportées), recalcul en arrière-plan au-delà de 20 h, version de format dans chaque ligne pour écarter les anciens instantanés. - Navigateur : lignes réutilisées au retour sur la Grille (< 10 min), Grille conservée pendant un changement de magasin ou une actualisation, React.memo sur la Grille, les filtres et la barre du bas, en-tête mémorisé. - Fiche produit : toutes les lectures en parallèle. - Ventes par mois et stock sans vente : plages de dates au lieu de TO_CHAR. - Accueil : vagues de 12 appels, cache de 30 min. - API v1 : comptage et page en parallèle, last_used_at au plus une fois par minute, version interne non exposée. - Plan : suivi du lot 4, avec ce qui reste à valider sur la base réelle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
This commit is contained in:
13 files changed
+393
-119
No files matched your search
+55
-11
@@ -61,6 +61,13 @@ export interface GridQueryResult {
|
||||
computedAt: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version du format des lignes persistées. À incrémenter dès qu'un champ de
|
||||
* `ProductRow` est ajouté ou change de sens : les instantanés plus anciens ne
|
||||
* seront plus servis à la Grille (recalcul), mais restent lisibles par /api/v1.
|
||||
*/
|
||||
export const GRID_PAYLOAD_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Persiste les lignes calculées pour un fournisseur.
|
||||
*
|
||||
@@ -97,7 +104,7 @@ export async function upsertGridRows(codeFournisseur: string, rows: ProductRow[]
|
||||
nbMagasinsReseau: r.nbMagasinsReseau ?? null,
|
||||
caParMagasinReseau: r.caParMagasinReseau != null ? String(r.caParMagasinReseau) : null,
|
||||
margePctReseau: r.margePctReseau != null ? String(r.margePctReseau) : null,
|
||||
payload: r,
|
||||
payload: { ...r, payloadVersion: GRID_PAYLOAD_VERSION },
|
||||
computedAt,
|
||||
}));
|
||||
if (values.length === 0) return 0;
|
||||
@@ -147,6 +154,39 @@ export async function upsertGridRows(codeFournisseur: string, rows: ProductRow[]
|
||||
return written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantané complet d'un fournisseur, tel que persisté par le dernier calcul.
|
||||
*
|
||||
* Renvoie `null` (→ recalcul) si rien n'est persisté, si une ligne est d'une
|
||||
* version de format antérieure, ou si le calcul date d'un autre mois que le mois
|
||||
* en cours : la fenêtre des 12 mois glissants aurait changé entre-temps.
|
||||
* Les lignes sont rendues dans l'ordre du calcul en direct (identifiant article).
|
||||
*/
|
||||
export async function readGridSnapshot(
|
||||
codeFournisseur: string,
|
||||
): Promise<{ rows: ProductRow[]; computedAt: Date } | null> {
|
||||
const found = await db
|
||||
.select({ payload: gridRows.payload, computedAt: gridRows.computedAt })
|
||||
.from(gridRows)
|
||||
.where(eq(gridRows.codeFournisseur, codeFournisseur));
|
||||
if (found.length === 0) return null;
|
||||
|
||||
let oldest: Date | null = null;
|
||||
const rows: ProductRow[] = [];
|
||||
for (const r of found) {
|
||||
const row = r.payload as ProductRow;
|
||||
if (row.payloadVersion !== GRID_PAYLOAD_VERSION) return null;
|
||||
if (!oldest || r.computedAt < oldest) oldest = r.computedAt;
|
||||
rows.push(row);
|
||||
}
|
||||
const now = new Date();
|
||||
if (!oldest || oldest.getFullYear() !== now.getFullYear() || oldest.getMonth() !== now.getMonth()) {
|
||||
return null;
|
||||
}
|
||||
rows.sort((a, b) => (a.noid ?? 0) - (b.noid ?? 0));
|
||||
return { rows, computedAt: oldest };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reporte des gammes enregistrées dans l'instantané persisté (colonne et payload),
|
||||
* pour que /api/v1 les voie sans attendre un recalcul complet de la grille.
|
||||
@@ -221,20 +261,17 @@ export async function queryGridRows(q: GridQuery): Promise<GridQueryResult> {
|
||||
// NULLS LAST dans les deux sens : un produit sans CA ne doit jamais occuper la tête.
|
||||
const orderBy = q.order === "asc" ? asc(sortCol) : desc(sortCol);
|
||||
|
||||
const [countRow] = await db
|
||||
.select({ total: sql<number>`count(*)::int` })
|
||||
.from(gridRows)
|
||||
.where(where);
|
||||
|
||||
const selection = db
|
||||
.select({ payload: gridRows.payload, computedAt: gridRows.computedAt })
|
||||
.from(gridRows)
|
||||
.where(where)
|
||||
.orderBy(orderBy, asc(gridRows.codein));
|
||||
|
||||
const found = limit != null
|
||||
? await selection.limit(limit).offset((page - 1) * limit)
|
||||
: await selection;
|
||||
// Comptage et page en parallèle : deux connexions du pool, un seul temps d'attente.
|
||||
const [[countRow], found] = await Promise.all([
|
||||
db.select({ total: sql<number>`count(*)::int` }).from(gridRows).where(where),
|
||||
limit != null ? selection.limit(limit).offset((page - 1) * limit) : selection,
|
||||
]);
|
||||
|
||||
let oldest: Date | null = null;
|
||||
for (const r of found) {
|
||||
@@ -242,12 +279,19 @@ export async function queryGridRows(q: GridQuery): Promise<GridQueryResult> {
|
||||
}
|
||||
|
||||
return {
|
||||
rows: found.map((r) => r.payload as ProductRow),
|
||||
rows: found.map((r) => versLigneApi(r.payload)),
|
||||
total: countRow?.total ?? 0,
|
||||
computedAt: oldest ? oldest.toISOString() : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Ligne exposée par l'API : la version de format est un détail interne. */
|
||||
function versLigneApi(payload: unknown): ProductRow {
|
||||
const ligne = { ...(payload as ProductRow) };
|
||||
delete ligne.payloadVersion;
|
||||
return ligne;
|
||||
}
|
||||
|
||||
/** Fiche complète d'un produit. `codeFournisseur` lève l'ambiguïté d'un article multi-fournisseurs. */
|
||||
export async function getGridRowByCodein(
|
||||
codein: string,
|
||||
@@ -265,7 +309,7 @@ export async function getGridRowByCodein(
|
||||
|
||||
if (!found) return null;
|
||||
return {
|
||||
row: found.payload as ProductRow,
|
||||
row: versLigneApi(found.payload),
|
||||
computedAt: found.computedAt ? found.computedAt.toISOString() : null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user