Files
btpxpress-frontend/app/(main)/factures/export/page.tsx
2025-10-13 05:29:32 +02:00

584 lines
25 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { Calendar } from 'primereact/calendar';
import { Dropdown } from 'primereact/dropdown';
import { MultiSelect } from 'primereact/multiselect';
import { Checkbox } from 'primereact/checkbox';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Toast } from 'primereact/toast';
import { ProgressBar } from 'primereact/progressbar';
import { Toolbar } from 'primereact/toolbar';
import { Tag } from 'primereact/tag';
import { Badge } from 'primereact/badge';
import { Divider } from 'primereact/divider';
import { factureService, clientService } from '../../../../services/api';
import { formatDate, formatCurrency } from '../../../../utils/formatters';
import type { Facture, Client } from '../../../../types/btp';
import { StatutFacture, TypeFacture } from '../../../../types/btp';
interface ExportConfig {
format: string;
dateDebut: Date;
dateFin: Date;
statuts: string[];
clients: Client[];
types: string[];
includeDetails: boolean;
includeStatistiques: boolean;
grouperParClient: boolean;
grouperParMois: boolean;
}
const FactureExportPage = () => {
const toast = useRef<Toast>(null);
const [factures, setFactures] = useState<Facture[]>([]);
const [clients, setClients] = useState<Client[]>([]);
const [loading, setLoading] = useState(false);
const [exporting, setExporting] = useState(false);
const [exportProgress, setExportProgress] = useState(0);
const [config, setConfig] = useState<ExportConfig>({
format: 'EXCEL',
dateDebut: new Date(new Date().getFullYear(), 0, 1),
dateFin: new Date(),
statuts: [],
clients: [],
types: [],
includeDetails: true,
includeStatistiques: false,
grouperParClient: false,
grouperParMois: false
});
const formatOptions = [
{ label: 'Excel (.xlsx)', value: 'EXCEL', icon: 'pi pi-file-excel' },
{ label: 'PDF', value: 'PDF', icon: 'pi pi-file-pdf' },
{ label: 'CSV', value: 'CSV', icon: 'pi pi-file' },
{ label: 'JSON', value: 'JSON', icon: 'pi pi-code' }
];
const statutOptions = [
{ label: 'Brouillon', value: 'BROUILLON' },
{ label: 'Envoyée', value: 'ENVOYEE' },
{ label: 'Payée', value: 'PAYEE' },
{ label: 'Partiellement payée', value: 'PARTIELLEMENT_PAYEE' },
{ label: 'En retard', value: 'EN_RETARD' }
];
const typeOptions = [
{ label: 'Facture', value: 'FACTURE' },
{ label: 'Acompte', value: 'ACOMPTE' },
{ label: 'Facture de situation', value: 'SITUATION' },
{ label: 'Facture de solde', value: 'SOLDE' }
];
useEffect(() => {
loadData();
}, []);
useEffect(() => {
loadFactures();
}, [config.dateDebut, config.dateFin, config.statuts, config.clients, config.types]);
const loadData = async () => {
try {
setLoading(true);
// Charger les clients
const clientsResponse = await clientService.getAll();
setClients(clientsResponse);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données'
});
} finally {
setLoading(false);
}
};
const loadFactures = async () => {
try {
setLoading(true);
// TODO: Appel API avec filtres
// const response = await factureService.getFiltered({
// dateDebut: config.dateDebut,
// dateFin: config.dateFin,
// statuts: config.statuts,
// clients: config.clients.map(c => c.id),
// types: config.types
// });
// Données simulées pour la démonstration
const mockFactures: Facture[] = [
{
id: '1',
numero: 'FAC-2024-001',
objet: 'Rénovation salle de bain',
typeFacture: TypeFacture.FACTURE,
statut: StatutFacture.PAYEE,
dateEmission: new Date('2024-01-15').toISOString(),
dateEcheance: new Date('2024-02-15').toISOString(),
client: { id: '1', nom: 'Dupont Construction' } as Client,
montantHT: 2500,
montantTTC: 3000,
tauxTVA: 20,
montantPaye: 3000
} as Facture,
{
id: '2',
numero: 'FAC-2024-002',
objet: 'Extension maison',
typeFacture: TypeFacture.ACOMPTE,
statut: StatutFacture.ENVOYEE,
dateEmission: new Date('2024-02-01').toISOString(),
dateEcheance: new Date('2024-03-01').toISOString(),
client: { id: '2', nom: 'Martin SARL' } as Client,
montantHT: 5000,
montantTTC: 6000,
tauxTVA: 20,
montantPaye: 0
} as Facture,
{
id: '3',
numero: 'FAC-2024-003',
objet: 'Travaux électricité',
typeFacture: TypeFacture.FACTURE,
statut: StatutFacture.ECHUE,
dateEmission: new Date('2024-01-20').toISOString(),
dateEcheance: new Date('2024-02-20').toISOString(),
client: { id: '3', nom: 'Bâti Plus' } as Client,
montantHT: 1800,
montantTTC: 2160,
tauxTVA: 20,
montantPaye: 0
} as Facture
];
// Appliquer les filtres
let facturesFiltrees = mockFactures;
if (config.statuts.length > 0) {
facturesFiltrees = facturesFiltrees.filter(f => config.statuts.includes(f.statut));
}
if (config.types.length > 0) {
facturesFiltrees = facturesFiltrees.filter(f => config.types.includes(f.typeFacture));
}
if (config.clients.length > 0) {
const clientIds = config.clients.map(c => c.id);
facturesFiltrees = facturesFiltrees.filter(f =>
typeof f.client === 'object' && clientIds.includes(f.client.id)
);
}
setFactures(facturesFiltrees);
} catch (error) {
console.error('Erreur lors du chargement des factures:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les factures'
});
} finally {
setLoading(false);
}
};
const handleExport = async () => {
try {
setExporting(true);
setExportProgress(0);
// Simulation du processus d'export
const steps = [
'Préparation des données...',
'Application des filtres...',
'Génération du fichier...',
'Finalisation...'
];
for (let i = 0; i < steps.length; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
setExportProgress((i + 1) * 25);
toast.current?.show({
severity: 'info',
summary: 'Export en cours',
detail: steps[i],
life: 1000
});
}
// TODO: Appel API réel pour l'export
// const response = await factureService.export(config);
// Simulation du téléchargement
const filename = `factures_${formatDate(config.dateDebut)}_${formatDate(config.dateFin)}.${config.format.toLowerCase()}`;
toast.current?.show({
severity: 'success',
summary: 'Export terminé',
detail: `Fichier ${filename} généré avec succès`
});
// Simuler le téléchargement
const link = document.createElement('a');
link.href = '#';
link.download = filename;
link.click();
} catch (error) {
console.error('Erreur lors de l\'export:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de l\'export'
});
} finally {
setExporting(false);
setExportProgress(0);
}
};
const getStatutSeverity = (statut: string) => {
switch (statut) {
case 'PAYEE': return 'success';
case 'EN_RETARD': return 'danger';
case 'PARTIELLEMENT_PAYEE': return 'warning';
case 'ENVOYEE': return 'info';
case 'BROUILLON': return 'secondary';
default: return 'info';
}
};
const calculateTotals = () => {
const montantTotal = factures.reduce((sum, f) => sum + f.montantTTC, 0);
const montantPaye = factures.reduce((sum, f) => sum + (f.montantPaye || 0), 0);
const montantEnAttente = montantTotal - montantPaye;
return { montantTotal, montantPaye, montantEnAttente };
};
const totals = calculateTotals();
const toolbarStartTemplate = () => (
<div className="flex align-items-center gap-2">
<h2 className="text-xl font-bold m-0">Export des Factures</h2>
</div>
);
const toolbarEndTemplate = () => (
<div className="flex align-items-center gap-2">
<Button
label="Réinitialiser"
icon="pi pi-refresh"
className="p-button-outlined"
onClick={() => {
setConfig({
format: 'EXCEL',
dateDebut: new Date(new Date().getFullYear(), 0, 1),
dateFin: new Date(),
statuts: [],
clients: [],
types: [],
includeDetails: true,
includeStatistiques: false,
grouperParClient: false,
grouperParMois: false
});
}}
/>
<Button
label="Exporter"
icon="pi pi-download"
onClick={handleExport}
loading={exporting}
disabled={factures.length === 0}
/>
</div>
);
return (
<div className="grid">
<Toast ref={toast} />
<div className="col-12">
<Toolbar start={toolbarStartTemplate} end={toolbarEndTemplate} />
</div>
{/* Configuration de l'export */}
<div className="col-12 lg:col-4">
<Card title="Configuration de l'export">
<div className="grid">
<div className="col-12">
<div className="field">
<label htmlFor="format" className="font-semibold">Format d'export *</label>
<Dropdown
id="format"
value={config.format}
options={formatOptions}
onChange={(e) => setConfig(prev => ({ ...prev, format: e.value }))}
className="w-full"
itemTemplate={(option) => (
<div className="flex align-items-center">
<i className={`${option.icon} mr-2`}></i>
{option.label}
</div>
)}
/>
</div>
</div>
<div className="col-12 md:col-6">
<div className="field">
<label htmlFor="dateDebut" className="font-semibold">Date début</label>
<Calendar
id="dateDebut"
value={config.dateDebut}
onChange={(e) => setConfig(prev => ({ ...prev, dateDebut: e.value || new Date() }))}
className="w-full"
dateFormat="dd/mm/yy"
/>
</div>
</div>
<div className="col-12 md:col-6">
<div className="field">
<label htmlFor="dateFin" className="font-semibold">Date fin</label>
<Calendar
id="dateFin"
value={config.dateFin}
onChange={(e) => setConfig(prev => ({ ...prev, dateFin: e.value || new Date() }))}
className="w-full"
dateFormat="dd/mm/yy"
/>
</div>
</div>
<div className="col-12">
<div className="field">
<label htmlFor="statuts" className="font-semibold">Statuts</label>
<MultiSelect
id="statuts"
value={config.statuts}
options={statutOptions}
onChange={(e) => setConfig(prev => ({ ...prev, statuts: e.value }))}
className="w-full"
placeholder="Tous les statuts"
maxSelectedLabels={2}
/>
</div>
</div>
<div className="col-12">
<div className="field">
<label htmlFor="types" className="font-semibold">Types</label>
<MultiSelect
id="types"
value={config.types}
options={typeOptions}
onChange={(e) => setConfig(prev => ({ ...prev, types: e.value }))}
className="w-full"
placeholder="Tous les types"
maxSelectedLabels={2}
/>
</div>
</div>
<div className="col-12">
<div className="field">
<label htmlFor="clients" className="font-semibold">Clients</label>
<MultiSelect
id="clients"
value={config.clients}
options={clients.map(client => ({ label: client.nom, value: client }))}
onChange={(e) => setConfig(prev => ({ ...prev, clients: e.value }))}
className="w-full"
placeholder="Tous les clients"
maxSelectedLabels={2}
filter
/>
</div>
</div>
</div>
<Divider />
<h6>Options d'export</h6>
<div className="grid">
<div className="col-12">
<div className="flex align-items-center mb-2">
<Checkbox
inputId="includeDetails"
checked={config.includeDetails}
onChange={(e) => setConfig(prev => ({ ...prev, includeDetails: e.checked || false }))}
/>
<label htmlFor="includeDetails" className="ml-2">Inclure les détails des lignes</label>
</div>
</div>
<div className="col-12">
<div className="flex align-items-center mb-2">
<Checkbox
inputId="includeStatistiques"
checked={config.includeStatistiques}
onChange={(e) => setConfig(prev => ({ ...prev, includeStatistiques: e.checked || false }))}
/>
<label htmlFor="includeStatistiques" className="ml-2">Inclure les statistiques</label>
</div>
</div>
<div className="col-12">
<div className="flex align-items-center mb-2">
<Checkbox
inputId="grouperParClient"
checked={config.grouperParClient}
onChange={(e) => setConfig(prev => ({ ...prev, grouperParClient: e.checked || false }))}
/>
<label htmlFor="grouperParClient" className="ml-2">Grouper par client</label>
</div>
</div>
<div className="col-12">
<div className="flex align-items-center mb-2">
<Checkbox
inputId="grouperParMois"
checked={config.grouperParMois}
onChange={(e) => setConfig(prev => ({ ...prev, grouperParMois: e.checked || false }))}
/>
<label htmlFor="grouperParMois" className="ml-2">Grouper par mois</label>
</div>
</div>
</div>
</Card>
</div>
{/* Aperçu des données */}
<div className="col-12 lg:col-8">
<Card title="Aperçu des données à exporter">
{exporting && (
<div className="mb-4">
<div className="flex justify-content-between align-items-center mb-2">
<span className="font-semibold">Export en cours...</span>
<span className="text-sm">{exportProgress}%</span>
</div>
<ProgressBar value={exportProgress} />
</div>
)}
<DataTable
value={factures}
loading={loading}
responsiveLayout="scroll"
paginator
rows={10}
emptyMessage="Aucune facture trouvée avec ces critères"
header={
<div className="flex justify-content-between align-items-center">
<span className="text-xl font-bold">Factures sélectionnées</span>
<Badge value={factures.length} />
</div>
}
>
<Column field="numero" header="Numéro" sortable />
<Column field="objet" header="Objet" />
<Column
field="type"
header="Type"
body={(rowData) => (
<Tag value={rowData.type} severity="info" />
)}
/>
<Column
field="statut"
header="Statut"
body={(rowData) => (
<Tag
value={rowData.statut}
severity={getStatutSeverity(rowData.statut) as any}
/>
)}
/>
<Column
field="client"
header="Client"
body={(rowData) =>
typeof rowData.client === 'string' ? rowData.client : rowData.client?.nom
}
/>
<Column
field="dateEmission"
header="Date émission"
body={(rowData) => formatDate(rowData.dateEmission)}
sortable
/>
<Column
field="montantTTC"
header="Montant TTC"
body={(rowData) => formatCurrency(rowData.montantTTC)}
sortable
/>
</DataTable>
</Card>
</div>
{/* Résumé financier */}
<div className="col-12">
<Card title="Résumé financier">
<div className="grid">
<div className="col-12 md:col-3">
<div className="text-center p-3 border-round bg-blue-50">
<div className="text-blue-600 font-bold text-xl mb-2">
{factures.length}
</div>
<div className="text-blue-900 font-semibold">Factures</div>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center p-3 border-round bg-green-50">
<div className="text-green-600 font-bold text-xl mb-2">
{formatCurrency(totals.montantTotal)}
</div>
<div className="text-green-900 font-semibold">Montant Total</div>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center p-3 border-round bg-teal-50">
<div className="text-teal-600 font-bold text-xl mb-2">
{formatCurrency(totals.montantPaye)}
</div>
<div className="text-teal-900 font-semibold">Montant Payé</div>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center p-3 border-round bg-orange-50">
<div className="text-orange-600 font-bold text-xl mb-2">
{formatCurrency(totals.montantEnAttente)}
</div>
<div className="text-orange-900 font-semibold">En Attente</div>
</div>
</div>
</div>
</Card>
</div>
</div>
);
};
export default FactureExportPage;