Files
btpxpress-frontend/app/(main)/factures/retard/page.tsx
2025-10-01 01:39:07 +00:00

715 lines
33 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
import { Card } from 'primereact/card';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Tag } from 'primereact/tag';
import { Dialog } from 'primereact/dialog';
import { Calendar } from 'primereact/calendar';
import { InputTextarea } from 'primereact/inputtextarea';
import { Dropdown } from 'primereact/dropdown';
import { Chip } from 'primereact/chip';
import { ProgressBar } from 'primereact/progressbar';
import { factureService } from '../../../../services/api';
import { formatDate, formatCurrency } from '../../../../utils/formatters';
import type { Facture } from '../../../../types/btp';
import factureActionsService from '../../../../services/factureActionsService';
const FacturesRetardPage = () => {
const [factures, setFactures] = useState<Facture[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [selectedFactures, setSelectedFactures] = useState<Facture[]>([]);
const [actionDialog, setActionDialog] = useState(false);
const [selectedFacture, setSelectedFacture] = useState<Facture | null>(null);
const [actionType, setActionType] = useState<'urgent_reminder' | 'legal_notice' | 'suspend_client'>('urgent_reminder');
const [urgentReminderData, setUrgentReminderData] = useState({
method: 'RECOMMANDE',
deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
message: '',
penaltyRate: 0
});
const [legalNoticeData, setLegalNoticeData] = useState({
type: 'MISE_EN_DEMEURE',
deadline: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000),
lawyer: '',
content: ''
});
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<Facture[]>>(null);
const reminderMethods = [
{ label: 'Courrier recommandé', value: 'RECOMMANDE' },
{ label: 'Huissier', value: 'HUISSIER' },
{ label: 'Avocat', value: 'AVOCAT' },
{ label: 'Email urgent', value: 'EMAIL_URGENT' },
{ label: 'Téléphone + Courrier', value: 'TELEPHONE_COURRIER' }
];
const legalNoticeTypes = [
{ label: 'Mise en demeure', value: 'MISE_EN_DEMEURE' },
{ label: 'Commandement de payer', value: 'COMMANDEMENT' },
{ label: 'Assignation en référé', value: 'REFERE' },
{ label: 'Procédure simplifiée', value: 'PROCEDURE_SIMPLIFIEE' }
];
useEffect(() => {
loadFactures();
}, []);
const loadFactures = async () => {
try {
setLoading(true);
const data = await factureService.getAll();
// Filtrer les factures en retard (échéance dépassée + statut EN_RETARD)
const facturesEnRetard = data.filter(facture => {
const today = new Date();
const echeanceDate = new Date(facture.dateEcheance);
return (facture.statut === 'EN_RETARD' ||
(echeanceDate < today && facture.statut !== 'PAYEE' && !facture.datePaiement));
});
setFactures(facturesEnRetard);
} catch (error) {
console.error('Erreur lors du chargement des factures:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les factures en retard',
life: 3000
});
} finally {
setLoading(false);
}
};
const getDaysOverdue = (dateEcheance: string | Date) => {
const today = new Date();
const dueDate = new Date(dateEcheance);
const diffTime = today.getTime() - dueDate.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return Math.max(0, diffDays);
};
const getOverdueCategory = (dateEcheance: string | Date) => {
const days = getDaysOverdue(dateEcheance);
if (days <= 15) return { category: 'RETARD RÉCENT', color: 'orange', severity: 'warning' as const, priority: 'MOYENNE' };
if (days <= 45) return { category: 'RETARD IMPORTANT', color: 'red', severity: 'danger' as const, priority: 'ÉLEVÉE' };
return { category: 'RETARD CRITIQUE', color: 'darkred', severity: 'danger' as const, priority: 'URGENTE' };
};
const calculateInterestPenalty = (amount: number, days: number, rate: number = 10) => {
// Calcul des pénalités de retard (taux annuel)
const dailyRate = rate / 365 / 100;
return amount * dailyRate * days;
};
const sendUrgentReminder = (facture: Facture) => {
setSelectedFacture(facture);
setActionType('urgent_reminder');
const days = getDaysOverdue(facture.dateEcheance);
setUrgentReminderData({
method: 'RECOMMANDE',
deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
message: `RELANCE URGENTE\n\nMadame, Monsieur,\n\nMalgré nos précédents rappels, nous constatons que la facture ${facture.numero} d'un montant de ${formatCurrency(facture.montantTTC || 0)} n'a toujours pas été réglée.\n\nCette facture est en retard de ${days} jours depuis son échéance du ${formatDate(facture.dateEcheance)}.\n\nEn l'absence de règlement sous 8 jours, nous nous verrons contraints d'engager une procédure de recouvrement contentieux.\n\nPénalités de retard applicables: ${formatCurrency(calculateInterestPenalty(facture.montantTTC || 0, days))}\n\nNous vous demandons de bien vouloir régulariser cette situation dans les plus brefs délais.`,
penaltyRate: 10
});
setActionDialog(true);
};
const issueLegalNotice = (facture: Facture) => {
setSelectedFacture(facture);
setActionType('legal_notice');
setLegalNoticeData({
type: 'MISE_EN_DEMEURE',
deadline: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000),
lawyer: '',
content: `MISE EN DEMEURE DE PAYER\n\nPar la présente, nous vous mettons en demeure de procéder au règlement de la facture ${facture.numero} d'un montant de ${formatCurrency(facture.montantTTC || 0)}, échue depuis le ${formatDate(facture.dateEcheance)}.\n\nVous disposez d'un délai de 15 jours à compter de la réception de cette mise en demeure pour procéder au règlement.\n\nÀ défaut, nous nous réservons le droit d'engager contre vous une procédure judiciaire en recouvrement de créances, sans autre préavis.`
});
setActionDialog(true);
};
const suspendClient = (facture: Facture) => {
setSelectedFacture(facture);
setActionType('suspend_client');
setActionDialog(true);
};
const handleAction = async () => {
if (!selectedFacture) return;
try {
let message = '';
switch (actionType) {
case 'urgent_reminder':
await factureActionsService.sendUrgentRelance(
selectedFacture.id,
urgentReminderData.message
);
message = 'Relance urgente envoyée avec succès';
break;
case 'legal_notice':
await factureActionsService.sendMiseEnDemeure({
factureId: selectedFacture.id,
delaiPaiement: legalNoticeData.delai,
mentionsLegales: legalNoticeData.mentions,
fraisDossier: legalNoticeData.frais
});
message = 'Mise en demeure émise avec succès';
break;
case 'suspend_client':
await factureActionsService.suspendClient({
clientId: selectedFacture.client.id,
motif: 'Factures impayées en retard',
temporaire: false
});
message = 'Client suspendu pour impayés';
break;
}
setActionDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: `${message} (simulation)`,
life: 3000
});
} catch (error) {
console.error('Erreur lors de l\'action:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible d\'effectuer l\'action',
life: 3000
});
}
};
const exportCSV = () => {
dt.current?.exportCSV();
};
const bulkLegalAction = async () => {
if (selectedFactures.length === 0) {
toast.current?.show({
severity: 'warn',
summary: 'Attention',
detail: 'Veuillez sélectionner au moins une facture',
life: 3000
});
return;
}
// Simulation de procédure contentieuse en lot
console.log('Procédure contentieuse en lot pour', selectedFactures.length, 'factures');
setSelectedFactures([]);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: `Procédure contentieuse engagée pour ${selectedFactures.length} facture(s) (simulation)`,
life: 3000
});
};
const generateOverdueAnalysis = () => {
const totalOverdue = factures.reduce((sum, f) => sum + (f.montantTTC || 0), 0);
const totalPenalties = factures.reduce((sum, f) => sum + calculateInterestPenalty(f.montantTTC || 0, getDaysOverdue(f.dateEcheance)), 0);
const criticalOverdue = factures.filter(f => getDaysOverdue(f.dateEcheance) > 45);
const recentOverdue = factures.filter(f => getDaysOverdue(f.dateEcheance) <= 15);
const report = `
=== ANALYSE FACTURES EN RETARD ===
Date du rapport: ${new Date().toLocaleDateString('fr-FR')}
SITUATION CRITIQUE:
- Nombre total de factures en retard: ${factures.length}
- Montant total en retard: ${formatCurrency(totalOverdue)}
- Pénalités de retard calculées: ${formatCurrency(totalPenalties)}
- Impact sur la trésorerie: ${formatCurrency(totalOverdue + totalPenalties)}
RÉPARTITION PAR GRAVITÉ:
- Retard récent (≤15 jours): ${recentOverdue.length} factures, ${formatCurrency(recentOverdue.reduce((sum, f) => sum + (f.montantTTC || 0), 0))}
- Retard critique (>45 jours): ${criticalOverdue.length} factures, ${formatCurrency(criticalOverdue.reduce((sum, f) => sum + (f.montantTTC || 0), 0))}
FACTURES PRIORITAIRES (>45 jours):
${criticalOverdue.map(f => {
const days = getDaysOverdue(f.dateEcheance);
const penalty = calculateInterestPenalty(f.montantTTC || 0, days);
return `
- ${f.numero} - ${f.objet}
Client: ${f.client ? `${f.client.prenom} ${f.client.nom}` : 'N/A'}
Montant: ${formatCurrency(f.montantTTC || 0)}
Retard: ${days} jours
Pénalités: ${formatCurrency(penalty)}
Total dû: ${formatCurrency((f.montantTTC || 0) + penalty)}
`;
}).join('')}
CLIENTS À RISQUE:
${getHighRiskClients()}
ACTIONS RECOMMANDÉES:
- Mise en demeure immédiate pour les retards >45 jours
- Suspension des prestations pour les clients récidivistes
- Engagement de procédures contentieuses si nécessaire
- Révision des conditions de paiement accordées
- Application systématique des pénalités de retard
`;
const blob = new Blob([report], { type: 'text/plain;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `analyse_retards_critiques_${new Date().toISOString().split('T')[0]}.txt`;
link.click();
toast.current?.show({
severity: 'success',
summary: 'Analyse générée',
detail: 'Le rapport d\'analyse a été téléchargé',
life: 3000
});
};
const getHighRiskClients = () => {
const clientStats = {};
factures.forEach(f => {
if (f.client) {
const clientKey = `${f.client.prenom} ${f.client.nom}`;
if (!clientStats[clientKey]) {
clientStats[clientKey] = { count: 0, value: 0, maxDays: 0, totalPenalties: 0 };
}
const days = getDaysOverdue(f.dateEcheance);
clientStats[clientKey].count++;
clientStats[clientKey].value += f.montantTTC || 0;
clientStats[clientKey].maxDays = Math.max(clientStats[clientKey].maxDays, days);
clientStats[clientKey].totalPenalties += calculateInterestPenalty(f.montantTTC || 0, days);
}
});
return Object.entries(clientStats)
.sort((a: [string, any], b: [string, any]) => b[1].value - a[1].value)
.slice(0, 5)
.map(([client, data]: [string, any]) => `- ${client}: ${data.count} factures, ${formatCurrency(data.value)}, retard max: ${data.maxDays} jours, pénalités: ${formatCurrency(data.totalPenalties)}`)
.join('\n');
};
const leftToolbarTemplate = () => {
return (
<div className="my-2 flex gap-2">
<h5 className="m-0 flex align-items-center text-red-700">
<i className="pi pi-exclamation-triangle mr-2"></i>
Factures en retard ({factures.length})
</h5>
<Chip
label={`Montant en retard: ${formatCurrency(factures.reduce((sum, f) => sum + (f.montantTTC || 0), 0))}`}
className="bg-red-100 text-red-800"
/>
<Chip
label={`Pénalités: ${formatCurrency(factures.reduce((sum, f) => sum + calculateInterestPenalty(f.montantTTC || 0, getDaysOverdue(f.dateEcheance)), 0))}`}
className="bg-red-200 text-red-900"
/>
<Button
label="Contentieux groupé"
icon="pi pi-exclamation-triangle"
severity="danger"
size="small"
onClick={bulkLegalAction}
disabled={selectedFactures.length === 0}
/>
<Button
label="Analyse des retards"
icon="pi pi-chart-line"
severity="danger"
size="small"
onClick={generateOverdueAnalysis}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
onClick={exportCSV}
/>
);
};
const actionBodyTemplate = (rowData: Facture) => {
const category = getOverdueCategory(rowData.dateEcheance);
const isCritical = category.category === 'RETARD CRITIQUE';
return (
<div className="flex gap-1">
<Button
icon="pi pi-send"
rounded
severity="warning"
size="small"
tooltip="Relance urgente"
onClick={() => sendUrgentReminder(rowData)}
/>
{isCritical && (
<Button
icon="pi pi-ban"
rounded
severity="danger"
size="small"
tooltip="Mise en demeure"
onClick={() => issueLegalNotice(rowData)}
/>
)}
<Button
icon="pi pi-user-minus"
rounded
severity="secondary"
size="small"
tooltip="Suspendre le client"
onClick={() => suspendClient(rowData)}
/>
<Button
icon="pi pi-eye"
rounded
severity="help"
size="small"
tooltip="Voir détails"
onClick={() => {
toast.current?.show({
severity: 'info',
summary: 'Info',
detail: `Détails de la facture ${rowData.numero}`,
life: 3000
});
}}
/>
</div>
);
};
const statusBodyTemplate = (rowData: Facture) => {
const category = getOverdueCategory(rowData.dateEcheance);
return (
<div className="flex align-items-center gap-2">
<Tag value="En retard" severity="danger" />
<Tag
value={category.category}
severity={category.severity}
/>
</div>
);
};
const overdueBodyTemplate = (rowData: Facture) => {
const days = getDaysOverdue(rowData.dateEcheance);
const category = getOverdueCategory(rowData.dateEcheance);
const penalty = calculateInterestPenalty(rowData.montantTTC || 0, days);
return (
<div className="text-red-700">
<div className="font-bold">{formatDate(rowData.dateEcheance)}</div>
<small className="text-red-600">
Retard de {days} jour{days > 1 ? 's' : ''}
</small>
<div className="mt-1">
<Chip
label={`Pénalités: ${formatCurrency(penalty)}`}
style={{ backgroundColor: category.color, color: 'white', fontSize: '0.7rem' }}
/>
</div>
</div>
);
};
const priorityBodyTemplate = (rowData: Facture) => {
const category = getOverdueCategory(rowData.dateEcheance);
const days = getDaysOverdue(rowData.dateEcheance);
const urgencyProgress = Math.min((days / 60) * 100, 100); // 60 jours = 100% urgent
return (
<div>
<div className="flex align-items-center gap-2 mb-1">
<Tag
value={category.priority}
severity={category.severity}
className="text-xs"
/>
</div>
<ProgressBar
value={urgencyProgress}
style={{ height: '6px' }}
color={category.color}
/>
<small className="text-600">{Math.round(urgencyProgress)}% critique</small>
</div>
);
};
const clientBodyTemplate = (rowData: Facture) => {
if (!rowData.client) return '';
return `${rowData.client.prenom} ${rowData.client.nom}`;
};
const amountBodyTemplate = (rowData: Facture) => {
const penalty = calculateInterestPenalty(rowData.montantTTC || 0, getDaysOverdue(rowData.dateEcheance));
const total = (rowData.montantTTC || 0) + penalty;
return (
<div>
<div className="font-bold">{formatCurrency(rowData.montantTTC || 0)}</div>
<small className="text-red-600">+ {formatCurrency(penalty)} pénalités</small>
<div className="font-bold text-red-700">{formatCurrency(total)}</div>
</div>
);
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Factures en retard - Recouvrement contentieux</h5>
<span className="block mt-2 md:mt-0 p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
</div>
);
const actionDialogFooter = (
<>
<Button
label="Annuler"
icon="pi pi-times"
text
onClick={() => setActionDialog(false)}
/>
<Button
label={
actionType === 'urgent_reminder' ? 'Envoyer la relance' :
actionType === 'legal_notice' ? 'Émettre la mise en demeure' :
'Suspendre le client'
}
icon={
actionType === 'urgent_reminder' ? 'pi pi-send' :
actionType === 'legal_notice' ? 'pi pi-ban' :
'pi pi-user-minus'
}
text
onClick={handleAction}
/>
</>
);
const getActionTitle = () => {
switch (actionType) {
case 'urgent_reminder': return 'Relance urgente';
case 'legal_notice': return 'Mise en demeure';
case 'suspend_client': return 'Suspension du client';
default: return 'Action';
}
};
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
<DataTable
ref={dt}
value={factures}
selection={selectedFactures}
onSelectionChange={(e) => setSelectedFactures(e.value)}
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
className="datatable-responsive"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} factures"
globalFilter={globalFilter}
emptyMessage="Aucune facture en retard trouvée."
header={header}
responsiveLayout="scroll"
loading={loading}
sortField="dateEcheance"
sortOrder={1}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="numero" header="Numéro" sortable headerStyle={{ minWidth: '10rem' }} />
<Column field="objet" header="Objet" sortable headerStyle={{ minWidth: '15rem' }} />
<Column field="client" header="Client" body={clientBodyTemplate} sortable headerStyle={{ minWidth: '12rem' }} />
<Column field="dateEcheance" header="Échéance / Retard" body={overdueBodyTemplate} sortable headerStyle={{ minWidth: '14rem' }} />
<Column field="montantTTC" header="Montant + Pénalités" body={amountBodyTemplate} sortable headerStyle={{ minWidth: '12rem' }} />
<Column field="priority" header="Priorité" body={priorityBodyTemplate} headerStyle={{ minWidth: '12rem' }} />
<Column field="statut" header="Statut" body={statusBodyTemplate} headerStyle={{ minWidth: '12rem' }} />
<Column body={actionBodyTemplate} headerStyle={{ minWidth: '15rem' }} />
</DataTable>
<Dialog
visible={actionDialog}
style={{ width: '700px' }}
header={getActionTitle()}
modal
className="p-fluid"
footer={actionDialogFooter}
onHide={() => setActionDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<p>
Facture: <strong>{selectedFacture?.numero} - {selectedFacture?.objet}</strong>
</p>
<p>
Client: <strong>{selectedFacture?.client ? `${selectedFacture.client.prenom} ${selectedFacture.client.nom}` : 'N/A'}</strong>
</p>
<p>
Montant initial: <strong>{selectedFacture ? formatCurrency(selectedFacture.montantTTC || 0) : ''}</strong>
</p>
{selectedFacture && (
<>
<p>
Retard: <strong>{getDaysOverdue(selectedFacture.dateEcheance)} jour(s)</strong>
</p>
<p>
Pénalités: <strong>{formatCurrency(calculateInterestPenalty(selectedFacture.montantTTC || 0, getDaysOverdue(selectedFacture.dateEcheance)))}</strong>
</p>
<p className="font-bold text-red-600">
Total : <strong>{formatCurrency((selectedFacture.montantTTC || 0) + calculateInterestPenalty(selectedFacture.montantTTC || 0, getDaysOverdue(selectedFacture.dateEcheance)))}</strong>
</p>
</>
)}
</div>
{actionType === 'urgent_reminder' && (
<>
<div className="field col-12 md:col-6">
<label htmlFor="reminderMethod">Méthode de relance</label>
<Dropdown
id="reminderMethod"
value={urgentReminderData.method}
options={reminderMethods}
onChange={(e) => setUrgentReminderData(prev => ({ ...prev, method: e.value }))}
placeholder="Sélectionnez la méthode"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="deadline">Délai de règlement</label>
<Calendar
id="deadline"
value={urgentReminderData.deadline}
onChange={(e) => setUrgentReminderData(prev => ({ ...prev, deadline: e.value || new Date() }))}
dateFormat="dd/mm/yy"
showIcon
minDate={new Date()}
/>
</div>
<div className="field col-12">
<label htmlFor="reminderMessage">Message de relance</label>
<InputTextarea
id="reminderMessage"
value={urgentReminderData.message}
onChange={(e) => setUrgentReminderData(prev => ({ ...prev, message: e.target.value }))}
rows={6}
placeholder="Message de relance urgente..."
/>
</div>
</>
)}
{actionType === 'legal_notice' && (
<>
<div className="field col-12 md:col-6">
<label htmlFor="legalType">Type de procédure</label>
<Dropdown
id="legalType"
value={legalNoticeData.type}
options={legalNoticeTypes}
onChange={(e) => setLegalNoticeData(prev => ({ ...prev, type: e.value }))}
placeholder="Sélectionnez le type"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="legalDeadline">Délai de mise en demeure</label>
<Calendar
id="legalDeadline"
value={legalNoticeData.deadline}
onChange={(e) => setLegalNoticeData(prev => ({ ...prev, deadline: e.value || new Date() }))}
dateFormat="dd/mm/yy"
showIcon
minDate={new Date()}
/>
</div>
<div className="field col-12">
<label htmlFor="lawyer">Avocat ou huissier</label>
<InputText
id="lawyer"
value={legalNoticeData.lawyer}
onChange={(e) => setLegalNoticeData(prev => ({ ...prev, lawyer: e.target.value }))}
placeholder="Nom du professionnel en charge"
/>
</div>
<div className="field col-12">
<label htmlFor="legalContent">Contenu de la mise en demeure</label>
<InputTextarea
id="legalContent"
value={legalNoticeData.content}
onChange={(e) => setLegalNoticeData(prev => ({ ...prev, content: e.target.value }))}
rows={6}
placeholder="Contenu de la mise en demeure..."
/>
</div>
</>
)}
{actionType === 'suspend_client' && (
<div className="field col-12">
<div className="bg-red-50 p-3 border-round">
<p className="text-red-700 font-bold">
<i className="pi pi-exclamation-triangle mr-2"></i>
Attention : Suspension du client
</p>
<p>
Cette action va suspendre ce client pour cause d'impayés. Les conséquences seront :
</p>
<ul className="text-red-600">
<li>Arrêt immédiat de tous les travaux en cours</li>
<li>Blocage de toute nouvelle commande</li>
<li>Notification automatique des équipes</li>
<li>Gel des livraisons de matériel</li>
</ul>
<p className="font-bold">
Confirmer la suspension de ce client ?
</p>
</div>
</div>
)}
</div>
</Dialog>
</Card>
</div>
</div>
);
};
export default FacturesRetardPage;