584 lines
25 KiB
TypeScript
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;
|