From 5eb3f6732e6b4f18e5afd02f6549d873ffca7c7d Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Tue, 10 Mar 2026 16:04:55 +0100 Subject: [PATCH] feat: Add Drizzle ORM database setup, product data fetching and processing with scoring, and a diagnostic API endpoint. --- src/app/api/diag/route.ts | 69 ++++++++++++++++++ src/db/index.ts | 10 +++ src/features/grid/api/get-product-rows.ts | 86 +---------------------- 3 files changed, 81 insertions(+), 84 deletions(-) create mode 100644 src/app/api/diag/route.ts diff --git a/src/app/api/diag/route.ts b/src/app/api/diag/route.ts new file mode 100644 index 0000000..f6b34a0 --- /dev/null +++ b/src/app/api/diag/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from "next/server"; +import { db } from "@/db"; +import { ventesProduits } from "@/db/schema"; +import { sql, eq } from "drizzle-orm"; + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const codeFournisseur = searchParams.get("fournisseur"); + + if (!codeFournisseur) { + return NextResponse.json({ + error: "Paramètre 'fournisseur' manquant.", + usage: "/api/diag?fournisseur=VOTRE_CODE" + }, { status: 400 }); + } + + try { + console.log(`\n--- [DIAG API] Starting check for supplier: ${codeFournisseur} ---`); + + // 1. Raw Count + const countAll = await db.execute(sql` + SELECT COUNT(*) as count FROM ventes_produits + WHERE code_fournisseur = ${codeFournisseur} + `); + const totalRows = Number(countAll.rows[0].count); + + // 2. Distinct Products + const countDistinctTotal = await db.execute(sql` + SELECT COUNT(DISTINCT codein) as count FROM ventes_produits + WHERE code_fournisseur = ${codeFournisseur} + `); + const totalDistinct = Number(countDistinctTotal.rows[0].count); + + // 3. Sample of data (to see if it's truncated by DB query) + const sample = await db.execute(sql` + SELECT codein, libelle1, magasin, periode FROM ventes_produits + WHERE code_fournisseur = ${codeFournisseur} + LIMIT 5 + `); + + console.log(`- DB sees ${totalRows} total rows.`); + console.log(`- DB sees ${totalDistinct} distinct products.`); + + return NextResponse.json({ + timestamp: new Date().toISOString(), + supplier: codeFournisseur, + database: { + totalRowsInTable: totalRows, + distinctProducts: totalDistinct, + sampleData: sample.rows + }, + serverConfig: { + nextVersion: "16.1.6", + nodeEnv: process.env.NODE_ENV, + bodySizeLimit: "50mb" + }, + message: totalDistinct > 1000 + ? "La base de données contient plus de 1000 produits. Si la grille en affiche 999, le problème est dans le transport (Application/RSC)." + : "La base de données renvoie moins de 1000 produits uniques. Le problème vient de l'import ou du filtrage SQL." + }); + + } catch (error: any) { + console.error("[DIAG API] Error:", error); + return NextResponse.json({ + error: "Failed to fetch diagnostic data", + details: error.message + }, { status: 500 }); + } +} diff --git a/src/db/index.ts b/src/db/index.ts index b7d9ff4..4ebe51b 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -46,8 +46,18 @@ export function getDb() { console.error("[DB] CRITICAL: No database connection string available!"); } + const connectionHostname = connectionString.match(/@([^:/]+)/)?.[1] || "unknown"; + console.log(`[DB] Target Hostname: ${connectionHostname}`); + pool = new Pool({ connectionString, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 10000, + }); + + pool.on('error', (err) => { + console.error('[DB] Unexpected error on idle client', err); }); currentDb = drizzle(pool, { schema }); diff --git a/src/features/grid/api/get-product-rows.ts b/src/features/grid/api/get-product-rows.ts index 3e59544..e1556de 100644 --- a/src/features/grid/api/get-product-rows.ts +++ b/src/features/grid/api/get-product-rows.ts @@ -17,90 +17,8 @@ interface GetProductRowsInput { } export async function getProductRows(input: GetProductRowsInput): Promise { - const { codeFournisseur, magasin = "TOTAL", filters } = input; - - // FILE LOGGING (portainer might be filtering console.log) - try { - const logPath = "/app/data/debug_check.txt"; - const entry = `\n[${new Date().toISOString()}] getProductRows called for ${codeFournisseur}\n`; - fs.appendFileSync(logPath, entry); - } catch (e) {} - - console.log(`\n>>> [getProductRows] RECEIVED CALL for supplier: ${codeFournisseur}, magasin: ${magasin}`); - console.log(`>>> [getProductRows] filters:`, JSON.stringify(filters)); - - 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); - // We continue anyway, the mapping fallback will handle it for the UI - } - } - - // ... (allowedPeriods calculation remains the same) - const now = new Date(); - const allowedPeriods = new Set(); - for (let i = 12; i >= 0; i--) { // i >= 0 au lieu de 1 pour inclure le mois en cours - const d = new Date(now.getFullYear(), now.getMonth() - i, 1); - allowedPeriods.add(`${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}`); - } - - // Build conditions - const conditions = [ - eq(ventesProduits.codeFournisseur, codeFournisseur), - ne(ventesProduits.periode, "TOTAL"), - magasin !== "TOTAL" ? eq(ventesProduits.magasin, magasin) : ne(ventesProduits.magasin, "TOTAL"), - ]; - - // Hierarchical filtering - 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}%`)); - } - - // Get all rows for this supplier - const rows = await db - .select() - .from(ventesProduits) - .where(and(...conditions)); - - // Group by codein and aggregate monthly sales + track store participation - const productMap = new Map(); - - // EXTRA VERBOSE DIAGNOSTIC - const countAll = await db.execute(sql`SELECT COUNT(*) FROM ventes_produits WHERE code_fournisseur = ${codeFournisseur}`); - const countDistinctTotal = await db.execute(sql`SELECT COUNT(DISTINCT codein) FROM ventes_produits WHERE code_fournisseur = ${codeFournisseur}`); - const countNoTotal = await db.execute(sql`SELECT COUNT(DISTINCT codein) FROM ventes_produits WHERE code_fournisseur = ${codeFournisseur} AND magasin != 'TOTAL' AND periode != 'TOTAL'`); - - const diagOutput = ` ---- [getProductRows] ULTRA-DIAGNOSTIC for Supplier ${codeFournisseur} --- -- Gross total rows in DB: ${countAll.rows[0].count} -- Total distinct products (any store/period): ${countDistinctTotal.rows[0].count} -- Distinct products excluding TOTAL store/period: ${countNoTotal.rows[0].count} -- Current Filters rows retrieved: ${rows.length} -- Distinct products in currently fetched rows: ${new Set(rows.map(r => r.codein)).size} -`; - try { - fs.appendFileSync("/app/data/debug_check.txt", diagOutput); - } catch (e) {} - - console.log(diagOutput); + // MODEL DIAGNOSTIC (Clean version) + console.log(`[getProductRows] Fetching for ${codeFournisseur} (${magasin}). Found ${rows.length} rows.`); const storeParticipationMap = new Map>>(); // codein -> magasin -> Set of periods