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"
/>
+
+
+ Colonne Date d'Échéance
+ 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
+
+
+
+
+
+
+
+ {availableMonths.map(month => (
+
+ {month.label}
+
+ ))}
+
+
+
+
+
+ {/* 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/Suppliers.tsx b/client/src/pages/Suppliers.tsx
index 5e2b093..bea80f4 100644
--- a/client/src/pages/Suppliers.tsx
+++ b/client/src/pages/Suppliers.tsx
@@ -5,6 +5,13 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { useToast } from "@/hooks/use-toast";
import { apiRequest } from "@/lib/queryClient";
@@ -39,6 +46,7 @@ export default function Suppliers() {
hasDlc: false,
automaticReconciliation: false,
requiresControl: false,
+ paymentMethod: "",
});
const { data: suppliers = [], isLoading } = useQuery({
@@ -193,6 +201,7 @@ export default function Suppliers() {
hasDlc: false,
automaticReconciliation: false,
requiresControl: false,
+ paymentMethod: "",
});
};
@@ -230,6 +239,7 @@ export default function Suppliers() {
hasDlc: supplier.hasDlc || false,
automaticReconciliation: supplier.automaticReconciliation || false,
requiresControl: supplier.requiresControl || false,
+ paymentMethod: supplier.paymentMethod || "",
});
setShowEditModal(true);
};
@@ -399,6 +409,13 @@ export default function Suppliers() {
{supplier.phone}
)}
+ {supplier.paymentMethod && (
+
+
+
Mode de paiement:
+
{supplier.paymentMethod}
+
+ )}
{supplier.hasDlc && (
@@ -511,6 +528,25 @@ export default function Suppliers() {
/>
+
+ Mode de paiement
+ handleChange('paymentMethod', value)}
+ >
+
+
+
+
+ Aucun
+ Virement
+ Traite
+ Traite Magnétique
+ Chèque
+
+
+
+
{
}
});
+ // Route pour récupérer l'échéancier des paiements fournisseurs
+ app.get('/api/payment-schedule', 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' });
+ }
+
+ // Validation du groupId avec Zod
+ const groupIdSchema = z.coerce.number().int().positive();
+ const validation = groupIdSchema.safeParse(req.query.groupId);
+
+ if (!validation.success) {
+ return res.status(400).json({ error: 'groupId invalide - doit être un entier positif' });
+ }
+
+ const groupId = validation.data;
+
+ // Vérifier l'autorisation : directeur ne peut voir que ses groupes
+ if (user.role === 'directeur') {
+ const userGroupIds = await storage.getUserGroups(user.id);
+ if (!userGroupIds.includes(groupId)) {
+ 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 avec la configuration NocoDB
+ const group = await storage.getGroup(groupId);
+ if (!group || !group.nocodbConfigId || !group.nocodbTableId) {
+ return res.json({ schedules: [], message: 'Pas de configuration NocoDB pour ce groupe' });
+ }
+
+ // Récupérer la configuration NocoDB
+ const nocodbConfig = await storage.getNocodbConfig(group.nocodbConfigId);
+ if (!nocodbConfig) {
+ return res.json({ schedules: [], message: 'Configuration NocoDB non trouvée' });
+ }
+
+ // Vérifier que les colonnes nécessaires sont configurées
+ if (!group.nocodbDueDateColumnName || !group.invoiceColumnName || !group.nocodbAmountColumnName) {
+ return res.json({
+ schedules: [],
+ message: 'Configuration incomplète: colonnes date échéance, facture ou montant manquantes'
+ });
+ }
+
+ // Récupérer tous les enregistrements de NocoDB
+ const url = `${nocodbConfig.baseUrl}/api/v1/db/data/noco/${nocodbConfig.projectId}/${group.nocodbTableId}`;
+
+ console.log('📅 Récupération échéances NocoDB:', {
+ url,
+ groupId,
+ dueDateColumn: group.nocodbDueDateColumnName
+ });
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ "xc-token": nocodbConfig.apiToken,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ console.error('❌ Erreur NocoDB:', response.status, response.statusText);
+ return res.status(500).json({ error: 'Erreur lors de la récupération des données NocoDB' });
+ }
+
+ const data = await response.json();
+
+ // Récupérer tous les fournisseurs pour le mapping
+ const allSuppliers = await storage.getSuppliers();
+
+ // Fonction de normalisation pour comparer les noms de fournisseurs
+ const normalizeName = (name: string) => {
+ return name
+ .toLowerCase()
+ .trim()
+ .normalize("NFD") // Décompose les accents
+ .replace(/[\u0300-\u036f]/g, "") // Supprime les diacritiques
+ .replace(/\s+/g, ' '); // Normalise les espaces multiples
+ };
+
+ // Créer une map avec noms normalisés
+ const supplierMap = new Map(allSuppliers.map(s => [normalizeName(s.name), s]));
+
+ // Traiter les données et filtrer les enregistrements avec une date d'échéance
+ const schedules = (data.list || [])
+ .map((record: any) => {
+ const dueDate = record[group.nocodbDueDateColumnName!];
+ const invoiceRef = record[group.invoiceColumnName!];
+ const amount = record[group.nocodbAmountColumnName!];
+ const supplierName = record[group.nocodbSupplierColumnName || 'Fournisseur'];
+
+ if (!dueDate) return null;
+
+ // Chercher le fournisseur correspondant avec normalisation
+ const normalizedSupplierName = normalizeName(supplierName || '');
+ const supplier = supplierMap.get(normalizedSupplierName);
+
+ return {
+ id: record.Id || record.id,
+ invoiceReference: invoiceRef,
+ dueDate: dueDate,
+ amount: amount ? parseFloat(amount) : 0,
+ supplierName: supplierName,
+ paymentMethod: supplier?.paymentMethod || null,
+ groupId: group.id,
+ groupName: group.name
+ };
+ })
+ .filter((s: any) => s !== null);
+
+ res.json({ schedules });
+
+ } catch (error: any) {
+ console.error('❌ Erreur récupération échéances:', error);
+ res.status(500).json({ error: 'Erreur serveur', details: error.message });
+ }
+ });
+
// Route BAP pour envoi webhook n8n
app.post('/api/bap/send-webhook', isAuthenticated, async (req: any, res) => {
try {
diff --git a/shared/schema.ts b/shared/schema.ts
index 9ed9b97..5a5136a 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -58,6 +58,7 @@ export const groups = pgTable("groups", {
nocodbBlColumnName: varchar("nocodb_bl_column_name"), // Nom de la colonne BL dans leur table
nocodbAmountColumnName: varchar("nocodb_amount_column_name"), // Nom de la colonne montant dans leur table
nocodbSupplierColumnName: varchar("nocodb_supplier_column_name"), // Nom de la colonne fournisseur dans leur table
+ nocodbDueDateColumnName: varchar("nocodb_due_date_column_name"), // Nom de la colonne date d'échéance dans leur table
webhookUrl: varchar("webhook_url", { length: 500 }), // URL de webhook pour notifications par magasin
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
@@ -79,6 +80,7 @@ export const suppliers = pgTable("suppliers", {
contact: varchar("contact"),
phone: varchar("phone"),
hasDlc: boolean("has_dlc").default(false), // Coche DLC pour la gestion DLC
+ paymentMethod: varchar("payment_method"), // Mode de paiement: Virement, Traite, Traite Magnétique, Chèque
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
automaticReconciliation: boolean("automatic_reconciliation").default(false), // Rapprochement automatique BL/Factures