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

590 lines
24 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 { devisService } from '../../../../services/api';
import { formatDate } from '../../../../utils/formatters';
interface DevisTemplate {
id: string;
nom: string;
description: string;
categorie: string;
lignes: Array<{
designation: string;
quantite: number;
unite: string;
prixUnitaire: number;
}>;
tauxTVA: number;
actif: boolean;
dateCreation: Date;
utilisations: number;
}
const DevisTemplatesPage = () => {
const toast = useRef<Toast>(null);
const menuRef = useRef<Menu>(null);
const [templates, setTemplates] = useState<DevisTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [showDialog, setShowDialog] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<DevisTemplate | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<DevisTemplate | null>(null);
const [globalFilter, setGlobalFilter] = useState('');
const [formData, setFormData] = useState<Partial<DevisTemplate>>({
nom: '',
description: '',
categorie: '',
lignes: [],
tauxTVA: 20,
actif: true
});
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 devisService.getTemplates();
// Données simulées pour la démonstration
const mockTemplates: DevisTemplate[] = [
{
id: '1',
nom: 'Rénovation Salle de Bain Standard',
description: 'Template pour rénovation complète salle de bain 6m²',
categorie: 'RENOVATION',
lignes: [
{ designation: 'Démolition existant', quantite: 1, unite: 'forfait', prixUnitaire: 800 },
{ designation: 'Carrelage sol', quantite: 6, unite: 'm²', prixUnitaire: 45 },
{ designation: 'Carrelage mural', quantite: 20, unite: 'm²', prixUnitaire: 35 },
{ designation: 'Sanitaires standard', quantite: 1, unite: 'forfait', prixUnitaire: 1200 }
],
tauxTVA: 20,
actif: true,
dateCreation: new Date('2024-01-15'),
utilisations: 23
},
{
id: '2',
nom: 'Extension Maison 20m²',
description: 'Template pour extension plain-pied 20m²',
categorie: 'GROS_OEUVRE',
lignes: [
{ designation: 'Fondations', quantite: 20, unite: 'm²', prixUnitaire: 120 },
{ designation: 'Murs parpaings', quantite: 40, unite: 'm²', prixUnitaire: 85 },
{ designation: 'Charpente', quantite: 20, unite: 'm²', prixUnitaire: 95 },
{ designation: 'Couverture', quantite: 22, unite: 'm²', prixUnitaire: 65 }
],
tauxTVA: 20,
actif: true,
dateCreation: new Date('2024-02-10'),
utilisations: 15
},
{
id: '3',
nom: 'Installation Électrique Complète',
description: 'Template pour installation électrique maison 100m²',
categorie: 'ELECTRICITE',
lignes: [
{ designation: 'Tableau électrique', quantite: 1, unite: 'unité', prixUnitaire: 450 },
{ designation: 'Prises de courant', quantite: 25, unite: 'unité', prixUnitaire: 35 },
{ designation: 'Points lumineux', quantite: 15, unite: 'unité', prixUnitaire: 45 },
{ designation: 'Câblage', quantite: 1, unite: 'forfait', prixUnitaire: 1200 }
],
tauxTVA: 20,
actif: true,
dateCreation: new Date('2024-03-05'),
utilisations: 31
}
];
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: '',
categorie: '',
lignes: [],
tauxTVA: 20,
actif: true
});
setShowDialog(true);
};
const handleEdit = (template: DevisTemplate) => {
setEditingTemplate(template);
setFormData({ ...template });
setShowDialog(true);
};
const handleSave = async () => {
try {
if (editingTemplate) {
// TODO: Appel API pour mise à jour
// await devisService.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 devisService.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: DevisTemplate) => {
try {
// TODO: Appel API pour suppression
// await devisService.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: DevisTemplate) => {
try {
// TODO: Créer un nouveau devis basé sur le template
toast.current?.show({
severity: 'info',
summary: 'Info',
detail: 'Redirection vers la création de devis...'
});
// Simuler la redirection
setTimeout(() => {
window.location.href = `/devis/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: DevisTemplate) => [
{
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: DevisTemplate) => (
<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 categorieBodyTemplate = (rowData: DevisTemplate) => {
const categorie = categorieOptions.find(opt => opt.value === rowData.categorie);
return (
<Tag
value={categorie?.label || rowData.categorie}
severity="info"
/>
);
};
const statutBodyTemplate = (rowData: DevisTemplate) => (
<Tag
value={rowData.actif ? 'Actif' : 'Inactif'}
severity={rowData.actif ? 'success' : 'danger'}
/>
);
const utilisationsBodyTemplate = (rowData: DevisTemplate) => (
<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 Devis</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-edit 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">Catégories</span>
<div className="text-900 font-medium text-xl">
{new Set(templates.map(t => t.categorie)).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="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="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">
<h6>Prestations du template</h6>
<p className="text-sm text-600 mb-3">
Les prestations 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 prestation définie</p>
<Button
label="Ajouter des prestations"
icon="pi pi-plus"
className="p-button-outlined p-button-sm"
onClick={() => {
// TODO: Ouvrir dialog pour ajouter des prestations
toast.current?.show({
severity: 'info',
summary: 'Info',
detail: 'Fonctionnalité en cours de développement'
});
}}
/>
</div>
)}
</div>
</div>
</Dialog>
</div>
);
};
export default DevisTemplatesPage;