Initial commit
This commit is contained in:
582
app/(main)/factures/export/page.tsx
Normal file
582
app/(main)/factures/export/page.tsx
Normal file
@@ -0,0 +1,582 @@
|
||||
'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';
|
||||
|
||||
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.data);
|
||||
|
||||
} 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',
|
||||
type: 'FACTURE',
|
||||
statut: 'PAYEE',
|
||||
dateEmission: new Date('2024-01-15'),
|
||||
dateEcheance: new Date('2024-02-15'),
|
||||
client: { id: '1', nom: 'Dupont Construction' } as Client,
|
||||
montantHT: 2500,
|
||||
montantTTC: 3000,
|
||||
tauxTVA: 20,
|
||||
montantPaye: 3000
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
numero: 'FAC-2024-002',
|
||||
objet: 'Extension maison',
|
||||
type: 'ACOMPTE',
|
||||
statut: 'ENVOYEE',
|
||||
dateEmission: new Date('2024-02-01'),
|
||||
dateEcheance: new Date('2024-03-01'),
|
||||
client: { id: '2', nom: 'Martin SARL' } as Client,
|
||||
montantHT: 5000,
|
||||
montantTTC: 6000,
|
||||
tauxTVA: 20,
|
||||
montantPaye: 0
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
numero: 'FAC-2024-003',
|
||||
objet: 'Travaux électricité',
|
||||
type: 'FACTURE',
|
||||
statut: 'EN_RETARD',
|
||||
dateEmission: new Date('2024-01-20'),
|
||||
dateEcheance: new Date('2024-02-20'),
|
||||
client: { id: '3', nom: 'Bâti Plus' } as Client,
|
||||
montantHT: 1800,
|
||||
montantTTC: 2160,
|
||||
tauxTVA: 20,
|
||||
montantPaye: 0
|
||||
}
|
||||
];
|
||||
|
||||
// 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.type));
|
||||
}
|
||||
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<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;
|
||||
Reference in New Issue
Block a user