mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-12 01:36:43 +02:00
Merge branch 'main' of https://github.com/R0m1k3/CollectFlow
This commit is contained in:
commit
12809b9924
23 files changed
+617
-193
No files matched your search
@@ -0,0 +1,31 @@
|
||||
|
||||
const { Pool } = require('pg');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG_PATH = path.join(process.cwd(), "data", ".db-config.json");
|
||||
let url = "";
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
url = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")).url;
|
||||
}
|
||||
|
||||
const pool = new Pool({ connectionString: url || process.env.DATABASE_URL });
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
const resSuppliers = await pool.query(\`SELECT DISTINCT code_fournisseur FROM ventes_produits LIMIT 5\`);
|
||||
console.log("Suppliers in DB:", resSuppliers.rows.map(r => r.code_fournisseur));
|
||||
|
||||
for (const row of resSuppliers.rows) {
|
||||
const supplier = row.code_fournisseur;
|
||||
const countRes = await pool.query(\`SELECT COUNT(*) FROM ventes_produits WHERE code_fournisseur = $1\`, [supplier]);
|
||||
const distinctCodein = await pool.query(\`SELECT COUNT(DISTINCT codein) FROM ventes_produits WHERE code_fournisseur = $1\`, [supplier]);
|
||||
console.log(\`Supplier \${supplier}: Total Rows = \${countRes.rows[0].count}, Distinct Products = \${distinctCodein.rows[0].count}\`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error:", e);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
check();
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
import { db } from "./src/db";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
const res = await db.execute(sql`SELECT COUNT(*) FROM ventes_produits`);
|
||||
console.log("Total rows in ventes_produits:", res.rows[0].count);
|
||||
|
||||
const resDistinct = await db.execute(sql`SELECT COUNT(DISTINCT codein) FROM ventes_produits`);
|
||||
console.log("Total distinct codein in ventes_produits:", resDistinct.rows[0].count);
|
||||
|
||||
const resSupplier = await db.execute(sql`SELECT code_fournisseur, COUNT(*) as row_count, COUNT(DISTINCT codein) as prod_count FROM ventes_produits GROUP BY code_fournisseur`);
|
||||
console.log("Counts per supplier:", resSupplier.rows);
|
||||
} catch (e) {
|
||||
console.error("Error:", e);
|
||||
}
|
||||
}
|
||||
check();
|
||||
+6
-1
@@ -1,8 +1,13 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
serverExternalPackages: ["pg"],
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: "50mb",
|
||||
},
|
||||
},
|
||||
typescript: {
|
||||
// Idem pour TypeScript
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
eslint: {
|
||||
|
||||
+18
-1
@@ -6,7 +6,7 @@ const cwd = path.resolve(__dirname, '..'); // get app root when inside /scripts
|
||||
|
||||
async function main() {
|
||||
console.log("[DB Init] Starting database migration & initialization...");
|
||||
let connectionString = process.env.DATABASE_URL;
|
||||
let connectionString = "";
|
||||
|
||||
try {
|
||||
const CONFIG_PATH = path.join(cwd, "data", ".db-config.json");
|
||||
@@ -21,6 +21,12 @@ async function main() {
|
||||
console.error("[DB Init] Error reading .db-config.json", e);
|
||||
}
|
||||
|
||||
// Use environment variable as default IF NOT already loaded from config
|
||||
if (!connectionString && process.env.DATABASE_URL) {
|
||||
connectionString = process.env.DATABASE_URL;
|
||||
console.log("[DB Init] Using environment variable DATABASE_URL as fallback.");
|
||||
}
|
||||
|
||||
if (!connectionString) {
|
||||
console.warn("[DB Init] No DATABASE_URL found in env or config. Skipping push.");
|
||||
process.exit(0);
|
||||
@@ -57,6 +63,17 @@ async function main() {
|
||||
`);
|
||||
console.log("[DB Init] Table session_snapshots is verified/created.");
|
||||
|
||||
await tempPool.query(`
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"username" varchar(50) NOT NULL UNIQUE,
|
||||
"password_hash" text NOT NULL,
|
||||
"role" varchar(20) NOT NULL DEFAULT 'user',
|
||||
"created_at" timestamp DEFAULT now()
|
||||
);
|
||||
`);
|
||||
console.log("[DB Init] Table users is verified/created.");
|
||||
|
||||
await tempPool.end();
|
||||
console.log("[DB Init] Initialization successful. Exiting.");
|
||||
process.exit(0);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
const { Pool } = require('pg');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function main() {
|
||||
console.log("--- DB DIAGNOSIS Start (Node) ---");
|
||||
|
||||
let connectionString = process.env.DATABASE_URL;
|
||||
|
||||
try {
|
||||
const CONFIG_PATH = path.join(process.cwd(), "data", ".db-config.json");
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
||||
if (config.url) {
|
||||
connectionString = config.url;
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
if (!connectionString) {
|
||||
console.error("No connection string found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const pool = new Pool({ connectionString });
|
||||
|
||||
try {
|
||||
const res = await pool.query(`
|
||||
SELECT
|
||||
code_fournisseur,
|
||||
nom_fournisseur,
|
||||
COUNT(*) as total_rows,
|
||||
COUNT(DISTINCT codein) as distinct_products
|
||||
FROM ventes_produits
|
||||
GROUP BY code_fournisseur, nom_fournisseur
|
||||
ORDER BY total_rows DESC
|
||||
`);
|
||||
|
||||
console.table(res.rows);
|
||||
} catch (error) {
|
||||
console.error("Diagnosis failed:", error);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
import { db } from "@/db";
|
||||
import { ventesProduits } from "@/db/schema";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
async function main() {
|
||||
console.log("--- DB DIAGNOSIS Start ---");
|
||||
try {
|
||||
const results = await db.execute(sql`
|
||||
SELECT
|
||||
code_fournisseur,
|
||||
nom_fournisseur,
|
||||
COUNT(*) as total_rows,
|
||||
COUNT(DISTINCT codein) as distinct_products
|
||||
FROM ventes_produits
|
||||
GROUP BY code_fournisseur, nom_fournisseur
|
||||
ORDER BY total_rows DESC
|
||||
`);
|
||||
|
||||
console.table(results.rows);
|
||||
} catch (error) {
|
||||
console.error("Diagnosis failed:", error);
|
||||
}
|
||||
console.log("--- DB DIAGNOSIS End ---");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,39 @@
|
||||
const { computeProductScores } = require('./src/lib/score-engine');
|
||||
|
||||
// Simulate 50,000 rows
|
||||
const largeDataset = Array.from({ length: 50000 }, (_, i) => ({
|
||||
codein: `item-${i}`,
|
||||
codeFournisseur: 'A032',
|
||||
nomFournisseur: 'ACCESS DECO',
|
||||
libelle1: `Product ${i}`,
|
||||
gtin: '123456789',
|
||||
reference: 'REF',
|
||||
code1: '20',
|
||||
libelleNiveau1: 'LIQUIDES',
|
||||
code2: '2001',
|
||||
libelleNiveau2: 'EAUX',
|
||||
code3: '200101',
|
||||
libelle3: 'EAU SOURCE',
|
||||
codeGamme: 'A',
|
||||
codeGammeInit: 'A',
|
||||
codeGammeDraft: null,
|
||||
sales12m: {},
|
||||
totalQuantite: Math.random() * 1000,
|
||||
totalCa: Math.random() * 5000,
|
||||
totalMarge: Math.random() * 1000,
|
||||
tauxMarge: 20,
|
||||
score: 0,
|
||||
workingStores: ['MAG1', 'MAG2']
|
||||
}));
|
||||
|
||||
console.log(`Starting test with ${largeDataset.length} rows...`);
|
||||
|
||||
try {
|
||||
const start = Date.now();
|
||||
const result = computeProductScores(largeDataset);
|
||||
const end = Date.now();
|
||||
console.log(`Success! Processed ${result.length} rows in ${end - start}ms.`);
|
||||
} catch (error) {
|
||||
console.error('Failed!', error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -35,7 +35,9 @@ export default async function GridPage({ searchParams }: GridPageProps) {
|
||||
}
|
||||
|
||||
// 3. Load real product data for the selected supplier
|
||||
console.log(`\n>>> [GridPage] Fetching data for: ${codeFournisseur}, shop: ${magasin}`);
|
||||
const rows = await getGridData(codeFournisseur, magasin, filters);
|
||||
console.log(`>>> [GridPage] Received ${rows.length} rows from getGridData`);
|
||||
const selectedFournisseur = fournisseurs.find((f: { code: string; nom: string }) => f.code === codeFournisseur);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
+20
-7
@@ -18,10 +18,7 @@ function maskUrl(url: string | undefined) {
|
||||
export function getDb() {
|
||||
if (currentDb) return currentDb;
|
||||
|
||||
let connectionString = process.env.DATABASE_URL;
|
||||
|
||||
console.log("[DB] Initializing connection...");
|
||||
console.log("[DB] Env DATABASE_URL:", maskUrl(process.env.DATABASE_URL));
|
||||
let connectionString = "";
|
||||
|
||||
try {
|
||||
const CONFIG_PATH = path.join(process.cwd(), "data", ".db-config.json");
|
||||
@@ -30,21 +27,37 @@ export function getDb() {
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
||||
if (config.url) {
|
||||
connectionString = config.url;
|
||||
console.log("[DB] Using saved database URL:", maskUrl(connectionString));
|
||||
console.log("[DB] Loaded saved database URL from config.");
|
||||
}
|
||||
} else {
|
||||
console.log("[DB] No saved config file found. Using environment variable.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[DB] Error reading config file:", err);
|
||||
}
|
||||
|
||||
// Use environment variable as fallback if no config is found
|
||||
if (!connectionString && process.env.DATABASE_URL) {
|
||||
connectionString = process.env.DATABASE_URL;
|
||||
console.log("[DB] Using environment variable as fallback.");
|
||||
}
|
||||
|
||||
console.log("[DB] Initializing connection with:", maskUrl(connectionString));
|
||||
|
||||
if (!connectionString) {
|
||||
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 });
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { scryptSync, randomBytes, timingSafeEqual } from "crypto";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const USERS_JSON_PATH = path.join(process.cwd(), "data", "users.json");
|
||||
|
||||
export interface UserJson {
|
||||
id: string;
|
||||
username: string;
|
||||
passwordHash: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes a password using scrypt.
|
||||
@@ -26,3 +37,40 @@ export function verifyPassword(password: string, storedHash: string): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads users from local JSON fallback.
|
||||
*/
|
||||
export function getFallbackUsers(): UserJson[] {
|
||||
try {
|
||||
if (!fs.existsSync(USERS_JSON_PATH)) {
|
||||
// Initial seed if file doesn't exist
|
||||
const defaultAdmin: UserJson = {
|
||||
id: "0",
|
||||
username: "admin",
|
||||
passwordHash: hashPassword("admin"),
|
||||
role: "admin"
|
||||
};
|
||||
saveFallbackUsers([defaultAdmin]);
|
||||
return [defaultAdmin];
|
||||
}
|
||||
const data = fs.readFileSync(USERS_JSON_PATH, "utf-8");
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error("[AUTH] Error reading users.json:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves users to local JSON fallback.
|
||||
*/
|
||||
export function saveFallbackUsers(users: UserJson[]) {
|
||||
try {
|
||||
const dir = path.dirname(USERS_JSON_PATH);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(USERS_JSON_PATH, JSON.stringify(users, null, 2));
|
||||
} catch (e) {
|
||||
console.error("[AUTH] Error writing users.json:", e);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@/db";
|
||||
import { users } from "@/db/schema";
|
||||
import { hashPassword } from "./auth-logic";
|
||||
import { hashPassword, getFallbackUsers, saveFallbackUsers } from "./auth-logic";
|
||||
import { count, eq, sql } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
@@ -9,10 +9,25 @@ import { count, eq, sql } from "drizzle-orm";
|
||||
* Also ensures the table exists (auto-repair).
|
||||
*/
|
||||
export async function ensureAdminExists() {
|
||||
// 1. Always ensure local JSON fallback has at least one admin
|
||||
const fallbackUsers = getFallbackUsers();
|
||||
if (fallbackUsers.length === 0) {
|
||||
console.log("[AUTH] Initializing local JSON fallback with admin/admin...");
|
||||
saveFallbackUsers([{
|
||||
id: "0",
|
||||
username: "admin",
|
||||
passwordHash: hashPassword("admin"),
|
||||
role: "admin"
|
||||
}]);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Auto-repair: Ensure table exists
|
||||
console.log("[AUTH] Testing database connection for seeding...");
|
||||
await db.execute(sql`SELECT 1`);
|
||||
|
||||
// 2. Auto-repair: Ensure table exists
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
@@ -21,14 +36,14 @@ export async function ensureAdminExists() {
|
||||
);
|
||||
`);
|
||||
|
||||
// 2. Check if admin specifically exists
|
||||
// 3. Sync admin to DB
|
||||
const adminUser = await db.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, "admin"))
|
||||
.limit(1);
|
||||
|
||||
if (adminUser.length === 0) {
|
||||
console.log("BMAD: Default admin account not found. Initializing (admin/admin)...");
|
||||
console.log("[AUTH] Seeding admin to DB...");
|
||||
await db.insert(users).values({
|
||||
username: "admin",
|
||||
passwordHash: hashPassword("admin"),
|
||||
@@ -36,8 +51,8 @@ export async function ensureAdminExists() {
|
||||
});
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("BMAD: CRITICAL Error checking/seeding users table.", err);
|
||||
} catch (err: any) {
|
||||
console.warn("[AUTH] Database seeding failed (expected if DB not configured yet):", err.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -5,6 +5,8 @@ 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;
|
||||
|
||||
@@ -16,146 +18,145 @@ interface GetProductRowsInput {
|
||||
|
||||
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}`);
|
||||
|
||||
// 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<string>();
|
||||
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<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 {
|
||||
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",
|
||||
// Rayons
|
||||
"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",
|
||||
};
|
||||
|
||||
productMap.set(codein, {
|
||||
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: { [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 >= 1) {
|
||||
workingStores.push(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);
|
||||
}
|
||||
product.workingStores = workingStores.sort();
|
||||
}
|
||||
}
|
||||
|
||||
return computeProductScores(Array.from(productMap.values()));
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,10 @@ export function FloatingSummaryBar() {
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-12 items-center">
|
||||
<Stat
|
||||
label="Nombre de Produits"
|
||||
value={summary.totalProducts.toLocaleString("fr-FR")}
|
||||
/>
|
||||
<Stat
|
||||
label="Volume Total Vendu"
|
||||
value={Math.round(summary.totalQuantite).toLocaleString("fr-FR")}
|
||||
|
||||
@@ -15,7 +15,7 @@ function getHeatmapClass(value: number | null): string {
|
||||
|
||||
export const HeatmapCell = React.memo(({ value }: HeatmapCellProps) => {
|
||||
const heatClass = getHeatmapClass(value);
|
||||
const display = value === null || value === 0 ? "—" : Math.round(Math.abs(value)).toString();
|
||||
const display = value === null || value === 0 ? "0" : Math.round(Math.abs(value)).toString();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -42,6 +42,7 @@ function computeSummary(rows: ProductRow[], drafts: Record<string, GammeCode>):
|
||||
const totalCa = active.reduce((s, r) => s + (r.totalCa ?? 0), 0);
|
||||
const totalMarge = active.reduce((s, r) => s + (r.totalMarge ?? 0), 0);
|
||||
return {
|
||||
totalProducts: rows.length,
|
||||
totalRows: active.length,
|
||||
totalQuantite: active.reduce((s, r) => s + (r.totalQuantite ?? 0), 0),
|
||||
totalCa,
|
||||
@@ -65,6 +66,7 @@ export const useGridStore = create<GridState>()(
|
||||
search: "",
|
||||
},
|
||||
summary: {
|
||||
totalProducts: 0,
|
||||
totalRows: 0,
|
||||
totalQuantite: 0,
|
||||
totalCa: 0,
|
||||
|
||||
@@ -2,23 +2,35 @@
|
||||
|
||||
import { db } from "@/db";
|
||||
import { sessionSnapshots } from "@/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
export async function deleteSnapshot(id: number) {
|
||||
const session = await auth();
|
||||
const userId = session?.user ? Number((session.user as any).id) : null;
|
||||
const rawUserId = (session?.user as any)?.id;
|
||||
const userId = rawUserId ? parseInt(String(rawUserId), 10) : null;
|
||||
|
||||
if (!userId) {
|
||||
return { success: false, error: "Non autorisé" };
|
||||
}
|
||||
console.log(`[deleteSnapshot] Request for ID: ${id}, Session UserId: ${userId}`);
|
||||
|
||||
try {
|
||||
await db.delete(sessionSnapshots)
|
||||
.where(and(eq(sessionSnapshots.id, id), eq(sessionSnapshots.userId, userId)));
|
||||
// Condition de sécurité : match ID ET match le propriétaire (ou anonyme)
|
||||
const userCondition = userId && !isNaN(userId)
|
||||
? eq(sessionSnapshots.userId, userId)
|
||||
: isNull(sessionSnapshots.userId);
|
||||
|
||||
const result = await db.delete(sessionSnapshots)
|
||||
.where(and(eq(sessionSnapshots.id, id), userCondition))
|
||||
.returning({ deletedId: sessionSnapshots.id });
|
||||
|
||||
if (result.length === 0) {
|
||||
console.warn(`[deleteSnapshot] No rows deleted for ID ${id}. Snapshot may not exist or belongs to another user.`);
|
||||
return { success: false, error: "Snapshot non trouvé ou non autorisé" };
|
||||
}
|
||||
|
||||
console.log(`[deleteSnapshot] Successfully deleted ID: ${id}`);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return { success: false, error: "Delete failed" };
|
||||
} catch (err: any) {
|
||||
console.error("[deleteSnapshot] DB Error:", err);
|
||||
return { success: false, error: err.message || "Delete failed" };
|
||||
}
|
||||
}
|
||||
@@ -3,23 +3,27 @@
|
||||
import { db } from "@/db";
|
||||
import { sessionSnapshots } from "@/db/schema";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||
|
||||
export async function getSnapshots(type?: "snapshot" | "export") {
|
||||
try {
|
||||
const session = await auth();
|
||||
const userId = session?.user ? Number((session.user as any).id) : null;
|
||||
const rawUserId = (session?.user as any)?.id;
|
||||
const userId = rawUserId ? parseInt(String(rawUserId), 10) : null;
|
||||
|
||||
let query = db.select().from(sessionSnapshots);
|
||||
const conditions = [];
|
||||
if (userId) conditions.push(eq(sessionSnapshots.userId, userId));
|
||||
// Si on a un userId, on filtre par celui-ci.
|
||||
// Sinon, on filtre par userId IS NULL (mode anonyme/local)
|
||||
const userCondition = userId && !isNaN(userId)
|
||||
? eq(sessionSnapshots.userId, userId)
|
||||
: isNull(sessionSnapshots.userId);
|
||||
|
||||
const conditions = [userCondition];
|
||||
if (type) conditions.push(eq(sessionSnapshots.type, type));
|
||||
|
||||
if (conditions.length > 0) {
|
||||
return query.where(and(...conditions)).orderBy(desc(sessionSnapshots.createdAt));
|
||||
}
|
||||
|
||||
return query.orderBy(desc(sessionSnapshots.createdAt));
|
||||
return await db.select()
|
||||
.from(sessionSnapshots)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(sessionSnapshots.createdAt));
|
||||
} catch (err: any) {
|
||||
const msg = (err?.message || String(err)).split("\n")[0];
|
||||
console.error(`[getSnapshots] ERROR (type=${type}):`, msg, err);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { sessionSnapshots } from "@/db/schema";
|
||||
import { z } from "zod";
|
||||
import { sql, and, eq } from "drizzle-orm";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
const SaveSnapshotSchema = z.object({
|
||||
codeFournisseur: z.string(),
|
||||
@@ -33,13 +34,18 @@ export async function saveSnapshot(raw: unknown) {
|
||||
|
||||
const { codeFournisseur, nomFournisseur, magasin, label, changes, summary, type } = parsed.data;
|
||||
const session = await auth();
|
||||
const userId = session?.user ? Number((session.user as any).id) : null;
|
||||
const rawUserId = (session?.user as any)?.id;
|
||||
const userId = rawUserId ? parseInt(String(rawUserId), 10) : null;
|
||||
|
||||
console.log(`[saveSnapshot] User: ${userId} (raw: ${rawUserId}), Supplier: ${codeFournisseur}, Type: ${type}`);
|
||||
|
||||
const finalUserId = (userId && !isNaN(userId)) ? userId : null;
|
||||
|
||||
try {
|
||||
const [created] = await db
|
||||
.insert(sessionSnapshots)
|
||||
.values({
|
||||
userId,
|
||||
userId: finalUserId,
|
||||
codeFournisseur,
|
||||
nomFournisseur: nomFournisseur ?? null,
|
||||
magasin,
|
||||
@@ -49,7 +55,9 @@ export async function saveSnapshot(raw: unknown) {
|
||||
type: type ?? "snapshot",
|
||||
})
|
||||
.returning({ id: sessionSnapshots.id });
|
||||
|
||||
|
||||
revalidatePath("/snapshots");
|
||||
revalidatePath("/exports");
|
||||
return { success: true, snapshotId: created?.id };
|
||||
} catch (err: any) {
|
||||
console.error("Initial snapshot save failed, attempting auto-repair...", err);
|
||||
@@ -83,7 +91,7 @@ export async function saveSnapshot(raw: unknown) {
|
||||
const [retryCreated] = await db
|
||||
.insert(sessionSnapshots)
|
||||
.values({
|
||||
userId,
|
||||
userId: finalUserId,
|
||||
codeFournisseur,
|
||||
nomFournisseur: nomFournisseur ?? null,
|
||||
magasin,
|
||||
|
||||
+33
-11
@@ -3,7 +3,7 @@ import Credentials from "next-auth/providers/credentials";
|
||||
import { db } from "@/db";
|
||||
import { users } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { verifyPassword } from "@/features/auth/logic/auth-logic";
|
||||
import { verifyPassword, getFallbackUsers } from "@/features/auth/logic/auth-logic";
|
||||
import { ensureAdminExists } from "@/features/auth/logic/seed-admin";
|
||||
import { authConfig } from "./auth.config";
|
||||
|
||||
@@ -22,26 +22,48 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
if (!credentials?.username || !credentials?.password) return null;
|
||||
|
||||
try {
|
||||
const [user] = await db.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, credentials.username as string));
|
||||
console.log(`[AUTH] Checking user "${credentials.username}"...`);
|
||||
|
||||
let user: any = null;
|
||||
|
||||
console.log(`BMAD: Auth attempt for "${credentials.username}". User found: ${!!user}`);
|
||||
// Try DB first
|
||||
try {
|
||||
const [dbUser] = await db.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, credentials.username as string));
|
||||
user = dbUser;
|
||||
console.log(`[AUTH] DB check: User found: ${!!user}`);
|
||||
} catch (dbErr) {
|
||||
console.warn("[AUTH] DB unreachable, falling back to JSON.");
|
||||
}
|
||||
|
||||
if (!user) return null;
|
||||
// Fallback to JSON if DB failed or user not found
|
||||
if (!user) {
|
||||
const fallbackUsers = getFallbackUsers();
|
||||
user = fallbackUsers.find(u => u.username === credentials.username);
|
||||
console.log(`[AUTH] JSON check: User found: ${!!user}`);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
console.warn(`[AUTH] User "${credentials.username}" not found anywhere.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = verifyPassword(credentials.password as string, user.passwordHash);
|
||||
console.log(`BMAD: Password valid for "${credentials.username}": ${isValid}`);
|
||||
console.log(`[AUTH] Password valid for "${credentials.username}": ${isValid}`);
|
||||
|
||||
if (!isValid) return null;
|
||||
if (!isValid) {
|
||||
console.warn(`[AUTH] Invalid password for user "${credentials.username}".`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id.toString(),
|
||||
name: user.username,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Auth Error:", err);
|
||||
} catch (err: any) {
|
||||
console.error("[AUTH] CRITICAL ERROR during authorize callback:", err.message || err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ export function computeProductScores(
|
||||
};
|
||||
});
|
||||
|
||||
const maxQty = Math.max(...weightedData.map((d) => d.wQty));
|
||||
const maxCa = Math.max(...weightedData.map((d) => d.wCa));
|
||||
const maxMarge = Math.max(...weightedData.map((d) => d.wMarge));
|
||||
const maxQty = weightedData.reduce((max, d) => Math.max(max, d.wQty), 0);
|
||||
const maxCa = weightedData.reduce((max, d) => Math.max(max, d.wCa), 0);
|
||||
const maxMarge = weightedData.reduce((max, d) => Math.max(max, d.wMarge), 0);
|
||||
|
||||
// 2. Calculer les moyennes et totaux globaux
|
||||
const totalQty = weightedData.reduce((acc, d) => acc + d.wQty, 0);
|
||||
|
||||
@@ -70,6 +70,7 @@ export interface ProductRow {
|
||||
|
||||
/** Summary bar totals for the currently visible/filtered rows */
|
||||
export interface GridSummary {
|
||||
totalProducts: number;
|
||||
totalRows: number;
|
||||
totalQuantite: number;
|
||||
totalCa: number;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
const { Pool } = require('pg');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG_PATH = path.join(process.cwd(), "data", ".db-config.json");
|
||||
let url = "";
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
url = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")).url;
|
||||
}
|
||||
if (!url) url = process.env.DATABASE_URL;
|
||||
|
||||
const pool = new Pool({ connectionString: url });
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
console.log("Checking DB URL:", url.replace(/:([^@]+)@/, ":****@"));
|
||||
|
||||
const resTotal = await pool.query('SELECT COUNT(*) FROM ventes_produits');
|
||||
console.log("Total rows in table:", resTotal.rows[0].count);
|
||||
|
||||
const resSuppliers = await pool.query('SELECT code_fournisseur, COUNT(*) as count FROM ventes_produits GROUP BY code_fournisseur ORDER BY count DESC LIMIT 10');
|
||||
console.log("Top suppliers by row count:", resSuppliers.rows);
|
||||
|
||||
// Check for specific supplier in user screenshot (if any, but I'll check all)
|
||||
for (const s of resSuppliers.rows) {
|
||||
const resDistinct = await pool.query('SELECT COUNT(DISTINCT codein) FROM ventes_produits WHERE code_fournisseur = $1', [s.code_fournisseur]);
|
||||
console.log(`Supplier ${s.code_fournisseur}: Distinct Products = ${resDistinct.rows[0].count}`);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("DB Error:", e);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
check();
|
||||
Reference in new issue
Block a user