import axios from 'axios'; import { API_CONFIG } from '../config/api'; import { PhaseChantier, PhaseChantierFormData, PhaseFilters, JalonPhase, PointagePhase, StatutPhase, ApiResponse } from '../types/btp-extended'; import { MaterielBTPService, MaterielBTP, CategorieMateriel } from './materielBTPService'; import { CalculsTechniquesService, ParametresCalculBriques, ResultatCalculBriques } from './calculsTechniquesService'; import { ZoneClimatiqueService, ZoneClimatique } from './zoneClimatiqueService'; class PhaseService { private readonly basePath = '/api/v1/phases-chantier'; private api = axios.create({ baseURL: API_CONFIG.baseURL, timeout: API_CONFIG.timeout, headers: API_CONFIG.headers, }); constructor() { // Interceptor pour ajouter le token Keycloak this.api.interceptors.request.use( async (config) => { // Vérifier si Keycloak est initialisé et l'utilisateur authentifié if (typeof window !== 'undefined') { const { keycloak, KEYCLOAK_TIMEOUTS } = await import('../config/keycloak'); if (keycloak.authenticated) { try { // Rafraîchir le token si nécessaire await keycloak.updateToken(KEYCLOAK_TIMEOUTS.TOKEN_REFRESH_BEFORE_EXPIRY); // Ajouter le token Bearer à l'en-tête Authorization if (keycloak.token) { config.headers['Authorization'] = `Bearer ${keycloak.token}`; } } catch (error) { console.error('Erreur lors de la mise à jour du token Keycloak:', error); keycloak.login(); throw error; } } else { // Fallback vers l'ancien système pour la rétrocompatibilité let token = null; try { const authTokenItem = sessionStorage.getItem('auth_token') || localStorage.getItem('auth_token'); if (authTokenItem) { const parsed = JSON.parse(authTokenItem); token = parsed.value; } } catch (e) { token = localStorage.getItem('token'); } if (token) { config.headers['Authorization'] = `Bearer ${token}`; } } } return config; }, (error) => Promise.reject(error) ); // Interceptor pour les réponses this.api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { localStorage.removeItem('token'); localStorage.removeItem('user'); window.location.href = '/auth/login'; } return Promise.reject(error); } ); } /** * Récupérer les phases d'un chantier */ async getByChantier(chantierId: string): Promise { // Vérifier si l'ID est valide if (!chantierId || chantierId === 'undefined' || chantierId === 'null' || chantierId === 'NaN') { console.warn(`ID de chantier invalide: ${chantierId}`); return []; } try { console.log(`🔍 Recherche des phases pour chantier: ${chantierId}`); // Ajouter un timestamp pour éviter le cache const response = await this.api.get(`${this.basePath}/chantier/${chantierId}`, { params: { _t: Date.now() // Forcer le rafraîchissement } }); console.log(`✅ ${response.data.length} phases récupérées pour chantier ${chantierId}:`, response.data); return response.data; } catch (error) { // Pour l'instant, retourner des données vides si l'endpoint n'existe pas console.warn(`❌ Endpoint ${this.basePath}/chantier/${chantierId} non disponible:`, error); return []; } } /** * Récupérer toutes les phases */ async getAll(): Promise { try { const response = await this.api.get(this.basePath); console.log('Toutes les phases récupérées:', response.data); return response.data; } catch (error) { console.warn('Erreur lors de la récupération de toutes les phases:', error); return []; } } /** * Récupérer une phase par ID */ async getById(id: string): Promise { const response = await this.api.get(`${this.basePath}/${id}`); return response.data; } /** * Créer une nouvelle phase */ async create(phase: PhaseChantierFormData): Promise { // D'abord récupérer les phases existantes pour calculer l'ordre d'exécution const chantierId = phase.chantier?.id || phase.chantierId; if (!chantierId) { throw new Error('ID du chantier requis'); } // Récupérer les phases existantes pour calculer le prochain ordre const existingPhases = await this.getByChantier(chantierId); const nextOrder = existingPhases.length > 0 ? Math.max(...existingPhases.map(p => p.ordreExecution || 0)) + 1 : 1; // S'assurer que chantierId est inclus dans la requête // et mapper phaseParent vers phaseParentId pour le backend const requestData = { ...phase, chantierId: chantierId, ordreExecution: phase.ordreExecution || nextOrder, phaseParentId: phase.phaseParent || phase.phaseParentId }; // Supprimer phaseParent si il existe pour éviter la confusion delete requestData.phaseParent; console.log('🚀 Création de phase avec données:', requestData); const response = await this.api.post(this.basePath, requestData); console.log('✅ Phase créée avec succès:', response.data); return response.data; } /** * Modifier une phase existante */ async update(id: string, phase: PhaseChantierFormData): Promise { const response = await this.api.put(`${this.basePath}/${id}`, phase); return response.data; } /** * Supprimer une phase */ async delete(id: string): Promise { await this.api.delete(`${this.basePath}/${id}`); } /** * Générer des phases à partir d'un template */ async generateFromTemplate( chantierId: number, templateId: string, configuration: { phasesSelectionnees: any[]; configurationsPersonnalisees?: any; optionsAvancees?: any; dateDebutSouhaitee?: string; dureeGlobale?: number; } ): Promise { try { console.log('Génération de phases depuis template:', { chantierId, templateId, configuration }); // Pour l'instant, on utilise la génération locale basée sur les templates // Cette méthode peut être remplacée par un appel API backend plus tard const phasesGenerees: PhaseChantier[] = []; // Récupérer le template et générer les phases const typeChantierService = await import('./typeChantierService'); const templates = await typeChantierService.default.getAllTemplates(); const template = templates.find(t => t.id === templateId); if (!template) { throw new Error(`Template ${templateId} non trouvé`); } // Générer les phases sélectionnées depuis la configuration (avec données utilisateur) for (let i = 0; i < configuration.phasesSelectionnees.length; i++) { const phaseSelectionnee = configuration.phasesSelectionnees[i]; // Utiliser les données personnalisées (saisies par l'utilisateur) const phaseData: any = { nom: phaseSelectionnee.nom, description: phaseSelectionnee.description, chantierId: chantierId.toString(), statut: 'PLANIFIEE' as StatutPhase, ordreExecution: phaseSelectionnee.ordre, dureeEstimeeHeures: (phaseSelectionnee.dureeEstimee || 1) * 8, // Convertir jours en heures (défaut 1 jour) budgetPrevu: phaseSelectionnee.budgetEstime || 0, coutReel: 0, priorite: phaseSelectionnee.categorieMetier === 'GROS_OEUVRE' ? 'CRITIQUE' : 'MOYENNE', critique: phaseSelectionnee.obligatoire, dateDebutPrevue: configuration.dateDebutSouhaitee, competencesRequises: phaseSelectionnee.competencesRequises || [], materielsNecessaires: [], fournisseursRecommandes: [] }; // Créer la phase via l'API const phaseCreee = await this.create(phaseData); phasesGenerees.push(phaseCreee); // Créer les sous-phases si elles existent if (phaseSelectionnee.sousPhases && phaseSelectionnee.sousPhases.length > 0) { for (let j = 0; j < phaseSelectionnee.sousPhases.length; j++) { const sousPhaseTemplate = phaseSelectionnee.sousPhases[j]; const sousPhaseData: any = { nom: sousPhaseTemplate.nom, description: sousPhaseTemplate.description, chantierId: chantierId.toString(), statut: 'PLANIFIEE' as StatutPhase, phaseParentId: phaseCreee.id, ordreExecution: sousPhaseTemplate.ordre, dureeEstimeeHeures: (sousPhaseTemplate.dureeEstimee || 1) * 8, budgetPrevu: sousPhaseTemplate.budgetEstime || 0, coutReel: 0, priorite: 'MOYENNE', critique: sousPhaseTemplate.obligatoire, competencesRequises: sousPhaseTemplate.competencesRequises || [], materielsNecessaires: [], fournisseursRecommandes: [] }; const sousPhaseCreee = await this.create(sousPhaseData); phasesGenerees.push(sousPhaseCreee); } } } console.log('Phases générées avec succès:', phasesGenerees); return phasesGenerees; } catch (error) { console.error('Erreur lors de la génération depuis template:', error); throw error; } } /** * Démarrer une phase */ async start(id: string): Promise { await this.api.post(`${this.basePath}/${id}/demarrer`); } /** * Terminer une phase */ async complete(id: string): Promise { await this.api.post(`${this.basePath}/${id}/terminer`); } /** * Suspendre une phase */ async suspend(id: string): Promise { await this.api.post(`${this.basePath}/${id}/suspendre`); } /** * Reprendre une phase suspendue */ async resume(id: string): Promise { await this.api.post(`${this.basePath}/${id}/reprendre`); } /** * Mettre à jour l'avancement d'une phase */ async updateProgress(id: string, pourcentage: number): Promise { await this.api.put(`${this.basePath}/${id}/avancement?pourcentage=${pourcentage}`); } /** * Alias français pour start() */ async demarrer(id: string): Promise { return this.start(id); } /** * Alias français pour complete() */ async terminer(id: string): Promise { return this.complete(id); } /** * Alias français pour suspend() */ async suspendre(id: string, motif?: string): Promise { return this.suspend(id); } /** * Alias français pour resume() */ async reprendre(id: string): Promise { return this.resume(id); } /** * Alias français pour updateProgress() */ async updateAvancement(id: string, pourcentage: number): Promise { return this.updateProgress(id, pourcentage); } /** * Récupérer les statuts disponibles */ async getStatuts(): Promise { const response = await this.api.get(`${this.basePath}/statuts`); return response.data; } /** * Récupérer les phases en retard */ async getEnRetard(): Promise { const response = await this.api.get(`${this.basePath}/en-retard`); return response.data; } /** * Récupérer les phases d'un responsable */ async getByResponsable(employeId: number): Promise { const response = await this.api.get(`${this.basePath}/responsable/${employeId}`); return response.data; } /** * Récupérer les jalons d'une phase */ async getJalons(phaseId: number): Promise { const response = await this.api.get(`${this.basePath}/${phaseId}/jalons`); return response.data; } /** * Récupérer les pointages d'une phase */ async getPointages(phaseId: number): Promise { const response = await this.api.get(`${this.basePath}/${phaseId}/pointages`); return response.data; } /** * Obtenir le libellé d'un statut */ getStatutLabel(statut: StatutPhase): string { const labels: Record = { PLANIFIEE: 'Planifiée', EN_ATTENTE: 'En attente', EN_COURS: 'En cours', EN_PAUSE: 'En pause', TERMINEE: 'Terminée', ANNULEE: 'Annulée', EN_RETARD: 'En retard' }; return labels[statut] || statut; } /** * Obtenir la couleur d'un statut */ getStatutColor(statut: StatutPhase): string { const colors: Record = { PLANIFIEE: '#6c757d', EN_ATTENTE: '#ffc107', EN_COURS: '#0d6efd', EN_PAUSE: '#fd7e14', TERMINEE: '#198754', ANNULEE: '#dc3545', EN_RETARD: '#dc3545' }; return colors[statut] || '#6c757d'; } /** * Vérifier si une phase est en retard */ isEnRetard(phase: PhaseChantier): boolean { if (phase.statut === 'TERMINEE') return false; if (!phase.dateFinPrevue) return false; return new Date() > new Date(phase.dateFinPrevue); } } export default new PhaseService();