Files
btpxpress-frontend/services/analysePrixService.ts
2025-10-13 05:29:32 +02:00

287 lines
11 KiB
TypeScript

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<AnalysePrixPhase | null> {
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<AnalysePrixPhase, 'id'>): Promise<AnalysePrixPhase> {
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<AnalysePrixPhase> {
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<Array<AnalysePrixPhase & { nomScenario: string }>> {
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<AnalysePrixPhase> {
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<number> {
const margeReelle = prixVenteReel - coutReel;
return (margeReelle / prixVenteReel) * 100;
}
/**
* Obtenir l'historique des analyses d'une phase
*/
async getHistorique(phaseId: string): Promise<AnalysePrixPhase[]> {
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();