'use client'; export const dynamic = 'force-dynamic'; import React, { useState, useEffect, useRef } from 'react'; import { Card } from 'primereact/card'; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import { Button } from 'primereact/button'; import { Tag } from 'primereact/tag'; import { Dialog } from 'primereact/dialog'; import { InputTextarea } from 'primereact/inputtextarea'; import { Toast } from 'primereact/toast'; import { Divider } from 'primereact/divider'; import { Badge } from 'primereact/badge'; import { Timeline } from 'primereact/timeline'; import { Dropdown } from 'primereact/dropdown'; import { Calendar } from 'primereact/calendar'; import { Checkbox } from 'primereact/checkbox'; import { ProgressBar } from 'primereact/progressbar'; import { Knob } from 'primereact/knob'; import { ActionButtonGroup, ViewButton, EditButton, DeleteButton, ActionButton } from '../../../../components/ui/ActionButton'; /** * Page Relances Automatiques Factures BTP Express * Gestion complète des relances clients avec workflows automatisés */ const RelancesFactures = () => { const [factures, setFactures] = useState([]); const [loading, setLoading] = useState(true); const [relanceDialog, setRelanceDialog] = useState(false); const [configDialog, setConfigDialog] = useState(false); const [selectedFactures, setSelectedFactures] = useState([]); const [selectedFacture, setSelectedFacture] = useState(null); const [messageRelance, setMessageRelance] = useState(''); const [typeRelance, setTypeRelance] = useState(''); const [dateRelance, setDateRelance] = useState(null); const [configRelances, setConfigRelances] = useState({}); const [metriques, setMetriques] = useState({}); const toast = useRef(null); const typesRelance = [ { label: 'Relance amiable 1', value: 'AMIABLE_1', delai: 30, ferme: false }, { label: 'Relance amiable 2', value: 'AMIABLE_2', delai: 45, ferme: false }, { label: 'Mise en demeure', value: 'MISE_EN_DEMEURE', delai: 60, ferme: true }, { label: 'Procédure contentieux', value: 'CONTENTIEUX', delai: 90, ferme: true } ]; const modeleMessages = { 'AMIABLE_1': `Madame, Monsieur, Nous vous informons que votre facture n°[NUMERO] d'un montant de [MONTANT]€ émise le [DATE_EMISSION] reste impayée à ce jour. Le délai de paiement étant dépassé, nous vous remercions de bien vouloir procéder au règlement dans les plus brefs délais. En cas d'oubli de votre part, vous pouvez procéder au paiement en ligne ou nous contacter. Cordialement, L'équipe BTP Express`, 'AMIABLE_2': `Madame, Monsieur, SECONDE RELANCE - Facture n°[NUMERO] Malgré notre précédent courrier, votre facture n°[NUMERO] d'un montant de [MONTANT]€ demeure impayée. Nous vous rappelons que le délai de paiement est largement dépassé. Pour éviter tout désagrément, nous vous demandons de régulariser votre situation sous 8 jours. En l'absence de règlement, nous nous verrons contraints d'engager une procédure de recouvrement. Cordialement, L'équipe BTP Express`, 'MISE_EN_DEMEURE': `MISE EN DEMEURE DE PAYER Facture n°[NUMERO] Madame, Monsieur, Nos précédentes relances étant restées sans effet, nous vous mettons en demeure de procéder au règlement de votre facture n°[NUMERO] d'un montant de [MONTANT]€. VOUS DISPOSEZ DE 8 JOURS pour régulariser votre situation. À défaut, nous engagerons contre vous une procédure de recouvrement contentieux qui entraînera des frais supplémentaires à votre charge. BTP Express` }; useEffect(() => { loadFactures(); loadConfiguration(); loadMetriques(); }, []); const loadFactures = async () => { try { setLoading(true); // Simulation factures avec relances const mockFactures = [ { id: 'FAC-2025-001', numero: 'FAC-2025-001', client: 'Claire Rousseau', montantTTC: 18000, dateEmission: '2024-12-15', dateEcheance: '2025-01-15', joursRetard: 15, statut: 'IMPAYEE', nbRelances: 1, derniereRelance: '2025-01-20', prochainNiveau: 'AMIABLE_2', risqueClient: 'FAIBLE', historique: [ { date: '2025-01-20', type: 'AMIABLE_1', canal: 'EMAIL', statut: 'ENVOYE' } ] }, { id: 'FAC-2025-002', numero: 'FAC-2025-002', client: 'Jean Dupont', montantTTC: 32000, dateEmission: '2024-11-30', dateEcheance: '2024-12-30', joursRetard: 31, statut: 'IMPAYEE', nbRelances: 2, derniereRelance: '2025-01-25', prochainNiveau: 'MISE_EN_DEMEURE', risqueClient: 'MOYEN', historique: [ { date: '2025-01-10', type: 'AMIABLE_1', canal: 'EMAIL', statut: 'ENVOYE' }, { date: '2025-01-25', type: 'AMIABLE_2', canal: 'EMAIL', statut: 'ENVOYE' } ] }, { id: 'FAC-2025-003', numero: 'FAC-2025-003', client: 'Sophie Martin', montantTTC: 8500, dateEmission: '2025-01-10', dateEcheance: '2025-02-10', joursRetard: -10, // Pas encore échue statut: 'EN_ATTENTE', nbRelances: 0, derniereRelance: null, prochainNiveau: 'AMIABLE_1', risqueClient: 'FAIBLE', historique: [] }, { id: 'FAC-2024-089', numero: 'FAC-2024-089', client: 'Michel Bernard', montantTTC: 45000, dateEmission: '2024-10-15', dateEcheance: '2024-11-15', joursRetard: 77, statut: 'CONTENTIEUX', nbRelances: 3, derniereRelance: '2025-01-15', prochainNiveau: 'CONTENTIEUX', risqueClient: 'FORT', historique: [ { date: '2024-12-01', type: 'AMIABLE_1', canal: 'EMAIL', statut: 'ENVOYE' }, { date: '2024-12-20', type: 'AMIABLE_2', canal: 'EMAIL', statut: 'ENVOYE' }, { date: '2025-01-15', type: 'MISE_EN_DEMEURE', canal: 'COURRIER', statut: 'ENVOYE' } ] } ]; setFactures(mockFactures); } catch (error) { console.error('Erreur chargement factures:', error); } finally { setLoading(false); } }; const loadConfiguration = () => { setConfigRelances({ delaiAmiable1: 30, delaiAmiable2: 45, delaiMiseEnDemeure: 60, automatique: true, canalPrioritaire: 'EMAIL', avecCopie: true, fraisRecouvrement: 40 }); }; const loadMetriques = () => { setMetriques({ montantEnAttente: 103500, nombreFacturesImpayees: 4, tauxRecouvrement: 87.5, delaiMoyenPaiement: 38, relancesEnvoyees: 12, montantRecouvre: 285000 }); }; const ouvrirRelance = (facture: any) => { setSelectedFacture(facture); setTypeRelance(facture.prochainNiveau); setMessageRelance(modeleMessages[facture.prochainNiveau as keyof typeof modeleMessages] || ''); setDateRelance(new Date()); setRelanceDialog(true); }; const envoyerRelance = async () => { if (!selectedFacture || !typeRelance) return; try { // Simulation envoi relance await new Promise(resolve => setTimeout(resolve, 1500)); const factureUpdated = { ...selectedFacture, nbRelances: selectedFacture.nbRelances + 1, derniereRelance: new Date().toISOString().split('T')[0], historique: [ ...selectedFacture.historique, { date: new Date().toISOString().split('T')[0], type: typeRelance, canal: 'EMAIL', statut: 'ENVOYE' } ] }; // Déterminer prochain niveau const indexActuel = typesRelance.findIndex(t => t.value === typeRelance); if (indexActuel < typesRelance.length - 1) { factureUpdated.prochainNiveau = typesRelance[indexActuel + 1].value; } // Mettre à jour la liste const facturesUpdated = factures.map(f => f.id === selectedFacture.id ? factureUpdated : f ); setFactures(facturesUpdated); toast.current?.show({ severity: 'success', summary: 'Relance envoyée', detail: `Relance ${typeRelance} envoyée à ${selectedFacture.client}`, life: 4000 }); setRelanceDialog(false); } catch (error) { toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible d\'envoyer la relance', life: 3000 }); } }; const envoyerRelancesGroupees = async () => { if (selectedFactures.length === 0) return; try { toast.current?.show({ severity: 'info', summary: 'Traitement en cours', detail: `Envoi de ${selectedFactures.length} relances...`, life: 3000 }); // Simulation envoi groupé await new Promise(resolve => setTimeout(resolve, 2000)); const facturesUpdated = factures.map(f => { if (selectedFactures.find(sf => sf.id === f.id)) { return { ...f, nbRelances: f.nbRelances + 1, derniereRelance: new Date().toISOString().split('T')[0] }; } return f; }); setFactures(facturesUpdated); setSelectedFactures([]); toast.current?.show({ severity: 'success', summary: 'Relances envoyées', detail: `${selectedFactures.length} relances envoyées avec succès`, life: 4000 }); } catch (error) { toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Erreur lors de l\'envoi groupé', life: 3000 }); } }; const retardBodyTemplate = (rowData: any) => { if (rowData.joursRetard <= 0) { return ; } let severity: "warning" | "danger" = 'warning'; if (rowData.joursRetard > 60) severity = 'danger'; else if (rowData.joursRetard > 30) severity = 'warning'; return (
{rowData.joursRetard > 60 && }
); }; const relancesBodyTemplate = (rowData: any) => { return (
2 ? 'danger' : 'warning'} /> {rowData.derniereRelance && ( {new Date(rowData.derniereRelance).toLocaleDateString('fr-FR')} )}
); }; const risqueBodyTemplate = (rowData: any) => { const config: Record = { 'FAIBLE': { color: 'success', icon: 'pi-check-circle' }, 'MOYEN': { color: 'warning', icon: 'pi-exclamation-triangle' }, 'FORT': { color: 'danger', icon: 'pi-times-circle' } }; const riskConfig = config[rowData.risqueClient as keyof typeof config]; return (
); }; const actionsBodyTemplate = (rowData: any) => { return ( ouvrirRelance(rowData)} disabled={rowData.joursRetard <= 0} /> {}} /> {}} /> ); }; const header = (
Gestion des Relances
); return (
{/* Métriques Relances */}
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(metriques.montantEnAttente)}
En attente
{metriques.nombreFacturesImpayees}
Factures impayées
Taux recouvrement
{metriques.delaiMoyenPaiement}j
Délai moyen
{metriques.relancesEnvoyees}
Relances ce mois
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(metriques.montantRecouvre)}
Recouvré ce mois
{/* Tableau factures */}
setSelectedFactures(e.value)} paginator rows={15} dataKey="id" header={header} emptyMessage="Aucune facture trouvée" responsiveLayout="scroll" > new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.montantTTC)} sortable style={{ minWidth: '120px' }} /> new Date(rowData.dateEcheance).toLocaleDateString('fr-FR')} sortable style={{ minWidth: '100px' }} />
{/* Dialog Relance */} setRelanceDialog(false)} > {selectedFacture && (
Client: {selectedFacture.client}
Montant: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(selectedFacture.montantTTC)}
Échéance: {new Date(selectedFacture.dateEcheance).toLocaleDateString('fr-FR')}
Retard:
Relances déjà envoyées: {selectedFacture.nbRelances}
{ setTypeRelance(e.value); setMessageRelance(modeleMessages[e.value as keyof typeof modeleMessages] || ''); }} placeholder="Sélectionnez le type" className="w-full" />
setDateRelance(e.value || null)} dateFormat="dd/mm/yy" showIcon className="w-full" />
setMessageRelance(e.target.value)} rows={12} className="w-full" placeholder="Rédigez votre message de relance..." />
)}
{/* Dialog Configuration */} setConfigDialog(false)} >
jours
jours
setConfigRelances({...configRelances, automatique: e.checked})} />
); }; export default RelancesFactures;