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

698 lines
28 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { InputTextarea } from 'primereact/inputtextarea';
import { InputNumber } from 'primereact/inputnumber';
import { Checkbox } from 'primereact/checkbox';
import { Toast } from 'primereact/toast';
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
import { TreeTable } from 'primereact/treetable';
import { Card } from 'primereact/card';
import { Badge } from 'primereact/badge';
import { ProgressBar } from 'primereact/progressbar';
import { apiClient } from '../../../../services/api-client';
/**
* Interface pour les templates de tâches
*/
interface TacheTemplate {
id: string;
nom: string;
description?: string;
ordreExecution: number;
dureeEstimeeMinutes?: number;
critique: boolean;
bloquante: boolean;
priorite: 'BASSE' | 'NORMALE' | 'HAUTE';
niveauQualification?: 'MANOEUVRE' | 'OUVRIER_SPECIALISE' | 'OUVRIER_QUALIFIE' | 'COMPAGNON' | 'CHEF_EQUIPE' | 'TECHNICIEN' | 'EXPERT';
nombreOperateursRequis: number;
outilsRequis?: string[];
materiauxRequis?: string[];
conditionsMeteo: 'TOUS_TEMPS' | 'TEMPS_SEC' | 'PAS_DE_VENT_FORT' | 'TEMPERATURE_POSITIVE' | 'PAS_DE_PLUIE' | 'INTERIEUR_UNIQUEMENT';
sousPhaseParent: {
id: string;
nom: string;
};
actif: boolean;
}
interface SousPhaseTemplate {
id: string;
nom: string;
description?: string;
ordreExecution: number;
taches: TacheTemplate[];
statistiques?: {
totalTaches: number;
tachesCritiques: number;
tachesBloquantes: number;
dureeEstimeeMinutes: number;
};
}
interface PhaseTemplate {
id: string;
nom: string;
description?: string;
ordreExecution: number;
sousPhases: SousPhaseTemplate[];
}
/**
* Page de gestion granulaire des templates de tâches
* Permet la gestion complète de la hiérarchie Phase → Sous-phase → Tâche
*/
const GestionTachesTemplates = () => {
const toast = useRef<Toast>(null);
// États pour les données
const [phases, setPhases] = useState<PhaseTemplate[]>([]);
const [selectedPhase, setSelectedPhase] = useState<PhaseTemplate | null>(null);
const [selectedSousPhase, setSelectedSousPhase] = useState<SousPhaseTemplate | null>(null);
const [taches, setTaches] = useState<TacheTemplate[]>([]);
// États pour les modals
const [showTacheDialog, setShowTacheDialog] = useState(false);
const [showSousPhaseDialog, setShowSousPhaseDialog] = useState(false);
const [editingTache, setEditingTache] = useState<TacheTemplate | null>(null);
// États pour les formulaires
const [formTache, setFormTache] = useState<Partial<TacheTemplate>>({
nom: '',
description: '',
critique: false,
bloquante: false,
priorite: 'NORMALE',
nombreOperateursRequis: 1,
conditionsMeteo: 'TOUS_TEMPS',
actif: true
});
// États pour l'interface
const [loading, setLoading] = useState(false);
const [expandedRows, setExpandedRows] = useState({});
// Options pour les dropdowns
const prioriteOptions = [
{ label: 'Basse', value: 'BASSE' },
{ label: 'Normale', value: 'NORMALE' },
{ label: 'Haute', value: 'HAUTE' }
];
const qualificationOptions = [
{ label: 'Manœuvre', value: 'MANOEUVRE' },
{ label: 'Ouvrier spécialisé', value: 'OUVRIER_SPECIALISE' },
{ label: 'Ouvrier qualifié', value: 'OUVRIER_QUALIFIE' },
{ label: 'Compagnon', value: 'COMPAGNON' },
{ label: 'Chef d\'équipe', value: 'CHEF_EQUIPE' },
{ label: 'Technicien', value: 'TECHNICIEN' },
{ label: 'Expert', value: 'EXPERT' }
];
const meteoOptions = [
{ label: 'Tous temps', value: 'TOUS_TEMPS' },
{ label: 'Temps sec uniquement', value: 'TEMPS_SEC' },
{ label: 'Pas de vent fort', value: 'PAS_DE_VENT_FORT' },
{ label: 'Température positive', value: 'TEMPERATURE_POSITIVE' },
{ label: 'Pas de pluie', value: 'PAS_DE_PLUIE' },
{ label: 'Intérieur uniquement', value: 'INTERIEUR_UNIQUEMENT' }
];
// Chargement des données au montage
useEffect(() => {
loadPhaseTemplates();
}, []);
// Chargement des tâches quand une sous-phase est sélectionnée
useEffect(() => {
if (selectedSousPhase) {
loadTachesForSousPhase(selectedSousPhase.id);
}
}, [selectedSousPhase]);
/**
* Charge tous les templates de phases pour MAISON_INDIVIDUELLE
*/
const loadPhaseTemplates = async () => {
try {
setLoading(true);
const response = await apiClient.get('/phase-templates/by-type/MAISON_INDIVIDUELLE');
// Pour chaque phase, charger ses sous-phases et tâches
const phasesWithDetails = await Promise.all(
response.data.map(async (phase: PhaseTemplate) => {
const sousPhases = await loadSousPhasesForPhase(phase.id);
return { ...phase, sousPhases };
})
);
setPhases(phasesWithDetails);
if (phasesWithDetails.length > 0) {
setSelectedPhase(phasesWithDetails[0]);
}
} catch (error) {
showToast('error', 'Erreur', 'Impossible de charger les templates de phases');
} finally {
setLoading(false);
}
};
/**
* Charge les sous-phases pour une phase donnée
*/
const loadSousPhasesForPhase = async (phaseId: string): Promise<SousPhaseTemplate[]> => {
try {
const response = await apiClient.get(`/sous-phase-templates/by-phase/${phaseId}`);
// Pour chaque sous-phase, charger ses tâches et statistiques
const sousPhasesWithTaches = await Promise.all(
response.data.map(async (sousPhase: SousPhaseTemplate) => {
const [taches, statistiques] = await Promise.all([
loadTachesForSousPhase(sousPhase.id),
loadStatistiquesForSousPhase(sousPhase.id)
]);
return { ...sousPhase, taches, statistiques };
})
);
return sousPhasesWithTaches;
} catch (error) {
console.error('Erreur lors du chargement des sous-phases:', error);
return [];
}
};
/**
* Charge les tâches pour une sous-phase donnée
*/
const loadTachesForSousPhase = async (sousPhaseId: string): Promise<TacheTemplate[]> => {
try {
const response = await apiClient.get(`/tache-templates/by-sous-phase/${sousPhaseId}`);
setTaches(response.data);
return response.data;
} catch (error) {
console.error('Erreur lors du chargement des tâches:', error);
return [];
}
};
/**
* Charge les statistiques pour une sous-phase donnée
*/
const loadStatistiquesForSousPhase = async (sousPhaseId: string) => {
try {
const response = await apiClient.get(`/tache-templates/stats/by-sous-phase/${sousPhaseId}`);
return response.data;
} catch (error) {
console.error('Erreur lors du chargement des statistiques:', error);
return null;
}
};
/**
* Affiche un toast message
*/
const showToast = (severity: 'success' | 'info' | 'warn' | 'error', summary: string, detail: string) => {
toast.current?.show({ severity, summary, detail, life: 3000 });
};
/**
* Ouvre le dialog de création/édition d'une tâche
*/
const openTacheDialog = (tache?: TacheTemplate) => {
if (!selectedSousPhase) {
showToast('warn', 'Attention', 'Veuillez d\'abord sélectionner une sous-phase');
return;
}
if (tache) {
setEditingTache(tache);
setFormTache({
nom: tache.nom,
description: tache.description,
dureeEstimeeMinutes: tache.dureeEstimeeMinutes,
critique: tache.critique,
bloquante: tache.bloquante,
priorite: tache.priorite,
niveauQualification: tache.niveauQualification,
nombreOperateursRequis: tache.nombreOperateursRequis,
conditionsMeteo: tache.conditionsMeteo,
outilsRequis: tache.outilsRequis,
materiauxRequis: tache.materiauxRequis
});
} else {
setEditingTache(null);
setFormTache({
nom: '',
description: '',
critique: false,
bloquante: false,
priorite: 'NORMALE',
nombreOperateursRequis: 1,
conditionsMeteo: 'TOUS_TEMPS',
actif: true
});
}
setShowTacheDialog(true);
};
/**
* Sauvegarde une tâche (création ou modification)
*/
const saveTache = async () => {
if (!selectedSousPhase || !formTache.nom?.trim()) {
showToast('warn', 'Attention', 'Le nom de la tâche est obligatoire');
return;
}
try {
const tacheData = {
...formTache,
sousPhaseParent: { id: selectedSousPhase.id }
};
if (editingTache) {
await apiClient.put(`/tache-templates/${editingTache.id}`, tacheData);
showToast('success', 'Succès', 'Tâche modifiée avec succès');
} else {
await apiClient.post('/tache-templates', tacheData);
showToast('success', 'Succès', 'Tâche créée avec succès');
}
setShowTacheDialog(false);
await loadTachesForSousPhase(selectedSousPhase.id);
await loadPhaseTemplates(); // Recharger pour mettre à jour les statistiques
} catch (error) {
showToast('error', 'Erreur', 'Impossible de sauvegarder la tâche');
}
};
/**
* Supprime une tâche avec confirmation
*/
const deleteTache = (tache: TacheTemplate) => {
confirmDialog({
message: `Êtes-vous sûr de vouloir supprimer la tâche "${tache.nom}" ?`,
header: 'Confirmation de suppression',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Oui',
rejectLabel: 'Non',
accept: async () => {
try {
await apiClient.delete(`/tache-templates/${tache.id}`);
showToast('success', 'Succès', 'Tâche supprimée avec succès');
await loadTachesForSousPhase(selectedSousPhase!.id);
await loadPhaseTemplates();
} catch (error) {
showToast('error', 'Erreur', 'Impossible de supprimer la tâche');
}
}
});
};
/**
* Template pour l'affichage des actions dans le tableau
*/
const actionBodyTemplate = (rowData: TacheTemplate) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-pencil"
className="p-button-sm p-button-text p-button-rounded"
onClick={() => openTacheDialog(rowData)}
tooltip="Modifier"
/>
<Button
icon="pi pi-trash"
className="p-button-sm p-button-text p-button-rounded p-button-danger"
onClick={() => deleteTache(rowData)}
tooltip="Supprimer"
/>
</div>
);
};
/**
* Template pour l'affichage de la priorité
*/
const prioriteBodyTemplate = (rowData: TacheTemplate) => {
const severity = rowData.priorite === 'HAUTE' ? 'danger' :
rowData.priorite === 'NORMALE' ? 'info' : 'success';
return <Badge value={rowData.priorite} severity={severity} />;
};
/**
* Template pour l'affichage de la durée
*/
const dureeBodyTemplate = (rowData: TacheTemplate) => {
if (!rowData.dureeEstimeeMinutes) return '-';
const heures = Math.floor(rowData.dureeEstimeeMinutes / 60);
const minutes = rowData.dureeEstimeeMinutes % 60;
if (heures > 0) {
return `${heures}h${minutes > 0 ? ` ${minutes}min` : ''}`;
}
return `${minutes}min`;
};
/**
* Template pour l'affichage des indicateurs critique/bloquante
*/
const indicateursBodyTemplate = (rowData: TacheTemplate) => {
return (
<div className="flex gap-1">
{rowData.critique && (
<Badge value="C" severity="danger" className="p-badge-sm" />
)}
{rowData.bloquante && (
<Badge value="B" severity="warning" className="p-badge-sm" />
)}
</div>
);
};
/**
* Template pour l'affichage des statistiques d'une sous-phase
*/
const renderSousPhaseStats = (sousPhase: SousPhaseTemplate) => {
const stats = sousPhase.statistiques;
if (!stats) return null;
const pourcentageCritiques = stats.totalTaches > 0 ?
(stats.tachesCritiques / stats.totalTaches * 100) : 0;
const dureeHeures = stats.dureeEstimeeMinutes / 60;
return (
<div className="flex gap-4 text-sm">
<span><i className="pi pi-list mr-1"></i>{stats.totalTaches} tâches</span>
<span><i className="pi pi-exclamation-triangle mr-1"></i>{stats.tachesCritiques} critiques</span>
<span><i className="pi pi-lock mr-1"></i>{stats.tachesBloquantes} bloquantes</span>
<span><i className="pi pi-clock mr-1"></i>{dureeHeures.toFixed(1)}h</span>
</div>
);
};
return (
<div className="grid">
<Toast ref={toast} />
<ConfirmDialog />
{/* Header */}
<div className="col-12">
<div className="card">
<div className="flex justify-content-between align-items-center">
<div>
<h5>Gestion Granulaire des Tâches</h5>
<p className="text-600 mt-0">
Système de tracking détaillé : Phase Sous-phase Tâche
</p>
</div>
<Button
label="Nouvelle Tâche"
icon="pi pi-plus"
onClick={() => openTacheDialog()}
disabled={!selectedSousPhase}
/>
</div>
</div>
</div>
{/* Sélection de Phase et Sous-phase */}
<div className="col-12">
<div className="grid">
{/* Phases */}
<div className="col-12 md:col-6">
<Card title="Phases" className="h-full">
<div className="flex flex-column gap-3">
{phases.map((phase) => (
<div
key={phase.id}
className={`p-3 border-round cursor-pointer transition-colors ${
selectedPhase?.id === phase.id
? 'bg-blue-50 border-blue-200 border-2'
: 'bg-gray-50 border-gray-200 border-1'
}`}
onClick={() => {
setSelectedPhase(phase);
setSelectedSousPhase(null);
setTaches([]);
}}
>
<div className="flex justify-content-between align-items-center">
<div>
<div className="font-semibold">{phase.nom}</div>
<div className="text-sm text-600">
{phase.sousPhases?.length || 0} sous-phases
</div>
</div>
<Badge value={phase.ordreExecution} className="p-badge-lg" />
</div>
</div>
))}
</div>
</Card>
</div>
{/* Sous-phases */}
<div className="col-12 md:col-6">
<Card title={`Sous-phases ${selectedPhase ? `- ${selectedPhase.nom}` : ''}`} className="h-full">
{selectedPhase ? (
<div className="flex flex-column gap-3">
{selectedPhase.sousPhases?.map((sousPhase) => (
<div
key={sousPhase.id}
className={`p-3 border-round cursor-pointer transition-colors ${
selectedSousPhase?.id === sousPhase.id
? 'bg-green-50 border-green-200 border-2'
: 'bg-gray-50 border-gray-200 border-1'
}`}
onClick={() => setSelectedSousPhase(sousPhase)}
>
<div className="flex justify-content-between align-items-start mb-2">
<div className="flex-1">
<div className="font-semibold">{sousPhase.nom}</div>
{sousPhase.description && (
<div className="text-sm text-600 mt-1">
{sousPhase.description}
</div>
)}
</div>
<Badge value={sousPhase.ordreExecution} />
</div>
{renderSousPhaseStats(sousPhase)}
</div>
))}
</div>
) : (
<p className="text-center text-500 mt-4">
Sélectionnez une phase pour voir ses sous-phases
</p>
)}
</Card>
</div>
</div>
</div>
{/* Tableau des Tâches */}
<div className="col-12">
<Card title={`Tâches ${selectedSousPhase ? `- ${selectedSousPhase.nom}` : ''}`}>
{selectedSousPhase ? (
<DataTable
value={taches}
loading={loading}
emptyMessage="Aucune tâche trouvée"
paginator
rows={10}
className="p-datatable-gridlines"
responsiveLayout="scroll"
>
<Column
field="ordreExecution"
header="Ordre"
style={{ width: '80px' }}
sortable
/>
<Column
field="nom"
header="Nom de la tâche"
sortable
/>
<Column
field="dureeEstimeeMinutes"
header="Durée"
body={dureeBodyTemplate}
style={{ width: '100px' }}
sortable
/>
<Column
field="priorite"
header="Priorité"
body={prioriteBodyTemplate}
style={{ width: '120px' }}
sortable
/>
<Column
field="nombreOperateursRequis"
header="Opérateurs"
style={{ width: '100px' }}
sortable
/>
<Column
header="Indicateurs"
body={indicateursBodyTemplate}
style={{ width: '100px' }}
/>
<Column
field="niveauQualification"
header="Qualification"
style={{ width: '140px' }}
sortable
/>
<Column
header="Actions"
body={actionBodyTemplate}
style={{ width: '120px' }}
/>
</DataTable>
) : (
<p className="text-center text-500 mt-4">
Sélectionnez une sous-phase pour voir ses tâches
</p>
)}
</Card>
</div>
{/* Dialog de Création/Édition de Tâche */}
<Dialog
header={editingTache ? 'Modifier la tâche' : 'Nouvelle tâche'}
visible={showTacheDialog}
onHide={() => setShowTacheDialog(false)}
style={{ width: '60vw' }}
breakpoints={{ '960px': '75vw', '641px': '90vw' }}
modal
>
<div className="grid formgrid p-fluid">
<div className="col-12 md:col-6 field">
<label htmlFor="nom">Nom de la tâche *</label>
<InputText
id="nom"
value={formTache.nom || ''}
onChange={(e) => setFormTache({ ...formTache, nom: e.target.value })}
placeholder="Nom de la tâche..."
/>
</div>
<div className="col-12 md:col-6 field">
<label htmlFor="duree">Durée estimée (minutes)</label>
<InputNumber
id="duree"
value={formTache.dureeEstimeeMinutes}
onValueChange={(e) => setFormTache({ ...formTache, dureeEstimeeMinutes: e.value || 0 })}
min={0}
placeholder="Durée en minutes"
/>
</div>
<div className="col-12 field">
<label htmlFor="description">Description</label>
<InputTextarea
id="description"
value={formTache.description || ''}
onChange={(e) => setFormTache({ ...formTache, description: e.target.value })}
rows={3}
placeholder="Description détaillée de la tâche..."
/>
</div>
<div className="col-12 md:col-4 field">
<label htmlFor="priorite">Priorité</label>
<Dropdown
id="priorite"
value={formTache.priorite}
options={prioriteOptions}
onChange={(e) => setFormTache({ ...formTache, priorite: e.value })}
/>
</div>
<div className="col-12 md:col-4 field">
<label htmlFor="operateurs">Nombre d'opérateurs</label>
<InputNumber
id="operateurs"
value={formTache.nombreOperateursRequis}
onValueChange={(e) => setFormTache({ ...formTache, nombreOperateursRequis: e.value || 1 })}
min={1}
max={10}
/>
</div>
<div className="col-12 md:col-4 field">
<label htmlFor="qualification">Niveau de qualification</label>
<Dropdown
id="qualification"
value={formTache.niveauQualification}
options={qualificationOptions}
onChange={(e) => setFormTache({ ...formTache, niveauQualification: e.value })}
placeholder="Sélectionner..."
/>
</div>
<div className="col-12 md:col-6 field">
<label htmlFor="meteo">Conditions météorologiques</label>
<Dropdown
id="meteo"
value={formTache.conditionsMeteo}
options={meteoOptions}
onChange={(e) => setFormTache({ ...formTache, conditionsMeteo: e.value })}
/>
</div>
<div className="col-12 md:col-6 field">
<div className="flex flex-column gap-3">
<div className="flex align-items-center">
<Checkbox
inputId="critique"
checked={formTache.critique || false}
onChange={(e) => setFormTache({ ...formTache, critique: e.checked })}
/>
<label htmlFor="critique" className="ml-2">Tâche critique</label>
</div>
<div className="flex align-items-center">
<Checkbox
inputId="bloquante"
checked={formTache.bloquante || false}
onChange={(e) => setFormTache({ ...formTache, bloquante: e.checked })}
/>
<label htmlFor="bloquante" className="ml-2">Tâche bloquante</label>
</div>
</div>
</div>
</div>
<div className="flex justify-content-end gap-2 mt-4">
<Button
label="Annuler"
icon="pi pi-times"
className="p-button-text"
onClick={() => setShowTacheDialog(false)}
/>
<Button
label="Sauvegarder"
icon="pi pi-check"
onClick={saveTache}
/>
</div>
</Dialog>
</div>
);
};
export default GestionTachesTemplates;