import axios from 'axios'; import { API_CONFIG } from '../config/api'; import { AnalysePrixPhase, ApiResponse } from '../types/btp-extended'; import materielPhaseService from './materielPhaseService'; import fournisseurPhaseService from './fournisseurPhaseService'; class AnalysePrixService { private readonly basePath = '/analyses-prix'; private api = axios.create({ baseURL: API_CONFIG.baseURL, timeout: API_CONFIG.timeout, headers: API_CONFIG.headers, }); constructor() { // Interceptor pour ajouter le token JWT this.api.interceptors.request.use( (config) => { 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 l'analyse de prix d'une phase */ async getByPhase(phaseId: string): Promise { if (!phaseId || phaseId === 'undefined' || phaseId === 'null' || phaseId === 'NaN') { console.warn(`ID de phase invalide: ${phaseId}`); return null; } try { const response = await this.api.get(`${this.basePath}/phase/${phaseId}`); return response.data; } catch (error) { console.warn(`Endpoint ${this.basePath}/phase/${phaseId} non disponible:`, error); return null; } } /** * Créer ou mettre à jour une analyse de prix */ async createOrUpdate(analyse: Omit): Promise { try { const response = await this.api.post(`${this.basePath}`, analyse); return response.data; } catch (error) { console.warn('Endpoint createOrUpdate non disponible, calcul local:', error); return { ...analyse, id: Date.now(), // ID temporaire dateAnalyse: new Date() }; } } /** * Calculer automatiquement l'analyse de prix d'une phase */ async calculerAnalyseComplete(phaseId: string): Promise { try { // Récupération des données de base const [materiels, fournisseurs] = await Promise.all([ materielPhaseService.getByPhase(phaseId), fournisseurPhaseService.getByPhase(phaseId) ]); // Calcul du coût des matériaux const coutMateriauxTotal = materiels.reduce((total, materiel) => { const prix = materiel.prixUnitaireNegocie || materiel.prixUnitaireCatalogue || 0; const quantite = materiel.quantiteUtilisee || materiel.quantitePrevue || 0; return total + (prix * quantite); }, 0); // Calcul du coût de main d'œuvre (estimation basée sur les matériaux) const coutMainOeuvreTotal = this.estimerCoutMainOeuvre(coutMateriauxTotal); // Calcul du coût de sous-traitance const coutSousTraitanceTotal = fournisseurs .filter(f => f.typeContribution === 'SOUS_TRAITANCE') .reduce((total, f) => total + (f.prixNegocie || f.prixCatalogue || 0), 0); // Calcul du coût des services et autres const coutAutresTotal = fournisseurs .filter(f => ['SERVICE', 'LOCATION', 'TRANSPORT'].includes(f.typeContribution)) .reduce((total, f) => total + (f.prixNegocie || f.prixCatalogue || 0), 0); // Coût total direct const coutTotalDirect = coutMateriauxTotal + coutMainOeuvreTotal + coutSousTraitanceTotal + coutAutresTotal; // Frais généraux (15% par défaut) const tauxFraisGeneraux = 0.15; const fraisGeneraux = coutTotalDirect * tauxFraisGeneraux; const coutTotalAvecFrais = coutTotalDirect + fraisGeneraux; // Marge objectif (20% par défaut) const tauxMargeObjectif = 0.20; const margeObjectif = coutTotalAvecFrais * tauxMargeObjectif; const prixVenteCalcule = coutTotalAvecFrais + margeObjectif; // Calcul de la rentabilité prévisionnelle const rentabilitePrevisionnelle = (margeObjectif / prixVenteCalcule) * 100; const analyse: AnalysePrixPhase = { phase: { id: parseInt(phaseId) } as any, coutMateriauxTotal, coutMainOeuvreTotal, coutSousTraitanceTotal, coutAutresTotal, coutTotalDirect, fraisGeneraux, tauxFraisGeneraux: tauxFraisGeneraux * 100, coutTotalAvecFrais, margeObjectif, tauxMargeObjectif: tauxMargeObjectif * 100, prixVenteCalcule, rentabilitePrevisionnelle, dateAnalyse: new Date() }; return await this.createOrUpdate(analyse); } catch (error) { console.error('Erreur lors du calcul de l\'analyse de prix:', error); throw error; } } /** * Comparer plusieurs scénarios de prix */ async comparerScenarios( phaseId: string, scenarios: Array<{ nom: string; tauxMarge: number; tauxFraisGeneraux?: number; }> ): Promise> { const analyseBase = await this.calculerAnalyseComplete(phaseId); return scenarios.map(scenario => { const tauxFraisGeneraux = scenario.tauxFraisGeneraux || 0.15; const fraisGeneraux = analyseBase.coutTotalDirect * tauxFraisGeneraux; const coutTotalAvecFrais = analyseBase.coutTotalDirect + fraisGeneraux; const margeObjectif = coutTotalAvecFrais * (scenario.tauxMarge / 100); const prixVenteCalcule = coutTotalAvecFrais + margeObjectif; const rentabilitePrevisionnelle = (margeObjectif / prixVenteCalcule) * 100; return { ...analyseBase, nomScenario: scenario.nom, tauxFraisGeneraux: tauxFraisGeneraux * 100, fraisGeneraux, coutTotalAvecFrais, tauxMargeObjectif: scenario.tauxMarge, margeObjectif, prixVenteCalcule, rentabilitePrevisionnelle }; }); } /** * Valider une analyse de prix */ async validerAnalyse(id: number, validePar: string): Promise { try { const response = await this.api.post(`${this.basePath}/${id}/valider`, { validePar, dateValidation: new Date(), validee: true }); return response.data; } catch (error) { console.warn('Validation non disponible côté serveur'); throw error; } } /** * Calculer la rentabilité réelle après réalisation */ async calculerRentabiliteReelle(phaseId: string, coutReel: number, prixVenteReel: number): Promise { const margeReelle = prixVenteReel - coutReel; return (margeReelle / prixVenteReel) * 100; } /** * Obtenir l'historique des analyses d'une phase */ async getHistorique(phaseId: string): Promise { try { const response = await this.api.get(`${this.basePath}/phase/${phaseId}/historique`); return response.data; } catch (error) { console.warn('Historique non disponible'); return []; } } /** * Estimation du coût de main d'œuvre basée sur les matériaux * (règle métier: environ 40-60% du coût des matériaux selon la complexité) */ private estimerCoutMainOeuvre(coutMateriaux: number): number { // Facteur de complexité moyen: 0.5 (50% des matériaux) return coutMateriaux * 0.5; } /** * Analyser la compétitivité d'un prix */ analyserCompetitivite(analyse: AnalysePrixPhase, prixMarche?: number): { niveau: 'TRÈS_COMPÉTITIF' | 'COMPÉTITIF' | 'MOYEN' | 'ÉLEVÉ' | 'TRÈS_ÉLEVÉ'; commentaire: string; recommandation: string; } { const tauxMarge = analyse.tauxMargeObjectif || 0; if (tauxMarge < 10) { return { niveau: 'TRÈS_COMPÉTITIF', commentaire: 'Prix très agressif avec marge faible', recommandation: 'Attention aux risques financiers, vérifier la viabilité' }; } else if (tauxMarge < 20) { return { niveau: 'COMPÉTITIF', commentaire: 'Prix compétitif avec marge raisonnable', recommandation: 'Bon équilibre entre compétitivité et rentabilité' }; } else if (tauxMarge < 30) { return { niveau: 'MOYEN', commentaire: 'Prix dans la moyenne du marché', recommandation: 'Position standard, opportunités d\'optimisation possibles' }; } else if (tauxMarge < 40) { return { niveau: 'ÉLEVÉ', commentaire: 'Prix élevé avec forte marge', recommandation: 'Risque de perte de compétitivité, justifier la valeur ajoutée' }; } else { return { niveau: 'TRÈS_ÉLEVÉ', commentaire: 'Prix très élevé', recommandation: 'Revoir la stratégie tarifaire, optimiser les coûts' }; } } } export default new AnalysePrixService();