feat(grid): ajout algorithme score produit avec parametrage dynamique et page d'aide

This commit is contained in:
Michael committed 2026-02-21 04:00:51 +01:00
1 parent f540e4f2b3
commit 3544c0c898
67 files changed
+19476 -15

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
"use server";
import { db } from "@/db";
import { ventesProduits } from "@/db/schema";
import { eq, and, ne, sql } from "drizzle-orm";
import type { ProductRow, GammeCode } from "@/types/grid";
import { computeProductScores } from "@/lib/score-engine";
interface GetProductRowsInput {
codeFournisseur: string;
magasin?: string;
}
export async function getProductRows(input: GetProductRowsInput): Promise<ProductRow[]> {
const { codeFournisseur, magasin = "TOTAL" } = input;
// Get all rows for this supplier (excluding the synthetic TOTAL period)
const rows = await db
.select()
.from(ventesProduits)
.where(
and(
eq(ventesProduits.codeFournisseur, codeFournisseur),
ne(ventesProduits.periode, "TOTAL"),
magasin !== "TOTAL" ? eq(ventesProduits.magasin, magasin) : ne(ventesProduits.magasin, "TOTAL"),
)
);
// Group by codein and aggregate monthly sales
const productMap = new Map<string, ProductRow>();
for (const row of rows) {
const existing = productMap.get(row.codein);
const monthKey = row.periode;
const qty = Math.abs(parseFloat(row.quantite?.toString() ?? "0"));
const ca = Math.abs(parseFloat(row.montantMvt?.toString() ?? "0"));
const marge = Math.abs(parseFloat(row.margeMvt?.toString() ?? "0"));
if (existing) {
existing.sales12m[monthKey] = (existing.sales12m[monthKey] ?? 0) + qty;
existing.totalQuantite += qty;
existing.totalCa += ca;
existing.totalMarge += marge;
existing.tauxMarge = existing.totalCa > 0 ? (existing.totalMarge / existing.totalCa) * 100 : 0;
} else {
productMap.set(row.codein, {
codein: row.codein,
codeFournisseur: row.codeFournisseur ?? "",
nomFournisseur: row.nomFournisseur ?? "",
libelle1: row.libelle1 ?? "",
gtin: row.gtin ?? "",
reference: row.reference ?? "",
code3: row.code3 ?? "",
libelle3: row.libelle3 ?? "",
codeGamme: (row.codeGamme as GammeCode | null),
codeGammeDraft: null,
sales12m: { [monthKey]: qty },
totalQuantite: qty,
totalCa: ca,
totalMarge: marge,
tauxMarge: ca > 0 ? (marge / ca) * 100 : 0,
score: 0,
aiRecommendation: null,
});
}
}
return computeProductScores(Array.from(productMap.values()));
}
@@ -0,0 +1,49 @@
"use server";
import { db } from "@/db";
import { ventesProduits } from "@/db/schema";
import { eq, and } from "drizzle-orm";
import { z } from "zod";
const SaveDraftsSchema = z.object({
changes: z.array(
z.object({
codein: z.string(),
magasin: z.string(),
codeGamme: z.enum(["A", "B", "C", "Z"]),
})
),
});
export async function saveDraftChanges(
raw: unknown
): Promise<{ success: boolean; saved: number; error?: string }> {
const parsed = SaveDraftsSchema.safeParse(raw);
if (!parsed.success) {
return { success: false, saved: 0, error: "Validation failed: " + parsed.error.message };
}
const { changes } = parsed.data;
try {
// Batch update all rows for given codein × magasin
await Promise.all(
changes.map(({ codein, magasin, codeGamme }) =>
db
.update(ventesProduits)
.set({ codeGamme })
.where(
and(
eq(ventesProduits.codein, codein),
eq(ventesProduits.magasin, magasin)
)
)
)
);
return { success: true, saved: changes.length };
} catch (err) {
const msg = err instanceof Error ? err.message : "Unknown error";
return { success: false, saved: 0, error: msg };
}
}