Initial commit
This commit is contained in:
610
app/(main)/factures/payees/page.tsx
Normal file
610
app/(main)/factures/payees/page.tsx
Normal file
@@ -0,0 +1,610 @@
|
||||
'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 { Divider } from 'primereact/divider';
|
||||
import { factureService } from '../../../../services/api';
|
||||
import { formatDate, formatCurrency } from '../../../../utils/formatters';
|
||||
import type { Facture } from '../../../../types/btp';
|
||||
|
||||
const FacturesPayeesPage = () => {
|
||||
const [factures, setFactures] = useState<Facture[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const [selectedFactures, setSelectedFactures] = useState<Facture[]>([]);
|
||||
const [detailDialog, setDetailDialog] = useState(false);
|
||||
const [selectedFacture, setSelectedFacture] = useState<Facture | null>(null);
|
||||
const [filterPeriod, setFilterPeriod] = useState('ALL');
|
||||
const [dateRange, setDateRange] = useState<Date[]>([]);
|
||||
const toast = useRef<Toast>(null);
|
||||
const dt = useRef<DataTable<Facture[]>>(null);
|
||||
|
||||
const periodOptions = [
|
||||
{ label: 'Toutes les factures', value: 'ALL' },
|
||||
{ label: 'Ce mois-ci', value: 'THIS_MONTH' },
|
||||
{ label: 'Mois dernier', value: 'LAST_MONTH' },
|
||||
{ label: 'Ce trimestre', value: 'THIS_QUARTER' },
|
||||
{ label: 'Cette année', value: 'THIS_YEAR' },
|
||||
{ label: 'Période personnalisée', value: 'CUSTOM' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadFactures();
|
||||
}, [filterPeriod, dateRange]);
|
||||
|
||||
const loadFactures = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await factureService.getAll();
|
||||
// Filtrer les factures payées
|
||||
let facturesPayees = data.filter(facture =>
|
||||
facture.statut === 'PAYEE' && facture.datePaiement
|
||||
);
|
||||
|
||||
// Appliquer le filtre de période
|
||||
facturesPayees = applyPeriodFilter(facturesPayees);
|
||||
|
||||
setFactures(facturesPayees);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des factures:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Impossible de charger les factures payées',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applyPeriodFilter = (facturesList: Facture[]) => {
|
||||
const now = new Date();
|
||||
|
||||
switch (filterPeriod) {
|
||||
case 'THIS_MONTH':
|
||||
return facturesList.filter(f => {
|
||||
const paymentDate = new Date(f.datePaiement!);
|
||||
return paymentDate.getMonth() === now.getMonth() &&
|
||||
paymentDate.getFullYear() === now.getFullYear();
|
||||
});
|
||||
|
||||
case 'LAST_MONTH':
|
||||
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1);
|
||||
return facturesList.filter(f => {
|
||||
const paymentDate = new Date(f.datePaiement!);
|
||||
return paymentDate.getMonth() === lastMonth.getMonth() &&
|
||||
paymentDate.getFullYear() === lastMonth.getFullYear();
|
||||
});
|
||||
|
||||
case 'THIS_QUARTER':
|
||||
const quarterStart = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);
|
||||
return facturesList.filter(f => {
|
||||
const paymentDate = new Date(f.datePaiement!);
|
||||
return paymentDate >= quarterStart && paymentDate <= now;
|
||||
});
|
||||
|
||||
case 'THIS_YEAR':
|
||||
return facturesList.filter(f => {
|
||||
const paymentDate = new Date(f.datePaiement!);
|
||||
return paymentDate.getFullYear() === now.getFullYear();
|
||||
});
|
||||
|
||||
case 'CUSTOM':
|
||||
if (dateRange.length === 2) {
|
||||
return facturesList.filter(f => {
|
||||
const paymentDate = new Date(f.datePaiement!);
|
||||
return paymentDate >= dateRange[0] && paymentDate <= dateRange[1];
|
||||
});
|
||||
}
|
||||
return facturesList;
|
||||
|
||||
default:
|
||||
return facturesList;
|
||||
}
|
||||
};
|
||||
|
||||
const getDaysToPayment = (dateEmission: string | Date, datePaiement: string | Date) => {
|
||||
const emissionDate = new Date(dateEmission);
|
||||
const paymentDate = new Date(datePaiement);
|
||||
const diffTime = paymentDate.getTime() - emissionDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays;
|
||||
};
|
||||
|
||||
const getPaymentPerformance = (dateEcheance: string | Date, datePaiement: string | Date) => {
|
||||
const dueDate = new Date(dateEcheance);
|
||||
const paymentDate = new Date(datePaiement);
|
||||
const diffTime = paymentDate.getTime() - dueDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays < 0) return { status: 'EN AVANCE', days: Math.abs(diffDays), severity: 'success' as const };
|
||||
if (diffDays === 0) return { status: 'À L\'ÉCHÉANCE', days: 0, severity: 'info' as const };
|
||||
return { status: 'EN RETARD', days: diffDays, severity: 'warning' as const };
|
||||
};
|
||||
|
||||
const viewDetails = (facture: Facture) => {
|
||||
setSelectedFacture(facture);
|
||||
setDetailDialog(true);
|
||||
};
|
||||
|
||||
const exportCSV = () => {
|
||||
dt.current?.exportCSV();
|
||||
};
|
||||
|
||||
const generatePaymentReport = () => {
|
||||
const totalReceived = factures.reduce((sum, f) => sum + (f.montantTTC || 0), 0);
|
||||
const avgPaymentTime = factures.length > 0 ?
|
||||
factures.reduce((sum, f) => sum + getDaysToPayment(f.dateEmission, f.datePaiement!), 0) / factures.length : 0;
|
||||
|
||||
const onTimePayments = factures.filter(f => {
|
||||
const perf = getPaymentPerformance(f.dateEcheance, f.datePaiement!);
|
||||
return perf.status !== 'EN RETARD';
|
||||
});
|
||||
|
||||
const earlyPayments = factures.filter(f => {
|
||||
const perf = getPaymentPerformance(f.dateEcheance, f.datePaiement!);
|
||||
return perf.status === 'EN AVANCE';
|
||||
});
|
||||
|
||||
const report = `
|
||||
=== RAPPORT ENCAISSEMENTS ===
|
||||
Période: ${getPeriodLabel()}
|
||||
Date du rapport: ${new Date().toLocaleDateString('fr-FR')}
|
||||
|
||||
STATISTIQUES GÉNÉRALES:
|
||||
- Nombre de factures payées: ${factures.length}
|
||||
- Montant total encaissé: ${formatCurrency(totalReceived)}
|
||||
- Montant moyen par facture: ${formatCurrency(totalReceived / (factures.length || 1))}
|
||||
- Délai moyen de paiement: ${Math.round(avgPaymentTime)} jours
|
||||
|
||||
PERFORMANCE DE PAIEMENT:
|
||||
- Paiements à l'heure: ${onTimePayments.length} (${Math.round((onTimePayments.length / factures.length) * 100)}%)
|
||||
- Paiements en avance: ${earlyPayments.length} (${Math.round((earlyPayments.length / factures.length) * 100)}%)
|
||||
- Taux de ponctualité: ${Math.round((onTimePayments.length / factures.length) * 100)}%
|
||||
|
||||
RÉPARTITION PAR MOIS:
|
||||
${getMonthlyBreakdown()}
|
||||
|
||||
ANALYSE PAR CLIENT:
|
||||
${getClientPaymentAnalysis()}
|
||||
|
||||
TOP 5 PLUS GROSSES FACTURES:
|
||||
${factures
|
||||
.sort((a, b) => (b.montantTTC || 0) - (a.montantTTC || 0))
|
||||
.slice(0, 5)
|
||||
.map(f => `- ${f.numero}: ${formatCurrency(f.montantTTC || 0)} - ${f.client ? `${f.client.prenom} ${f.client.nom}` : 'N/A'} - ${formatDate(f.datePaiement!)}`)
|
||||
.join('\n')}
|
||||
|
||||
CLIENTS LES PLUS PONCTUELS:
|
||||
${getBestPayingClients()}
|
||||
|
||||
RECOMMANDATIONS:
|
||||
- Maintenir les bonnes relations avec les clients ponctuels
|
||||
- Analyser les facteurs de succès pour améliorer les délais globaux
|
||||
- Proposer des remises pour paiement anticipé
|
||||
- Utiliser cette base pour évaluer la solvabilité des clients
|
||||
`;
|
||||
|
||||
const blob = new Blob([report], { type: 'text/plain;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `rapport_encaissements_${new Date().toISOString().split('T')[0]}.txt`;
|
||||
link.click();
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Rapport généré',
|
||||
detail: 'Le rapport d\'encaissements a été téléchargé',
|
||||
life: 3000
|
||||
});
|
||||
};
|
||||
|
||||
const getPeriodLabel = () => {
|
||||
switch (filterPeriod) {
|
||||
case 'THIS_MONTH': return 'Ce mois-ci';
|
||||
case 'LAST_MONTH': return 'Mois dernier';
|
||||
case 'THIS_QUARTER': return 'Ce trimestre';
|
||||
case 'THIS_YEAR': return 'Cette année';
|
||||
case 'CUSTOM':
|
||||
return dateRange.length === 2 ?
|
||||
`Du ${formatDate(dateRange[0])} au ${formatDate(dateRange[1])}` :
|
||||
'Période personnalisée';
|
||||
default: return 'Toutes les factures';
|
||||
}
|
||||
};
|
||||
|
||||
const getMonthlyBreakdown = () => {
|
||||
const months = {};
|
||||
factures.forEach(f => {
|
||||
const month = new Date(f.datePaiement!).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long' });
|
||||
if (!months[month]) {
|
||||
months[month] = { count: 0, value: 0 };
|
||||
}
|
||||
months[month].count++;
|
||||
months[month].value += f.montantTTC || 0;
|
||||
});
|
||||
|
||||
return Object.entries(months)
|
||||
.sort((a, b) => new Date(a[0]).getTime() - new Date(b[0]).getTime())
|
||||
.map(([month, data]: [string, any]) => `- ${month}: ${data.count} factures, ${formatCurrency(data.value)}`)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const getClientPaymentAnalysis = () => {
|
||||
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, totalDays: 0 };
|
||||
}
|
||||
clientStats[clientKey].count++;
|
||||
clientStats[clientKey].value += f.montantTTC || 0;
|
||||
clientStats[clientKey].totalDays += getDaysToPayment(f.dateEmission, f.datePaiement!);
|
||||
}
|
||||
});
|
||||
|
||||
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)}, délai moyen: ${Math.round(data.totalDays / data.count)} jours`)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const getBestPayingClients = () => {
|
||||
const clientStats = {};
|
||||
factures.forEach(f => {
|
||||
if (f.client) {
|
||||
const clientKey = `${f.client.prenom} ${f.client.nom}`;
|
||||
if (!clientStats[clientKey]) {
|
||||
clientStats[clientKey] = { onTime: 0, total: 0 };
|
||||
}
|
||||
clientStats[clientKey].total++;
|
||||
const perf = getPaymentPerformance(f.dateEcheance, f.datePaiement!);
|
||||
if (perf.status !== 'EN RETARD') {
|
||||
clientStats[clientKey].onTime++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(clientStats)
|
||||
.filter(([_, data]: [string, any]) => data.total >= 2) // Au moins 2 factures
|
||||
.sort((a: [string, any], b: [string, any]) => (b[1].onTime / b[1].total) - (a[1].onTime / a[1].total))
|
||||
.slice(0, 5)
|
||||
.map(([client, data]: [string, any]) => `- ${client}: ${Math.round((data.onTime / data.total) * 100)}% ponctuel (${data.onTime}/${data.total})`)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const leftToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="my-2 flex gap-2 align-items-center">
|
||||
<h5 className="m-0 flex align-items-center text-green-600">
|
||||
<i className="pi pi-check-circle mr-2"></i>
|
||||
Factures payées ({factures.length})
|
||||
</h5>
|
||||
<Chip
|
||||
label={`Total encaissé: ${formatCurrency(factures.reduce((sum, f) => sum + (f.montantTTC || 0), 0))}`}
|
||||
className="bg-green-100 text-green-800"
|
||||
/>
|
||||
<Dropdown
|
||||
value={filterPeriod}
|
||||
options={periodOptions}
|
||||
onChange={(e) => setFilterPeriod(e.value)}
|
||||
className="w-12rem"
|
||||
/>
|
||||
{filterPeriod === 'CUSTOM' && (
|
||||
<Calendar
|
||||
value={dateRange}
|
||||
onChange={(e) => setDateRange(e.value as Date[])}
|
||||
selectionMode="range"
|
||||
placeholder="Sélectionner la période"
|
||||
className="w-15rem"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
label="Rapport d'encaissements"
|
||||
icon="pi pi-chart-bar"
|
||||
severity="success"
|
||||
size="small"
|
||||
onClick={generatePaymentReport}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const rightToolbarTemplate = () => {
|
||||
return (
|
||||
<Button
|
||||
label="Exporter"
|
||||
icon="pi pi-upload"
|
||||
severity="help"
|
||||
onClick={exportCSV}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const actionBodyTemplate = (rowData: Facture) => {
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
rounded
|
||||
severity="info"
|
||||
size="small"
|
||||
tooltip="Voir détails du paiement"
|
||||
onClick={() => viewDetails(rowData)}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-print"
|
||||
rounded
|
||||
severity="help"
|
||||
size="small"
|
||||
tooltip="Imprimer reçu"
|
||||
onClick={() => {
|
||||
toast.current?.show({
|
||||
severity: 'info',
|
||||
summary: 'Impression',
|
||||
detail: `Impression du reçu pour ${rowData.numero}`,
|
||||
life: 3000
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file-pdf"
|
||||
rounded
|
||||
severity="secondary"
|
||||
size="small"
|
||||
tooltip="Générer attestation de paiement"
|
||||
onClick={() => {
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Attestation',
|
||||
detail: `Attestation générée pour ${rowData.numero}`,
|
||||
life: 3000
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const statusBodyTemplate = (rowData: Facture) => {
|
||||
const performance = getPaymentPerformance(rowData.dateEcheance, rowData.datePaiement!);
|
||||
|
||||
return (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<Tag value="Payée" severity="success" />
|
||||
<Tag
|
||||
value={performance.status}
|
||||
severity={performance.severity}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const paymentBodyTemplate = (rowData: Facture) => {
|
||||
const performance = getPaymentPerformance(rowData.dateEcheance, rowData.datePaiement!);
|
||||
const paymentDays = getDaysToPayment(rowData.dateEmission, rowData.datePaiement!);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="font-bold text-green-600">{formatDate(rowData.datePaiement!)}</div>
|
||||
<small className="text-600">
|
||||
Payée en {paymentDays} jour{paymentDays > 1 ? 's' : ''}
|
||||
</small>
|
||||
{performance.status === 'EN AVANCE' && (
|
||||
<div className="text-green-600 text-xs">
|
||||
<i className="pi pi-check mr-1"></i>
|
||||
{performance.days} jour{performance.days > 1 ? 's' : ''} en avance
|
||||
</div>
|
||||
)}
|
||||
{performance.status === 'EN RETARD' && (
|
||||
<div className="text-orange-600 text-xs">
|
||||
<i className="pi pi-clock mr-1"></i>
|
||||
{performance.days} jour{performance.days > 1 ? 's' : ''} de retard
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const clientBodyTemplate = (rowData: Facture) => {
|
||||
if (!rowData.client) return '';
|
||||
return `${rowData.client.prenom} ${rowData.client.nom}`;
|
||||
};
|
||||
|
||||
const performanceBodyTemplate = (rowData: Facture) => {
|
||||
const performance = getPaymentPerformance(rowData.dateEcheance, rowData.datePaiement!);
|
||||
const paymentDays = getDaysToPayment(rowData.dateEmission, rowData.datePaiement!);
|
||||
|
||||
// Score de performance (0-100)
|
||||
let score = 100;
|
||||
if (performance.status === 'EN RETARD') {
|
||||
score = Math.max(0, 100 - (performance.days * 5)); // -5 points par jour de retard
|
||||
} else if (performance.status === 'EN AVANCE') {
|
||||
score = 100 + Math.min(20, performance.days * 2); // +2 points par jour d'avance (max +20)
|
||||
}
|
||||
|
||||
let scoreColor = '#22c55e'; // green
|
||||
if (score < 70) scoreColor = '#ef4444'; // red
|
||||
else if (score < 85) scoreColor = '#f59e0b'; // orange
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold mb-1" style={{ color: scoreColor }}>
|
||||
{Math.round(score)}
|
||||
</div>
|
||||
<small className="text-600">Score de ponctualité</small>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const header = (
|
||||
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
|
||||
<h5 className="m-0">Factures payées - Suivi des encaissements</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 detailDialogFooter = (
|
||||
<Button
|
||||
label="Fermer"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
onClick={() => setDetailDialog(false)}
|
||||
/>
|
||||
);
|
||||
|
||||
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 payée trouvée."
|
||||
header={header}
|
||||
responsiveLayout="scroll"
|
||||
loading={loading}
|
||||
sortField="datePaiement"
|
||||
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="dateEmission" header="Date émission" body={(rowData) => formatDate(rowData.dateEmission)} sortable headerStyle={{ minWidth: '10rem' }} />
|
||||
<Column field="dateEcheance" header="Échéance" body={(rowData) => formatDate(rowData.dateEcheance)} sortable headerStyle={{ minWidth: '10rem' }} />
|
||||
<Column field="datePaiement" header="Paiement" body={paymentBodyTemplate} sortable headerStyle={{ minWidth: '12rem' }} />
|
||||
<Column field="montantTTC" header="Montant TTC" body={(rowData) => formatCurrency(rowData.montantTTC)} sortable headerStyle={{ minWidth: '10rem' }} />
|
||||
<Column field="performance" header="Performance" body={performanceBodyTemplate} headerStyle={{ minWidth: '10rem' }} />
|
||||
<Column field="statut" header="Statut" body={statusBodyTemplate} headerStyle={{ minWidth: '12rem' }} />
|
||||
<Column body={actionBodyTemplate} headerStyle={{ minWidth: '12rem' }} />
|
||||
</DataTable>
|
||||
|
||||
<Dialog
|
||||
visible={detailDialog}
|
||||
style={{ width: '600px' }}
|
||||
header="Détails du paiement"
|
||||
modal
|
||||
className="p-fluid"
|
||||
footer={detailDialogFooter}
|
||||
onHide={() => setDetailDialog(false)}
|
||||
>
|
||||
{selectedFacture && (
|
||||
<div className="formgrid grid">
|
||||
<div className="field col-12">
|
||||
<h6>Informations de la facture</h6>
|
||||
<p><strong>Numéro:</strong> {selectedFacture.numero}</p>
|
||||
<p><strong>Objet:</strong> {selectedFacture.objet}</p>
|
||||
<p><strong>Client:</strong> {selectedFacture.client ? `${selectedFacture.client.prenom} ${selectedFacture.client.nom}` : 'N/A'}</p>
|
||||
<p><strong>Date d'émission:</strong> {formatDate(selectedFacture.dateEmission)}</p>
|
||||
<p><strong>Date d'échéance:</strong> {formatDate(selectedFacture.dateEcheance)}</p>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="field col-12">
|
||||
<h6>Détails du paiement</h6>
|
||||
<p><strong>Date de paiement:</strong> {formatDate(selectedFacture.datePaiement!)}</p>
|
||||
<p><strong>Montant payé:</strong> {formatCurrency(selectedFacture.montantTTC || 0)}</p>
|
||||
<p><strong>Délai de paiement:</strong> {getDaysToPayment(selectedFacture.dateEmission, selectedFacture.datePaiement!)} jours</p>
|
||||
|
||||
{(() => {
|
||||
const perf = getPaymentPerformance(selectedFacture.dateEcheance, selectedFacture.datePaiement!);
|
||||
return (
|
||||
<p>
|
||||
<strong>Performance:</strong>
|
||||
<Tag
|
||||
value={perf.status}
|
||||
severity={perf.severity}
|
||||
className="ml-2"
|
||||
/>
|
||||
{perf.days > 0 && (
|
||||
<span className="ml-2">
|
||||
({perf.days} jour{perf.days > 1 ? 's' : ''})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="field col-12">
|
||||
<h6>Actions disponibles</h6>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
label="Imprimer reçu"
|
||||
icon="pi pi-print"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
toast.current?.show({
|
||||
severity: 'info',
|
||||
summary: 'Impression',
|
||||
detail: `Impression du reçu pour ${selectedFacture.numero}`,
|
||||
life: 3000
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Attestation"
|
||||
icon="pi pi-file-pdf"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Attestation',
|
||||
detail: `Attestation générée pour ${selectedFacture.numero}`,
|
||||
life: 3000
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FacturesPayeesPage;
|
||||
Reference in New Issue
Block a user