/** * 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; } { const phaseValidations = new Map(); 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();