mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
105 lines
4.1 KiB
TypeScript
105 lines
4.1 KiB
TypeScript
"use server";
|
|
|
|
import { db } from "@/db";
|
|
import { ventesProduits } from "@/db/schema";
|
|
import { eq, and, ne } 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;
|
|
|
|
// Calculer les 12 derniers mois COMPLETS (excluant le mois en cours)
|
|
const allowedPeriods = new Set<string>();
|
|
const now = new Date();
|
|
for (let i = 12; i >= 1; i--) {
|
|
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
|
allowedPeriods.add(`${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}`);
|
|
}
|
|
|
|
// 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 + track store participation
|
|
const productMap = new Map<string, ProductRow>();
|
|
const storeParticipationMap = new Map<string, Map<string, Set<string>>>(); // codein -> magasin -> Set of periods
|
|
|
|
for (const row of rows) {
|
|
// Ignorer les données qui ne font pas partie des 12 mois complets
|
|
if (!allowedPeriods.has(row.periode)) continue;
|
|
|
|
const { codein, magasin, periode } = row;
|
|
|
|
// Suivi de la participation par magasin
|
|
if (!storeParticipationMap.has(codein)) storeParticipationMap.set(codein, new Map());
|
|
const productStores = storeParticipationMap.get(codein)!;
|
|
if (!productStores.has(magasin)) productStores.set(magasin, new Set());
|
|
productStores.get(magasin)!.add(periode);
|
|
|
|
const existing = productMap.get(codein);
|
|
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[periode] = (existing.sales12m[periode] ?? 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(codein, {
|
|
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: { [periode]: qty },
|
|
totalQuantite: qty,
|
|
totalCa: ca,
|
|
totalMarge: marge,
|
|
tauxMarge: ca > 0 ? (marge / ca) * 100 : 0,
|
|
score: 0,
|
|
workingStores: [],
|
|
aiRecommendation: null,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Finaliser la liste des magasins "actifs" (travaillant le produit : >= 3 mois de vente)
|
|
for (const [codein, product] of productMap.entries()) {
|
|
const productStores = storeParticipationMap.get(codein);
|
|
if (productStores) {
|
|
const workingStores: string[] = [];
|
|
for (const [magasin, periods] of productStores.entries()) {
|
|
if (magasin === "TOTAL") continue;
|
|
if (periods.size >= 3) {
|
|
workingStores.push(magasin);
|
|
}
|
|
}
|
|
product.workingStores = workingStores.sort();
|
|
}
|
|
}
|
|
|
|
return computeProductScores(Array.from(productMap.values()));
|
|
}
|