mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-16 11:49:18 +02:00
Lot 5 : changement de magasin instantané et contrôle des droits
Grille - Corrige le lot 4 : au changement de magasin, les chiffres et le tri affichés suivent tout de suite (nouvelle liste), la sélection est gardée. - Lignes toujours chargées « Nos 2 magasins » ; changement de magasin sans aller-retour serveur (URL mise à jour par l'historique du navigateur). - Complément de l'API FF par magasin servi à part (/api/grid/rows/store-patch), appliqué à son arrivée, calcul identique à l'ancien (lib/store-patch.ts). - La recherche ne reconstruit plus toutes les lignes à chaque frappe ; la sélection n'est remise à zéro qu'au changement de fournisseur. Sécurité - lib/authz.ts : session relue en base (cache 1 min), rôle de la base prioritaire ; variantes pour actions, routes et pages. - Paramètres réservés aux administrateurs ; session vérifiée dans les actions Grille, Commandes, Historique et les routes ; diagnostics réservés admin. - Mots de passe et clés d'IA plus envoyés au navigateur ; champ vide = mot de passe conservé ; plus de mot de passe dans le localStorage ; l'enregistrement de la base n'efface plus les clés d'IA. - admin/admin créé seulement si aucun utilisateur ; fichier de secours utilisé uniquement si la base est injoignable ; data/ ignoré par git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
This commit is contained in:
44 files changed
+1155
-531
No files matched your search
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Complément des chiffres d'un magasin par l'API FF (ventes mensuelles).
|
||||
*
|
||||
* Le calcul SQL de la Grille peut manquer les ventes d'un magasin pour certains
|
||||
* articles (quantité à 0 pour ce magasin). L'API FF les connaît : on en tire,
|
||||
* pour ces articles seulement, les séries et totaux du magasin.
|
||||
*
|
||||
* Module sans dépendance d'exécution : utilisé côté serveur (calcul du
|
||||
* complément) comme côté navigateur (application aux lignes affichées).
|
||||
*/
|
||||
|
||||
import type { ProductRow } from "@/types/grid";
|
||||
import type { FfMensuelEntry } from "@/lib/api-ff-client";
|
||||
|
||||
/** Chiffres d'un article pour le magasin complété. */
|
||||
export interface StorePatchEntry {
|
||||
codein: string;
|
||||
/** Ventes par mois (« YYYYMM »). */
|
||||
sales: Record<string, number>;
|
||||
/** Stock de fin de mois, reporté sur les mois sans donnée. */
|
||||
stock: Record<string, number>;
|
||||
receptions: Record<string, number>;
|
||||
quantite: number;
|
||||
ca: number;
|
||||
marge: number;
|
||||
}
|
||||
|
||||
export interface StorePatch {
|
||||
magasin: string;
|
||||
/** Fenêtre des 12 mois du calcul : un complément d'un autre mois est ignoré. */
|
||||
periods: string[];
|
||||
entries: StorePatchEntry[];
|
||||
}
|
||||
|
||||
/** Articles à compléter : connus de FF (noid) et sans aucune vente SQL dans ce magasin. */
|
||||
export function storePatchCandidates(rows: ProductRow[], magasin: string): ProductRow[] {
|
||||
return rows.filter((row) => !!row.noid && (row.quantiteByStore?.[magasin] ?? 0) === 0);
|
||||
}
|
||||
|
||||
/** Chiffres du magasin tirés de l'API, pour les articles où elle montre des ventes. */
|
||||
export function buildStorePatchEntries(
|
||||
retenus: ProductRow[],
|
||||
magasin: string,
|
||||
periods: string[],
|
||||
mensuel: Map<string, FfMensuelEntry[]>,
|
||||
): StorePatchEntry[] {
|
||||
const resultat: StorePatchEntry[] = [];
|
||||
for (const row of retenus) {
|
||||
const entries = (mensuel.get(row.codein) ?? []).filter((entry) => entry.site === magasin);
|
||||
if (entries.length === 0) continue;
|
||||
|
||||
const byPeriod = new Map<string, { qty: number; ca: number; marge: number; stock: number; receptions: number }>();
|
||||
for (const entry of entries) {
|
||||
const period = entry.mois.replace("-", "");
|
||||
if (!periods.includes(period)) continue;
|
||||
// qte_vendue / ca_ht sont NÉGATIFS côté API (ventes nettes) : on
|
||||
// les nie au lieu de Math.abs pour que les retours restent déduits.
|
||||
const qty = -(Number(entry.ventes?.qte_vendue ?? 0) || 0);
|
||||
const ca = -(Number(entry.ventes?.ca_ht ?? 0) || 0);
|
||||
const marge = Number(entry.ventes?.marge ?? 0) || 0;
|
||||
const stock = Number(entry.stock_fin_mois ?? 0) || 0;
|
||||
const receptions = Number(entry.receptions?.qte_recue ?? 0) || 0;
|
||||
byPeriod.set(period, { qty, ca, marge, stock, receptions });
|
||||
}
|
||||
|
||||
const apiQty = [...byPeriod.values()].reduce((sum, value) => sum + value.qty, 0);
|
||||
if (apiQty === 0) continue;
|
||||
|
||||
const e: StorePatchEntry = { codein: row.codein, sales: {}, stock: {}, receptions: {}, quantite: 0, ca: 0, marge: 0 };
|
||||
let lastStock = 0;
|
||||
for (const period of periods) {
|
||||
const value = byPeriod.get(period);
|
||||
e.sales[period] = value?.qty ?? 0;
|
||||
e.receptions[period] = value?.receptions ?? 0;
|
||||
if (value) lastStock = value.stock;
|
||||
e.stock[period] = lastStock;
|
||||
e.quantite += value?.qty ?? 0;
|
||||
e.ca += value?.ca ?? 0;
|
||||
e.marge += value?.marge ?? 0;
|
||||
}
|
||||
resultat.push(e);
|
||||
}
|
||||
return resultat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application historique, côté serveur (`/api/grid/rows?magasin=…`) : corrige le
|
||||
* magasin ET reporte les écarts sur les totaux « tous magasins » de la ligne.
|
||||
* Renvoie le nombre de lignes corrigées.
|
||||
*/
|
||||
export function applyStorePatchInPlace(
|
||||
rows: ProductRow[],
|
||||
magasin: string,
|
||||
periods: string[],
|
||||
entries: StorePatchEntry[],
|
||||
): number {
|
||||
const parCode = new Map(entries.map((e) => [e.codein, e]));
|
||||
let corrigees = 0;
|
||||
for (const row of rows) {
|
||||
const e = parCode.get(row.codein);
|
||||
if (!e) continue;
|
||||
|
||||
row.sales12mByStore ??= {};
|
||||
row.stock12mByStore ??= {};
|
||||
row.receptions12mByStore ??= {};
|
||||
row.caByStore ??= {};
|
||||
row.quantiteByStore ??= {};
|
||||
row.margeByStore ??= {};
|
||||
row.sales12mByStore[magasin] ??= {};
|
||||
row.stock12mByStore[magasin] ??= {};
|
||||
row.receptions12mByStore[magasin] ??= {};
|
||||
|
||||
for (const period of periods) {
|
||||
const currentQty = row.sales12mByStore[magasin][period] ?? 0;
|
||||
const nextQty = e.sales[period] ?? 0;
|
||||
row.sales12mByStore[magasin][period] = nextQty;
|
||||
row.receptions12mByStore[magasin][period] = e.receptions[period] ?? 0;
|
||||
row.stock12mByStore[magasin][period] = e.stock[period] ?? 0;
|
||||
row.sales12m[period] = (row.sales12m[period] ?? 0) + (nextQty - currentQty);
|
||||
}
|
||||
|
||||
const deltaTotalQty = e.quantite - (row.quantiteByStore[magasin] ?? 0);
|
||||
const deltaTotalCa = e.ca - (row.caByStore[magasin] ?? 0);
|
||||
const deltaTotalMarge = e.marge - (row.margeByStore[magasin] ?? 0);
|
||||
|
||||
row.quantiteByStore[magasin] = e.quantite;
|
||||
row.caByStore[magasin] = e.ca;
|
||||
row.margeByStore[magasin] = e.marge;
|
||||
row.totalQuantite += deltaTotalQty;
|
||||
row.totalCa += deltaTotalCa;
|
||||
row.totalMarge += deltaTotalMarge;
|
||||
row.tauxMarge = row.totalCa > 0 ? (row.totalMarge / row.totalCa) * 100 : 0;
|
||||
if (!row.workingStores.includes(magasin)) row.workingStores.push(magasin);
|
||||
corrigees++;
|
||||
}
|
||||
return corrigees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application dans le navigateur : nouvelle ligne, chiffres du magasin seuls.
|
||||
* Les totaux « tous magasins » ne bougent pas, pour que la vue « Nos 2
|
||||
* magasins » reste identique quel que soit le magasin consulté avant.
|
||||
*/
|
||||
export function withStorePatch(row: ProductRow, magasin: string, e: StorePatchEntry): ProductRow {
|
||||
return {
|
||||
...row,
|
||||
sales12mByStore: { ...row.sales12mByStore, [magasin]: e.sales },
|
||||
stock12mByStore: { ...row.stock12mByStore, [magasin]: e.stock },
|
||||
receptions12mByStore: { ...row.receptions12mByStore, [magasin]: e.receptions },
|
||||
quantiteByStore: { ...row.quantiteByStore, [magasin]: e.quantite },
|
||||
caByStore: { ...row.caByStore, [magasin]: e.ca },
|
||||
margeByStore: { ...row.margeByStore, [magasin]: e.marge },
|
||||
workingStores: row.workingStores.includes(magasin) ? row.workingStores : [...row.workingStores, magasin],
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user