Initial commit
This commit is contained in:
715
app/(main)/factures/retard/page.tsx
Normal file
715
app/(main)/factures/retard/page.tsx
Normal file
@@ -0,0 +1,715 @@
|
||||
'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 dû: <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;
|
||||
Reference in New Issue
Block a user