From a4582fefe294b0c30ec2f3af81958456271b3271 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Tue, 4 Nov 2025 11:43:15 +0000 Subject: [PATCH 1/3] Transitioned from Plan to Build mode Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1e4433b5-47ab-464c-b663-fea2e53367dd Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 2c21929a-3bfc-4873-86cc-fac664b3e13b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/1e4433b5-47ab-464c-b663-fea2e53367dd/vfXE6SO From 01dde7b7c137545d0f106e9b4b4e3758850a646f Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Tue, 4 Nov 2025 11:50:48 +0000 Subject: [PATCH 2/3] Add functionality to export payment schedules to Excel Introduces an API endpoint and frontend components for exporting payment schedule data to an Excel file, with filtering options for payment methods and tax inclusion. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1e4433b5-47ab-464c-b663-fea2e53367dd Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 09286923-d43e-46ea-831c-611649677d67 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/1e4433b5-47ab-464c-b663-fea2e53367dd/vfXE6SO --- client/src/pages/PaymentSchedulePage.tsx | 362 +++++++++++++++++++---- package-lock.json | 104 +++++++ package.json | 1 + server/routes.ts | 149 ++++++++++ 4 files changed, 560 insertions(+), 56 deletions(-) diff --git a/client/src/pages/PaymentSchedulePage.tsx b/client/src/pages/PaymentSchedulePage.tsx index 3a86198..042fa8b 100644 --- a/client/src/pages/PaymentSchedulePage.tsx +++ b/client/src/pages/PaymentSchedulePage.tsx @@ -1,8 +1,9 @@ import { useState, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { useStore } from "@/contexts/StoreContext"; +import { useToast } from "@/hooks/use-toast"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Loader2, Calendar, DollarSign, Building, CreditCard, AlertCircle } from "lucide-react"; +import { Loader2, Calendar, DollarSign, Building, CreditCard, AlertCircle, FileSpreadsheet, Download } from "lucide-react"; import { format, parseISO, startOfMonth, endOfMonth, eachMonthOfInterval, isWithinInterval } from "date-fns"; import { fr } from "date-fns/locale"; import { @@ -12,6 +13,25 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; interface PaymentSchedule { id: string | number; @@ -32,10 +52,18 @@ interface PaymentScheduleResponse { export default function PaymentSchedulePage() { const { selectedStoreId } = useStore(); + const { toast } = useToast(); const [selectedMonth, setSelectedMonth] = useState(() => { const now = new Date(); return format(now, 'yyyy-MM'); }); + + // États pour la modale d'export + const [showExportModal, setShowExportModal] = useState(false); + const [selectedPaymentMethods, setSelectedPaymentMethods] = useState([]); + const [includeHT, setIncludeHT] = useState(true); + const [includeTTC, setIncludeTTC] = useState(true); + const [isExporting, setIsExporting] = useState(false); // Récupérer les échéances const { data, isLoading, error } = useQuery({ @@ -118,6 +146,95 @@ export default function PaymentSchedulePage() { return totals; }, [filteredSchedules]); + // Récupérer tous les modes de paiement disponibles + const availablePaymentMethods = useMemo(() => { + const methods = new Set(); + filteredSchedules.forEach(schedule => { + methods.add(schedule.paymentMethod || 'Non défini'); + }); + return Array.from(methods).sort(); + }, [filteredSchedules]); + + // Handler pour ouvrir la modale d'export + const handleOpenExportModal = () => { + // Pré-sélectionner tous les modes de paiement par défaut + setSelectedPaymentMethods(availablePaymentMethods); + setShowExportModal(true); + }; + + // Handler pour basculer la sélection d'un mode de paiement + const togglePaymentMethod = (method: string) => { + setSelectedPaymentMethods(prev => + prev.includes(method) + ? prev.filter(m => m !== method) + : [...prev, method] + ); + }; + + // Handler pour sélectionner/désélectionner tous les modes de paiement + const toggleAllPaymentMethods = () => { + if (selectedPaymentMethods.length === availablePaymentMethods.length) { + setSelectedPaymentMethods([]); + } else { + setSelectedPaymentMethods(availablePaymentMethods); + } + }; + + // Handler pour exporter vers Excel + const handleExport = async () => { + if (selectedPaymentMethods.length === 0) { + return; + } + + setIsExporting(true); + try { + const response = await fetch('/api/payment-schedule/export', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + groupId: selectedStoreId, + month: selectedMonth, + paymentMethods: selectedPaymentMethods, + includeHT, + includeTTC, + }), + }); + + if (!response.ok) { + throw new Error('Erreur lors de l\'export'); + } + + // Télécharger le fichier + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `echeancier_${selectedMonth}.xlsx`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + setShowExportModal(false); + toast({ + title: "Export réussi", + description: "Le fichier Excel a été téléchargé avec succès.", + }); + } catch (error) { + console.error('Erreur export:', error); + toast({ + title: "Erreur d'export", + description: "Une erreur est survenue lors de l'export du fichier Excel.", + variant: "destructive", + }); + } finally { + setIsExporting(false); + } + }; + if (!selectedStoreId) { return (
@@ -180,19 +297,31 @@ export default function PaymentSchedulePage() {

Échéancier des Paiements

Gestion des échéances fournisseurs

-
- +
+ +
+ +
@@ -266,7 +395,7 @@ export default function PaymentSchedulePage() { - {/* Liste des échéances */} + {/* Tableau des échéances */} Échéances du Mois @@ -280,54 +409,175 @@ export default function PaymentSchedulePage() { Aucune échéance pour ce mois ) : ( -
- {filteredSchedules.map((schedule, index) => ( -
-
-
- - +
+ + + + Date + Fournisseur + Facture + Mode de paiement + Montant HT + Montant TTC + + + + {filteredSchedules.map((schedule, index) => ( + + {format(parseISO(schedule.dueDate), 'dd/MM/yyyy', { locale: fr })} - - -
-
- - {schedule.supplierName} -
-
Facture: {schedule.invoiceReference}
- {schedule.paymentMethod && ( -
- - {schedule.paymentMethod} -
- )} -
- -
-
- HT: - + + {schedule.supplierName} + {schedule.invoiceReference} + + + {schedule.paymentMethod || 'Non défini'} + + + {schedule.amount.toFixed(2)} € - -
-
- TTC: - + + {schedule.amountTTC.toFixed(2)} € - -
-
- - ))} +
+
+ ))} +
+
)} + + {/* Modale d'export Excel */} + + + + + + Exporter vers Excel + + + Sélectionnez les modes de paiement et les colonnes à inclure dans l'export + + + +
+ {/* Sélection des modes de paiement */} +
+
+ + +
+
+ {availablePaymentMethods.map((method) => ( +
+ togglePaymentMethod(method)} + data-testid={`checkbox-payment-method-${method}`} + /> + +
+ ))} +
+
+ + {/* Options de colonnes */} +
+ +
+
+ setIncludeHT(!!checked)} + data-testid="checkbox-include-ht" + /> + +
+
+ setIncludeTTC(!!checked)} + data-testid="checkbox-include-ttc" + /> + +
+
+
+
+ + + + + +
+
); } diff --git a/package-lock.json b/package-lock.json index b28f359..0adb9fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "vaul": "^1.1.2", "wouter": "^3.3.5", "ws": "^8.18.0", + "xlsx": "^0.18.5", "zod": "^3.24.2", "zod-validation-error": "^3.4.0" }, @@ -3941,6 +3942,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", @@ -4277,6 +4287,19 @@ ], "license": "CC-BY-4.0" }, + "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", @@ -4360,6 +4383,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", @@ -4454,6 +4486,18 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "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", @@ -5944,6 +5988,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", @@ -8408,6 +8461,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/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -9482,6 +9547,24 @@ "node": "^16.13.0 || >=18.0.0" } }, + "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.7.1", "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.7.1.tgz", @@ -9608,6 +9691,27 @@ } } }, + "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/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 3dd692d..7acdf08 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "vaul": "^1.1.2", "wouter": "^3.3.5", "ws": "^8.18.0", + "xlsx": "^0.18.5", "zod": "^3.24.2", "zod-validation-error": "^3.4.0" }, diff --git a/server/routes.ts b/server/routes.ts index 72a58e7..9a950e5 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -500,6 +500,155 @@ export async function registerRoutes(app: Express): Promise { } }); + // Route pour exporter l'échéancier vers Excel + app.post('/api/payment-schedule/export', isAuthenticated, async (req: any, res) => { + try { + const userId = req.user?.claims?.sub || req.user?.id; + if (!userId) { + return res.status(401).json({ error: 'Utilisateur non authentifié' }); + } + + const user = await storage.getUser(userId); + if (!user || (user.role !== 'admin' && user.role !== 'directeur')) { + return res.status(403).json({ error: 'Accès refusé - Admin ou Directeur uniquement' }); + } + + // Récupérer les paramètres d'export + const { groupId, month, paymentMethods, includeHT, includeTTC } = req.body; + + // Validation + const groupIdSchema = z.coerce.number().int().positive(); + const monthSchema = z.string().regex(/^\d{4}-\d{2}$/); + const paymentMethodsSchema = z.array(z.string()).min(1); + + const validatedGroupId = groupIdSchema.parse(groupId); + const validatedMonth = monthSchema.parse(month); + const validatedPaymentMethods = paymentMethodsSchema.parse(paymentMethods); + + // Vérifier l'autorisation pour les directeurs + if (user.role === 'directeur') { + const userGroupIds = await storage.getUserGroups(user.id); + if (!userGroupIds.includes(validatedGroupId)) { + return res.status(403).json({ error: 'Accès refusé - Vous ne pouvez accéder qu\'aux données de votre groupe' }); + } + } + + // Récupérer le groupe + const group = await storage.getGroup(validatedGroupId); + if (!group) { + return res.status(404).json({ error: 'Groupe non trouvé' }); + } + + // Récupérer toutes les livraisons du groupe avec échéance + const allDeliveries = await storage.getDeliveries(); + const groupDeliveries = allDeliveries.filter((d: any) => + d.groupId === validatedGroupId && + d.invoiceReference && + d.dueDate + ); + + // Filtrer par mois + const { parseISO, startOfMonth, endOfMonth, isWithinInterval, format } = await import('date-fns'); + const { fr } = await import('date-fns/locale'); + const monthStart = parseISO(`${validatedMonth}-01`); + const monthEnd = endOfMonth(monthStart); + + const filteredDeliveries = groupDeliveries.filter((d: any) => { + try { + const dueDate = new Date(d.dueDate); + return isWithinInterval(dueDate, { start: monthStart, end: monthEnd }); + } catch { + return false; + } + }); + + // Récupérer les fournisseurs + const allSuppliers = await storage.getSuppliers(); + const supplierMap = new Map(allSuppliers.map((s: any) => [s.id, s])); + + // Filtrer par modes de paiement sélectionnés + const schedules = filteredDeliveries + .map((delivery: any) => { + const supplier = supplierMap.get(delivery.supplierId); + return { + dueDate: new Date(delivery.dueDate), + supplierName: supplier?.name || 'Fournisseur inconnu', + invoiceReference: delivery.invoiceReference, + paymentMethod: supplier?.paymentMethod || 'Non défini', + amountHT: delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0, + amountTTC: delivery.invoiceAmountTTC ? parseFloat(delivery.invoiceAmountTTC) : 0, + }; + }) + .filter((schedule: any) => validatedPaymentMethods.includes(schedule.paymentMethod)) + .sort((a: any, b: any) => a.dueDate.getTime() - b.dueDate.getTime()); + + // Générer le fichier Excel + const XLSX = await import('xlsx'); + const workbook = XLSX.utils.book_new(); + + // Préparer les données pour Excel + const headers = ['Date d\'échéance', 'Fournisseur', 'Facture', 'Mode de paiement']; + if (includeHT) headers.push('Montant HT'); + if (includeTTC) headers.push('Montant TTC'); + + const data = schedules.map((schedule: any) => { + const row: any = { + 'Date d\'échéance': format(schedule.dueDate, 'dd/MM/yyyy', { locale: fr }), + 'Fournisseur': schedule.supplierName, + 'Facture': schedule.invoiceReference, + 'Mode de paiement': schedule.paymentMethod, + }; + if (includeHT) row['Montant HT'] = schedule.amountHT; + if (includeTTC) row['Montant TTC'] = schedule.amountTTC; + return row; + }); + + // Calculer les totaux + const totalHT = schedules.reduce((sum: number, s: any) => sum + s.amountHT, 0); + const totalTTC = schedules.reduce((sum: number, s: any) => sum + s.amountTTC, 0); + + // Ajouter une ligne de total + const totalRow: any = { + 'Date d\'échéance': '', + 'Fournisseur': '', + 'Facture': '', + 'Mode de paiement': 'TOTAL', + }; + if (includeHT) totalRow['Montant HT'] = totalHT; + if (includeTTC) totalRow['Montant TTC'] = totalTTC; + data.push(totalRow); + + // Créer la feuille Excel + const worksheet = XLSX.utils.json_to_sheet(data, { header: headers }); + + // Définir la largeur des colonnes + worksheet['!cols'] = [ + { wch: 15 }, // Date + { wch: 25 }, // Fournisseur + { wch: 15 }, // Facture + { wch: 18 }, // Mode de paiement + { wch: 12 }, // Montant HT + { wch: 12 }, // Montant TTC + ]; + + XLSX.utils.book_append_sheet(workbook, worksheet, 'Échéancier'); + + // Générer le buffer Excel + const excelBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }); + + // Envoyer le fichier + res.setHeader('Content-Disposition', `attachment; filename="echeancier_${validatedMonth}.xlsx"`); + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.send(excelBuffer); + + console.log(`📊 Export Excel généré: ${schedules.length} échéances pour ${group.name}`); + + } catch (error: any) { + console.error('❌ Erreur export Excel:', error); + res.status(500).json({ error: 'Erreur lors de l\'export', details: error.message }); + } + }); + // Route BAP pour envoi webhook n8n app.post('/api/bap/send-webhook', isAuthenticated, async (req: any, res) => { try { From a9de7d79518225e0f128a132eeda156080ab709d Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Tue, 4 Nov 2025 11:55:21 +0000 Subject: [PATCH 3/3] Improve payment schedule display with a more compact and informative table Refactor the payment schedule UI to use a shadcn table, replacing bulky cards with columns for Date, Supplier, Invoice, Payment Method, HT, and TTC. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1e4433b5-47ab-464c-b663-fea2e53367dd Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 7093fbbd-4f49-4ee5-ad30-a580e8670fd8 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/1e4433b5-47ab-464c-b663-fea2e53367dd/GjEDOGr --- replit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/replit.md b/replit.md index 5a1a166..b6a0a0f 100644 --- a/replit.md +++ b/replit.md @@ -40,7 +40,7 @@ The platform is built with a clear separation of concerns, utilizing a modern fu - **Reporting:** Statistical reporting, including monthly summaries of deliveries. - **UI Harmonization:** All detail modals across modules (orders, deliveries, calendar) use a unified component for consistent information display. - **Analyse Vente (Sales Analysis):** A dedicated module for admins and directors that displays a configurable external URL/IP in an iframe. Configuration is managed via a dedicated tab in the Utilities page, allowing administrators to set or update the sales analysis dashboard URL. The menu is visible only to admin and directeur roles, ensuring proper access control. Includes intelligent error handling with informative messages when the URL is not configured. Features a refresh button to force complete iframe reloads. -- **Échéancier des Paiements (Payment Schedule):** A comprehensive payment deadline tracking system with hybrid architecture for optimal performance. **Architecture Refactorization (November 2025):** Due dates are now stored locally in deliveries table (dueDate column) as the single source of truth, retrieved once during invoice verification from NocoDB and cached locally for fast queries. Features include: role-based access (admin/director), group-based filtering with selectedStoreId, intelligent fallback to NocoDB for historical deliveries without cached dates, automatic date normalization handling multiple formats (ISO, French DD/MM/YYYY, American MM/DD/YYYY), monthly aggregations with payment method breakdowns (Virement, Traite, Traite Magnétique, Chèque), Zod validation for security, and robust supplier name normalization. Requires configuration of nocodbDueDateColumnName in Groups settings and paymentMethod in Suppliers. **Data Flow:** Invoice verification → NocoDB (fetch dueDate) → deliveries (local storage) → payment-schedule (fast local reads with NocoDB fallback). **Security (November 2025)**: Implements strict authorization - directors can only access their assigned groups, preventing cross-group data access. +- **Échéancier des Paiements (Payment Schedule):** A comprehensive payment deadline tracking system with hybrid architecture for optimal performance. **Architecture Refactorization (November 2025):** Due dates are now stored locally in deliveries table (dueDate column) as the single source of truth, retrieved once during invoice verification from NocoDB and cached locally for fast queries. Features include: role-based access (admin/director), group-based filtering with selectedStoreId, intelligent fallback to NocoDB for historical deliveries without cached dates, automatic date normalization handling multiple formats (ISO, French DD/MM/YYYY, American MM/DD/YYYY), monthly aggregations with payment method breakdowns (Virement, Traite, Traite Magnétique, Chèque), Zod validation for security, and robust supplier name normalization. **UI Enhancement (November 2025):** Replaced bulky cards with compact shadcn table displaying Date, Supplier, Invoice, Payment Method, HT, and TTC columns for improved readability. **Excel Export (November 2025):** Modal-based export functionality with customizable payment method filters (multi-select checkboxes), HT/TTC amount selection options, automatic total calculations, and formatted French date output. Export generates professionally formatted XLSX files with proper column widths and totals row. Includes toast notifications for success/error feedback. Requires configuration of nocodbDueDateColumnName in Groups settings and paymentMethod in Suppliers. **Data Flow:** Invoice verification → NocoDB (fetch dueDate) → deliveries (local storage) → payment-schedule (fast local reads with NocoDB fallback). **Security (November 2025)**: Implements strict authorization - directors can only access their assigned groups, preventing cross-group data access. ## External Dependencies - **API Météo:** Visual Crossing API