diff --git a/src/features/grid/components/export-dropdown.tsx b/src/features/grid/components/export-dropdown.tsx
new file mode 100644
index 0000000..2771595
--- /dev/null
+++ b/src/features/grid/components/export-dropdown.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { useState } from "react";
+import { Download, FileSpreadsheet, FileText, ChevronDown } from "lucide-react";
+import * as ExcelJS from "exceljs";
+import { useGridStore } from "@/features/grid/store/use-grid-store";
+
+export function ExportDropdown() {
+ const [isOpen, setIsOpen] = useState(false);
+ const { rows, draftChanges } = useGridStore();
+
+ 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);
+ window.print();
+ };
+
+ return (
+
+
{/* Bulk toolbar (contextual) */}
-
+
setSelectedCodeins([])}
@@ -134,7 +138,9 @@ export function GridClient({ initialRows, nomFournisseur, fournisseurs, magasins
{/* Summary bar */}
-
+
+
+
);
}
diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx
index af235b4..51fd9ff 100644
--- a/src/features/grid/components/heatmap-grid.tsx
+++ b/src/features/grid/components/heatmap-grid.tsx
@@ -41,33 +41,30 @@ function formatMonthLabel(key: string): string {
}
/**
- * Returns premium styling and icons for stores.
- * Indigo for first store, Amber for second, Slate for others.
+ * Returns premium styling and color for stores based on the store name's hash.
+ * This guarantees a consistent color for a given store, regardless of its position in the list.
*/
-function getStoreConfig(name: string, index: number) {
- const initial = name.charAt(0).toUpperCase();
- if (index === 0) {
- return {
- bg: "rgba(99, 102, 241, 0.1)", // Indigo
- text: "#818cf8",
- border: "rgba(99, 102, 241, 0.2)",
- label: initial
- };
+function getStoreConfig(name: string) {
+ const words = name.trim().split(/\s+/);
+ let label = words[0].charAt(0).toUpperCase();
+ if (words.length > 1) {
+ label += words[1].charAt(0).toUpperCase();
}
- if (index === 1) {
- return {
- bg: "rgba(245, 158, 11, 0.1)", // Amber
- text: "#fbbf24",
- border: "rgba(245, 158, 11, 0.2)",
- label: initial
- };
+
+ let hash = 0;
+ for (let i = 0; i < name.length; i++) {
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ const colorInt = Math.abs(hash) % 5;
+
+ switch (colorInt) {
+ case 0: return { bg: "rgba(99, 102, 241, 0.1)", text: "#818cf8", border: "rgba(99, 102, 241, 0.2)", label }; // Indigo
+ case 1: return { bg: "rgba(245, 158, 11, 0.1)", text: "#fbbf24", border: "rgba(245, 158, 11, 0.2)", label }; // Amber
+ case 2: return { bg: "rgba(16, 185, 129, 0.1)", text: "#10b981", border: "rgba(16, 185, 129, 0.2)", label }; // Emerald
+ case 3: return { bg: "rgba(236, 72, 153, 0.1)", text: "#ec4899", border: "rgba(236, 72, 153, 0.2)", label }; // Pink
+ case 4: return { bg: "rgba(14, 165, 233, 0.1)", text: "#0ea5e9", border: "rgba(14, 165, 233, 0.2)", label }; // Sky
+ default: return { bg: "var(--bg-elevated)", text: "var(--text-muted)", border: "var(--border)", label };
}
- return {
- bg: "var(--bg-elevated)",
- text: "var(--text-muted)",
- border: "var(--border)",
- label: initial
- };
}
interface HeatmapGridProps {
@@ -278,6 +275,16 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
);
},
},
+ {
+ accessorKey: "reference",
+ header: () =>
Réf. Fournisseur
,
+ size: 130,
+ cell: ({ getValue }) => (
+
+ {getValue() || "-"}
+
+ ),
+ },
{
accessorKey: "libelle1",
header: "Désignation",
@@ -288,12 +295,12 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
{row.original.libelle1}
- {row.original.workingStores.map((magasin, idx) => {
- const config = getStoreConfig(magasin, idx);
+ {row.original.workingStores.map((magasin) => {
+ const config = getStoreConfig(magasin);
return (
Init.
,
+ header: () =>
Init.
,
size: 56,
cell: ({ row }) => (
-
+
{row.original.codeGammeInit || "-"}
),
@@ -402,10 +409,14 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
},
{
id: "ai",
- header: "Recommandation IA",
+ header: () =>
Recommandation IA
,
size: 230,
enableSorting: false,
- cell: ({ row }) =>
,
+ cell: ({ row }) => (
+
+ ),
},
], [MONTHS_12]); // Dépendances extrêmement stables : pas de re-render du header !