Files
btpxpress-frontend/app/(main)/factures/templates/page.tsx
2025-10-01 01:39:07 +00:00

669 lines
27 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { Dropdown } from 'primereact/dropdown';
import { Tag } from 'primereact/tag';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Dialog } from 'primereact/dialog';
import { ConfirmDialog } from 'primereact/confirmdialog';
import { Menu } from 'primereact/menu';
import { Badge } from 'primereact/badge';
import { Checkbox } from 'primereact/checkbox';
import { factureService } from '../../../../services/api';
import { formatDate } from '../../../../utils/formatters';
interface FactureTemplate {
id: string;
nom: string;
description: string;
type: string;
categorie: string;
lignes: Array<{
designation: string;
quantite: number;
unite: string;
prixUnitaire: number;
}>;
tauxTVA: number;
conditionsPaiement: string;
actif: boolean;
dateCreation: Date;
utilisations: number;
}
const FactureTemplatesPage = () => {
const toast = useRef<Toast>(null);
const menuRef = useRef<Menu>(null);
const [templates, setTemplates] = useState<FactureTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [showDialog, setShowDialog] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<FactureTemplate | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<FactureTemplate | null>(null);
const [globalFilter, setGlobalFilter] = useState('');
const [formData, setFormData] = useState<Partial<FactureTemplate>>({
nom: '',
description: '',
type: 'FACTURE',
categorie: '',
lignes: [],
tauxTVA: 20,
conditionsPaiement: 'Paiement à 30 jours',
actif: true
});
const typeOptions = [
{ label: 'Facture', value: 'FACTURE' },
{ label: 'Acompte', value: 'ACOMPTE' },
{ label: 'Facture de situation', value: 'SITUATION' },
{ label: 'Facture de solde', value: 'SOLDE' }
];
const categorieOptions = [
{ label: 'Gros œuvre', value: 'GROS_OEUVRE' },
{ label: 'Second œuvre', value: 'SECOND_OEUVRE' },
{ label: 'Finitions', value: 'FINITIONS' },
{ label: 'Plomberie', value: 'PLOMBERIE' },
{ label: 'Électricité', value: 'ELECTRICITE' },
{ label: 'Chauffage', value: 'CHAUFFAGE' },
{ label: 'Rénovation', value: 'RENOVATION' },
{ label: 'Maintenance', value: 'MAINTENANCE' }
];
useEffect(() => {
loadTemplates();
}, []);
const loadTemplates = async () => {
try {
setLoading(true);
// TODO: Remplacer par un vrai appel API
// const response = await factureService.getTemplates();
// Données simulées pour la démonstration
const mockTemplates: FactureTemplate[] = [
{
id: '1',
nom: 'Facture Rénovation Standard',
description: 'Template pour factures de rénovation complète',
type: 'FACTURE',
categorie: 'RENOVATION',
lignes: [
{ designation: 'Main d\'œuvre', quantite: 1, unite: 'forfait', prixUnitaire: 2500 },
{ designation: 'Matériaux', quantite: 1, unite: 'forfait', prixUnitaire: 1800 },
{ designation: 'Évacuation déchets', quantite: 1, unite: 'forfait', prixUnitaire: 300 }
],
tauxTVA: 20,
conditionsPaiement: 'Paiement à 30 jours',
actif: true,
dateCreation: new Date('2024-01-15'),
utilisations: 45
},
{
id: '2',
nom: 'Acompte Gros Œuvre',
description: 'Template pour acomptes sur travaux de gros œuvre',
type: 'ACOMPTE',
categorie: 'GROS_OEUVRE',
lignes: [
{ designation: 'Acompte 30% - Fondations', quantite: 1, unite: 'forfait', prixUnitaire: 0 }
],
tauxTVA: 20,
conditionsPaiement: 'Paiement à réception',
actif: true,
dateCreation: new Date('2024-02-10'),
utilisations: 28
},
{
id: '3',
nom: 'Facture Maintenance Préventive',
description: 'Template pour factures de maintenance préventive',
type: 'FACTURE',
categorie: 'MAINTENANCE',
lignes: [
{ designation: 'Contrôle technique', quantite: 1, unite: 'forfait', prixUnitaire: 150 },
{ designation: 'Remplacement pièces', quantite: 1, unite: 'forfait', prixUnitaire: 200 },
{ designation: 'Rapport de maintenance', quantite: 1, unite: 'forfait', prixUnitaire: 50 }
],
tauxTVA: 20,
conditionsPaiement: 'Paiement à 15 jours',
actif: true,
dateCreation: new Date('2024-03-05'),
utilisations: 67
}
];
setTemplates(mockTemplates);
} catch (error) {
console.error('Erreur lors du chargement des templates:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les templates'
});
} finally {
setLoading(false);
}
};
const handleNew = () => {
setEditingTemplate(null);
setFormData({
nom: '',
description: '',
type: 'FACTURE',
categorie: '',
lignes: [],
tauxTVA: 20,
conditionsPaiement: 'Paiement à 30 jours',
actif: true
});
setShowDialog(true);
};
const handleEdit = (template: FactureTemplate) => {
setEditingTemplate(template);
setFormData({ ...template });
setShowDialog(true);
};
const handleSave = async () => {
try {
if (editingTemplate) {
// TODO: Appel API pour mise à jour
// await factureService.updateTemplate(editingTemplate.id, formData);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Template modifié avec succès'
});
} else {
// TODO: Appel API pour création
// await factureService.createTemplate(formData);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Template créé avec succès'
});
}
setShowDialog(false);
loadTemplates();
} catch (error) {
console.error('Erreur lors de la sauvegarde:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de la sauvegarde'
});
}
};
const handleDelete = async (template: FactureTemplate) => {
try {
// TODO: Appel API pour suppression
// await factureService.deleteTemplate(template.id);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Template supprimé avec succès'
});
loadTemplates();
} catch (error) {
console.error('Erreur lors de la suppression:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de la suppression'
});
}
};
const handleUseTemplate = async (template: FactureTemplate) => {
try {
// TODO: Créer une nouvelle facture basée sur le template
toast.current?.show({
severity: 'info',
summary: 'Info',
detail: 'Redirection vers la création de facture...'
});
// Simuler la redirection
setTimeout(() => {
window.location.href = `/factures/nouveau?template=${template.id}`;
}, 1000);
} catch (error) {
console.error('Erreur lors de l\'utilisation du template:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de l\'utilisation du template'
});
}
};
const getMenuItems = (template: FactureTemplate) => [
{
label: 'Utiliser',
icon: 'pi pi-plus',
command: () => handleUseTemplate(template)
},
{
label: 'Modifier',
icon: 'pi pi-pencil',
command: () => handleEdit(template)
},
{
label: 'Dupliquer',
icon: 'pi pi-copy',
command: () => {
setEditingTemplate(null);
setFormData({
...template,
nom: `${template.nom} (Copie)`,
id: undefined
});
setShowDialog(true);
}
},
{
separator: true
},
{
label: 'Supprimer',
icon: 'pi pi-trash',
className: 'text-red-500',
command: () => {
setSelectedTemplate(template);
// TODO: Afficher dialog de confirmation
}
}
];
const actionBodyTemplate = (rowData: FactureTemplate) => (
<div className="flex gap-2">
<Button
icon="pi pi-plus"
className="p-button-text p-button-sm p-button-success"
tooltip="Utiliser ce template"
onClick={() => handleUseTemplate(rowData)}
/>
<Button
icon="pi pi-ellipsis-v"
className="p-button-text p-button-sm"
onClick={(e) => {
setSelectedTemplate(rowData);
menuRef.current?.toggle(e);
}}
/>
</div>
);
const typeBodyTemplate = (rowData: FactureTemplate) => (
<Tag
value={rowData.type}
severity={rowData.type === 'FACTURE' ? 'primary' : 'info'}
/>
);
const categorieBodyTemplate = (rowData: FactureTemplate) => {
const categorie = categorieOptions.find(opt => opt.value === rowData.categorie);
return (
<Tag
value={categorie?.label || rowData.categorie}
severity="info"
/>
);
};
const statutBodyTemplate = (rowData: FactureTemplate) => (
<Tag
value={rowData.actif ? 'Actif' : 'Inactif'}
severity={rowData.actif ? 'success' : 'danger'}
/>
);
const utilisationsBodyTemplate = (rowData: FactureTemplate) => (
<Badge value={rowData.utilisations} severity="info" />
);
const toolbarStartTemplate = () => (
<div className="flex align-items-center gap-2">
<h2 className="text-xl font-bold m-0">Templates de Factures</h2>
</div>
);
const toolbarEndTemplate = () => (
<div className="flex align-items-center gap-2">
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Rechercher..."
/>
</span>
<Button
label="Nouveau Template"
icon="pi pi-plus"
onClick={handleNew}
/>
</div>
);
return (
<div className="grid">
<Toast ref={toast} />
<ConfirmDialog />
<Menu ref={menuRef} model={selectedTemplate ? getMenuItems(selectedTemplate) : []} popup />
<div className="col-12">
<Toolbar start={toolbarStartTemplate} end={toolbarEndTemplate} />
</div>
{/* Statistiques rapides */}
<div className="col-12 lg:col-3 md:col-6">
<Card className="h-full">
<div className="flex justify-content-between mb-3">
<div>
<span className="block text-500 font-medium mb-3">Total Templates</span>
<div className="text-900 font-medium text-xl">{templates.length}</div>
</div>
<div className="flex align-items-center justify-content-center bg-blue-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
<i className="pi pi-file text-blue-500 text-xl"></i>
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-3 md:col-6">
<Card className="h-full">
<div className="flex justify-content-between mb-3">
<div>
<span className="block text-500 font-medium mb-3">Templates Actifs</span>
<div className="text-900 font-medium text-xl">
{templates.filter(t => t.actif).length}
</div>
</div>
<div className="flex align-items-center justify-content-center bg-green-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
<i className="pi pi-check-circle text-green-500 text-xl"></i>
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-3 md:col-6">
<Card className="h-full">
<div className="flex justify-content-between mb-3">
<div>
<span className="block text-500 font-medium mb-3">Plus Utilisé</span>
<div className="text-900 font-medium text-xl">
{Math.max(...templates.map(t => t.utilisations), 0)} fois
</div>
</div>
<div className="flex align-items-center justify-content-center bg-orange-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
<i className="pi pi-star text-orange-500 text-xl"></i>
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-3 md:col-6">
<Card className="h-full">
<div className="flex justify-content-between mb-3">
<div>
<span className="block text-500 font-medium mb-3">Types</span>
<div className="text-900 font-medium text-xl">
{new Set(templates.map(t => t.type)).size}
</div>
</div>
<div className="flex align-items-center justify-content-center bg-purple-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
<i className="pi pi-tags text-purple-500 text-xl"></i>
</div>
</div>
</Card>
</div>
{/* Table des templates */}
<div className="col-12">
<Card>
<DataTable
value={templates}
loading={loading}
globalFilter={globalFilter}
responsiveLayout="scroll"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
emptyMessage="Aucun template trouvé"
header={
<div className="flex justify-content-between align-items-center">
<span className="text-xl font-bold">Liste des Templates</span>
<span className="text-sm text-600">{templates.length} template(s)</span>
</div>
}
>
<Column field="nom" header="Nom" sortable />
<Column field="description" header="Description" />
<Column
field="type"
header="Type"
body={typeBodyTemplate}
sortable
/>
<Column
field="categorie"
header="Catégorie"
body={categorieBodyTemplate}
sortable
/>
<Column
field="lignes"
header="Nb Lignes"
body={(rowData) => rowData.lignes?.length || 0}
style={{ width: '100px' }}
/>
<Column
field="utilisations"
header="Utilisations"
body={utilisationsBodyTemplate}
sortable
style={{ width: '120px' }}
/>
<Column
field="actif"
header="Statut"
body={statutBodyTemplate}
style={{ width: '100px' }}
/>
<Column
field="dateCreation"
header="Créé le"
body={(rowData) => formatDate(rowData.dateCreation)}
sortable
style={{ width: '120px' }}
/>
<Column
header="Actions"
body={actionBodyTemplate}
style={{ width: '120px' }}
/>
</DataTable>
</Card>
</div>
{/* Dialog de création/modification */}
<Dialog
header={editingTemplate ? "Modifier le template" : "Nouveau template"}
visible={showDialog}
onHide={() => setShowDialog(false)}
style={{ width: '800px' }}
footer={
<div className="flex justify-content-end gap-2">
<Button
label="Annuler"
icon="pi pi-times"
className="p-button-outlined"
onClick={() => setShowDialog(false)}
/>
<Button
label="Enregistrer"
icon="pi pi-save"
onClick={handleSave}
/>
</div>
}
>
<div className="grid">
<div className="col-12 md:col-6">
<div className="field">
<label htmlFor="nom" className="font-semibold">Nom *</label>
<InputText
id="nom"
value={formData.nom}
onChange={(e) => setFormData(prev => ({ ...prev, nom: e.target.value }))}
className="w-full"
required
/>
</div>
</div>
<div className="col-12 md:col-6">
<div className="field">
<label htmlFor="type" className="font-semibold">Type *</label>
<Dropdown
id="type"
value={formData.type}
options={typeOptions}
onChange={(e) => setFormData(prev => ({ ...prev, type: e.value }))}
className="w-full"
placeholder="Sélectionner un type"
/>
</div>
</div>
<div className="col-12">
<div className="field">
<label htmlFor="categorie" className="font-semibold">Catégorie *</label>
<Dropdown
id="categorie"
value={formData.categorie}
options={categorieOptions}
onChange={(e) => setFormData(prev => ({ ...prev, categorie: e.value }))}
className="w-full"
placeholder="Sélectionner une catégorie"
/>
</div>
</div>
<div className="col-12">
<div className="field">
<label htmlFor="description" className="font-semibold">Description</label>
<InputTextarea
id="description"
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
className="w-full"
rows={3}
/>
</div>
</div>
<div className="col-12 md:col-6">
<div className="field">
<label htmlFor="tauxTVA" className="font-semibold">Taux TVA (%)</label>
<InputText
id="tauxTVA"
value={formData.tauxTVA?.toString()}
onChange={(e) => setFormData(prev => ({ ...prev, tauxTVA: parseFloat(e.target.value) || 0 }))}
className="w-full"
/>
</div>
</div>
<div className="col-12 md:col-6">
<div className="field">
<label htmlFor="conditionsPaiement" className="font-semibold">Conditions de paiement</label>
<InputText
id="conditionsPaiement"
value={formData.conditionsPaiement}
onChange={(e) => setFormData(prev => ({ ...prev, conditionsPaiement: e.target.value }))}
className="w-full"
/>
</div>
</div>
<div className="col-12">
<div className="field">
<div className="flex align-items-center">
<Checkbox
inputId="actif"
checked={formData.actif}
onChange={(e) => setFormData(prev => ({ ...prev, actif: e.checked || false }))}
/>
<label htmlFor="actif" className="ml-2">Template actif</label>
</div>
</div>
</div>
<div className="col-12">
<h6>Lignes du template</h6>
<p className="text-sm text-600 mb-3">
Les lignes seront automatiquement ajoutées lors de l'utilisation du template.
</p>
{formData.lignes && formData.lignes.length > 0 ? (
<DataTable value={formData.lignes} responsiveLayout="scroll">
<Column field="designation" header="Désignation" />
<Column field="quantite" header="Quantité" style={{ width: '100px' }} />
<Column field="unite" header="Unité" style={{ width: '80px' }} />
<Column
field="prixUnitaire"
header="Prix unitaire"
style={{ width: '120px' }}
body={(rowData) => `${rowData.prixUnitaire}€`}
/>
</DataTable>
) : (
<div className="text-center p-4 border-2 border-dashed border-300 border-round">
<i className="pi pi-inbox text-4xl text-400 mb-3"></i>
<p className="text-600">Aucune ligne définie</p>
<Button
label="Ajouter des lignes"
icon="pi pi-plus"
className="p-button-outlined p-button-sm"
onClick={() => {
// TODO: Ouvrir dialog pour ajouter des lignes
toast.current?.show({
severity: 'info',
summary: 'Info',
detail: 'Fonctionnalité en cours de développement'
});
}}
/>
</div>
)}
</div>
</div>
</Dialog>
</div>
);
};
export default FactureTemplatesPage;