/** * Service pour gérer la cohérence des budgets entre chantiers et phases * Assure que le montant_prevu du chantier correspond à la somme des budget_prevu des phases */ import { API_CONFIG } from '../config/api'; import axios from 'axios'; interface BudgetCoherence { chantierId: string; chantierNom: string; budgetChantier: number; budgetPhasesTotal: number; ecartAbsolu: number; ecartPourcentage: number; coherent: boolean; nombrePhases: number; } interface ValidationBudget { valide: boolean; message: string; recommandation?: 'METTRE_A_JOUR_CHANTIER' | 'AJUSTER_PHASES' | 'AUCUNE_ACTION'; nouveauBudgetSuggere?: number; } class BudgetCoherenceService { 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) ); } /** * Vérifier la cohérence budgétaire pour un chantier donné */ async verifierCoherence(chantierId: string): Promise { try { // Pour l'instant, on fait toujours le calcul côté client // L'endpoint backend sera implémenté plus tard const [chantierResponse, phasesResponse] = await Promise.all([ this.api.get(`/api/v1/chantiers/${chantierId}`), this.api.get(`/api/v1/phases-chantier/chantier/${chantierId}`) ]); const chantier = chantierResponse.data; const phases = phasesResponse.data || []; const budgetChantier = parseFloat(chantier.montantPrevu || 0); const budgetPhasesTotal = phases.reduce((sum: number, phase: any) => sum + parseFloat(phase.budgetPrevu || 0), 0); const ecartAbsolu = Math.abs(budgetChantier - budgetPhasesTotal); const ecartPourcentage = budgetChantier > 0 ? (ecartAbsolu / budgetChantier) * 100 : 0; return { chantierId, chantierNom: chantier.nom, budgetChantier, budgetPhasesTotal, ecartAbsolu, ecartPourcentage, coherent: ecartPourcentage <= 5, // Tolérance de 5% nombrePhases: phases.length }; } catch (error) { console.warn('Erreur lors de la vérification de cohérence:', error); // Retourner des valeurs par défaut en cas d'erreur return { chantierId, chantierNom: 'Chantier inconnu', budgetChantier: 0, budgetPhasesTotal: 0, ecartAbsolu: 0, ecartPourcentage: 0, coherent: true, // Considérer comme cohérent en cas d'erreur nombrePhases: 0 }; } } /** * Valider un budget de phases avant génération */ async validerBudgetPhases(chantierId: string, budgetPhasesPrevu: number): Promise { try { const coherence = await this.verifierCoherence(chantierId); const nouveauTotal = budgetPhasesPrevu; const ecart = Math.abs(coherence.budgetChantier - nouveauTotal); const ecartPourcentage = coherence.budgetChantier > 0 ? (ecart / coherence.budgetChantier) * 100 : 0; if (ecartPourcentage <= 5) { return { valide: true, message: 'Le budget des phases est cohérent avec le budget du chantier', recommandation: 'AUCUNE_ACTION' }; } else if (nouveauTotal > coherence.budgetChantier) { return { valide: false, message: `Le budget des phases (${this.formatCurrency(nouveauTotal)}) dépasse le budget du chantier (${this.formatCurrency(coherence.budgetChantier)}) de ${ecartPourcentage.toFixed(1)}%`, recommandation: 'METTRE_A_JOUR_CHANTIER', nouveauBudgetSuggere: Math.ceil(nouveauTotal * 1.1) // +10% de marge }; } else { return { valide: false, message: `Le budget des phases (${this.formatCurrency(nouveauTotal)}) est inférieur au budget du chantier (${this.formatCurrency(coherence.budgetChantier)}) de ${ecartPourcentage.toFixed(1)}%`, recommandation: 'AJUSTER_PHASES', nouveauBudgetSuggere: coherence.budgetChantier }; } } catch (error) { console.error('Erreur lors de la validation budgétaire:', error); return { valide: true, // En cas d'erreur, on laisse passer message: 'Impossible de valider la cohérence budgétaire' }; } } /** * Mettre à jour le budget du chantier pour le faire correspondre aux phases */ async synchroniserBudgetChantier(chantierId: string, nouveauBudget: number): Promise { try { await this.api.put(`/api/v1/chantiers/${chantierId}`, { montantPrevu: nouveauBudget }); console.log(`Budget du chantier mis à jour: ${this.formatCurrency(nouveauBudget)}`); return true; } catch (error) { console.error('Erreur lors de la mise à jour du budget:', error); return false; } } /** * Suggérer une répartition équilibrée du budget sur les phases */ suggererRepartitionBudget(budgetTotal: number, phases: any[]): any[] { if (!phases.length) return []; // Répartition basée sur la complexité/durée des phases const phasesAvecPoids = phases.map(phase => ({ ...phase, poids: this.calculerPoidsPhase(phase) })); const poidsTotal = phasesAvecPoids.reduce((sum, p) => sum + p.poids, 0); return phasesAvecPoids.map(phase => ({ ...phase, budgetSuggere: Math.round((phase.poids / poidsTotal) * budgetTotal) })); } /** * Calculer le poids d'une phase pour la répartition budgétaire */ private calculerPoidsPhase(phase: any): number { let poids = 1; // Basé sur la durée if (phase.dureeEstimee) { poids *= Math.max(1, phase.dureeEstimee / 5); // Phases plus longues = plus de poids } // Basé sur le type de phase const typesPoids: Record = { 'GROS_OEUVRE': 2.0, 'FONDATIONS': 1.8, 'CHARPENTE': 1.5, 'SECOND_OEUVRE': 1.2, 'FINITIONS': 1.0, 'EQUIPEMENTS': 1.3 }; if (phase.categorieMetier && typesPoids[phase.categorieMetier]) { poids *= typesPoids[phase.categorieMetier]; } // Phase critique = plus de poids if (phase.obligatoire || phase.critique) { poids *= 1.2; } return poids; } /** * Formater un montant en devise */ private formatCurrency(amount: number): string { return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0 }).format(amount); } /** * Obtenir des recommandations budgétaires pour un chantier */ async obtenirRecommandations(chantierId: string): Promise { const coherence = await this.verifierCoherence(chantierId); const recommandations: string[] = []; if (!coherence.coherent) { if (coherence.budgetPhasesTotal > coherence.budgetChantier) { recommandations.push( `💰 Le budget des phases dépasse celui du chantier de ${this.formatCurrency(coherence.ecartAbsolu)}` ); recommandations.push( `🔧 Recommandation : Augmenter le budget du chantier à ${this.formatCurrency(coherence.budgetPhasesTotal * 1.1)}` ); } else { recommandations.push( `📉 Le budget des phases est inférieur à celui du chantier de ${this.formatCurrency(coherence.ecartAbsolu)}` ); recommandations.push( `🔧 Recommandation : Réajuster les budgets des phases ou prévoir des phases supplémentaires` ); } } if (coherence.nombrePhases === 0) { recommandations.push( `⚠️ Aucune phase définie pour ce chantier. Utilisez l'assistant de génération de phases.` ); } return recommandations; } } export default new BudgetCoherenceService();