192 lines
8.6 KiB
TypeScript
192 lines
8.6 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Button } from 'primereact/button';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import type { PhaseChantier } from '../../types/btp';
|
|
|
|
interface PhasesQuickPreviewProps {
|
|
phases: PhaseChantier[];
|
|
className?: string;
|
|
onViewDetails?: () => void;
|
|
}
|
|
|
|
const PhasesQuickPreview: React.FC<PhasesQuickPreviewProps> = ({
|
|
phases,
|
|
className = '',
|
|
onViewDetails
|
|
}) => {
|
|
const getPhaseStats = () => {
|
|
const total = phases.length;
|
|
const principales = phases.filter(p => !p.phaseParent).length;
|
|
const sousPhases = phases.filter(p => p.phaseParent).length;
|
|
const enCours = phases.filter(p => p.statut === 'EN_COURS').length;
|
|
const terminees = phases.filter(p => p.statut === 'TERMINEE').length;
|
|
const critiques = phases.filter(p => p.critique).length;
|
|
const enRetard = phases.filter(p => {
|
|
if (p.statut === 'TERMINEE') return false;
|
|
const maintenant = new Date();
|
|
const dateFinPrevue = p.dateFinPrevue ? new Date(p.dateFinPrevue) : null;
|
|
return dateFinPrevue ? dateFinPrevue < maintenant : false;
|
|
}).length;
|
|
|
|
const avancementMoyen = total > 0 ?
|
|
Math.round(phases.reduce((acc, p) => acc + (p.pourcentageAvancement || 0), 0) / total) : 0;
|
|
|
|
return { total, principales, sousPhases, enCours, terminees, critiques, enRetard, avancementMoyen };
|
|
};
|
|
|
|
const stats = getPhaseStats();
|
|
|
|
const getProgressColor = (percentage: number) => {
|
|
if (percentage >= 80) return 'success';
|
|
if (percentage >= 50) return 'info';
|
|
if (percentage >= 25) return 'warning';
|
|
return 'danger';
|
|
};
|
|
|
|
const getNextPhases = () => {
|
|
return phases
|
|
.filter(p => p.statut === 'PLANIFIEE' || p.statut === 'EN_COURS')
|
|
.sort((a, b) => (a.ordreExecution || 0) - (b.ordreExecution || 0))
|
|
.slice(0, 3);
|
|
};
|
|
|
|
const nextPhases = getNextPhases();
|
|
|
|
return (
|
|
<Card title="Aperçu des phases" className={className}>
|
|
{/* Statistiques rapides */}
|
|
<div className="grid mb-4">
|
|
<div className="col-6 md:col-3">
|
|
<div className="text-center p-2 border-1 surface-border border-round">
|
|
<div className="text-xl font-bold text-primary">{stats.total}</div>
|
|
<div className="text-600 text-sm">Total phases</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6 md:col-3">
|
|
<div className="text-center p-2 border-1 surface-border border-round">
|
|
<div className="text-xl font-bold text-green-500">{stats.terminees}</div>
|
|
<div className="text-600 text-sm">Terminées</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6 md:col-3">
|
|
<div className="text-center p-2 border-1 surface-border border-round">
|
|
<div className="text-xl font-bold text-blue-500">{stats.enCours}</div>
|
|
<div className="text-600 text-sm">En cours</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6 md:col-3">
|
|
<div className="text-center p-2 border-1 surface-border border-round">
|
|
<div className="text-xl font-bold text-red-500">{stats.critiques}</div>
|
|
<div className="text-600 text-sm">Critiques</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Avancement global */}
|
|
<div className="mb-4">
|
|
<div className="flex justify-content-between align-items-center mb-2">
|
|
<span className="font-semibold">Avancement global</span>
|
|
<Badge value={`${stats.avancementMoyen}%`} severity={getProgressColor(stats.avancementMoyen)} />
|
|
</div>
|
|
<ProgressBar
|
|
value={stats.avancementMoyen}
|
|
color={
|
|
stats.avancementMoyen >= 80 ? '#10b981' :
|
|
stats.avancementMoyen >= 50 ? '#3b82f6' :
|
|
stats.avancementMoyen >= 25 ? '#f59e0b' : '#ef4444'
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* Alertes */}
|
|
{(stats.enRetard > 0 || stats.critiques > 0) && (
|
|
<div className="mb-4">
|
|
<div className="flex flex-wrap gap-2">
|
|
{stats.enRetard > 0 && (
|
|
<Tag
|
|
value={`${stats.enRetard} phase${stats.enRetard > 1 ? 's' : ''} en retard`}
|
|
severity="danger"
|
|
icon="pi pi-exclamation-triangle"
|
|
/>
|
|
)}
|
|
{stats.critiques > 0 && (
|
|
<Tag
|
|
value={`${stats.critiques} phase${stats.critiques > 1 ? 's' : ''} critique${stats.critiques > 1 ? 's' : ''}`}
|
|
severity="warning"
|
|
icon="pi pi-flag"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Prochaines phases */}
|
|
{nextPhases.length > 0 && (
|
|
<div className="mb-4">
|
|
<h4 className="text-lg font-semibold mb-3">Prochaines phases</h4>
|
|
<div className="flex flex-column gap-2">
|
|
{nextPhases.map((phase, index) => (
|
|
<div key={phase.id} className="flex align-items-center gap-3 p-2 surface-100 border-round">
|
|
<div className="flex align-items-center justify-content-center w-2rem h-2rem border-circle bg-primary text-white font-bold">
|
|
{index + 1}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="font-semibold">{phase.nom}</div>
|
|
<div className="text-600 text-sm">
|
|
{phase.dateFinPrevue && `Prévue: ${new Date(phase.dateFinPrevue).toLocaleDateString('fr-FR')}`}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-1">
|
|
{phase.critique && (
|
|
<Tag value="Critique" severity="danger" className="text-xs" />
|
|
)}
|
|
<Tag
|
|
value={phase.statut}
|
|
severity={phase.statut === 'EN_COURS' ? 'info' : 'secondary'}
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Structure hiérarchique */}
|
|
<div className="mb-4">
|
|
<div className="flex justify-content-between align-items-center">
|
|
<span className="font-semibold">Structure du projet</span>
|
|
<div className="flex gap-2">
|
|
<div className="flex align-items-center gap-1">
|
|
<div className="w-0.5rem h-0.5rem border-circle bg-primary"></div>
|
|
<span className="text-sm text-600">{stats.principales} principales</span>
|
|
</div>
|
|
<div className="flex align-items-center gap-1">
|
|
<div className="w-0.5rem h-0.5rem border-circle bg-gray-400"></div>
|
|
<span className="text-sm text-600">{stats.sousPhases} sous-phases</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
{onViewDetails && (
|
|
<div className="text-center">
|
|
<Button
|
|
label="Voir le détail des phases"
|
|
icon="pi pi-eye"
|
|
className="p-button-outlined"
|
|
onClick={onViewDetails}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default PhasesQuickPreview; |