287 lines
11 KiB
TypeScript
Executable File
287 lines
11 KiB
TypeScript
Executable File
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(); |