Files
btpxpress-frontend/components/phases/PhaseValidationPanel.tsx
2025-10-01 01:39:07 +00:00

364 lines
14 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { Panel } from 'primereact/panel';
import { Message } from 'primereact/message';
import { Button } from 'primereact/button';
import { Tag } from 'primereact/tag';
import { Badge } from 'primereact/badge';
import { Timeline } from 'primereact/timeline';
import { Accordion, AccordionTab } from 'primereact/accordion';
import { Tooltip } from 'primereact/tooltip';
import phaseValidationService, {
type PhaseValidationResult,
type ValidationError,
type ValidationWarning
} from '../../services/phaseValidationService';
import type { PhaseChantier } from '../../types/btp';
interface PhaseValidationPanelProps {
phase: PhaseChantier;
allPhases: PhaseChantier[];
onStartPhase?: (phaseId: string) => void;
onViewPrerequisite?: (prerequisiteId: string) => void;
className?: string;
compact?: boolean;
}
const PhaseValidationPanel: React.FC<PhaseValidationPanelProps> = ({
phase,
allPhases,
onStartPhase,
onViewPrerequisite,
className = '',
compact = false
}) => {
const [validation, setValidation] = useState<PhaseValidationResult | null>(null);
const [loading, setLoading] = useState(true);
const [expandedSections, setExpandedSections] = useState<string[]>(['errors']);
useEffect(() => {
validatePhase();
}, [phase, allPhases]);
const validatePhase = async () => {
setLoading(true);
try {
const result = phaseValidationService.validatePhaseStart(phase, allPhases, {
strictMode: false
});
setValidation(result);
} catch (error) {
console.error('Erreur lors de la validation de la phase:', error);
} finally {
setLoading(false);
}
};
const getSeverityIcon = (severity: string) => {
switch (severity) {
case 'error': return 'pi pi-times-circle';
case 'warning': return 'pi pi-exclamation-triangle';
case 'info': return 'pi pi-info-circle';
default: return 'pi pi-circle';
}
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'error': return 'danger';
case 'warning': return 'warning';
case 'info': return 'info';
default: return 'secondary';
}
};
const renderValidationStatus = () => {
if (!validation) return null;
const statusColor = validation.readyToStart ? 'success' :
validation.canStart ? 'warning' : 'danger';
const statusIcon = validation.readyToStart ? 'pi pi-check-circle' :
validation.canStart ? 'pi pi-exclamation-triangle' : 'pi pi-times-circle';
const statusText = validation.readyToStart ? 'Prête à démarrer' :
validation.canStart ? 'Peut démarrer avec précautions' : 'Ne peut pas démarrer';
return (
<div className="flex align-items-center gap-2 mb-3">
<i className={`${statusIcon} text-${statusColor === 'danger' ? 'red' : statusColor === 'warning' ? 'yellow' : 'green'}-500 text-lg`}></i>
<span className="font-semibold">{statusText}</span>
{validation.errors.length > 0 && (
<Badge value={validation.errors.length} severity="danger" />
)}
{validation.warnings.length > 0 && (
<Badge value={validation.warnings.length} severity="warning" />
)}
</div>
);
};
const renderErrorsAndWarnings = () => {
if (!validation || (validation.errors.length === 0 && validation.warnings.length === 0)) {
return (
<Message
severity="success"
text="Aucun problème détecté"
className="w-full"
/>
);
}
return (
<div className="flex flex-column gap-2">
{validation.errors.map((error, index) => (
<Message
key={`error-${index}`}
severity={getSeverityColor(error.severity) as any}
className="w-full"
>
<div className="flex align-items-center justify-content-between w-full">
<span>{error.message}</span>
{error.phaseId && onViewPrerequisite && (
<Button
icon="pi pi-external-link"
className="p-button-text p-button-sm"
onClick={() => onViewPrerequisite(error.phaseId!)}
tooltip="Voir la phase"
/>
)}
</div>
</Message>
))}
{validation.warnings.map((warning, index) => (
<Message
key={`warning-${index}`}
severity="warn"
className="w-full"
>
<div>
<div>{warning.message}</div>
{warning.recommendation && (
<small className="text-600 mt-1 block">
💡 {warning.recommendation}
</small>
)}
</div>
</Message>
))}
</div>
);
};
const renderPrerequisites = () => {
if (!phase.prerequis || phase.prerequis.length === 0) {
return (
<Message
severity="info"
text="Aucun prérequis défini"
className="w-full"
/>
);
}
const prerequisitePhases = phase.prerequis
.map(prereqId => allPhases.find(p => p.id === prereqId))
.filter(Boolean) as PhaseChantier[];
if (prerequisitePhases.length === 0) {
return (
<Message
severity="warn"
text="Prérequis non trouvés dans le projet"
className="w-full"
/>
);
}
return (
<div className="flex flex-column gap-2">
{prerequisitePhases.map(prereq => {
const isCompleted = prereq.statut === 'TERMINEE';
const isInProgress = prereq.statut === 'EN_COURS';
return (
<div
key={prereq.id}
className="flex align-items-center justify-content-between p-3 border-1 surface-border border-round"
>
<div className="flex align-items-center gap-2">
<i className={`pi ${isCompleted ? 'pi-check-circle text-green-500' :
isInProgress ? 'pi-clock text-blue-500' :
'pi-circle text-gray-400'}`}></i>
<span className={isCompleted ? 'text-green-700' : isInProgress ? 'text-blue-700' : 'text-600'}>
{prereq.nom}
</span>
<Tag
value={prereq.statut}
severity={isCompleted ? 'success' : isInProgress ? 'info' : 'secondary'}
className="text-xs"
/>
{prereq.critique && (
<Tag value="Critique" severity="danger" className="text-xs" />
)}
</div>
<div className="flex align-items-center gap-2">
{prereq.dateFinReelle && (
<small className="text-600">
Terminé le {new Date(prereq.dateFinReelle).toLocaleDateString('fr-FR')}
</small>
)}
{prereq.dateFinPrevue && !prereq.dateFinReelle && (
<small className="text-600">
Prévu le {new Date(prereq.dateFinPrevue).toLocaleDateString('fr-FR')}
</small>
)}
{onViewPrerequisite && (
<Button
icon="pi pi-eye"
className="p-button-text p-button-sm"
onClick={() => onViewPrerequisite(prereq.id!)}
tooltip="Voir les détails"
/>
)}
</div>
</div>
);
})}
</div>
);
};
const renderBlockingPhases = () => {
if (!validation || validation.blockedBy.length === 0) {
return null;
}
return (
<Message severity="error" className="w-full">
<div>
<div className="font-semibold mb-2">Phase bloquée par :</div>
<div className="flex flex-wrap gap-1">
{validation.blockedBy.map((blocker, index) => (
<Tag key={index} value={blocker} severity="danger" />
))}
</div>
</div>
</Message>
);
};
const renderActionButtons = () => {
if (!validation || !onStartPhase) return null;
return (
<div className="flex gap-2 pt-3 border-top-1 surface-border">
<Button
label="Démarrer la phase"
icon="pi pi-play"
className="p-button-success"
disabled={!validation.canStart}
onClick={() => onStartPhase(phase.id!)}
/>
<Button
label="Revalider"
icon="pi pi-refresh"
className="p-button-outlined"
onClick={validatePhase}
/>
{validation.warnings.length > 0 && (
<Button
label="Ignorer les avertissements"
icon="pi pi-exclamation-triangle"
className="p-button-warning p-button-outlined"
disabled={validation.errors.length > 0}
onClick={() => onStartPhase && onStartPhase(phase.id!)}
/>
)}
</div>
);
};
if (loading) {
return (
<Panel header="Validation des prérequis" className={className}>
<div className="text-center p-4">
<i className="pi pi-spinner pi-spin text-2xl text-primary"></i>
<div className="mt-2">Validation en cours...</div>
</div>
</Panel>
);
}
if (compact) {
return (
<div className={`surface-card p-3 border-round ${className}`}>
{renderValidationStatus()}
{validation && validation.blockedBy.length > 0 && (
<div className="text-sm text-red-600">
Bloquée par : {validation.blockedBy.join(', ')}
</div>
)}
</div>
);
}
return (
<Panel
header={
<div className="flex align-items-center gap-2">
<span>Validation des prérequis</span>
{validation && !validation.readyToStart && (
<i className="pi pi-exclamation-triangle text-yellow-500"></i>
)}
</div>
}
className={className}
toggleable
>
{renderValidationStatus()}
{renderBlockingPhases()}
<Accordion
multiple
activeIndex={expandedSections}
onTabChange={(e) => setExpandedSections(e.index as string[])}
>
<AccordionTab
header={
<div className="flex align-items-center gap-2">
<span>Erreurs et avertissements</span>
{validation && (validation.errors.length > 0 || validation.warnings.length > 0) && (
<div className="flex gap-1">
{validation.errors.length > 0 && (
<Badge value={validation.errors.length} severity="danger" />
)}
{validation.warnings.length > 0 && (
<Badge value={validation.warnings.length} severity="warning" />
)}
</div>
)}
</div>
}
>
{renderErrorsAndWarnings()}
</AccordionTab>
<AccordionTab header="Prérequis">
{renderPrerequisites()}
</AccordionTab>
</Accordion>
{renderActionButtons()}
<Tooltip target=".validation-tooltip" />
</Panel>
);
};
export default PhaseValidationPanel;