Initial commit
This commit is contained in:
563
app/(main)/chantiers/[id]/execution-granulaire/page.tsx
Normal file
563
app/(main)/chantiers/[id]/execution-granulaire/page.tsx
Normal file
@@ -0,0 +1,563 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
import { Button } from 'primereact/button';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Accordion, AccordionTab } from 'primereact/accordion';
|
||||
import { TreeTable } from 'primereact/treetable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Avatar } from 'primereact/avatar';
|
||||
import { apiClient } from '../../../../../services/api-client';
|
||||
|
||||
/**
|
||||
* Interface pour les tâches d'exécution avec état de completion
|
||||
*/
|
||||
interface TacheExecution {
|
||||
id: string;
|
||||
nom: string;
|
||||
description?: string;
|
||||
ordreExecution: number;
|
||||
dureeEstimeeMinutes?: number;
|
||||
critique: boolean;
|
||||
bloquante: boolean;
|
||||
priorite: 'BASSE' | 'NORMALE' | 'HAUTE';
|
||||
niveauQualification?: string;
|
||||
nombreOperateursRequis: number;
|
||||
conditionsMeteo: string;
|
||||
outilsRequis?: string[];
|
||||
materiauxRequis?: string[];
|
||||
|
||||
// État d'exécution
|
||||
terminee: boolean;
|
||||
dateCompletion?: Date;
|
||||
completeepar?: string;
|
||||
commentaires?: string;
|
||||
tempsRealise?: number; // en minutes
|
||||
difficulteRencontree?: 'AUCUNE' | 'FAIBLE' | 'MOYENNE' | 'ELEVEE';
|
||||
}
|
||||
|
||||
interface SousPhaseExecution {
|
||||
id: string;
|
||||
nom: string;
|
||||
description?: string;
|
||||
ordreExecution: number;
|
||||
taches: TacheExecution[];
|
||||
|
||||
// Calculé automatiquement
|
||||
avancement: number; // % basé sur les tâches terminées
|
||||
tachesTerminees: number;
|
||||
totalTaches: number;
|
||||
dureeRealisee: number; // en minutes
|
||||
dureeEstimee: number; // en minutes
|
||||
}
|
||||
|
||||
interface PhaseExecution {
|
||||
id: string;
|
||||
nom: string;
|
||||
description?: string;
|
||||
ordreExecution: number;
|
||||
sousPhases: SousPhaseExecution[];
|
||||
|
||||
// Calculé automatiquement
|
||||
avancement: number; // % basé sur les sous-phases
|
||||
sousPhasesTerminees: number;
|
||||
totalSousPhases: number;
|
||||
}
|
||||
|
||||
interface ChantierExecution {
|
||||
id: string;
|
||||
nom: string;
|
||||
client: string;
|
||||
dateDebut: Date;
|
||||
dateFinPrevue: Date;
|
||||
phases: PhaseExecution[];
|
||||
|
||||
// Avancement global calculé
|
||||
avancementGlobal: number; // % basé sur toutes les tâches
|
||||
totalTachesTerminees: number;
|
||||
totalTaches: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page d'exécution granulaire d'un chantier
|
||||
* Permet de cocher les tâches terminées pour un suivi précis de l'avancement
|
||||
*/
|
||||
const ExecutionGranulaireChantier = () => {
|
||||
const { id } = useParams();
|
||||
const toast = useRef<Toast>(null);
|
||||
|
||||
// États principaux
|
||||
const [chantier, setChantier] = useState<ChantierExecution | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// États pour les modals
|
||||
const [showCommentDialog, setShowCommentDialog] = useState(false);
|
||||
const [selectedTache, setSelectedTache] = useState<TacheExecution | null>(null);
|
||||
const [commentaires, setCommentaires] = useState('');
|
||||
const [tempsRealise, setTempsRealise] = useState<number>(0);
|
||||
const [difficulte, setDifficulte] = useState<'AUCUNE' | 'FAIBLE' | 'MOYENNE' | 'ELEVEE'>('AUCUNE');
|
||||
|
||||
// États pour la vue
|
||||
const [activeIndex, setActiveIndex] = useState<number>(0);
|
||||
const [expandedKeys, setExpandedKeys] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
loadChantierExecution(id as string);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
/**
|
||||
* Charge les données d'exécution du chantier
|
||||
*/
|
||||
const loadChantierExecution = async (chantierId: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Charger les informations de base du chantier
|
||||
const chantierResponse = await apiClient.get(`/chantiers/${chantierId}`);
|
||||
const chantierData = chantierResponse.data;
|
||||
|
||||
// Charger les phases avec leurs sous-phases et tâches
|
||||
const phasesResponse = await apiClient.get(`/chantiers/${chantierId}/phases`);
|
||||
const phasesData = phasesResponse.data;
|
||||
|
||||
// Pour chaque phase, charger les tâches d'exécution
|
||||
const phasesWithExecution = await Promise.all(
|
||||
phasesData.map(async (phase: any) => {
|
||||
const sousPhasesWithTaches = await Promise.all(
|
||||
phase.sousPhases?.map(async (sousPhase: any) => {
|
||||
// Charger les tâches et leur état d'exécution
|
||||
const tachesExecution = await loadTachesExecution(chantierId, sousPhase.id);
|
||||
|
||||
// Calculer l'avancement de la sous-phase
|
||||
const tachesTerminees = tachesExecution.filter(t => t.terminee).length;
|
||||
const avancement = tachesExecution.length > 0 ?
|
||||
(tachesTerminees / tachesExecution.length) * 100 : 0;
|
||||
|
||||
const dureeRealisee = tachesExecution
|
||||
.filter(t => t.terminee && t.tempsRealise)
|
||||
.reduce((sum, t) => sum + (t.tempsRealise || 0), 0);
|
||||
|
||||
const dureeEstimee = tachesExecution
|
||||
.reduce((sum, t) => sum + (t.dureeEstimeeMinutes || 0), 0);
|
||||
|
||||
return {
|
||||
...sousPhase,
|
||||
taches: tachesExecution,
|
||||
avancement: Math.round(avancement),
|
||||
tachesTerminees,
|
||||
totalTaches: tachesExecution.length,
|
||||
dureeRealisee,
|
||||
dureeEstimee
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
|
||||
// Calculer l'avancement de la phase
|
||||
const totalTaches = sousPhasesWithTaches.reduce((sum, sp) => sum + sp.totalTaches, 0);
|
||||
const totalTachesTerminees = sousPhasesWithTaches.reduce((sum, sp) => sum + sp.tachesTerminees, 0);
|
||||
const avancement = totalTaches > 0 ? (totalTachesTerminees / totalTaches) * 100 : 0;
|
||||
|
||||
return {
|
||||
...phase,
|
||||
sousPhases: sousPhasesWithTaches,
|
||||
avancement: Math.round(avancement),
|
||||
totalSousPhases: sousPhasesWithTaches.length,
|
||||
sousPhasesTerminees: sousPhasesWithTaches.filter(sp => sp.avancement === 100).length
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Calculer l'avancement global du chantier
|
||||
const totalTaches = phasesWithExecution.reduce((sum, phase) =>
|
||||
sum + phase.sousPhases.reduce((spSum, sp) => spSum + sp.totalTaches, 0), 0
|
||||
);
|
||||
const totalTachesTerminees = phasesWithExecution.reduce((sum, phase) =>
|
||||
sum + phase.sousPhases.reduce((spSum, sp) => spSum + sp.tachesTerminees, 0), 0
|
||||
);
|
||||
const avancementGlobal = totalTaches > 0 ? (totalTachesTerminees / totalTaches) * 100 : 0;
|
||||
|
||||
setChantier({
|
||||
id: chantierData.id,
|
||||
nom: chantierData.nom,
|
||||
client: chantierData.client?.nom || chantierData.client,
|
||||
dateDebut: new Date(chantierData.dateDebut),
|
||||
dateFinPrevue: new Date(chantierData.dateFinPrevue),
|
||||
phases: phasesWithExecution,
|
||||
avancementGlobal: Math.round(avancementGlobal),
|
||||
totalTaches,
|
||||
totalTachesTerminees
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement:', error);
|
||||
showToast('error', 'Erreur', 'Impossible de charger les données d\'exécution');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Charge les tâches d'exécution pour une sous-phase
|
||||
*/
|
||||
const loadTachesExecution = async (chantierId: string, sousPhaseId: string): Promise<TacheExecution[]> => {
|
||||
try {
|
||||
// Charger les templates de tâches
|
||||
const templatesResponse = await apiClient.get(`/tache-templates/by-sous-phase/${sousPhaseId}`);
|
||||
const templates = templatesResponse.data;
|
||||
|
||||
// Charger les états d'exécution (s'ils existent)
|
||||
let etatsExecution = [];
|
||||
try {
|
||||
const etatsResponse = await apiClient.get(`/chantiers/${chantierId}/taches-execution/${sousPhaseId}`);
|
||||
etatsExecution = etatsResponse.data;
|
||||
} catch (error) {
|
||||
// Pas encore d'états d'exécution, on démarre avec toutes les tâches non terminées
|
||||
console.log('Aucun état d\'exécution trouvé, initialisation...');
|
||||
}
|
||||
|
||||
// Combiner templates avec états d'exécution
|
||||
return templates.map((template: any) => {
|
||||
const etat = etatsExecution.find((e: any) => e.tacheTemplateId === template.id);
|
||||
return {
|
||||
...template,
|
||||
terminee: etat?.terminee || false,
|
||||
dateCompletion: etat?.dateCompletion ? new Date(etat.dateCompletion) : undefined,
|
||||
completeepar: etat?.completeepar,
|
||||
commentaires: etat?.commentaires,
|
||||
tempsRealise: etat?.tempsRealise,
|
||||
difficulteRencontree: etat?.difficulteRencontree || 'AUCUNE'
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des tâches:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Marque une tâche comme terminée ou non terminée
|
||||
*/
|
||||
const toggleTacheTerminee = async (tache: TacheExecution) => {
|
||||
if (!tache.terminee) {
|
||||
// Marquer comme terminée - ouvrir le dialog pour les détails
|
||||
setSelectedTache(tache);
|
||||
setCommentaires(tache.commentaires || '');
|
||||
setTempsRealise(tache.tempsRealise || tache.dureeEstimeeMinutes || 0);
|
||||
setDifficulte(tache.difficulteRencontree || 'AUCUNE');
|
||||
setShowCommentDialog(true);
|
||||
} else {
|
||||
// Marquer comme non terminée
|
||||
await saveTacheExecution(tache, false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sauvegarde l'état d'exécution d'une tâche
|
||||
*/
|
||||
const saveTacheExecution = async (tache: TacheExecution, terminee: boolean, details?: any) => {
|
||||
try {
|
||||
setSaving(true);
|
||||
|
||||
const executionData = {
|
||||
chantierID: id,
|
||||
tacheTemplateId: tache.id,
|
||||
terminee,
|
||||
dateCompletion: terminee ? new Date().toISOString() : null,
|
||||
completeepar: terminee ? 'CURRENT_USER' : null, // À remplacer par l'utilisateur connecté
|
||||
commentaires: details?.commentaires || '',
|
||||
tempsRealise: details?.tempsRealise || null,
|
||||
difficulteRencontree: details?.difficulte || 'AUCUNE'
|
||||
};
|
||||
|
||||
await apiClient.post(`/chantiers/${id}/taches-execution`, executionData);
|
||||
|
||||
showToast('success', 'Succès',
|
||||
terminee ? 'Tâche marquée comme terminée' : 'Tâche marquée comme non terminée'
|
||||
);
|
||||
|
||||
// Recharger les données pour mettre à jour l'avancement
|
||||
await loadChantierExecution(id as string);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde:', error);
|
||||
showToast('error', 'Erreur', 'Impossible de sauvegarder l\'état de la tâche');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Confirme la completion d'une tâche avec les détails
|
||||
*/
|
||||
const confirmerCompletion = async () => {
|
||||
if (!selectedTache) return;
|
||||
|
||||
await saveTacheExecution(selectedTache, true, {
|
||||
commentaires,
|
||||
tempsRealise,
|
||||
difficulte
|
||||
});
|
||||
|
||||
setShowCommentDialog(false);
|
||||
setSelectedTache(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Affiche un toast message
|
||||
*/
|
||||
const showToast = (severity: 'success' | 'info' | 'warn' | 'error', summary: string, detail: string) => {
|
||||
toast.current?.show({ severity, summary, detail, life: 3000 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Template pour l'affichage d'une tâche avec checkbox
|
||||
*/
|
||||
const renderTacheCheckbox = (tache: TacheExecution) => {
|
||||
const isDisabled = saving;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex align-items-center gap-3 p-3 border-round transition-colors ${
|
||||
tache.terminee ? 'bg-green-50 border-green-200' : 'bg-white border-gray-200'
|
||||
} border-1 hover:bg-gray-50`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={tache.terminee}
|
||||
onChange={() => toggleTacheTerminee(tache)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className={`font-medium ${tache.terminee ? 'line-through text-600' : ''}`}>
|
||||
{tache.nom}
|
||||
</div>
|
||||
{tache.description && (
|
||||
<div className="text-sm text-600 mt-1">{tache.description}</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 mt-2">
|
||||
{tache.critique && <Badge value="Critique" severity="danger" className="p-badge-sm" />}
|
||||
{tache.bloquante && <Badge value="Bloquante" severity="warning" className="p-badge-sm" />}
|
||||
{tache.priorite === 'HAUTE' && <Badge value="Haute priorité" severity="info" className="p-badge-sm" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right text-sm">
|
||||
{tache.dureeEstimeeMinutes && (
|
||||
<div className="text-600">
|
||||
Estimé: {Math.floor(tache.dureeEstimeeMinutes / 60)}h{tache.dureeEstimeeMinutes % 60}min
|
||||
</div>
|
||||
)}
|
||||
{tache.terminee && tache.tempsRealise && (
|
||||
<div className="text-green-600 font-medium">
|
||||
Réalisé: {Math.floor(tache.tempsRealise / 60)}h{tache.tempsRealise % 60}min
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-content-center align-items-center h-20rem">
|
||||
<i className="pi pi-spin pi-spinner" style={{ fontSize: '2rem' }}></i>
|
||||
<span className="ml-2">Chargement de l'exécution du chantier...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!chantier) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h5>Chantier non trouvé</h5>
|
||||
<p>Impossible de charger les données d'exécution du chantier.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<Toast ref={toast} />
|
||||
|
||||
{/* En-tête avec avancement global */}
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<div className="flex justify-content-between align-items-start">
|
||||
<div className="flex-1">
|
||||
<h4 className="mt-0 mb-2">{chantier.nom}</h4>
|
||||
<div className="text-600 mb-3">
|
||||
Client: {chantier.client} •
|
||||
Début: {chantier.dateDebut.toLocaleDateString('fr-FR')} •
|
||||
Fin prévue: {chantier.dateFinPrevue.toLocaleDateString('fr-FR')}
|
||||
</div>
|
||||
|
||||
<div className="flex align-items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-content-between mb-2">
|
||||
<span className="font-medium">Avancement Global</span>
|
||||
<span className="font-bold text-lg">{chantier.avancementGlobal}%</span>
|
||||
</div>
|
||||
<ProgressBar
|
||||
value={chantier.avancementGlobal}
|
||||
className="mb-2"
|
||||
style={{ height: '12px' }}
|
||||
/>
|
||||
<div className="text-sm text-600">
|
||||
{chantier.totalTachesTerminees} / {chantier.totalTaches} tâches terminées
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<Avatar
|
||||
icon="pi pi-chart-line"
|
||||
size="xlarge"
|
||||
className="bg-blue-100 text-blue-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Phases avec sous-phases et tâches */}
|
||||
<div className="col-12">
|
||||
<Accordion activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
||||
{chantier.phases.map((phase, phaseIndex) => (
|
||||
<AccordionTab
|
||||
key={phase.id}
|
||||
header={
|
||||
<div className="flex justify-content-between align-items-center w-full pr-4">
|
||||
<div className="flex align-items-center gap-3">
|
||||
<Badge value={phase.ordreExecution} />
|
||||
<span className="font-medium">{phase.nom}</span>
|
||||
</div>
|
||||
<div className="flex align-items-center gap-2">
|
||||
<span className="text-sm">{phase.avancement}%</span>
|
||||
<ProgressBar
|
||||
value={phase.avancement}
|
||||
style={{ width: '100px', height: '8px' }}
|
||||
showValue={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid">
|
||||
{phase.sousPhases.map((sousPhase) => (
|
||||
<div key={sousPhase.id} className="col-12">
|
||||
<Card className="mb-3">
|
||||
<div className="flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h6 className="mt-0 mb-1">{sousPhase.nom}</h6>
|
||||
{sousPhase.description && (
|
||||
<p className="text-600 mt-0 mb-2">{sousPhase.description}</p>
|
||||
)}
|
||||
<div className="text-sm text-500">
|
||||
{sousPhase.tachesTerminees} / {sousPhase.totalTaches} tâches terminées
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold mb-1">{sousPhase.avancement}%</div>
|
||||
<ProgressBar
|
||||
value={sousPhase.avancement}
|
||||
style={{ width: '120px', height: '8px' }}
|
||||
showValue={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-column gap-2">
|
||||
{sousPhase.taches.map((tache) => (
|
||||
<div key={tache.id}>
|
||||
{renderTacheCheckbox(tache)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AccordionTab>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
{/* Dialog de completion de tâche */}
|
||||
<Dialog
|
||||
header="Finaliser la tâche"
|
||||
visible={showCommentDialog}
|
||||
onHide={() => setShowCommentDialog(false)}
|
||||
style={{ width: '50vw' }}
|
||||
breakpoints={{ '960px': '75vw', '641px': '90vw' }}
|
||||
modal
|
||||
>
|
||||
{selectedTache && (
|
||||
<div className="p-fluid">
|
||||
<div className="mb-4">
|
||||
<h6 className="text-primary">{selectedTache.nom}</h6>
|
||||
{selectedTache.description && (
|
||||
<p className="text-600 mt-1">{selectedTache.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="commentaires">Commentaires (optionnel)</label>
|
||||
<InputTextarea
|
||||
id="commentaires"
|
||||
value={commentaires}
|
||||
onChange={(e) => setCommentaires(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="Commentaires sur la réalisation de cette tâche..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label>Temps réalisé (minutes)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="p-inputtext p-component"
|
||||
value={tempsRealise}
|
||||
onChange={(e) => setTempsRealise(parseInt(e.target.value) || 0)}
|
||||
min={0}
|
||||
/>
|
||||
{selectedTache.dureeEstimeeMinutes && (
|
||||
<small className="text-600">
|
||||
Temps estimé: {selectedTache.dureeEstimeeMinutes} minutes
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-content-end gap-2 mt-4">
|
||||
<Button
|
||||
label="Annuler"
|
||||
icon="pi pi-times"
|
||||
className="p-button-text"
|
||||
onClick={() => setShowCommentDialog(false)}
|
||||
/>
|
||||
<Button
|
||||
label="Marquer comme terminée"
|
||||
icon="pi pi-check"
|
||||
onClick={confirmerCompletion}
|
||||
loading={saving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExecutionGranulaireChantier;
|
||||
324
app/(main)/chantiers/[id]/phases-clean/page.tsx
Normal file
324
app/(main)/chantiers/[id]/phases-clean/page.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { Badge } from 'primereact/badge';
|
||||
|
||||
import PhasesTable from '../../../../../components/phases/PhasesTable';
|
||||
import usePhasesManager from '../../../../../hooks/usePhasesManager';
|
||||
import type { PhaseChantier, PhaseFormData } from '../../../../../types/btp-extended';
|
||||
|
||||
const PhasesCleanPage: React.FC = () => {
|
||||
const params = useParams();
|
||||
const chantierId = params?.id as string;
|
||||
const toast = useRef<Toast>(null);
|
||||
|
||||
// Hook de gestion des phases
|
||||
const {
|
||||
phases,
|
||||
loading,
|
||||
selectedPhase,
|
||||
setSelectedPhase,
|
||||
loadPhases,
|
||||
createPhase,
|
||||
updatePhase,
|
||||
deletePhase,
|
||||
startPhase,
|
||||
setToastRef
|
||||
} = usePhasesManager({ chantierId });
|
||||
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
|
||||
// États pour les dialogues
|
||||
const [showPhaseDialog, setShowPhaseDialog] = useState(false);
|
||||
const [editingPhase, setEditingPhase] = useState(false);
|
||||
|
||||
// États pour les formulaires
|
||||
const [phaseForm, setPhaseForm] = useState<PhaseFormData>({
|
||||
nom: '',
|
||||
description: '',
|
||||
dateDebutPrevue: '',
|
||||
dateFinPrevue: '',
|
||||
dureeEstimeeHeures: 8,
|
||||
priorite: 'MOYENNE',
|
||||
critique: false,
|
||||
ordreExecution: 1,
|
||||
budgetPrevu: 0,
|
||||
coutReel: 0,
|
||||
prerequisPhases: [],
|
||||
competencesRequises: [],
|
||||
materielsNecessaires: [],
|
||||
fournisseursRecommandes: []
|
||||
});
|
||||
|
||||
// Initialisation
|
||||
useEffect(() => {
|
||||
setToastRef(toast.current);
|
||||
if (chantierId) {
|
||||
loadPhases();
|
||||
}
|
||||
}, [chantierId, setToastRef, loadPhases]);
|
||||
|
||||
// Gestionnaires d'événements
|
||||
const handleEditPhase = (phase: PhaseChantier) => {
|
||||
setSelectedPhase(phase);
|
||||
setEditingPhase(true);
|
||||
setPhaseForm({
|
||||
nom: phase.nom,
|
||||
description: phase.description || '',
|
||||
dateDebutPrevue: phase.dateDebutPrevue || '',
|
||||
dateFinPrevue: phase.dateFinPrevue || '',
|
||||
dureeEstimeeHeures: phase.dureeEstimeeHeures || 8,
|
||||
priorite: phase.priorite || 'MOYENNE',
|
||||
critique: phase.critique || false,
|
||||
ordreExecution: phase.ordreExecution || 1,
|
||||
budgetPrevu: phase.budgetPrevu || 0,
|
||||
coutReel: phase.coutReel || 0,
|
||||
prerequisPhases: phase.prerequisPhases || [],
|
||||
competencesRequises: phase.competencesRequises || [],
|
||||
materielsNecessaires: phase.materielsNecessaires || [],
|
||||
fournisseursRecommandes: phase.fournisseursRecommandes || []
|
||||
});
|
||||
setShowPhaseDialog(true);
|
||||
};
|
||||
|
||||
const handleSavePhase = async () => {
|
||||
if (!phaseForm.nom.trim()) {
|
||||
toast.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'Données manquantes',
|
||||
detail: 'Veuillez remplir le nom de la phase',
|
||||
life: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const phaseData = {
|
||||
...phaseForm,
|
||||
chantierId: chantierId
|
||||
};
|
||||
|
||||
if (editingPhase && selectedPhase) {
|
||||
await updatePhase(selectedPhase.id!, phaseData);
|
||||
} else {
|
||||
await createPhase(phaseData);
|
||||
}
|
||||
|
||||
setShowPhaseDialog(false);
|
||||
setEditingPhase(false);
|
||||
setSelectedPhase(null);
|
||||
resetPhaseForm();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde de la phase:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetPhaseForm = () => {
|
||||
setPhaseForm({
|
||||
nom: '',
|
||||
description: '',
|
||||
dateDebutPrevue: '',
|
||||
dateFinPrevue: '',
|
||||
dureeEstimeeHeures: 8,
|
||||
priorite: 'MOYENNE',
|
||||
critique: false,
|
||||
ordreExecution: 1,
|
||||
budgetPrevu: 0,
|
||||
coutReel: 0,
|
||||
prerequisPhases: [],
|
||||
competencesRequises: [],
|
||||
materielsNecessaires: [],
|
||||
fournisseursRecommandes: []
|
||||
});
|
||||
};
|
||||
|
||||
// Templates de la toolbar
|
||||
const toolbarStartTemplate = (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<h5 className="m-0 text-color">Phases - Version Clean</h5>
|
||||
<Badge value={phases.length} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const toolbarEndTemplate = (
|
||||
<div className="flex 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="Nouvelle phase"
|
||||
icon="pi pi-plus"
|
||||
className="p-button-success"
|
||||
onClick={() => {
|
||||
resetPhaseForm();
|
||||
setEditingPhase(false);
|
||||
setShowPhaseDialog(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<Toast ref={toast} />
|
||||
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<Toolbar
|
||||
start={toolbarStartTemplate}
|
||||
end={toolbarEndTemplate}
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<PhasesTable
|
||||
phases={phases}
|
||||
loading={loading}
|
||||
chantierId={chantierId}
|
||||
showStats={false}
|
||||
showChantierColumn={false}
|
||||
showSubPhases={true}
|
||||
showBudget={true}
|
||||
showExpansion={true}
|
||||
actions={['view', 'edit', 'delete', 'start']}
|
||||
onRefresh={loadPhases}
|
||||
onPhaseEdit={handleEditPhase}
|
||||
onPhaseDelete={deletePhase}
|
||||
onPhaseStart={startPhase}
|
||||
rows={15}
|
||||
globalFilter={globalFilter}
|
||||
showGlobalFilter={true}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Dialog pour créer/modifier une phase */}
|
||||
<Dialog
|
||||
header={editingPhase ? "Modifier la phase" : "Nouvelle phase"}
|
||||
visible={showPhaseDialog}
|
||||
onHide={() => {
|
||||
setShowPhaseDialog(false);
|
||||
setEditingPhase(false);
|
||||
resetPhaseForm();
|
||||
}}
|
||||
footer={
|
||||
<div>
|
||||
<Button
|
||||
label="Annuler"
|
||||
icon="pi pi-times"
|
||||
className="p-button-text"
|
||||
onClick={() => {
|
||||
setShowPhaseDialog(false);
|
||||
setEditingPhase(false);
|
||||
resetPhaseForm();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label={editingPhase ? "Modifier" : "Créer"}
|
||||
icon="pi pi-check"
|
||||
className="p-button-success"
|
||||
onClick={handleSavePhase}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
style={{ width: '50vw', maxWidth: '600px' }}
|
||||
modal
|
||||
>
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<div className="field">
|
||||
<label htmlFor="phaseNom" className="font-semibold">
|
||||
Nom de la phase <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
id="phaseNom"
|
||||
value={phaseForm.nom}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, nom: e.target.value }))}
|
||||
className="w-full"
|
||||
placeholder="Ex: Fondations, Gros œuvre..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="field">
|
||||
<label htmlFor="phaseDescription" className="font-semibold">Description</label>
|
||||
<InputTextarea
|
||||
id="phaseDescription"
|
||||
value={phaseForm.description}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="w-full"
|
||||
rows={3}
|
||||
placeholder="Description détaillée de la phase..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="field">
|
||||
<label htmlFor="phaseDuree" className="font-semibold">Durée estimée (heures)</label>
|
||||
<InputNumber
|
||||
id="phaseDuree"
|
||||
value={phaseForm.dureeEstimeeHeures}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, dureeEstimeeHeures: e.value || 0 }))}
|
||||
className="w-full"
|
||||
min={1}
|
||||
suffix=" h"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="field">
|
||||
<label htmlFor="phasePriorite" className="font-semibold">Priorité</label>
|
||||
<Dropdown
|
||||
id="phasePriorite"
|
||||
value={phaseForm.priorite}
|
||||
options={[
|
||||
{ label: 'Faible', value: 'FAIBLE' },
|
||||
{ label: 'Moyenne', value: 'MOYENNE' },
|
||||
{ label: 'Élevée', value: 'ELEVEE' },
|
||||
{ label: 'Critique', value: 'CRITIQUE' }
|
||||
]}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, priorite: e.value }))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="field-checkbox">
|
||||
<Checkbox
|
||||
id="phaseCritique"
|
||||
checked={phaseForm.critique}
|
||||
onChange={(e) => setPhaseForm(prev => ({ ...prev, critique: e.checked }))}
|
||||
/>
|
||||
<label htmlFor="phaseCritique" className="ml-2 font-semibold">
|
||||
Phase critique
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhasesCleanPage;
|
||||
269
app/(main)/chantiers/[id]/phases-responsive/page.tsx
Normal file
269
app/(main)/chantiers/[id]/phases-responsive/page.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { Panel } from 'primereact/panel';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { useRef } from 'react';
|
||||
import { LayoutContext } from '../../../../../layout/context/layoutcontext';
|
||||
import AtlantisResponsivePhasesTable from '../../../../../components/phases/AtlantisResponsivePhasesTable';
|
||||
import AtlantisAccessibilityControls from '../../../../../components/phases/AtlantisAccessibilityControls';
|
||||
import PhasesQuickPreview from '../../../../../components/phases/PhasesQuickPreview';
|
||||
import PhaseValidationPanel from '../../../../../components/phases/PhaseValidationPanel';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import testDataService from '../../../../../services/testDataService';
|
||||
import type { PhaseChantier } from '../../../../../types/btp';
|
||||
|
||||
const ResponsivePhasesPage: React.FC = () => {
|
||||
const [phases, setPhases] = useState<PhaseChantier[]>([]);
|
||||
const [selectedPhase, setSelectedPhase] = useState<PhaseChantier | null>(null);
|
||||
const [validationDialogVisible, setValidationDialogVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { isDesktop } = useContext(LayoutContext);
|
||||
const toast = useRef<Toast>(null);
|
||||
|
||||
// Générer des données de test au chargement
|
||||
React.useEffect(() => {
|
||||
const client = testDataService.generateTestClient(1);
|
||||
const chantier = testDataService.generateTestChantier(1, 'MAISON_INDIVIDUELLE', client);
|
||||
const generatedPhases = testDataService.generatePhasesWithPrerequisites(chantier);
|
||||
setPhases(generatedPhases);
|
||||
}, []);
|
||||
|
||||
const handlePhaseSelect = (phase: PhaseChantier) => {
|
||||
setSelectedPhase(phase);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'info',
|
||||
summary: 'Phase sélectionnée',
|
||||
detail: `${phase.nom} - ${phase.statut}`,
|
||||
life: 3000
|
||||
});
|
||||
};
|
||||
|
||||
const handlePhaseStart = (phaseId: string) => {
|
||||
setPhases(prev => prev.map(p =>
|
||||
p.id === phaseId
|
||||
? { ...p, statut: 'EN_COURS' as const, dateDebutReelle: new Date().toISOString().split('T')[0] }
|
||||
: p
|
||||
));
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Phase démarrée',
|
||||
detail: 'La phase a été mise en cours',
|
||||
life: 3000
|
||||
});
|
||||
};
|
||||
|
||||
const handlePhaseValidation = (phase: PhaseChantier) => {
|
||||
setSelectedPhase(phase);
|
||||
setValidationDialogVisible(true);
|
||||
};
|
||||
|
||||
const headerTemplate = (
|
||||
<div className="flex align-items-center justify-content-between">
|
||||
<div className="flex align-items-center gap-3">
|
||||
<i className="pi pi-building text-2xl text-primary" />
|
||||
<div>
|
||||
<h2 className="m-0 text-color">Gestion des phases - Vue responsive</h2>
|
||||
<p className="m-0 text-color-secondary text-sm">
|
||||
Interface adaptative utilisant le template Atlantis React
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDesktop() && (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<Button
|
||||
label="Actualiser"
|
||||
icon="pi pi-refresh"
|
||||
className="p-button-outlined"
|
||||
onClick={() => window.location.reload()}
|
||||
/>
|
||||
<Button
|
||||
label="Paramètres"
|
||||
icon="pi pi-cog"
|
||||
className="p-button-text"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<Toast ref={toast} />
|
||||
|
||||
{/* En-tête de page avec style Atlantis */}
|
||||
<div className="col-12">
|
||||
<div className="card">
|
||||
{headerTemplate}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contrôles d'accessibilité */}
|
||||
<div className="col-12">
|
||||
<AtlantisAccessibilityControls />
|
||||
</div>
|
||||
|
||||
{/* Vue d'ensemble rapide */}
|
||||
{phases.length > 0 && (
|
||||
<div className="col-12 lg:col-4">
|
||||
<PhasesQuickPreview
|
||||
phases={phases}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tableau principal responsive */}
|
||||
<div className={`col-12 ${phases.length > 0 ? 'lg:col-8' : ''}`}>
|
||||
<AtlantisResponsivePhasesTable
|
||||
phases={phases}
|
||||
onPhaseSelect={handlePhaseSelect}
|
||||
onPhaseStart={handlePhaseStart}
|
||||
onPhaseValidate={handlePhaseValidation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Informations complémentaires sur mobile */}
|
||||
{!isDesktop() && selectedPhase && (
|
||||
<div className="col-12">
|
||||
<div className="card">
|
||||
<h6 className="mt-0 mb-3 text-color">Phase sélectionnée</h6>
|
||||
<div className="grid">
|
||||
<div className="col-12 sm:col-6">
|
||||
<div className="field">
|
||||
<label className="font-semibold text-color-secondary">Nom</label>
|
||||
<p className="m-0 text-color">{selectedPhase.nom}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 sm:col-6">
|
||||
<div className="field">
|
||||
<label className="font-semibold text-color-secondary">Statut</label>
|
||||
<p className="m-0 text-color">{selectedPhase.statut}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 sm:col-6">
|
||||
<div className="field">
|
||||
<label className="font-semibold text-color-secondary">Avancement</label>
|
||||
<p className="m-0 text-color">{selectedPhase.pourcentageAvancement || 0}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 sm:col-6">
|
||||
<div className="field">
|
||||
<label className="font-semibold text-color-secondary">Date début prévue</label>
|
||||
<p className="m-0 text-color">
|
||||
{selectedPhase.dateDebutPrevue ?
|
||||
new Date(selectedPhase.dateDebutPrevue).toLocaleDateString('fr-FR') :
|
||||
'-'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{selectedPhase.description && (
|
||||
<div className="col-12">
|
||||
<div className="field">
|
||||
<label className="font-semibold text-color-secondary">Description</label>
|
||||
<p className="m-0 text-color-secondary">{selectedPhase.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions rapides pour mobile */}
|
||||
{!isDesktop() && (
|
||||
<div className="col-12">
|
||||
<div className="card">
|
||||
<h6 className="mt-0 mb-3 text-color">Actions rapides</h6>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
label="Nouvelle phase"
|
||||
icon="pi pi-plus"
|
||||
className="p-button-success flex-1 sm:flex-none"
|
||||
/>
|
||||
<Button
|
||||
label="Filtrer"
|
||||
icon="pi pi-filter"
|
||||
className="p-button-outlined flex-1 sm:flex-none"
|
||||
/>
|
||||
<Button
|
||||
label="Exporter"
|
||||
icon="pi pi-download"
|
||||
className="p-button-outlined flex-1 sm:flex-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Statistiques rapides */}
|
||||
<div className="col-12">
|
||||
<div className="grid">
|
||||
<div className="col-6 md:col-3">
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-primary">
|
||||
{phases.length}
|
||||
</div>
|
||||
<div className="text-color-secondary text-sm">Total phases</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6 md:col-3">
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-green-500">
|
||||
{phases.filter(p => p.statut === 'TERMINEE').length}
|
||||
</div>
|
||||
<div className="text-color-secondary text-sm">Terminées</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6 md:col-3">
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-blue-500">
|
||||
{phases.filter(p => p.statut === 'EN_COURS').length}
|
||||
</div>
|
||||
<div className="text-color-secondary text-sm">En cours</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6 md:col-3">
|
||||
<div className="card text-center">
|
||||
<div className="text-2xl font-bold text-orange-500">
|
||||
{phases.filter(p => p.critique).length}
|
||||
</div>
|
||||
<div className="text-color-secondary text-sm">Critiques</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dialog de validation */}
|
||||
<Dialog
|
||||
header="Validation des prérequis"
|
||||
visible={validationDialogVisible}
|
||||
onHide={() => setValidationDialogVisible(false)}
|
||||
style={{ width: isDesktop() ? '800px' : '95vw' }}
|
||||
modal
|
||||
className="p-fluid"
|
||||
>
|
||||
{selectedPhase && (
|
||||
<PhaseValidationPanel
|
||||
phase={selectedPhase}
|
||||
allPhases={phases}
|
||||
onStartPhase={handlePhaseStart}
|
||||
onViewPrerequisite={(prereqId) => {
|
||||
const prereq = phases.find(p => p.id === prereqId);
|
||||
if (prereq) {
|
||||
setSelectedPhase(prereq);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponsivePhasesPage;
|
||||
1124
app/(main)/chantiers/[id]/phases/page.tsx
Normal file
1124
app/(main)/chantiers/[id]/phases/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user