import axios from 'axios'; import { API_CONFIG } from '../config/api'; import { PhaseChantier, PhaseChantierFormData, PhaseFilters, JalonPhase, PointagePhase, StatutPhase, ApiResponse } from '../types/btp-extended'; class PhaseChantierService { private readonly basePath = '/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 = '/api/auth/login'; } return Promise.reject(error); } ); } /** * Récupérer les phases d'un chantier */ async getByChantier(chantierId: number): Promise { const response = await this.api.api.get(`${this.basePath}/chantier/${chantierId}`); return response.data; } /** * Récupérer une phase par ID */ async getById(id: number): Promise { const response = await this.api.get(`${this.basePath}/${id}`); return response.data; } /** * Créer une nouvelle phase */ async create(phase: PhaseChantierFormData): Promise { const response = await this.api.post(this.basePath, phase); return response.data; } /** * Modifier une phase existante */ async update(id: number, phase: PhaseChantierFormData): Promise { const response = await this.api.put(`${this.basePath}/${id}`, phase); return response.data; } /** * Supprimer une phase */ async delete(id: number): Promise { await this.api.delete(`${this.basePath}/${id}`); } /** * Démarrer une phase */ async start(id: number): Promise { await this.api.post(`${this.basePath}/${id}/demarrer`); } /** * Terminer une phase */ async complete(id: number): Promise { await this.api.post(`${this.basePath}/${id}/terminer`); } /** * Suspendre une phase */ async suspend(id: number): Promise { await this.api.post(`${this.basePath}/${id}/suspendre`); } /** * Reprendre une phase suspendue */ async resume(id: number): Promise { await this.api.post(`${this.basePath}/${id}/reprendre`); } /** * Mettre à jour l'avancement d'une phase */ async updateProgress(id: number, pourcentage: number): Promise { await this.api.put(`${this.basePath}/${id}/avancement?pourcentage=${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; } /** * Valider les données d'une phase */ validatePhase(phase: PhaseChantierFormData): string[] { const errors: string[] = []; if (!phase.nom || phase.nom.trim().length === 0) { errors.push('Le nom de la phase est obligatoire'); } if (phase.nom && phase.nom.length > 100) { errors.push('Le nom ne peut pas dépasser 100 caractères'); } if (!phase.dateDebutPrevue) { errors.push('La date de début prévue est obligatoire'); } if (!phase.dateFinPrevue) { errors.push('La date de fin prévue est obligatoire'); } if (phase.dateDebutPrevue && phase.dateFinPrevue) { const debut = new Date(phase.dateDebutPrevue); const fin = new Date(phase.dateFinPrevue); if (debut >= fin) { errors.push('La date de fin doit être postérieure à la date de début'); } } if (phase.budgetPrevu !== undefined && phase.budgetPrevu < 0) { errors.push('Le budget prévu doit être positif'); } return errors; } /** * Calculer la durée prévue d'une phase en jours */ calculateDureePrevue(phase: PhaseChantier): number { if (!phase.dateDebutPrevue || !phase.dateFinPrevue) return 0; const debut = new Date(phase.dateDebutPrevue); const fin = new Date(phase.dateFinPrevue); const diffTime = Math.abs(fin.getTime() - debut.getTime()); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } /** * Calculer la durée réelle d'une phase en jours */ calculateDureeReelle(phase: PhaseChantier): number { if (!phase.dateDebutReelle) return 0; const debut = new Date(phase.dateDebutReelle); const fin = phase.dateFinReelle ? new Date(phase.dateFinReelle) : new Date(); const diffTime = Math.abs(fin.getTime() - debut.getTime()); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } /** * 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); } /** * Calculer le retard en jours */ calculateRetard(phase: PhaseChantier): number { if (!this.isEnRetard(phase)) return 0; const dateFinPrevue = new Date(phase.dateFinPrevue!); const maintenant = new Date(); const diffTime = maintenant.getTime() - dateFinPrevue.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } /** * 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'; } /** * Calculer les statistiques des phases d'un chantier */ calculateStatistiques(phases: PhaseChantier[]): { total: number; planifiees: number; enCours: number; terminees: number; enRetard: number; avancementMoyen: number; budgetTotal: number; coutTotal: number; } { const stats = { total: phases.length, planifiees: 0, enCours: 0, terminees: 0, enRetard: 0, avancementMoyen: 0, budgetTotal: 0, coutTotal: 0 }; let avancementTotal = 0; phases.forEach(phase => { // Compter par statut switch (phase.statut) { case 'PLANIFIEE': case 'EN_ATTENTE': stats.planifiees++; break; case 'EN_COURS': case 'EN_PAUSE': stats.enCours++; break; case 'TERMINEE': stats.terminees++; break; case 'EN_RETARD': stats.enRetard++; break; } // Calculer avancement moyen avancementTotal += phase.pourcentageAvancement || 0; // Calculer budget et coût stats.budgetTotal += phase.budgetPrevu || 0; stats.coutTotal += phase.coutReel || 0; }); stats.avancementMoyen = phases.length > 0 ? avancementTotal / phases.length : 0; return stats; } /** * Générer un planning Gantt simple */ generateGanttData(phases: PhaseChantier[]): any[] { return phases.map(phase => ({ id: phase.id, name: phase.nom, start: phase.dateDebutPrevue, end: phase.dateFinPrevue, progress: (phase.pourcentageAvancement || 0) / 100, status: phase.statut, parent: phase.phaseParent?.id, dependencies: [], // TODO: Implémenter les dépendances color: this.getStatutColor(phase.statut) })); } /** * Exporter les phases au format CSV */ async exportToCsv(chantierId: number): Promise { const phases = await this.getByChantier(chantierId); const headers = [ 'ID', 'Nom', 'Statut', 'Date Début Prévue', 'Date Fin Prévue', 'Date Début Réelle', 'Date Fin Réelle', 'Avancement (%)', 'Budget Prévu', 'Coût Réel', 'Responsable', 'Critique' ]; const csvContent = [ headers.join(';'), ...phases.map(p => [ p.id || '', p.nom || '', this.getStatutLabel(p.statut), p.dateDebutPrevue ? new Date(p.dateDebutPrevue).toLocaleDateString('fr-FR') : '', p.dateFinPrevue ? new Date(p.dateFinPrevue).toLocaleDateString('fr-FR') : '', p.dateDebutReelle ? new Date(p.dateDebutReelle).toLocaleDateString('fr-FR') : '', p.dateFinReelle ? new Date(p.dateFinReelle).toLocaleDateString('fr-FR') : '', p.pourcentageAvancement || 0, p.budgetPrevu || 0, p.coutReel || 0, p.responsable ? `${p.responsable.nom} ${p.responsable.prenom}` : '', p.critique ? 'Oui' : 'Non' ].join(';')) ].join('\n'); return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); } } export default new PhaseChantierService();