mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-12 01:36:43 +02:00
163 lines
7.0 KiB
TypeScript
163 lines
7.0 KiB
TypeScript
"use server";
|
|
|
|
import { db } from "@/db";
|
|
import { ventesProduits } from "@/db/schema";
|
|
import { eq, and, ne, like, sql } from "drizzle-orm";
|
|
import type { ProductRow, GammeCode, GridFilters } from "@/types/grid";
|
|
import { computeProductScores } from "@/lib/score-engine";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
let isSchemaInitialized = false;
|
|
|
|
interface GetProductRowsInput {
|
|
codeFournisseur: string;
|
|
magasin?: string;
|
|
filters?: Partial<GridFilters>;
|
|
}
|
|
|
|
export async function getProductRows(input: GetProductRowsInput): Promise<ProductRow[]> {
|
|
const { codeFournisseur, magasin = "TOTAL", filters } = input;
|
|
console.log(`\n>>> [getProductRows] RECEIVED CALL for supplier: ${codeFournisseur}, magasin: ${magasin}`);
|
|
|
|
try {
|
|
// Self-healing: Ensure codeGammeInit column exists and is populated
|
|
if (!isSchemaInitialized) {
|
|
try {
|
|
await db.execute(sql`
|
|
ALTER TABLE ventes_produits
|
|
ADD COLUMN IF NOT EXISTS code_gamme_init VARCHAR(20)
|
|
`);
|
|
await db.execute(sql`
|
|
UPDATE ventes_produits
|
|
SET code_gamme_init = code_gamme
|
|
WHERE code_gamme_init IS NULL
|
|
`);
|
|
isSchemaInitialized = true;
|
|
console.log("[Schema] codeGammeInit initialized successfully.");
|
|
} catch (err) {
|
|
console.error("[Schema] Failed to initialize codeGammeInit:", err);
|
|
}
|
|
}
|
|
|
|
const now = new Date();
|
|
const allowedPeriods = new Set<string>();
|
|
for (let i = 12; i >= 0; i--) {
|
|
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
|
allowedPeriods.add(`${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}`);
|
|
}
|
|
|
|
const conditions = [
|
|
eq(ventesProduits.codeFournisseur, codeFournisseur),
|
|
ne(ventesProduits.periode, "TOTAL"),
|
|
magasin !== "TOTAL" ? eq(ventesProduits.magasin, magasin) : ne(ventesProduits.magasin, "TOTAL"),
|
|
];
|
|
|
|
if (filters?.code3) {
|
|
conditions.push(eq(ventesProduits.code3, filters.code3));
|
|
} else if (filters?.code2) {
|
|
conditions.push(like(ventesProduits.code3, `${filters.code2}%`));
|
|
} else if (filters?.code1) {
|
|
conditions.push(like(ventesProduits.code3, `${filters.code1}%`));
|
|
}
|
|
|
|
const rows = await db
|
|
.select()
|
|
.from(ventesProduits)
|
|
.where(and(...conditions));
|
|
|
|
console.log(`[getProductRows] Fetching for ${codeFournisseur} (${magasin}). Found ${rows.length} rows.`);
|
|
|
|
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) {
|
|
const { codein, magasin, periode } = row;
|
|
const isInPeriod = allowedPeriods.has(periode);
|
|
|
|
if (isInPeriod && magasin !== "TOTAL") {
|
|
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);
|
|
}
|
|
|
|
let existing = productMap.get(codein);
|
|
|
|
if (!existing) {
|
|
const c3 = row.code3 || "";
|
|
const c1 = c3.slice(0, 2);
|
|
const c2 = c3.slice(0, 4);
|
|
|
|
const NOMENCLATURE_LABELS: Record<string, string> = {
|
|
"20": "LIQUIDES", "30": "ÉPICERIE", "40": "DPH", "50": "BAZAR", "60": "TEXTILE", "70": "FRAIS",
|
|
"2001": "EAUX", "2002": "JUS DE FRUITS", "2003": "SODAS / BOUILLONS", "2004": "BIERES", "2005": "VINS", "2006": "CIDRES", "2007": "APERITIFS", "2008": "ALCOOLS",
|
|
"3001": "ÉPICERIE SALÉE", "3002": "ÉPICERIE SUCRÉE", "3003": "PETIT DÉJEUNER", "3004": "CONSERVES", "3005": "PLAT CUISINES",
|
|
"4001": "HYGIÈNE", "4002": "ENTRETIEN", "4003": "PARFUMERIE",
|
|
"5001": "BRICOLAGE", "5002": "MAISON", "5003": "LOISIRS", "5004": "JARDIN",
|
|
"7001": "FRUITS ET LEGUMES", "7002": "CREMERIE", "7003": "VOLAILLE", "7004": "CHARCUTERIE", "7005": "TRAITEUR", "7006": "BOULANGERIE",
|
|
};
|
|
|
|
existing = {
|
|
codein,
|
|
codeFournisseur: row.codeFournisseur ?? "",
|
|
nomFournisseur: row.nomFournisseur ?? "",
|
|
libelle1: row.libelle1 ?? "",
|
|
gtin: row.gtin ?? "",
|
|
reference: row.reference ?? "",
|
|
code1: c1,
|
|
libelleNiveau1: NOMENCLATURE_LABELS[c1] || `Secteur ${c1}`,
|
|
code2: c2,
|
|
libelleNiveau2: NOMENCLATURE_LABELS[c2] || `Rayon ${c2}`,
|
|
code3: c3,
|
|
libelle3: row.libelle3 ?? "",
|
|
codeGamme: (row.codeGamme as GammeCode | null),
|
|
codeGammeInit: (row.codeGammeInit as GammeCode | null) ?? (row.codeGamme as GammeCode | null),
|
|
codeGammeDraft: null,
|
|
sales12m: {},
|
|
totalQuantite: 0,
|
|
totalCa: 0,
|
|
totalMarge: 0,
|
|
tauxMarge: 0,
|
|
score: 0,
|
|
workingStores: [],
|
|
aiRecommendation: null,
|
|
};
|
|
productMap.set(codein, existing);
|
|
}
|
|
|
|
if (isInPeriod) {
|
|
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"));
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
console.log(`- Final ProductMap Size: ${productMap.size}`);
|
|
return computeProductScores(Array.from(productMap.values()));
|
|
} catch (error) {
|
|
console.error(`[getProductRows] Error for supplier ${codeFournisseur}:`, error);
|
|
return [];
|
|
}
|
|
}
|