Files
btpxpress-frontend/app/(main)/devis/workflow/[id]/page.tsx

528 lines
22 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { Steps } from 'primereact/steps';
import { Timeline } from 'primereact/timeline';
import { Tag } from 'primereact/tag';
import { Badge } from 'primereact/badge';
import { Toast } from 'primereact/toast';
import { ProgressSpinner } from 'primereact/progressspinner';
import { Toolbar } from 'primereact/toolbar';
import { Dialog } from 'primereact/dialog';
import { InputTextarea } from 'primereact/inputtextarea';
import { Dropdown } from 'primereact/dropdown';
import { Calendar } from 'primereact/calendar';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { devisService } from '../../../../../services/api';
import { formatDate, formatCurrency } from '../../../../../utils/formatters';
import type { Devis } from '../../../../../types/btp';
interface WorkflowStep {
id: string;
nom: string;
description: string;
statut: 'EN_ATTENTE' | 'EN_COURS' | 'TERMINE' | 'BLOQUE';
dateDebut?: Date;
dateFin?: Date;
responsable?: string;
commentaires?: string;
documents?: string[];
}
interface WorkflowEvent {
date: Date;
type: 'CREATION' | 'MODIFICATION' | 'VALIDATION' | 'REFUS' | 'COMMENTAIRE' | 'DOCUMENT';
description: string;
utilisateur: string;
details?: any;
}
const DevisWorkflowPage = () => {
const params = useParams();
const router = useRouter();
const toast = useRef<Toast>(null);
const [devis, setDevis] = useState<Devis | null>(null);
const [workflowSteps, setWorkflowSteps] = useState<WorkflowStep[]>([]);
const [workflowEvents, setWorkflowEvents] = useState<WorkflowEvent[]>([]);
const [loading, setLoading] = useState(true);
const [activeStep, setActiveStep] = useState(0);
const [showActionDialog, setShowActionDialog] = useState(false);
const [actionType, setActionType] = useState<string>('');
const [actionComment, setActionComment] = useState('');
const [actionDate, setActionDate] = useState<Date>(new Date());
const devisId = params.id as string;
const actionOptions = [
{ label: 'Valider l\'étape', value: 'VALIDER' },
{ label: 'Rejeter', value: 'REJETER' },
{ label: 'Demander modification', value: 'MODIFIER' },
{ label: 'Mettre en attente', value: 'ATTENDRE' },
{ label: 'Ajouter commentaire', value: 'COMMENTER' }
];
useEffect(() => {
loadWorkflowData();
}, [devisId]);
const loadWorkflowData = async () => {
try {
setLoading(true);
// Charger le devis
const devisData = await devisService.getById(devisId);
setDevis(devisData);
// TODO: Charger les données de workflow depuis l'API
// const workflowResponse = await devisService.getWorkflow(devisId);
// Données simulées pour la démonstration
const mockSteps: WorkflowStep[] = [
{
id: '1',
nom: 'Création du devis',
description: 'Rédaction initiale du devis avec toutes les prestations',
statut: 'TERMINE',
dateDebut: new Date('2024-01-15'),
dateFin: new Date('2024-01-16'),
responsable: 'Jean Dupont',
commentaires: 'Devis créé selon les spécifications client'
},
{
id: '2',
nom: 'Validation technique',
description: 'Vérification de la faisabilité technique et des quantités',
statut: 'TERMINE',
dateDebut: new Date('2024-01-16'),
dateFin: new Date('2024-01-17'),
responsable: 'Marie Martin',
commentaires: 'Validation technique OK, ajustement des quantités'
},
{
id: '3',
nom: 'Validation commerciale',
description: 'Vérification des prix et des conditions commerciales',
statut: 'EN_COURS',
dateDebut: new Date('2024-01-17'),
responsable: 'Pierre Durand'
},
{
id: '4',
nom: 'Envoi au client',
description: 'Transmission du devis au client pour validation',
statut: 'EN_ATTENTE',
responsable: 'Sophie Bernard'
},
{
id: '5',
nom: 'Suivi client',
description: 'Relance et négociation avec le client',
statut: 'EN_ATTENTE'
}
];
const mockEvents: WorkflowEvent[] = [
{
date: new Date('2024-01-15T09:00:00'),
type: 'CREATION',
description: 'Création du devis #DEV-2024-001',
utilisateur: 'Jean Dupont'
},
{
date: new Date('2024-01-16T14:30:00'),
type: 'VALIDATION',
description: 'Validation de l\'étape "Création du devis"',
utilisateur: 'Jean Dupont'
},
{
date: new Date('2024-01-16T15:00:00'),
type: 'MODIFICATION',
description: 'Ajustement des quantités de carrelage',
utilisateur: 'Marie Martin'
},
{
date: new Date('2024-01-17T10:15:00'),
type: 'VALIDATION',
description: 'Validation technique approuvée',
utilisateur: 'Marie Martin'
},
{
date: new Date('2024-01-17T11:00:00'),
type: 'COMMENTAIRE',
description: 'Demande de révision des prix pour être plus compétitif',
utilisateur: 'Pierre Durand'
}
];
setWorkflowSteps(mockSteps);
setWorkflowEvents(mockEvents);
// Déterminer l'étape active
const currentStepIndex = mockSteps.findIndex(step => step.statut === 'EN_COURS');
setActiveStep(currentStepIndex >= 0 ? currentStepIndex : mockSteps.findIndex(step => step.statut === 'EN_ATTENTE'));
} catch (error) {
console.error('Erreur lors du chargement du workflow:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données du workflow'
});
} finally {
setLoading(false);
}
};
const handleAction = async () => {
try {
if (!actionType || !actionComment.trim()) {
toast.current?.show({
severity: 'warn',
summary: 'Attention',
detail: 'Veuillez sélectionner une action et ajouter un commentaire'
});
return;
}
// TODO: Appel API pour exécuter l'action
// await devisService.executeWorkflowAction(devisId, {
// type: actionType,
// commentaire: actionComment,
// date: actionDate
// });
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Action exécutée avec succès'
});
setShowActionDialog(false);
setActionComment('');
loadWorkflowData();
} catch (error) {
console.error('Erreur lors de l\'exécution de l\'action:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de l\'exécution de l\'action'
});
}
};
const getStepStatus = (step: WorkflowStep, index: number) => {
if (step.statut === 'TERMINE') return 'success';
if (step.statut === 'EN_COURS') return 'info';
if (step.statut === 'BLOQUE') return 'danger';
return 'secondary';
};
const getEventIcon = (type: string) => {
switch (type) {
case 'CREATION': return 'pi pi-plus';
case 'MODIFICATION': return 'pi pi-pencil';
case 'VALIDATION': return 'pi pi-check';
case 'REFUS': return 'pi pi-times';
case 'COMMENTAIRE': return 'pi pi-comment';
case 'DOCUMENT': return 'pi pi-file';
default: return 'pi pi-circle';
}
};
const getEventColor = (type: string) => {
switch (type) {
case 'CREATION': return '#3B82F6';
case 'MODIFICATION': return '#F59E0B';
case 'VALIDATION': return '#10B981';
case 'REFUS': return '#EF4444';
case 'COMMENTAIRE': return '#8B5CF6';
case 'DOCUMENT': return '#6B7280';
default: return '#6B7280';
}
};
const toolbarStartTemplate = () => (
<div className="flex align-items-center gap-2">
<Button
icon="pi pi-arrow-left"
label="Retour"
className="p-button-outlined"
onClick={() => router.push(`/devis/${devisId}`)}
/>
</div>
);
const toolbarEndTemplate = () => (
<div className="flex align-items-center gap-2">
<Button
label="Nouvelle action"
icon="pi pi-cog"
onClick={() => setShowActionDialog(true)}
disabled={!workflowSteps[activeStep] || workflowSteps[activeStep].statut === 'TERMINE'}
/>
</div>
);
if (loading) {
return (
<div className="flex justify-content-center align-items-center min-h-screen">
<ProgressSpinner />
</div>
);
}
if (!devis) {
return (
<div className="flex justify-content-center align-items-center min-h-screen">
<div className="text-center">
<i className="pi pi-exclamation-triangle text-6xl text-orange-500 mb-3"></i>
<h3>Devis introuvable</h3>
<p className="text-600 mb-4">Le devis demandé n'existe pas</p>
<Button
label="Retour à la liste"
icon="pi pi-arrow-left"
onClick={() => router.push('/devis')}
/>
</div>
</div>
);
}
return (
<div className="grid">
<Toast ref={toast} />
<div className="col-12">
<Toolbar start={toolbarStartTemplate} end={toolbarEndTemplate} />
</div>
{/* En-tête du devis */}
<div className="col-12">
<Card>
<div className="flex justify-content-between align-items-start mb-4">
<div>
<h2 className="text-2xl font-bold mb-2">Workflow - Devis #{devis.numero}</h2>
<p className="text-600 mb-3">{devis.objet}</p>
<Tag
value={devis.statut}
severity={devis.statut === 'ACCEPTE' ? 'success' : 'info'}
className="mb-2"
/>
</div>
<div className="text-right">
<div className="text-3xl font-bold text-primary mb-2">
{formatCurrency(devis.montantTTC)}
</div>
<div className="text-sm text-600">
Client: {typeof devis.client === 'string' ? devis.client : devis.client?.nom}
</div>
</div>
</div>
</Card>
</div>
{/* Étapes du workflow */}
<div className="col-12">
<Card title="Progression du workflow">
<Steps
model={workflowSteps.map((step, index) => ({
label: step.nom,
command: () => setActiveStep(index)
}))}
activeIndex={activeStep}
className="mb-4"
/>
{workflowSteps[activeStep] && (
<div className="mt-4 p-4 border-round bg-blue-50">
<div className="flex justify-content-between align-items-start mb-3">
<div>
<h5 className="text-blue-900 mb-2">{workflowSteps[activeStep].nom}</h5>
<p className="text-blue-800 mb-2">{workflowSteps[activeStep].description}</p>
</div>
<Tag
value={workflowSteps[activeStep].statut}
severity={getStepStatus(workflowSteps[activeStep], activeStep) as any}
/>
</div>
<div className="grid">
{workflowSteps[activeStep].responsable && (
<div className="col-12 md:col-4">
<label className="font-semibold text-blue-900">Responsable:</label>
<p className="text-blue-800">{workflowSteps[activeStep].responsable}</p>
</div>
)}
{workflowSteps[activeStep].dateDebut && (
<div className="col-12 md:col-4">
<label className="font-semibold text-blue-900">Date de début:</label>
<p className="text-blue-800">{formatDate(workflowSteps[activeStep].dateDebut!)}</p>
</div>
)}
{workflowSteps[activeStep].dateFin && (
<div className="col-12 md:col-4">
<label className="font-semibold text-blue-900">Date de fin:</label>
<p className="text-blue-800">{formatDate(workflowSteps[activeStep].dateFin!)}</p>
</div>
)}
{workflowSteps[activeStep].commentaires && (
<div className="col-12">
<label className="font-semibold text-blue-900">Commentaires:</label>
<p className="text-blue-800">{workflowSteps[activeStep].commentaires}</p>
</div>
)}
</div>
</div>
)}
</Card>
</div>
{/* Historique des événements */}
<div className="col-12 lg:col-8">
<Card title="Historique des événements">
<Timeline
value={workflowEvents}
opposite={(item) => (
<div className="text-right">
<div className="text-sm font-semibold">{formatDate(item.date)}</div>
<div className="text-xs text-600">{item.utilisateur}</div>
</div>
)}
content={(item) => (
<div className="flex align-items-center">
<Badge
value={item.type}
style={{ backgroundColor: getEventColor(item.type) }}
className="mr-2"
/>
<div>
<div className="font-semibold">{item.description}</div>
{item.details && (
<div className="text-sm text-600 mt-1">{item.details}</div>
)}
</div>
</div>
)}
marker={(item) => (
<span
className={`flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1 shadow-1`}
style={{ backgroundColor: getEventColor(item.type) }}
>
<i className={getEventIcon(item.type)}></i>
</span>
)}
/>
</Card>
</div>
{/* Résumé des étapes */}
<div className="col-12 lg:col-4">
<Card title="Résumé des étapes">
<DataTable value={workflowSteps} responsiveLayout="scroll">
<Column
field="nom"
header="Étape"
body={(rowData, options) => (
<div className="flex align-items-center">
<Badge value={options.rowIndex + 1} className="mr-2" />
<span className={options.rowIndex === activeStep ? 'font-bold' : ''}>
{rowData.nom}
</span>
</div>
)}
/>
<Column
field="statut"
header="Statut"
body={(rowData, options) => (
<Tag
value={rowData.statut}
severity={getStepStatus(rowData, options.rowIndex) as any}
/>
)}
/>
</DataTable>
</Card>
</div>
{/* Dialog d'action */}
<Dialog
header="Nouvelle action sur le workflow"
visible={showActionDialog}
onHide={() => setShowActionDialog(false)}
style={{ width: '600px' }}
footer={
<div className="flex justify-content-end gap-2">
<Button
label="Annuler"
icon="pi pi-times"
className="p-button-outlined"
onClick={() => setShowActionDialog(false)}
/>
<Button
label="Exécuter"
icon="pi pi-check"
onClick={handleAction}
/>
</div>
}
>
<div className="grid">
<div className="col-12">
<div className="field">
<label htmlFor="actionType" className="font-semibold">Type d'action *</label>
<Dropdown
id="actionType"
value={actionType}
options={actionOptions}
onChange={(e) => setActionType(e.value)}
className="w-full"
placeholder="Sélectionner une action"
/>
</div>
</div>
<div className="col-12">
<div className="field">
<label htmlFor="actionDate" className="font-semibold">Date d'exécution</label>
<Calendar
id="actionDate"
value={actionDate}
onChange={(e) => setActionDate(e.value || new Date())}
className="w-full"
dateFormat="dd/mm/yy"
showTime
/>
</div>
</div>
<div className="col-12">
<div className="field">
<label htmlFor="actionComment" className="font-semibold">Commentaire *</label>
<InputTextarea
id="actionComment"
value={actionComment}
onChange={(e) => setActionComment(e.target.value)}
className="w-full"
rows={4}
placeholder="Décrivez l'action effectuée ou la raison de cette action..."
/>
</div>
</div>
</div>
</Dialog>
</div>
);
};
export default DevisWorkflowPage;