diff --git a/client/src/components/SalesHistory.tsx b/client/src/components/SalesHistory.tsx index e14f663..8baeac3 100644 --- a/client/src/components/SalesHistory.tsx +++ b/client/src/components/SalesHistory.tsx @@ -12,6 +12,7 @@ import { apiRequest } from '@/lib/queryClient'; import type { Sale } from '@shared/schema'; import { History, Download, Trash2, Eye, Filter, FileText, Search, X, Calendar, Users } from 'lucide-react'; import SaleDetailsModal from '@/components/SaleDetailsModal'; +import { formatDateTime } from '@/lib/export'; interface SalesHistoryProps { canDelete?: boolean; @@ -364,6 +365,35 @@ export default function SalesHistory({ canDelete = false }: SalesHistoryProps) { setSearchQuery(''); }; + const exportToExcel = async () => { + if (filteredSales.length === 0) { + toast({ + title: "Aucune donnée", + description: "Aucune vente à exporter", + variant: "destructive", + }); + return; + } + + try { + const { exportToExcel } = await import('@/lib/export'); + await exportToExcel(filteredSales); + + toast({ + title: "Export réussi", + description: "Le fichier Excel a été téléchargé", + variant: "success", + }); + } catch (error) { + console.error('Erreur lors de l\'export Excel:', error); + toast({ + title: "Erreur d'export", + description: "Impossible de générer le fichier Excel", + variant: "destructive", + }); + } + }; + if (isLoading) { return (
@@ -475,12 +505,12 @@ export default function SalesHistory({ canDelete = false }: SalesHistoryProps) { Registre PDF Détaillé
diff --git a/client/src/lib/export.ts b/client/src/lib/export.ts index 1fb45af..cd18be6 100644 --- a/client/src/lib/export.ts +++ b/client/src/lib/export.ts @@ -1,5 +1,137 @@ import { Sale } from '@/types/sale'; +export async function exportToExcel(sales: Sale[]): Promise { + if (sales.length === 0) return; + + try { + const XLSX = await import('xlsx'); + const { utils, writeFile } = XLSX; + + // Créer un nouveau classeur + const workbook = utils.book_new(); + + // Préparer les données pour la feuille principale + const salesData: any[] = []; + + sales.forEach(sale => { + if (sale.products && sale.products.length > 0) { + // Pour chaque produit, créer une ligne avec les infos de la vente + sale.products.forEach((product, productIndex) => { + salesData.push({ + 'ID Vente': sale.id, + 'Date et Heure': new Date(sale.timestamp).toLocaleString('fr-FR'), + 'Vendeur': sale.vendeur, + 'Produit N°': productIndex + 1, + 'Type Article': product.typeArticle, + 'Catégorie': product.categorie, + 'Quantité': product.quantite, + 'Code Générique': product.gencode || '', + 'Nom Client': sale.nom, + 'Prénom Client': sale.prenom, + 'Date Naissance': sale.dateNaissance || '', + 'Lieu Naissance': sale.lieuNaissance || '', + 'Mode Paiement': sale.modePaiement || '', + 'Type Identité': sale.typeIdentite || '', + 'Numéro Identité': sale.numeroIdentite || '', + 'Autorité Délivrance': sale.autoriteDelivrance || '', + 'Date Délivrance': sale.dateDelivrance || '', + 'Photo Recto': sale.photoRecto ? 'Oui' : 'Non', + 'Photo Verso': sale.photoVerso ? 'Oui' : 'Non', + 'Photo Ticket': sale.photoTicket ? 'Oui' : 'Non' + }); + }); + } else { + // Fallback pour les ventes sans produits + salesData.push({ + 'ID Vente': sale.id, + 'Date et Heure': new Date(sale.timestamp).toLocaleString('fr-FR'), + 'Vendeur': sale.vendeur, + 'Produit N°': 'N/A', + 'Type Article': 'N/A', + 'Catégorie': 'N/A', + 'Quantité': 0, + 'Code Générique': '', + 'Nom Client': sale.nom, + 'Prénom Client': sale.prenom, + 'Date Naissance': sale.dateNaissance || '', + 'Lieu Naissance': sale.lieuNaissance || '', + 'Mode Paiement': sale.modePaiement || '', + 'Type Identité': sale.typeIdentite || '', + 'Numéro Identité': sale.numeroIdentite || '', + 'Autorité Délivrance': sale.autoriteDelivrance || '', + 'Date Délivrance': sale.dateDelivrance || '', + 'Photo Recto': sale.photoRecto ? 'Oui' : 'Non', + 'Photo Verso': sale.photoVerso ? 'Oui' : 'Non', + 'Photo Ticket': sale.photoTicket ? 'Oui' : 'Non' + }); + } + }); + + // Créer la feuille des ventes + const salesWorksheet = utils.json_to_sheet(salesData); + + // Ajuster la largeur des colonnes + const columnWidths = [ + { wch: 10 }, // ID Vente + { wch: 18 }, // Date et Heure + { wch: 15 }, // Vendeur + { wch: 10 }, // Produit N° + { wch: 25 }, // Type Article + { wch: 10 }, // Catégorie + { wch: 8 }, // Quantité + { wch: 15 }, // Code Générique + { wch: 15 }, // Nom Client + { wch: 15 }, // Prénom Client + { wch: 12 }, // Date Naissance + { wch: 15 }, // Lieu Naissance + { wch: 12 }, // Mode Paiement + { wch: 12 }, // Type Identité + { wch: 15 }, // Numéro Identité + { wch: 18 }, // Autorité Délivrance + { wch: 12 }, // Date Délivrance + { wch: 10 }, // Photo Recto + { wch: 10 }, // Photo Verso + { wch: 10 } // Photo Ticket + ]; + + salesWorksheet['!cols'] = columnWidths; + + // Ajouter la feuille au classeur + utils.book_append_sheet(workbook, salesWorksheet, 'Ventes Détaillées'); + + // Créer une feuille de résumé + const summaryData = [ + { 'Statistique': 'Nombre total de ventes', 'Valeur': sales.length }, + { 'Statistique': 'Nombre total de produits vendus', 'Valeur': salesData.length }, + { 'Statistique': 'Date de génération', 'Valeur': new Date().toLocaleString('fr-FR') }, + { 'Statistique': 'Ventes avec photos recto', 'Valeur': sales.filter(s => s.photoRecto).length }, + { 'Statistique': 'Ventes avec photos verso', 'Valeur': sales.filter(s => s.photoVerso).length }, + { 'Statistique': 'Ventes avec photos ticket', 'Valeur': sales.filter(s => s.photoTicket).length } + ]; + + const summaryWorksheet = utils.json_to_sheet(summaryData); + summaryWorksheet['!cols'] = [{ wch: 30 }, { wch: 20 }]; + utils.book_append_sheet(workbook, summaryWorksheet, 'Résumé'); + + // Exporter le fichier + const fileName = `registre-feux-artifice-${new Date().toISOString().split('T')[0]}.xlsx`; + writeFile(workbook, fileName); + + } catch (error) { + console.error('Erreur lors de l\'export Excel:', error); + throw error; + } +} + +export function formatDate(dateString: string): string { + return new Date(dateString).toLocaleDateString('fr-FR'); +} + +export function formatDateTime(dateString: string): string { + return new Date(dateString).toLocaleString('fr-FR'); +} + +// Fonction CSV basique maintenue pour compatibilité si nécessaire export function exportToCSV(sales: Sale[]): void { if (sales.length === 0) return; @@ -85,11 +217,3 @@ export function exportToCSV(sales: Sale[]): void { URL.revokeObjectURL(link.href); } - -export function formatDate(dateString: string): string { - return new Date(dateString).toLocaleDateString('fr-FR'); -} - -export function formatDateTime(dateString: string): string { - return new Date(dateString).toLocaleString('fr-FR'); -} diff --git a/package-lock.json b/package-lock.json index 45082c9..8e3bfdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,8 @@ "vaul": "^1.1.2", "wouter": "^3.3.5", "ws": "^8.18.0", + "xlsx": "^0.18.5", + "xlsx-js-style": "^1.2.0", "zod": "^3.24.2", "zod-validation-error": "^3.4.0" }, @@ -3771,6 +3773,15 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -4136,6 +4147,19 @@ "license": "MIT", "optional": true }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4206,6 +4230,15 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4300,6 +4333,18 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5439,6 +5484,15 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -5679,6 +5733,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -7349,6 +7412,18 @@ "optional": true, "peer": true }, + "node_modules/printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "license": "Apache-2.0", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8051,6 +8126,18 @@ "node": ">= 10.x" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stackblur-canvas": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", @@ -9545,6 +9632,24 @@ "node": ">= 8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wouter": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.3.5.tgz", @@ -9671,6 +9776,101 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz", + "integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.2.0", + "cfb": "^1.1.4", + "codepage": "~1.14.0", + "commander": "~2.17.1", + "crc-32": "~1.2.0", + "exit-on-epipe": "~1.0.1", + "fflate": "^0.3.8", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style/node_modules/adler-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", + "integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==", + "license": "Apache-2.0", + "dependencies": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + }, + "bin": { + "adler32": "bin/adler32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style/node_modules/codepage": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", + "integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==", + "license": "Apache-2.0", + "dependencies": { + "commander": "~2.14.1", + "exit-on-epipe": "~1.0.1" + }, + "bin": { + "codepage": "bin/codepage.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style/node_modules/codepage/node_modules/commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", + "license": "MIT" + }, + "node_modules/xlsx-js-style/node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "license": "MIT" + }, + "node_modules/xlsx-js-style/node_modules/fflate": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", + "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==", + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 5538135..629d329 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,8 @@ "vaul": "^1.1.2", "wouter": "^3.3.5", "ws": "^8.18.0", + "xlsx": "^0.18.5", + "xlsx-js-style": "^1.2.0", "zod": "^3.24.2", "zod-validation-error": "^3.4.0" },