Files
CollectFlow/src/app/api/export/excel/route.ts
T
Claude 26b9364232 Lot 5 : changement de magasin instantané et contrôle des droits
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
2026-10-04 06:39:49 +00:00

113 lines
4.1 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import ExcelJS from "exceljs";
import { z } from "zod";
import { sessionOuReponse } from "@/lib/authz";
const ExportSchema = z.object({
nomFournisseur: z.string(),
rows: z.array(z.object({
codein: z.string(),
libelle1: z.string(),
code3: z.string(),
libelle3: z.string(),
codeGamme: z.string().nullable(),
totalQuantite: z.number(),
totalCa: z.number(),
tauxMarge: z.number(),
// Before gamme for delta
gammeAvant: z.string().nullable().optional(),
})),
});
export async function POST(req: NextRequest) {
const acces = await sessionOuReponse();
if (acces instanceof Response) return acces;
const body = await req.json();
const parsed = ExportSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
}
const { nomFournisseur, rows } = parsed.data;
const workbook = new ExcelJS.Workbook();
workbook.creator = "CollectFlow";
workbook.created = new Date();
const sheet = workbook.addWorksheet("Assortiment", {
views: [{ state: "frozen", ySplit: 1 }],
});
// Header style
const headerStyle: Partial<ExcelJS.Style> = {
font: { bold: true, color: { argb: "FFFFFFFF" } },
fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FF0F172A" } },
alignment: { horizontal: "center" },
border: { bottom: { style: "thin", color: { argb: "FF334155" } } },
};
sheet.columns = [
{ header: "Code", key: "codein", width: 14 },
{ header: "Désignation", key: "libelle1", width: 40 },
{ header: "Nomenclature", key: "libelle3", width: 25 },
{ header: "Gamme Avant", key: "gammeAvant", width: 14 },
{ header: "Gamme Après", key: "codeGamme", width: 14 },
{ header: "Delta", key: "delta", width: 10 },
{ header: "Vol. 12m", key: "totalQuantite", width: 12 },
{ header: "CA 12m (€)", key: "totalCa", width: 14 },
{ header: "Marge (%)", key: "tauxMarge", width: 12 },
];
// Apply header style
sheet.getRow(1).eachCell((cell) => { Object.assign(cell, headerStyle); });
sheet.getRow(1).height = 22;
// Gamme colours
const GAMME_COLORS: Record<string, string> = {
A: "FF064E3B", B: "FF1E3A5F", C: "FF78350F", Z: "FF4C0519",
};
for (const row of rows) {
const delta = row.gammeAvant !== row.codeGamme ? `${row.gammeAvant ?? "—"} → ${row.codeGamme ?? "—"}` : "";
const excelRow = sheet.addRow({
codein: row.codein,
libelle1: row.libelle1,
libelle3: row.libelle3,
gammeAvant: row.gammeAvant ?? "—",
codeGamme: row.codeGamme ?? "—",
delta,
totalQuantite: Math.round(row.totalQuantite),
totalCa: parseFloat(row.totalCa.toFixed(2)),
tauxMarge: parseFloat(row.tauxMarge.toFixed(2)),
});
// Colour the "Gamme Après" cell
const gammeCell = excelRow.getCell("codeGamme");
if (row.codeGamme && GAMME_COLORS[row.codeGamme]) {
gammeCell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: GAMME_COLORS[row.codeGamme] } };
gammeCell.font = { bold: true, color: { argb: "FFFFFFFF" } };
}
// Highlight changed rows
if (delta) {
excelRow.getCell("delta").font = { bold: true, color: { argb: "FFFBBF24" } };
}
// Format numbers
excelRow.getCell("totalCa").numFmt = '#,##0.00 "€"';
excelRow.getCell("tauxMarge").numFmt = '0.00"%"';
}
// Generate buffer
const buffer = await workbook.xlsx.writeBuffer();
const filename = `Assortiment_${nomFournisseur.replace(/\s+/g, "_")}_${new Date().toISOString().slice(0, 10)}.xlsx`;
return new NextResponse(buffer, {
headers: {
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Content-Disposition": `attachment; filename="${filename}"`,
},
});
}