mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-20 13:38:08 +02:00
Remove ranking, AI analysis (routes + ai-copilot module), and score-engine from the grid. Add Qlik Sense network metrics (CA, Qte, nb magasins per product) joined by code centrale (articles.artcentrale). - qlik-client.ts: NTLM + ticket SSO auth, QIX hypercube extraction (master items) - qlik-network-cache.ts + qlik_network_metrics table (db-init): cache layer - POST /api/qlik/sync?fournisseur=: per-supplier sync into cache - SyncQlikButton: per-supplier sync, grid reload, last-update date display - get-product-rows Phase 8: enrich rows with cached network metrics - heatmap-grid: CA reseau / Qte reseau / Magasins (/270) / % presence columns - discovery: app 9872ee6e, master items CA N / Quantite N / Magasin Ventes Nb N - deps: httpntlm, ws Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
236 lines
9.8 KiB
TypeScript
236 lines
9.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Download, FileSpreadsheet, FileText, ChevronDown, Table2 } from "lucide-react";
|
|
import * as ExcelJS from "exceljs";
|
|
import jsPDF from "jspdf";
|
|
import autoTable from "jspdf-autotable";
|
|
import { useGridStore } from "@/features/grid/store/use-grid-store";
|
|
|
|
export function ExportDropdown({ nomFournisseur }: { nomFournisseur?: string }) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const { rows, draftChanges } = useGridStore();
|
|
|
|
const handleExportAllProducts = async () => {
|
|
setIsOpen(false);
|
|
if (rows.length === 0) {
|
|
alert("Aucune donnée à exporter.");
|
|
return;
|
|
}
|
|
|
|
const supplier = nomFournisseur || rows[0]?.nomFournisseur || "Fournisseur";
|
|
|
|
const payload = {
|
|
nomFournisseur: supplier,
|
|
rows: rows.map(r => ({
|
|
codein: r.codein,
|
|
libelle1: r.libelle1 || "",
|
|
code3: r.code3 || "",
|
|
libelle3: r.libelle3 || "",
|
|
codeGamme: (draftChanges[r.codein] ?? r.codeGamme) as string | null,
|
|
totalQuantite: r.totalQuantite,
|
|
totalCa: r.totalCa,
|
|
tauxMarge: r.tauxMarge,
|
|
gammeAvant: r.codeGammeInit ?? null,
|
|
})),
|
|
};
|
|
|
|
try {
|
|
const res = await fetch("/api/export/excel", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!res.ok) throw new Error("Erreur lors de l'export");
|
|
|
|
const blob = await res.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `Assortiment_Complet_${supplier.replace(/\s+/g, "_")}_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
} catch (error) {
|
|
console.error(error);
|
|
alert("Une erreur s'est produite lors de l'export Excel.");
|
|
}
|
|
};
|
|
|
|
const handleExportExcel = async () => {
|
|
setIsOpen(false);
|
|
const workbook = new ExcelJS.Workbook();
|
|
const worksheet = workbook.addWorksheet("Gamme A");
|
|
|
|
worksheet.columns = [
|
|
{ header: "Magasin(s)", key: "magasins", width: 25 },
|
|
{ header: "Gencode", key: "gtin", width: 15 },
|
|
{ header: "Référence", key: "reference", width: 20 },
|
|
{ header: "Libellé", key: "libelle", width: 45 },
|
|
{ header: "Gamme", key: "gamme", width: 10 },
|
|
];
|
|
|
|
// Format Header
|
|
worksheet.getRow(1).font = { bold: true };
|
|
|
|
// Filter for effective Gamme A
|
|
const gammeARows = rows.filter((row) => {
|
|
const effectiveGamme = draftChanges[row.codein] ?? row.codeGamme;
|
|
return effectiveGamme === "A";
|
|
});
|
|
|
|
gammeARows.forEach((row) => {
|
|
worksheet.addRow({
|
|
magasins: row.workingStores.join(", "),
|
|
gtin: row.gtin || row.codein, // Fallback if no true gtin
|
|
reference: row.reference || "",
|
|
libelle: row.libelle1 || "",
|
|
gamme: draftChanges[row.codein] ?? row.codeGamme,
|
|
});
|
|
});
|
|
|
|
const buffer = await workbook.xlsx.writeBuffer();
|
|
const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `Export_Gamme_A_${new Date().toISOString().split("T")[0]}.xlsx`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const handlePrintPDF = () => {
|
|
setIsOpen(false);
|
|
const doc = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4" });
|
|
|
|
doc.setFontSize(16);
|
|
doc.text("Analyse d'Assortiment", 14, 15);
|
|
|
|
doc.setFontSize(14);
|
|
doc.setTextColor(50);
|
|
if (nomFournisseur) {
|
|
doc.text(nomFournisseur, 14, 23);
|
|
}
|
|
|
|
doc.setFontSize(10);
|
|
doc.setTextColor(100);
|
|
doc.text(`Export généré le : ${new Date().toLocaleDateString("fr-FR")}`, 14, 30);
|
|
|
|
const head = [
|
|
["Gencode", "Code In", "Réf.", "Libellé", "Mag. Rés.", "Vol.", "CA", "Marge", "Gamme"]
|
|
];
|
|
|
|
const body = rows.map(r => {
|
|
const effectiveGamme = draftChanges[r.codein] ?? r.codeGamme;
|
|
return [
|
|
r.gtin || r.codein,
|
|
r.codein,
|
|
r.reference || "-",
|
|
r.libelle1 ? r.libelle1.substring(0, 60) + (r.libelle1.length > 60 ? "..." : "") : "",
|
|
r.nbMagasinsReseau != null ? r.nbMagasinsReseau.toString() : "-",
|
|
Math.round(r.totalQuantite).toLocaleString("fr-FR"),
|
|
`${Math.round(r.totalCa).toLocaleString("fr-FR")} €`,
|
|
`${Math.round(r.totalMarge).toLocaleString("fr-FR")} €\n(${r.tauxMarge.toFixed(1)}%)`,
|
|
effectiveGamme || "-"
|
|
];
|
|
});
|
|
|
|
// @ts-ignore (autotable acts as a plugin)
|
|
autoTable(doc, {
|
|
head,
|
|
body,
|
|
startY: 35,
|
|
styles: { fontSize: 8, cellPadding: 2, lineColor: [200, 200, 200], lineWidth: 0.1 },
|
|
headStyles: { fillColor: [15, 23, 42], textColor: [255, 255, 255], fontStyle: "bold", halign: "center" },
|
|
columnStyles: {
|
|
0: { cellWidth: 28 }, // Gencode
|
|
1: { cellWidth: 18, fontStyle: "bold", halign: "center" }, // Code In
|
|
2: { cellWidth: 25 }, // Réf
|
|
3: { cellWidth: 80 }, // Libellé
|
|
4: { cellWidth: 12, halign: "center", fontStyle: "bold", textColor: [16, 185, 129] }, // Score
|
|
},
|
|
theme: "grid",
|
|
didDrawPage: (data: any) => {
|
|
// Footer pagination
|
|
doc.setFontSize(8);
|
|
doc.text(
|
|
`Page ${data.pageNumber}`,
|
|
data.settings.margin.left,
|
|
doc.internal.pageSize.height - 10
|
|
);
|
|
}
|
|
});
|
|
|
|
doc.save(`Assortiment_${new Date().toISOString().split("T")[0]}.pdf`);
|
|
};
|
|
|
|
return (
|
|
<div className="relative z-50 print:hidden">
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="btn-action btn-action-secondary flex items-center gap-2"
|
|
title="Exporter les données"
|
|
>
|
|
<Download className="w-[14px] h-[14px]" />
|
|
<span className="text-[13px] font-semibold">Exporter</span>
|
|
<ChevronDown className="w-[12px] h-[12px] opacity-60" />
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<>
|
|
<div
|
|
className="fixed inset-0 z-40"
|
|
onClick={() => setIsOpen(false)}
|
|
/>
|
|
<div className="absolute right-0 mt-2 w-56 rounded-xl border z-50 overflow-hidden shadow-lg animate-in fade-in slide-in-from-top-2"
|
|
style={{
|
|
background: "var(--bg-elevated)",
|
|
borderColor: "var(--border-strong)"
|
|
}}
|
|
>
|
|
<div className="p-1">
|
|
<button
|
|
onClick={handleExportExcel}
|
|
className="w-full flex items-center gap-3 px-3 py-2 text-sm text-left rounded-lg transition-colors hover:bg-white/5"
|
|
>
|
|
<FileSpreadsheet className="w-4 h-4 text-emerald-500" />
|
|
<div>
|
|
<div className="font-semibold text-[var(--text-primary)]">Gamme A vers Excel</div>
|
|
<div className="text-[11px] text-[var(--text-muted)] mt-0.5">Gencode, Réf, Libellé...</div>
|
|
</div>
|
|
</button>
|
|
|
|
<div className="my-1 h-px bg-[var(--border)] mx-2" />
|
|
|
|
<button
|
|
onClick={handleExportAllProducts}
|
|
className="w-full flex items-center gap-3 px-3 py-2 text-sm text-left rounded-lg transition-colors hover:bg-white/5"
|
|
>
|
|
<Table2 className="w-4 h-4 text-blue-400" />
|
|
<div>
|
|
<div className="font-semibold text-[var(--text-primary)]">Tous les produits (Excel)</div>
|
|
<div className="text-[11px] text-[var(--text-muted)] mt-0.5">Gamme avant/après, delta, stats 12m</div>
|
|
</div>
|
|
</button>
|
|
|
|
<div className="my-1 h-px bg-[var(--border)] mx-2" />
|
|
|
|
<button
|
|
onClick={handlePrintPDF}
|
|
className="w-full flex items-center gap-3 px-3 py-2 text-sm text-left rounded-lg transition-colors hover:bg-white/5"
|
|
>
|
|
<FileText className="w-4 h-4 text-rose-500" />
|
|
<div>
|
|
<div className="font-semibold text-[var(--text-primary)]">Tableau en PDF</div>
|
|
<div className="text-[11px] text-[var(--text-muted)] mt-0.5">Mise en page épurée (Réf. incluse)</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|