Files
btpxpress-frontend/services/phaseValidationService.ts

570 lines
21 KiB
TypeScript
Executable File

/**
* Service de validation des prérequis de phases
* Gère la validation des dépendances entre phases et leurs prérequis métier
*/
import type { PhaseChantier } from '../types/btp';
export interface PhaseValidationResult {
canStart: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
blockedBy: string[];
readyToStart: boolean;
}
export interface ValidationError {
code: string;
message: string;
severity: 'error' | 'warning' | 'info';
phaseId?: string;
prerequisiteId?: string;
}
export interface ValidationWarning {
code: string;
message: string;
recommendation?: string;
}
export interface PrerequisiteStatus {
prerequisiteId: string;
prerequisiteName: string;
status: 'completed' | 'in_progress' | 'not_started' | 'not_found';
completionDate?: Date;
isBlocking: boolean;
}
class PhaseValidationService {
/**
* Valide si une phase peut être démarrée en fonction de ses prérequis
*/
validatePhaseStart(
phase: PhaseChantier,
allPhases: PhaseChantier[],
options: { strictMode?: boolean } = {}
): PhaseValidationResult {
const { strictMode = false } = options;
const result: PhaseValidationResult = {
canStart: true,
errors: [],
warnings: [],
blockedBy: [],
readyToStart: false
};
// Vérifier les prérequis de base
this.validateBasicPrerequisites(phase, result);
// Vérifier les dépendances entre phases
this.validatePhaseDependencies(phase, allPhases, result, strictMode);
// Vérifier les prérequis métier
this.validateBusinessPrerequisites(phase, allPhases, result);
// Vérifier les contraintes temporelles
this.validateTemporalConstraints(phase, allPhases, result);
// Déterminer si la phase est prête à démarrer
result.readyToStart = result.canStart && result.errors.length === 0;
return result;
}
/**
* Valide les prérequis de base d'une phase
*/
private validateBasicPrerequisites(phase: PhaseChantier, result: PhaseValidationResult): void {
// Vérifier le statut de la phase
if (phase.statut === 'TERMINEE') {
result.errors.push({
code: 'PHASE_ALREADY_COMPLETED',
message: 'Cette phase est déjà terminée',
severity: 'info'
});
result.canStart = false;
}
if (phase.statut === 'EN_COURS') {
result.warnings.push({
code: 'PHASE_ALREADY_STARTED',
message: 'Cette phase est déjà en cours',
recommendation: 'Vérifiez l\'avancement actuel'
});
}
// Vérifier les dates
const today = new Date();
today.setHours(0, 0, 0, 0);
if (phase.dateDebutPrevue) {
const dateDebut = new Date(phase.dateDebutPrevue);
dateDebut.setHours(0, 0, 0, 0);
if (dateDebut > today) {
result.warnings.push({
code: 'PHASE_FUTURE_START',
message: `Cette phase est prévue pour commencer le ${dateDebut.toLocaleDateString('fr-FR')}`,
recommendation: 'Vérifiez si un démarrage anticipé est possible'
});
}
}
if (phase.dateFinPrevue) {
const dateFin = new Date(phase.dateFinPrevue);
dateFin.setHours(0, 0, 0, 0);
if (dateFin < today) {
result.warnings.push({
code: 'PHASE_OVERDUE',
message: 'Cette phase aurait dû être terminée',
recommendation: 'Réviser la planification'
});
}
}
}
/**
* Valide les dépendances entre phases
*/
private validatePhaseDependencies(
phase: PhaseChantier,
allPhases: PhaseChantier[],
result: PhaseValidationResult,
strictMode: boolean
): void {
if (!phase.prerequis || phase.prerequis.length === 0) {
return;
}
const prerequisiteStatuses = this.getPrerequisiteStatuses(phase, allPhases);
prerequisiteStatuses.forEach(prereq => {
switch (prereq.status) {
case 'not_found':
result.errors.push({
code: 'PREREQUISITE_NOT_FOUND',
message: `Prérequis non trouvé: ${prereq.prerequisiteName}`,
severity: 'error',
prerequisiteId: prereq.prerequisiteId
});
result.canStart = false;
break;
case 'not_started':
if (prereq.isBlocking || strictMode) {
result.errors.push({
code: 'PREREQUISITE_NOT_STARTED',
message: `Prérequis non démarré: ${prereq.prerequisiteName}`,
severity: 'error',
prerequisiteId: prereq.prerequisiteId
});
result.blockedBy.push(prereq.prerequisiteName);
result.canStart = false;
} else {
result.warnings.push({
code: 'PREREQUISITE_NOT_STARTED_WARNING',
message: `Prérequis non démarré: ${prereq.prerequisiteName}`,
recommendation: 'Considérez démarrer ce prérequis en parallèle'
});
}
break;
case 'in_progress':
if (prereq.isBlocking || strictMode) {
result.warnings.push({
code: 'PREREQUISITE_IN_PROGRESS',
message: `Prérequis en cours: ${prereq.prerequisiteName}`,
recommendation: 'Attendez la fin de cette phase ou vérifiez si un démarrage parallèle est possible'
});
}
break;
case 'completed':
// Tout va bien
break;
}
});
}
/**
* Valide les prérequis métier spécifiques
*/
private validateBusinessPrerequisites(
phase: PhaseChantier,
allPhases: PhaseChantier[],
result: PhaseValidationResult
): void {
// Règles métier spécifiques au BTP
this.validateBTPBusinessRules(phase, allPhases, result);
// Vérifier les compétences requises
this.validateRequiredSkills(phase, result);
// Vérifier les ressources disponibles
this.validateResourceAvailability(phase, result);
}
/**
* Valide les règles métier spécifiques au BTP
*/
private validateBTPBusinessRules(
phase: PhaseChantier,
allPhases: PhaseChantier[],
result: PhaseValidationResult
): void {
const phaseName = phase.nom.toLowerCase();
// Règles pour la maçonnerie
if (phaseName.includes('maçonnerie') || phaseName.includes('béton')) {
const fondations = allPhases.find(p =>
p.nom.toLowerCase().includes('fondation') ||
p.nom.toLowerCase().includes('excavation')
);
if (fondations && fondations.statut !== 'TERMINEE') {
result.errors.push({
code: 'FOUNDATION_NOT_COMPLETED',
message: 'Les fondations doivent être terminées avant les travaux de maçonnerie',
severity: 'error',
phaseId: fondations.id
});
result.canStart = false;
result.blockedBy.push('Fondations');
}
}
// Règles pour l'électricité
if (phaseName.includes('électric') || phaseName.includes('électro')) {
const grosOeuvre = allPhases.find(p =>
p.nom.toLowerCase().includes('gros œuvre') ||
p.nom.toLowerCase().includes('gros oeuvre') ||
p.nom.toLowerCase().includes('structure')
);
if (grosOeuvre && grosOeuvre.statut !== 'TERMINEE') {
result.warnings.push({
code: 'STRUCTURE_NOT_COMPLETED',
message: 'Il est recommandé d\'attendre la fin du gros œuvre',
recommendation: 'Certains travaux électriques peuvent commencer en parallèle selon les zones'
});
}
}
// Règles pour la plomberie
if (phaseName.includes('plomberie') || phaseName.includes('sanitaire')) {
const cloisons = allPhases.find(p =>
p.nom.toLowerCase().includes('cloison') ||
p.nom.toLowerCase().includes('doublage')
);
if (cloisons && cloisons.statut === 'TERMINEE') {
result.warnings.push({
code: 'PARTITIONS_ALREADY_DONE',
message: 'Les cloisons sont déjà terminées - travaux de plomberie plus complexes',
recommendation: 'Prévoir des saignées ou passages techniques'
});
}
}
// Règles pour les finitions
if (phaseName.includes('peinture') || phaseName.includes('revêtement') || phaseName.includes('carrelage')) {
const secondOeuvre = allPhases.filter(p => {
const nom = p.nom.toLowerCase();
return nom.includes('électric') || nom.includes('plomberie') || nom.includes('chauffage');
});
const unfinishedSecondOeuvre = secondOeuvre.filter(p => p.statut !== 'TERMINEE');
if (unfinishedSecondOeuvre.length > 0) {
result.warnings.push({
code: 'SECOND_WORK_NOT_COMPLETED',
message: 'Certains travaux de second œuvre ne sont pas terminés',
recommendation: 'Terminer électricité, plomberie et chauffage avant les finitions'
});
}
}
}
/**
* Valide les compétences requises
*/
private validateRequiredSkills(phase: PhaseChantier, result: PhaseValidationResult): void {
// Cette validation serait étoffée avec une vraie base de données des compétences
const requiredSkills = this.getRequiredSkillsForPhase(phase);
if (requiredSkills.length > 0) {
result.warnings.push({
code: 'SKILLS_REQUIRED',
message: `Compétences requises: ${requiredSkills.join(', ')}`,
recommendation: 'Vérifiez la disponibilité des artisans qualifiés'
});
}
}
/**
* Valide la disponibilité des ressources
*/
private validateResourceAvailability(phase: PhaseChantier, result: PhaseValidationResult): void {
// Vérifications météorologiques pour certaines phases
if (this.isWeatherSensitivePhase(phase)) {
const season = this.getCurrentSeason();
if (season === 'winter') {
result.warnings.push({
code: 'WEATHER_SENSITIVE_WINTER',
message: 'Phase sensible aux conditions météorologiques - période hivernale',
recommendation: 'Prévoir des protections contre le gel et les intempéries'
});
}
}
// Vérification des matériaux
result.warnings.push({
code: 'MATERIALS_CHECK',
message: 'Vérifiez la disponibilité des matériaux',
recommendation: 'Confirmez les livraisons avant le démarrage'
});
}
/**
* Valide les contraintes temporelles
*/
private validateTemporalConstraints(
phase: PhaseChantier,
allPhases: PhaseChantier[],
result: PhaseValidationResult
): void {
const today = new Date();
// Vérifier les chevauchements problématiques
const overlappingPhases = this.findOverlappingPhases(phase, allPhases);
overlappingPhases.forEach(overlapping => {
if (this.arePhasesMutuallyExclusive(phase, overlapping)) {
result.errors.push({
code: 'PHASE_CONFLICT',
message: `Conflit avec la phase: ${overlapping.nom}`,
severity: 'error',
phaseId: overlapping.id
});
result.canStart = false;
}
});
// Vérifier les délais critiques
if (phase.critique && phase.dateFinPrevue) {
const dateFin = new Date(phase.dateFinPrevue);
const diffDays = Math.ceil((dateFin.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays < 7) {
result.warnings.push({
code: 'CRITICAL_PHASE_URGENT',
message: 'Phase critique avec délai serré',
recommendation: 'Mobiliser des ressources supplémentaires'
});
}
}
}
/**
* Obtient le statut des prérequis d'une phase
*/
private getPrerequisiteStatuses(phase: PhaseChantier, allPhases: PhaseChantier[]): PrerequisiteStatus[] {
if (!phase.prerequis) return [];
return phase.prerequis.map(prerequisiteId => {
const prerequisitePhase = allPhases.find(p => p.id === prerequisiteId);
return {
prerequisiteId,
prerequisiteName: prerequisitePhase?.nom || prerequisiteId,
status: prerequisitePhase ?
(prerequisitePhase.statut === 'TERMINEE' ? 'completed' :
prerequisitePhase.statut === 'EN_COURS' ? 'in_progress' : 'not_started') :
'not_found',
completionDate: prerequisitePhase?.dateFinReelle ? new Date(prerequisitePhase.dateFinReelle) : undefined,
isBlocking: this.isBlockingPrerequisite(prerequisiteId, phase)
};
});
}
/**
* Détermine si un prérequis est bloquant
*/
private isBlockingPrerequisite(prerequisiteId: string, phase: PhaseChantier): boolean {
// Logique pour déterminer si un prérequis est critique/bloquant
// Basée sur le type de phase et les règles métier
const criticalPrerequisites = [
'fondations', 'excavation', 'gros-oeuvre', 'structure'
];
return criticalPrerequisites.some(critical =>
prerequisiteId.toLowerCase().includes(critical)
);
}
/**
* Obtient les compétences requises pour une phase
*/
private getRequiredSkillsForPhase(phase: PhaseChantier): string[] {
const phaseName = phase.nom.toLowerCase();
const skills: string[] = [];
if (phaseName.includes('maçonnerie')) skills.push('Maçon');
if (phaseName.includes('électric')) skills.push('Électricien');
if (phaseName.includes('plomberie')) skills.push('Plombier');
if (phaseName.includes('charpente')) skills.push('Charpentier');
if (phaseName.includes('couverture')) skills.push('Couvreur');
if (phaseName.includes('peinture')) skills.push('Peintre');
if (phaseName.includes('carrelage')) skills.push('Carreleur');
return skills;
}
/**
* Détermine si une phase est sensible aux conditions météorologiques
*/
private isWeatherSensitivePhase(phase: PhaseChantier): boolean {
const weatherSensitive = [
'couverture', 'étanchéité', 'façade', 'maçonnerie extérieure',
'terrassement', 'fondations', 'béton'
];
return weatherSensitive.some(sensitive =>
phase.nom.toLowerCase().includes(sensitive)
);
}
/**
* Obtient la saison actuelle
*/
private getCurrentSeason(): 'spring' | 'summer' | 'fall' | 'winter' {
const month = new Date().getMonth() + 1;
if (month >= 3 && month <= 5) return 'spring';
if (month >= 6 && month <= 8) return 'summer';
if (month >= 9 && month <= 11) return 'fall';
return 'winter';
}
/**
* Trouve les phases qui se chevauchent temporellement
*/
private findOverlappingPhases(phase: PhaseChantier, allPhases: PhaseChantier[]): PhaseChantier[] {
if (!phase.dateDebutPrevue || !phase.dateFinPrevue) return [];
const phaseStart = new Date(phase.dateDebutPrevue);
const phaseEnd = new Date(phase.dateFinPrevue);
return allPhases.filter(otherPhase => {
if (otherPhase.id === phase.id) return false;
if (!otherPhase.dateDebutPrevue || !otherPhase.dateFinPrevue) return false;
const otherStart = new Date(otherPhase.dateDebutPrevue);
const otherEnd = new Date(otherPhase.dateFinPrevue);
return (phaseStart <= otherEnd && phaseEnd >= otherStart);
});
}
/**
* Détermine si deux phases sont mutuellement exclusives
*/
private arePhasesMutuallyExclusive(phase1: PhaseChantier, phase2: PhaseChantier): boolean {
const exclusiveGroups = [
['maçonnerie', 'béton'],
['peinture', 'électricité'],
['carrelage', 'plomberie']
];
return exclusiveGroups.some(group => {
const phase1InGroup = group.some(term => phase1.nom.toLowerCase().includes(term));
const phase2InGroup = group.some(term => phase2.nom.toLowerCase().includes(term));
return phase1InGroup && phase2InGroup;
});
}
/**
* Valide l'ensemble d'un planning de phases
*/
validateProjectSchedule(phases: PhaseChantier[]): {
isValid: boolean;
globalErrors: ValidationError[];
phaseValidations: Map<string, PhaseValidationResult>;
} {
const phaseValidations = new Map<string, PhaseValidationResult>();
const globalErrors: ValidationError[] = [];
// Valider chaque phase individuellement
phases.forEach(phase => {
const validation = this.validatePhaseStart(phase, phases);
phaseValidations.set(phase.id!, validation);
});
// Validations globales
this.validateGlobalScheduleConstraints(phases, globalErrors);
const isValid = globalErrors.length === 0 &&
Array.from(phaseValidations.values()).every(v => v.readyToStart || v.errors.length === 0);
return {
isValid,
globalErrors,
phaseValidations
};
}
/**
* Valide les contraintes globales du planning
*/
private validateGlobalScheduleConstraints(phases: PhaseChantier[], globalErrors: ValidationError[]): void {
// Vérifier l'ordre logique des phases
const orderedPhases = phases
.filter(p => p.ordreExecution !== undefined)
.sort((a, b) => (a.ordreExecution || 0) - (b.ordreExecution || 0));
for (let i = 1; i < orderedPhases.length; i++) {
const currentPhase = orderedPhases[i];
const previousPhase = orderedPhases[i - 1];
if (currentPhase.dateDebutPrevue && previousPhase.dateFinPrevue) {
const currentStart = new Date(currentPhase.dateDebutPrevue);
const previousEnd = new Date(previousPhase.dateFinPrevue);
if (currentStart < previousEnd) {
globalErrors.push({
code: 'SCHEDULE_ORDER_VIOLATION',
message: `La phase "${currentPhase.nom}" commence avant la fin de "${previousPhase.nom}"`,
severity: 'warning',
phaseId: currentPhase.id
});
}
}
}
// Vérifier la durée totale du projet
if (phases.length > 0) {
const startDates = phases
.filter(p => p.dateDebutPrevue)
.map(p => new Date(p.dateDebutPrevue!));
const endDates = phases
.filter(p => p.dateFinPrevue)
.map(p => new Date(p.dateFinPrevue!));
if (startDates.length > 0 && endDates.length > 0) {
const projectStart = new Date(Math.min(...startDates.map(d => d.getTime())));
const projectEnd = new Date(Math.max(...endDates.map(d => d.getTime())));
const projectDuration = Math.ceil((projectEnd.getTime() - projectStart.getTime()) / (1000 * 60 * 60 * 24));
if (projectDuration > 730) { // 2 ans
globalErrors.push({
code: 'PROJECT_TOO_LONG',
message: `Durée du projet très longue: ${projectDuration} jours`,
severity: 'warning'
});
}
}
}
}
}
export default new PhaseValidationService();