mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-19 05:06:56 +02:00
Grille - Corrige le lot 4 : au changement de magasin, les chiffres et le tri affichés suivent tout de suite (nouvelle liste), la sélection est gardée. - Lignes toujours chargées « Nos 2 magasins » ; changement de magasin sans aller-retour serveur (URL mise à jour par l'historique du navigateur). - Complément de l'API FF par magasin servi à part (/api/grid/rows/store-patch), appliqué à son arrivée, calcul identique à l'ancien (lib/store-patch.ts). - La recherche ne reconstruit plus toutes les lignes à chaque frappe ; la sélection n'est remise à zéro qu'au changement de fournisseur. Sécurité - lib/authz.ts : session relue en base (cache 1 min), rôle de la base prioritaire ; variantes pour actions, routes et pages. - Paramètres réservés aux administrateurs ; session vérifiée dans les actions Grille, Commandes, Historique et les routes ; diagnostics réservés admin. - Mots de passe et clés d'IA plus envoyés au navigateur ; champ vide = mot de passe conservé ; plus de mot de passe dans le localStorage ; l'enregistrement de la base n'efface plus les clés d'IA. - admin/admin créé seulement si aucun utilisateur ; fichier de secours utilisé uniquement si la base est injoignable ; data/ ignoré par git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
295 lines
13 KiB
TypeScript
295 lines
13 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { db } from "@/db";
|
|
import { sql } from "drizzle-orm";
|
|
import { adminOuReponse } from "@/lib/authz";
|
|
|
|
const FF_API_BASE = process.env.FF_API_BASE_URL ?? "https://api.ffnancy.fr";
|
|
|
|
async function probe(url: string) {
|
|
try {
|
|
const res = await fetch(url, { cache: "no-store" });
|
|
const text = await res.text();
|
|
let parsed: unknown = null;
|
|
try { parsed = JSON.parse(text); } catch { parsed = text.slice(0, 800); }
|
|
let summary: unknown = parsed;
|
|
if (Array.isArray(parsed)) {
|
|
summary = { count: parsed.length, firstKeys: parsed[0] ? Object.keys(parsed[0]) : [], sample: parsed.slice(0, 2) };
|
|
} else if (parsed && typeof parsed === "object") {
|
|
const keys = Object.keys(parsed as object);
|
|
const firstArrayKey = keys.find(k => Array.isArray((parsed as Record<string, unknown>)[k]));
|
|
if (firstArrayKey) {
|
|
const arr = (parsed as Record<string, unknown[]>)[firstArrayKey];
|
|
summary = { wrapper: firstArrayKey, count: arr.length, firstKeys: arr[0] ? Object.keys(arr[0] as object) : [], sample: arr.slice(0, 2) };
|
|
} else {
|
|
// Objet simple : montrer toutes les clés et le contenu complet
|
|
summary = { keys, full: parsed };
|
|
}
|
|
}
|
|
return { status: res.status, url, summary };
|
|
} catch (e) {
|
|
return { error: String(e), url };
|
|
}
|
|
}
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const acces = await adminOuReponse();
|
|
if (acces instanceof Response) return acces;
|
|
|
|
const { searchParams } = new URL(req.url);
|
|
|
|
// Date range: 12 derniers mois
|
|
const now = new Date();
|
|
const dateFin = new Date(now.getFullYear(), now.getMonth(), 0).toISOString().slice(0, 10);
|
|
const dateDebut = new Date(now.getFullYear(), now.getMonth() - 12, 1).toISOString().slice(0, 10);
|
|
|
|
// Récupérer un codefou réel + un codein réel depuis l'API
|
|
let codefou = searchParams.get("codefou");
|
|
let firstCodein: string | null = searchParams.get("codein");
|
|
const forcedNoid = searchParams.get("noid"); // ex: ?noid=129632
|
|
|
|
if (!codefou) {
|
|
try {
|
|
const res = await fetch(`${FF_API_BASE}/api/fournisseurs?limit=1`, { cache: "no-store" });
|
|
const data = await res.json();
|
|
const list = Array.isArray(data) ? data : (Object.values(data).find(v => Array.isArray(v)) as unknown[] ?? []);
|
|
if (list.length > 0) codefou = (list[0] as Record<string, unknown>).codefou as string ?? null;
|
|
} catch { /* ignore */ }
|
|
codefou ??= "D005";
|
|
}
|
|
|
|
if (!firstCodein) {
|
|
try {
|
|
const res = await fetch(`${FF_API_BASE}/api/articles?codefou=${encodeURIComponent(codefou)}&limit=1`, { cache: "no-store" });
|
|
const data = await res.json();
|
|
const list = Array.isArray(data) ? data : (Object.values(data).find(v => Array.isArray(v)) as unknown[] ?? []);
|
|
if (list.length > 0) firstCodein = (list[0] as Record<string, unknown>).codein as string ?? null;
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
// Si un noid est forcé, on probe directement mensuel + referentiel pour cet article
|
|
const mensuelNoidForced = forcedNoid
|
|
? probe(`${FF_API_BASE}/api/articles/${encodeURIComponent(forcedNoid)}/mensuel?dateDebut=${dateDebut}&dateFin=${dateFin}`)
|
|
: Promise.resolve({ skipped: "no ?noid= param" });
|
|
const referentielNoidForced = forcedNoid
|
|
? probe(`${FF_API_BASE}/api/articles/${encodeURIComponent(forcedNoid)}/referentiel`)
|
|
: Promise.resolve({ skipped: "no ?noid= param" });
|
|
|
|
const [
|
|
fournisseurs,
|
|
articles,
|
|
mvt_dateDebut,
|
|
mensuel_noid,
|
|
referentiel,
|
|
mensuel_forced,
|
|
referentiel_forced,
|
|
] = await Promise.all([
|
|
probe(`${FF_API_BASE}/api/fournisseurs?limit=2`),
|
|
probe(`${FF_API_BASE}/api/articles?codefou=${encodeURIComponent(codefou)}&limit=2`),
|
|
probe(`${FF_API_BASE}/api/mouvements/articles?codefou=${encodeURIComponent(codefou)}&dateDebut=${dateDebut}&dateFin=${dateFin}&limit=2`),
|
|
// Test mensuel avec no_id auto-résolu
|
|
firstCodein
|
|
? (async () => {
|
|
try {
|
|
const res = await fetch(`${FF_API_BASE}/api/articles?codefou=${encodeURIComponent(codefou)}&limit=1`, { cache: "no-store" });
|
|
const data = await res.json();
|
|
const list = Array.isArray(data) ? data : (Object.values(data).find(v => Array.isArray(v)) as unknown[] ?? []);
|
|
const noid = list[0] ? (list[0] as Record<string, unknown>).no_id as string : null;
|
|
if (noid) return probe(`${FF_API_BASE}/api/articles/${encodeURIComponent(noid)}/mensuel?dateDebut=${dateDebut}&dateFin=${dateFin}`);
|
|
} catch { /* ignore */ }
|
|
return { skipped: "no no_id available" };
|
|
})()
|
|
: Promise.resolve({ skipped: "no article" }),
|
|
// Referentiel complet (nomenclature + gamme + stock + prix)
|
|
firstCodein
|
|
? (async () => {
|
|
try {
|
|
const res = await fetch(`${FF_API_BASE}/api/articles?codefou=${encodeURIComponent(codefou)}&limit=5`, { cache: "no-store" });
|
|
const data = await res.json();
|
|
const list = Array.isArray(data) ? data : (Object.values(data).find(v => Array.isArray(v)) as unknown[] ?? []);
|
|
const art = (list[4] ?? list[0]) as Record<string, unknown> | undefined;
|
|
const noId = art?.no_id as string ?? null;
|
|
if (noId) return probe(`${FF_API_BASE}/api/articles/${encodeURIComponent(noId)}/referentiel`);
|
|
} catch { /* ignore */ }
|
|
return { skipped: "no no_id" };
|
|
})()
|
|
: Promise.resolve({ skipped: "no codefou" }),
|
|
mensuelNoidForced,
|
|
referentielNoidForced,
|
|
]);
|
|
|
|
// Test PostgreSQL + diagnostic ventesProduits
|
|
let postgresql: unknown;
|
|
let ventesProduitsDiag: unknown;
|
|
try {
|
|
const r = await db.execute(sql`SELECT COUNT(*) as count FROM session_snapshots`);
|
|
|
|
// Diagnostic ventesProduits : cherche par les 2 codes possibles d'AUXENCE
|
|
const vpByName = await db.execute(sql`
|
|
SELECT code_fournisseur, COUNT(*) as nb_rows,
|
|
COUNT(code_gamme) as nb_gamme,
|
|
COUNT(code_gamme_init) as nb_gamme_init,
|
|
COUNT(code3) as nb_code3
|
|
FROM ventes_produits
|
|
WHERE code_fournisseur IN ('AUXENCE', 'A025', ${codefou})
|
|
GROUP BY code_fournisseur
|
|
`);
|
|
|
|
// Sample de quelques codeins dans ventesProduits pour ce fournisseur
|
|
const vpSample = await db.execute(sql`
|
|
SELECT codein, code_fournisseur, code_gamme, code_gamme_init, code3, libelle3
|
|
FROM ventes_produits
|
|
WHERE code_fournisseur IN ('AUXENCE', 'A025', ${codefou})
|
|
LIMIT 3
|
|
`);
|
|
|
|
postgresql = {
|
|
ok: true,
|
|
snapshots: Number(r.rows[0]?.count ?? 0),
|
|
};
|
|
ventesProduitsDiag = {
|
|
byFournisseur: vpByName.rows,
|
|
sample: vpSample.rows,
|
|
};
|
|
} catch (e) {
|
|
postgresql = { ok: false, error: String(e) };
|
|
ventesProduitsDiag = { error: String(e) };
|
|
}
|
|
|
|
// Diagnostic analytics CA TTC
|
|
let mvtartDiag: unknown;
|
|
try {
|
|
// 1. Totaux mars 2025 avec mntmvtht et mntmvtttc pour comparer avec API (HT=225k, TTC=269k)
|
|
const totaux = await db.execute(sql`
|
|
SELECT site,
|
|
ABS(SUM(mntmvtht))::float AS sum_ht,
|
|
ABS(SUM(mntmvtttc))::float AS sum_ttc,
|
|
COUNT(*) AS nb_lignes
|
|
FROM mvtart
|
|
WHERE genremvt = 3
|
|
AND TO_CHAR(datmvt, 'YYYY-MM') = '2025-03'
|
|
AND site IN ('292', '579')
|
|
GROUP BY site
|
|
`);
|
|
// 2. Distribution des valeurs de preference dans artfou1
|
|
const prefDist = await db.execute(sql`
|
|
SELECT preference, COUNT(*) AS nb
|
|
FROM artfou1
|
|
GROUP BY preference
|
|
ORDER BY preference
|
|
`);
|
|
// 3. Ventes mars 2025 non attribuées (aucune ligne artfou1 avec preference=1)
|
|
const sansPreference = await db.execute(sql`
|
|
SELECT COUNT(DISTINCT m.artnoid) AS nb_articles_sans_pref
|
|
FROM mvtart m
|
|
JOIN articles a ON a.no_id = m.artnoid
|
|
WHERE m.genremvt = 3
|
|
AND TO_CHAR(m.datmvt, 'YYYY-MM') = '2025-03'
|
|
AND m.site IN ('292','579')
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM artfou1 af
|
|
WHERE af.art_no_id = a.no_id AND af.preference = 1
|
|
)
|
|
`);
|
|
mvtartDiag = {
|
|
totaux_202503: totaux.rows,
|
|
artfou1_preference_distribution: prefDist.rows,
|
|
articles_vendus_sans_pref1: sansPreference.rows,
|
|
};
|
|
} catch (e) {
|
|
mvtartDiag = { error: String(e) };
|
|
}
|
|
|
|
// Diagnostic franco : colonnes DB avec "franco" + sample fouident + raw commandes-auto
|
|
let francoDiag: unknown;
|
|
try {
|
|
const francoColumns = await db.execute(sql`
|
|
SELECT table_name, column_name, data_type
|
|
FROM information_schema.columns
|
|
WHERE column_name ILIKE '%franco%'
|
|
OR column_name ILIKE '%min_cmd%'
|
|
OR column_name ILIKE '%mincde%'
|
|
OR column_name ILIKE '%min_commande%'
|
|
ORDER BY table_name, column_name
|
|
`);
|
|
// Sample des 3 premières lignes de fouident pour voir toutes les colonnes
|
|
const fouidentSample = await db.execute(sql`SELECT * FROM fouident LIMIT 3`);
|
|
const foucadSample = await db.execute(sql`SELECT * FROM foucad LIMIT 5`);
|
|
francoDiag = {
|
|
francoColumns: francoColumns.rows,
|
|
fouidentColumns: fouidentSample.rows.length > 0 ? Object.keys(fouidentSample.rows[0] as object) : [],
|
|
fouidentSample: fouidentSample.rows,
|
|
foucadColumns: foucadSample.rows.length > 0 ? Object.keys(foucadSample.rows[0] as object) : [],
|
|
foucadSample: foucadSample.rows,
|
|
};
|
|
} catch (e) {
|
|
francoDiag = { error: String(e) };
|
|
}
|
|
|
|
// Raw commandes-auto : récupère toutes les lignes et filtre celles avec franco_ht manquant
|
|
let commandesAutoFrancoAudit: unknown;
|
|
try {
|
|
const caRes = await fetch(`${FF_API_BASE}/api/commandes-auto`, { cache: "no-store" });
|
|
const caData = await caRes.json();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const caRows: any[] = Array.isArray(caData) ? caData : (caData?.propositions ?? caData?.data ?? []);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const withFranco = caRows.filter((r: any) => r.franco_ht != null && Number(r.franco_ht) > 0);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const withoutFranco = caRows.filter((r: any) => r.franco_ht == null || Number(r.franco_ht) === 0);
|
|
commandesAutoFrancoAudit = {
|
|
total: caRows.length,
|
|
avecFranco: withFranco.length,
|
|
sansFranco: withoutFranco.length,
|
|
lignesSansFranco: withoutFranco.map((r: any) => ({
|
|
site: r.site, codefou: r.codefou, nom_fou: r.nom_fou,
|
|
franco_ht: r.franco_ht, franco_atteint: r.franco_atteint,
|
|
})),
|
|
};
|
|
} catch (e) {
|
|
commandesAutoFrancoAudit = { error: String(e) };
|
|
}
|
|
|
|
// Probe des fournisseurs N/A pour trouver où est leur franco
|
|
const [
|
|
detailM036_292,
|
|
detailM030_579,
|
|
fournisseurM036,
|
|
fournisseurM030,
|
|
] = await Promise.all([
|
|
probe(`${FF_API_BASE}/api/commandes-auto/M036?site=292`),
|
|
probe(`${FF_API_BASE}/api/commandes-auto/M030?site=579`),
|
|
probe(`${FF_API_BASE}/api/fournisseurs/M036`),
|
|
probe(`${FF_API_BASE}/api/fournisseurs/M030`),
|
|
]);
|
|
const francoNaProbe = { detailM036_292, detailM030_579, fournisseurM036, fournisseurM030 };
|
|
|
|
// Raw commandes-auto (premier fournisseur) pour voir les champs réels retournés par l'API
|
|
const rawCommandesAuto = await probe(`${FF_API_BASE}/api/commandes-auto`);
|
|
|
|
return NextResponse.json({
|
|
timestamp: new Date().toISOString(),
|
|
dateRange: { dateDebut, dateFin },
|
|
codefouTested: codefou,
|
|
firstCodeinTested: firstCodein,
|
|
forcedNoid: forcedNoid ?? "(none — pass ?noid=129632 to test a specific article)",
|
|
fournisseurs,
|
|
articles,
|
|
mouvements_dateDebut: mvt_dateDebut,
|
|
endpoints_auto: {
|
|
"articles/:no_id/mensuel": mensuel_noid,
|
|
"articles/:no_id/referentiel": referentiel,
|
|
},
|
|
endpoints_forced_noid: {
|
|
"articles/:noid/mensuel": mensuel_forced,
|
|
"articles/:noid/referentiel": referentiel_forced,
|
|
},
|
|
postgresql,
|
|
ventesProduitsDiag,
|
|
mvtartDiag,
|
|
francoDiag,
|
|
rawCommandesAuto,
|
|
commandesAutoFrancoAudit,
|
|
francoNaProbe,
|
|
});
|
|
}
|