364 lines
14 KiB
TypeScript
Executable File
364 lines
14 KiB
TypeScript
Executable File
'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; |