239 lines
8.5 KiB
TypeScript
Executable File
239 lines
8.5 KiB
TypeScript
Executable File
import axios from 'axios';
|
|
import { API_CONFIG } from '../config/api';
|
|
import {
|
|
FournisseurPhase,
|
|
ApiResponse
|
|
} from '../types/btp-extended';
|
|
|
|
class FournisseurPhaseService {
|
|
private readonly basePath = '/fournisseurs-phases';
|
|
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 fournisseurs d'une phase
|
|
*/
|
|
async getByPhase(phaseId: string): Promise<FournisseurPhase[]> {
|
|
if (!phaseId || phaseId === 'undefined' || phaseId === 'null' || phaseId === 'NaN') {
|
|
console.warn(`ID de phase invalide: ${phaseId}`);
|
|
return [];
|
|
}
|
|
|
|
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 [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupérer un fournisseur de phase par ID
|
|
*/
|
|
async getById(id: number): Promise<FournisseurPhase> {
|
|
const response = await this.api.get(`${this.basePath}/${id}`);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Créer un nouveau fournisseur de phase
|
|
*/
|
|
async create(fournisseurPhase: Omit<FournisseurPhase, 'id'>): Promise<FournisseurPhase> {
|
|
console.log('Creating fournisseur phase with data:', fournisseurPhase);
|
|
const response = await this.api.post(this.basePath, fournisseurPhase);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Modifier un fournisseur de phase existant
|
|
*/
|
|
async update(id: number, fournisseurPhase: Partial<FournisseurPhase>): Promise<FournisseurPhase> {
|
|
const response = await this.api.put(`${this.basePath}/${id}`, fournisseurPhase);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Supprimer un fournisseur de phase
|
|
*/
|
|
async delete(id: number): Promise<void> {
|
|
await this.api.delete(`${this.basePath}/${id}`);
|
|
}
|
|
|
|
/**
|
|
* Obtenir les fournisseurs par type de contribution
|
|
*/
|
|
async getByTypeContribution(phaseId: string, type: string): Promise<FournisseurPhase[]> {
|
|
try {
|
|
const fournisseurs = await this.getByPhase(phaseId);
|
|
return fournisseurs.filter(f => f.typeContribution === type);
|
|
} catch (error) {
|
|
console.error('Erreur lors de la récupération par type:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculer les économies réalisées avec les négociations
|
|
*/
|
|
async calculerEconomies(phaseId: string): Promise<number> {
|
|
try {
|
|
const fournisseurs = await this.getByPhase(phaseId);
|
|
return fournisseurs.reduce((total, fournisseur) => {
|
|
const prixCatalogue = fournisseur.prixCatalogue || 0;
|
|
const prixNegocie = fournisseur.prixNegocie || prixCatalogue;
|
|
return total + (prixCatalogue - prixNegocie);
|
|
}, 0);
|
|
} catch (error) {
|
|
console.error('Erreur lors du calcul des économies:', error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtenir le fournisseur principal d'une phase
|
|
*/
|
|
async getFournisseurPrincipal(phaseId: string): Promise<FournisseurPhase | null> {
|
|
try {
|
|
const fournisseurs = await this.getByPhase(phaseId);
|
|
return fournisseurs.find(f => f.priorite === 1) || null;
|
|
} catch (error) {
|
|
console.error('Erreur lors de la récupération du fournisseur principal:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lancer un appel d'offres pour une phase
|
|
*/
|
|
async lancerAppelOffres(phaseId: string, fournisseurIds: number[]): Promise<FournisseurPhase[]> {
|
|
try {
|
|
const response = await this.api.post(`${this.basePath}/appel-offres`, {
|
|
phaseId,
|
|
fournisseurIds
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
console.warn('Appel d\'offres non disponible:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Comparer les offres de fournisseurs
|
|
*/
|
|
async comparerOffres(phaseId: string): Promise<FournisseurPhase[]> {
|
|
try {
|
|
const fournisseurs = await this.getByPhase(phaseId);
|
|
// Tri par prix négocié croissant
|
|
return fournisseurs.sort((a, b) => {
|
|
const prixA = a.prixNegocie || a.prixCatalogue || Infinity;
|
|
const prixB = b.prixNegocie || b.prixCatalogue || Infinity;
|
|
return prixA - prixB;
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur lors de la comparaison des offres:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Valider une négociation
|
|
*/
|
|
async validerNegociation(id: number, validePar: string): Promise<FournisseurPhase> {
|
|
const response = await this.api.post(`${this.basePath}/${id}/valider`, {
|
|
validePar,
|
|
dateValidation: new Date()
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Calculer le score d'un fournisseur (prix, délai, qualité)
|
|
*/
|
|
calculerScoreFournisseur(fournisseur: FournisseurPhase): number {
|
|
let score = 0;
|
|
|
|
// Score prix (40% du total)
|
|
const prixFinal = fournisseur.prixNegocie || fournisseur.prixCatalogue || 0;
|
|
const remise = fournisseur.remise || 0;
|
|
const scoreRemise = Math.min(remise / 100, 0.3); // Max 30% de remise
|
|
score += scoreRemise * 40;
|
|
|
|
// Score délai (30% du total)
|
|
const delai = fournisseur.delaiLivraison || 30;
|
|
const scoreDelai = Math.max(0, (30 - delai) / 30); // Meilleur si délai < 30 jours
|
|
score += scoreDelai * 30;
|
|
|
|
// Score priorité/historique (30% du total)
|
|
const priorite = fournisseur.priorite || 5;
|
|
const scorePriorite = Math.max(0, (6 - priorite) / 5); // Meilleur si priorité = 1
|
|
score += scorePriorite * 30;
|
|
|
|
return Math.round(score);
|
|
}
|
|
}
|
|
|
|
export default new FournisseurPhaseService(); |