'use client'; import React, { useState, useEffect, useRef } from 'react'; import { Card } from 'primereact/card'; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import { Button } from 'primereact/button'; import { Tag } from 'primereact/tag'; import { Dialog } from 'primereact/dialog'; import { InputTextarea } from 'primereact/inputtextarea'; import { Toast } from 'primereact/toast'; import { Divider } from 'primereact/divider'; import { ProgressBar } from 'primereact/progressbar'; import { FileUpload } from 'primereact/fileupload'; import { Dropdown } from 'primereact/dropdown'; import { Calendar } from 'primereact/calendar'; import { Badge } from 'primereact/badge'; import { Timeline } from 'primereact/timeline'; /** * Page Workflow Devis BTP Express * Gestion cycle de vie complet des devis avec génération PDF et signature électronique */ const WorkflowDevis = () => { const [devis, setDevis] = useState([]); const [loading, setLoading] = useState(true); const [workflowDialog, setWorkflowDialog] = useState(false); const [pdfDialog, setPdfDialog] = useState(false); const [signatureDialog, setSignatureDialog] = useState(false); const [selectedDevis, setSelectedDevis] = useState(null); const [nouveauStatut, setNouveauStatut] = useState(''); const [commentaire, setCommentaire] = useState(''); const [historique, setHistorique] = useState([]); const [modeleDevis, setModeleDevis] = useState(''); const [dateValidite, setDateValidite] = useState(null); const toast = useRef(null); // Workflow états devis BTP const workflowTransitions = { 'BROUILLON': [ { label: 'Envoyer au client', value: 'ENVOYE', icon: 'pi-send', color: 'info', action: 'SEND_EMAIL' }, { label: 'Générer PDF', value: 'BROUILLON', icon: 'pi-file-pdf', color: 'help', action: 'GENERATE_PDF' } ], 'ENVOYE': [ { label: 'Marquer accepté', value: 'ACCEPTE', icon: 'pi-check', color: 'success', action: 'ACCEPT' }, { label: 'Marquer refusé', value: 'REFUSE', icon: 'pi-times', color: 'danger', action: 'REJECT' }, { label: 'Relancer client', value: 'ENVOYE', icon: 'pi-refresh', color: 'warning', action: 'REMIND' } ], 'ACCEPTE': [ { label: 'Créer chantier', value: 'ACCEPTE', icon: 'pi-map', color: 'success', action: 'CREATE_CHANTIER' }, { label: 'Générer contrat', value: 'ACCEPTE', icon: 'pi-file-edit', color: 'info', action: 'GENERATE_CONTRACT' } ], 'REFUSE': [ { label: 'Créer nouveau devis', value: 'BROUILLON', icon: 'pi-plus', color: 'info', action: 'CREATE_NEW' } ], 'EXPIRE': [ { label: 'Renouveler', value: 'BROUILLON', icon: 'pi-refresh', color: 'warning', action: 'RENEW' } ] }; const statutsConfig = { 'BROUILLON': { color: 'secondary', icon: 'pi-file-edit', label: 'Brouillon' }, 'ENVOYE': { color: 'info', icon: 'pi-send', label: 'Envoyé' }, 'ACCEPTE': { color: 'success', icon: 'pi-check-circle', label: 'Accepté' }, 'REFUSE': { color: 'danger', icon: 'pi-times-circle', label: 'Refusé' }, 'EXPIRE': { color: 'warning', icon: 'pi-clock', label: 'Expiré' } }; const modelesDevis = [ { label: 'Modèle Standard BTP', value: 'standard_btp' }, { label: 'Modèle Rénovation', value: 'renovation' }, { label: 'Modèle Gros Œuvre', value: 'gros_oeuvre' }, { label: 'Modèle Maintenance', value: 'maintenance' } ]; useEffect(() => { loadDevis(); }, []); const loadDevis = async () => { try { setLoading(true); // Simulation données devis avec workflow const mockDevis = [ { id: 'DEV-2025-001', numero: 'DEV-2025-001', client: 'Claire Rousseau', objet: 'Rénovation cuisine complète', statut: 'ENVOYE', montantHT: 25000, montantTTC: 30000, dateEmission: '2025-01-25', dateValidite: '2025-02-25', dateEnvoi: '2025-01-26', nbRelances: 1, commercial: 'Mme Petit', priorite: 'HAUTE', tempsEcoule: 5, // jours depuis envoi lignes: [ { designation: 'Démolition existant', quantite: 1, prixUnitaire: 3000, total: 3000 }, { designation: 'Mobilier cuisine haut de gamme', quantite: 1, prixUnitaire: 18000, total: 18000 }, { designation: 'Installation plomberie', quantite: 1, prixUnitaire: 2500, total: 2500 }, { designation: 'Installation électrique', quantite: 1, prixUnitaire: 1500, total: 1500 } ] }, { id: 'DEV-2025-002', numero: 'DEV-2025-002', client: 'Jean Dupont', objet: 'Extension maison 40m²', statut: 'BROUILLON', montantHT: 48000, montantTTC: 57600, dateEmission: '2025-01-30', dateValidite: '2025-03-01', dateEnvoi: null, nbRelances: 0, commercial: 'M. Laurent', priorite: 'NORMALE', tempsEcoule: 0, lignes: [ { designation: 'Fondations extension', quantite: 40, prixUnitaire: 150, total: 6000 }, { designation: 'Élévation murs', quantite: 40, prixUnitaire: 300, total: 12000 }, { designation: 'Couverture', quantite: 45, prixUnitaire: 120, total: 5400 }, { designation: 'Cloisons et isolation', quantite: 40, prixUnitaire: 180, total: 7200 }, { designation: 'Électricité et plomberie', quantite: 1, prixUnitaire: 8500, total: 8500 }, { designation: 'Revêtements sols et murs', quantite: 40, prixUnitaire: 220, total: 8800 } ] }, { id: 'DEV-2025-003', numero: 'DEV-2025-003', client: 'Sophie Martin', objet: 'Réfection toiture 120m²', statut: 'ACCEPTE', montantHT: 15000, montantTTC: 18000, dateEmission: '2025-01-20', dateValidite: '2025-02-20', dateEnvoi: '2025-01-21', dateAcceptation: '2025-01-28', nbRelances: 0, commercial: 'M. Thomas', priorite: 'NORMALE', tempsEcoule: 10, lignes: [ { designation: 'Dépose ancienne couverture', quantite: 120, prixUnitaire: 25, total: 3000 }, { designation: 'Contrôle et réfection charpente', quantite: 1, prixUnitaire: 4000, total: 4000 }, { designation: 'Nouvelle couverture tuiles', quantite: 120, prixUnitaire: 45, total: 5400 }, { designation: 'Isolation sous-toiture', quantite: 120, prixUnitaire: 22, total: 2640 } ] } ]; setDevis(mockDevis); } catch (error) { console.error('Erreur chargement devis:', error); } finally { setLoading(false); } }; const ouvrirWorkflow = (devisItem: any) => { setSelectedDevis(devisItem); setNouveauStatut(''); setCommentaire(''); // Charger historique du devis const mockHistorique = [ { date: new Date(devisItem.dateEmission), statut: 'BROUILLON', utilisateur: devisItem.commercial, commentaire: 'Création du devis', action: 'CREATE' } ]; if (devisItem.dateEnvoi) { mockHistorique.push({ date: new Date(devisItem.dateEnvoi), statut: 'ENVOYE', utilisateur: devisItem.commercial, commentaire: 'Devis envoyé par email au client', action: 'SEND_EMAIL' }); } if (devisItem.dateAcceptation) { mockHistorique.push({ date: new Date(devisItem.dateAcceptation), statut: 'ACCEPTE', utilisateur: 'Client', commentaire: 'Devis accepté par signature électronique', action: 'ACCEPT' }); } setHistorique(mockHistorique.reverse()); setWorkflowDialog(true); }; const executerAction = async (action: string) => { if (!selectedDevis) return; try { let messageSucces = ''; let devisMisAJour = { ...selectedDevis }; switch (action) { case 'SEND_EMAIL': devisMisAJour.statut = 'ENVOYE'; devisMisAJour.dateEnvoi = new Date().toISOString().split('T')[0]; messageSucces = 'Devis envoyé par email au client'; break; case 'GENERATE_PDF': setPdfDialog(true); return; case 'ACCEPT': devisMisAJour.statut = 'ACCEPTE'; devisMisAJour.dateAcceptation = new Date().toISOString().split('T')[0]; messageSucces = 'Devis marqué comme accepté'; break; case 'REJECT': devisMisAJour.statut = 'REFUSE'; devisMisAJour.dateRefus = new Date().toISOString().split('T')[0]; messageSucces = 'Devis marqué comme refusé'; break; case 'REMIND': devisMisAJour.nbRelances = (devisMisAJour.nbRelances || 0) + 1; devisMisAJour.dateRelance = new Date().toISOString().split('T')[0]; messageSucces = 'Relance envoyée au client'; break; case 'CREATE_CHANTIER': messageSucces = 'Chantier créé à partir du devis'; break; case 'GENERATE_CONTRACT': messageSucces = 'Contrat généré et envoyé'; break; default: return; } // Mettre à jour la liste const devisUpdated = devis.map(d => d.id === selectedDevis.id ? devisMisAJour : d ); setDevis(devisUpdated); toast.current?.show({ severity: 'success', summary: 'Action réussie', detail: messageSucces, life: 4000 }); setWorkflowDialog(false); } catch (error) { console.error('Erreur action:', error); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible d\'effectuer l\'action', life: 3000 }); } }; const genererPDF = async () => { if (!selectedDevis || !modeleDevis) return; try { // Simulation génération PDF await new Promise(resolve => setTimeout(resolve, 2000)); toast.current?.show({ severity: 'success', summary: 'PDF généré', detail: `Devis ${selectedDevis.numero} généré avec le modèle ${modeleDevis}`, life: 4000 }); setPdfDialog(false); setModeleDevis(''); } catch (error) { toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible de générer le PDF', life: 3000 }); } }; const statutBodyTemplate = (rowData: any) => { const config = statutsConfig[rowData.statut as keyof typeof statutsConfig]; return (
); }; const montantBodyTemplate = (rowData: any) => { return (
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.montantTTC)} HT: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.montantHT)}
); }; const urgenceBodyTemplate = (rowData: any) => { if (rowData.statut === 'ENVOYE') { const joursPasses = rowData.tempsEcoule; const joursRestants = Math.max(0, 30 - joursPasses); // 30 jours de validité standard let severity = 'success'; if (joursRestants <= 5) severity = 'danger'; else if (joursRestants <= 10) severity = 'warning'; return (
{rowData.nbRelances > 0 && ( )}
); } return -; }; const actionsBodyTemplate = (rowData: any) => { return (
); }; return (
{/* Métriques Devis */}
{devis.filter(d => d.statut === 'BROUILLON').length}
Brouillons
{devis.filter(d => d.statut === 'ENVOYE').length}
En attente
{devis.filter(d => d.statut === 'ACCEPTE').length}
Acceptés
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(devis.reduce((sum, d) => sum + (d.statut === 'ACCEPTE' ? d.montantTTC : 0), 0))}
CA potentiel
{/* Tableau devis */}
new Date(rowData.dateEmission).toLocaleDateString('fr-FR')} sortable style={{ minWidth: '100px' }} />
{/* Dialog Workflow */} setWorkflowDialog(false)} maximizable > {selectedDevis && (
{/* Détails devis */}
Client: {selectedDevis.client}
Objet: {selectedDevis.objet}
Statut:
Montant TTC: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(selectedDevis.montantTTC)}
Validité: {new Date(selectedDevis.dateValidite).toLocaleDateString('fr-FR')}
Commercial: {selectedDevis.commercial}
{selectedDevis.nbRelances > 0 && (
Relances:
)}
{/* Actions workflow */}
{workflowTransitions[selectedDevis.statut as keyof typeof workflowTransitions]?.map((transition) => (
{/* Historique */}
(
{item.date.toLocaleString('fr-FR')}
{item.utilisateur}
{item.commentaire}
)} />
)}
{/* Dialog Génération PDF */} setPdfDialog(false)} >
setModeleDevis(e.value)} placeholder="Sélectionnez un modèle" className="w-full" />
setDateValidite(e.value || null)} dateFormat="dd/mm/yy" showIcon className="w-full" />
); }; export default WorkflowDevis;