diff --git a/attached_assets/image_1761903272202.png b/attached_assets/image_1761903272202.png new file mode 100644 index 0000000..720d613 Binary files /dev/null and b/attached_assets/image_1761903272202.png differ diff --git a/client/src/components/RouterProduction.tsx b/client/src/components/RouterProduction.tsx index f65bd7f..421731e 100644 --- a/client/src/components/RouterProduction.tsx +++ b/client/src/components/RouterProduction.tsx @@ -25,6 +25,7 @@ import Avoirs from "@/pages/Avoirs"; import WeatherSettings from "@/pages/WeatherSettings"; import Analytics from "@/pages/Analytics"; import SalesAnalysisPage from "@/pages/SalesAnalysisPage"; +import PaymentSchedulePage from "@/pages/PaymentSchedulePage"; import Layout from "@/components/Layout"; function RouterProduction() { @@ -106,6 +107,7 @@ function RouterProduction() { + {/* Routes de compatibilité - redirection vers utilities */} diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 44c798b..d141104 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -35,7 +35,8 @@ import { FileUp, Send, Receipt, - TrendingUp + TrendingUp, + CreditCard } from "lucide-react"; export default function Sidebar() { @@ -270,6 +271,12 @@ export default function Sidebar() { icon: TrendingUp, roles: ["admin", "directeur"] }, + { + path: "/payment-schedule", + label: "Échéance", + icon: CreditCard, + roles: ["admin", "directeur"] + }, { path: "/avoirs", label: "Avoirs", diff --git a/client/src/pages/Groups.tsx b/client/src/pages/Groups.tsx index c99de45..8b7b776 100644 --- a/client/src/pages/Groups.tsx +++ b/client/src/pages/Groups.tsx @@ -51,6 +51,7 @@ export default function Groups() { nocodbBlColumnName: "", nocodbAmountColumnName: "", nocodbSupplierColumnName: "", + nocodbDueDateColumnName: "", webhookUrl: "", }); @@ -101,9 +102,13 @@ export default function Groups() { name: "", color: "#1976D2", nocodbConfigId: "", - nocodbTableId: "", nocodbTableName: "", - invoiceColumnName: "Ref Facture", + invoiceColumnName: "", + nocodbBlColumnName: "", + nocodbAmountColumnName: "", + nocodbSupplierColumnName: "", + nocodbDueDateColumnName: "", + webhookUrl: "", }); }, onError: (error) => { @@ -142,9 +147,13 @@ export default function Groups() { name: "", color: "#1976D2", nocodbConfigId: "", - nocodbTableId: "", nocodbTableName: "", - invoiceColumnName: "Ref Facture", + invoiceColumnName: "", + nocodbBlColumnName: "", + nocodbAmountColumnName: "", + nocodbSupplierColumnName: "", + nocodbDueDateColumnName: "", + webhookUrl: "", }); }, onError: (error) => { @@ -251,6 +260,7 @@ export default function Groups() { nocodbBlColumnName: "", nocodbAmountColumnName: "", nocodbSupplierColumnName: "", + nocodbDueDateColumnName: "", webhookUrl: "", }); setShowCreateModal(true); @@ -267,6 +277,7 @@ export default function Groups() { nocodbBlColumnName: group.nocodbBlColumnName || "", nocodbAmountColumnName: group.nocodbAmountColumnName || "", nocodbSupplierColumnName: group.nocodbSupplierColumnName || "", + nocodbDueDateColumnName: group.nocodbDueDateColumnName || "", webhookUrl: group.webhookUrl || "", }); setShowEditModal(true); @@ -631,6 +642,16 @@ export default function Groups() { placeholder="ex: Fournisseur" /> + +
+ + handleChange('nocodbDueDateColumnName', e.target.value)} + placeholder="ex: Date_Echeance" + /> +
@@ -693,6 +714,7 @@ export default function Groups() { nocodbBlColumnName: "", nocodbAmountColumnName: "", nocodbSupplierColumnName: "", + nocodbDueDateColumnName: "", webhookUrl: "", }); }} diff --git a/client/src/pages/PaymentSchedulePage.tsx b/client/src/pages/PaymentSchedulePage.tsx new file mode 100644 index 0000000..6334206 --- /dev/null +++ b/client/src/pages/PaymentSchedulePage.tsx @@ -0,0 +1,288 @@ +import { useState, useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useStore } from "@/contexts/StoreContext"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Loader2, Calendar, DollarSign, Building, CreditCard, AlertCircle } from "lucide-react"; +import { format, parseISO, startOfMonth, endOfMonth, eachMonthOfInterval, isWithinInterval } from "date-fns"; +import { fr } from "date-fns/locale"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface PaymentSchedule { + id: string | number; + invoiceReference: string; + dueDate: string; + amount: number; + supplierName: string; + paymentMethod: string | null; + groupId: number; + groupName: string; +} + +interface PaymentScheduleResponse { + schedules: PaymentSchedule[]; + message?: string; +} + +export default function PaymentSchedulePage() { + const { selectedStoreId } = useStore(); + const [selectedMonth, setSelectedMonth] = useState(() => { + const now = new Date(); + return format(now, 'yyyy-MM'); + }); + + // Récupérer les échéances + const { data, isLoading, error } = useQuery({ + queryKey: ['/api/payment-schedule', selectedStoreId], + queryFn: async () => { + const response = await fetch(`/api/payment-schedule?groupId=${selectedStoreId}`, { + credentials: 'include' + }); + if (!response.ok) { + throw new Error(`Erreur ${response.status}: ${response.statusText}`); + } + return response.json(); + }, + enabled: !!selectedStoreId, + }); + + // Calculer les mois disponibles (12 derniers mois + 12 prochains mois) + const availableMonths = useMemo(() => { + const now = new Date(); + const startDate = new Date(now.getFullYear(), now.getMonth() - 12, 1); + const endDate = new Date(now.getFullYear(), now.getMonth() + 12, 1); + + return eachMonthOfInterval({ start: startDate, end: endDate }) + .map(date => ({ + value: format(date, 'yyyy-MM'), + label: format(date, 'MMMM yyyy', { locale: fr }) + })); + }, []); + + // Filtrer les échéances par mois sélectionné + const filteredSchedules = useMemo(() => { + if (!data?.schedules) return []; + + const monthStart = parseISO(`${selectedMonth}-01`); + const monthEnd = endOfMonth(monthStart); + + return data.schedules + .filter(schedule => { + try { + const dueDate = parseISO(schedule.dueDate); + return isWithinInterval(dueDate, { start: monthStart, end: monthEnd }); + } catch { + return false; + } + }) + .sort((a, b) => { + const dateA = parseISO(a.dueDate); + const dateB = parseISO(b.dueDate); + return dateA.getTime() - dateB.getTime(); + }); + }, [data?.schedules, selectedMonth]); + + // Calculer le total du mois + const monthTotal = useMemo(() => { + return filteredSchedules.reduce((sum, schedule) => sum + schedule.amount, 0); + }, [filteredSchedules]); + + // Grouper par mode de paiement + const paymentMethodTotals = useMemo(() => { + const totals: Record = {}; + filteredSchedules.forEach(schedule => { + const method = schedule.paymentMethod || 'Non défini'; + totals[method] = (totals[method] || 0) + schedule.amount; + }); + return totals; + }, [filteredSchedules]); + + if (!selectedStoreId) { + return ( +
+ + + + Sélectionner un magasin + + Veuillez sélectionner un magasin pour afficher l'échéancier + + + +
+ ); + } + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ + + + Erreur + + Une erreur est survenue lors du chargement des échéances + + + +
+ ); + } + + if (data?.message) { + return ( +
+ + + + Configuration requise + {data.message} + + +
+ ); + } + + return ( +
+ {/* En-tête */} +
+
+

Échéancier des Paiements

+

Gestion des échéances fournisseurs

+
+
+ +
+
+ + {/* Statistiques du mois */} +
+ + + Total du Mois + + + +
+ {monthTotal.toFixed(2)} € +
+

+ {filteredSchedules.length} échéance{filteredSchedules.length > 1 ? 's' : ''} +

+
+
+ + + + Modes de Paiement + + + +
+ {Object.entries(paymentMethodTotals).map(([method, total]) => ( +
+ {method}: + {total.toFixed(2)} € +
+ ))} +
+
+
+ + + + Période + + + +
+ {format(parseISO(`${selectedMonth}-01`), 'MMMM', { locale: fr })} +
+

+ {format(parseISO(`${selectedMonth}-01`), 'yyyy', { locale: fr })} +

+
+
+
+ + {/* Liste des échéances */} + + + Échéances du Mois + + Liste détaillée des paiements à effectuer pour le mois sélectionné + + + + {filteredSchedules.length === 0 ? ( +
+ Aucune échéance pour ce mois +
+ ) : ( +
+ {filteredSchedules.map((schedule, index) => ( +
+
+
+ + + {format(parseISO(schedule.dueDate), 'dd/MM/yyyy', { locale: fr })} + +
+
+
+ + {schedule.supplierName} +
+
Facture: {schedule.invoiceReference}
+ {schedule.paymentMethod && ( +
+ + {schedule.paymentMethod} +
+ )} +
+
+
+
+ {schedule.amount.toFixed(2)} € +
+
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/client/src/pages/SalesAnalysisPage.tsx b/client/src/pages/SalesAnalysisPage.tsx index 1b64665..c942eb1 100644 --- a/client/src/pages/SalesAnalysisPage.tsx +++ b/client/src/pages/SalesAnalysisPage.tsx @@ -1,6 +1,7 @@ +import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Loader2, TrendingUp, AlertCircle } from "lucide-react"; +import { Loader2, TrendingUp, AlertCircle, RefreshCw } from "lucide-react"; import { Link } from "wouter"; import { Button } from "@/components/ui/button"; @@ -12,10 +13,18 @@ interface UtilitiesConfig { } export default function SalesAnalysisPage() { + // State pour forcer le rechargement de l'iframe + const [iframeKey, setIframeKey] = useState(0); + // Récupérer l'URL configurée const { data: config, isLoading } = useQuery({ queryKey: ['/api/utilities'] }); + + // Fonction pour recharger l'iframe + const handleRefresh = () => { + setIframeKey(prev => prev + 1); + }; if (isLoading) { return ( @@ -52,8 +61,24 @@ export default function SalesAnalysisPage() { } return ( -
+
+ {/* Bouton de rechargement flottant */} +
+ +
+ + {/* iframe avec clé pour forcer le rechargement */}