Initial commit
This commit is contained in:
527
app/(main)/devis/workflow/[id]/page.tsx
Normal file
527
app/(main)/devis/workflow/[id]/page.tsx
Normal file
@@ -0,0 +1,527 @@
|
||||
'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 devisResponse = await devisService.getById(devisId);
|
||||
setDevis(devisResponse.data);
|
||||
|
||||
// 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)}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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;
|
||||
Reference in New Issue
Block a user