Initial commit
This commit is contained in:
418
services/phaseChantierService.ts
Normal file
418
services/phaseChantierService.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import axios from 'axios';
|
||||
import { API_CONFIG } from '../config/api';
|
||||
import {
|
||||
PhaseChantier,
|
||||
PhaseChantierFormData,
|
||||
PhaseFilters,
|
||||
JalonPhase,
|
||||
PointagePhase,
|
||||
StatutPhase,
|
||||
ApiResponse
|
||||
} from '../types/btp-extended';
|
||||
|
||||
class PhaseChantierService {
|
||||
private readonly basePath = '/phases-chantier';
|
||||
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 phases d'un chantier
|
||||
*/
|
||||
async getByChantier(chantierId: number): Promise<PhaseChantier[]> {
|
||||
const response = await this.api.api.get(`${this.basePath}/chantier/${chantierId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer une phase par ID
|
||||
*/
|
||||
async getById(id: number): Promise<PhaseChantier> {
|
||||
const response = await this.api.get(`${this.basePath}/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer une nouvelle phase
|
||||
*/
|
||||
async create(phase: PhaseChantierFormData): Promise<PhaseChantier> {
|
||||
const response = await this.api.post(this.basePath, phase);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier une phase existante
|
||||
*/
|
||||
async update(id: number, phase: PhaseChantierFormData): Promise<PhaseChantier> {
|
||||
const response = await this.api.put(`${this.basePath}/${id}`, phase);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer une phase
|
||||
*/
|
||||
async delete(id: number): Promise<void> {
|
||||
await this.api.delete(`${this.basePath}/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarrer une phase
|
||||
*/
|
||||
async start(id: number): Promise<void> {
|
||||
await this.api.post(`${this.basePath}/${id}/demarrer`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminer une phase
|
||||
*/
|
||||
async complete(id: number): Promise<void> {
|
||||
await this.api.post(`${this.basePath}/${id}/terminer`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspendre une phase
|
||||
*/
|
||||
async suspend(id: number): Promise<void> {
|
||||
await this.api.post(`${this.basePath}/${id}/suspendre`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reprendre une phase suspendue
|
||||
*/
|
||||
async resume(id: number): Promise<void> {
|
||||
await this.api.post(`${this.basePath}/${id}/reprendre`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour l'avancement d'une phase
|
||||
*/
|
||||
async updateProgress(id: number, pourcentage: number): Promise<void> {
|
||||
await this.api.put(`${this.basePath}/${id}/avancement?pourcentage=${pourcentage}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les statuts disponibles
|
||||
*/
|
||||
async getStatuts(): Promise<StatutPhase[]> {
|
||||
const response = await this.api.get(`${this.basePath}/statuts`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les phases en retard
|
||||
*/
|
||||
async getEnRetard(): Promise<PhaseChantier[]> {
|
||||
const response = await this.api.get(`${this.basePath}/en-retard`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les phases d'un responsable
|
||||
*/
|
||||
async getByResponsable(employeId: number): Promise<PhaseChantier[]> {
|
||||
const response = await this.api.get(`${this.basePath}/responsable/${employeId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les jalons d'une phase
|
||||
*/
|
||||
async getJalons(phaseId: number): Promise<JalonPhase[]> {
|
||||
const response = await this.api.get(`${this.basePath}/${phaseId}/jalons`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les pointages d'une phase
|
||||
*/
|
||||
async getPointages(phaseId: number): Promise<PointagePhase[]> {
|
||||
const response = await this.api.get(`${this.basePath}/${phaseId}/pointages`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valider les données d'une phase
|
||||
*/
|
||||
validatePhase(phase: PhaseChantierFormData): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!phase.nom || phase.nom.trim().length === 0) {
|
||||
errors.push('Le nom de la phase est obligatoire');
|
||||
}
|
||||
|
||||
if (phase.nom && phase.nom.length > 100) {
|
||||
errors.push('Le nom ne peut pas dépasser 100 caractères');
|
||||
}
|
||||
|
||||
if (!phase.dateDebutPrevue) {
|
||||
errors.push('La date de début prévue est obligatoire');
|
||||
}
|
||||
|
||||
if (!phase.dateFinPrevue) {
|
||||
errors.push('La date de fin prévue est obligatoire');
|
||||
}
|
||||
|
||||
if (phase.dateDebutPrevue && phase.dateFinPrevue) {
|
||||
const debut = new Date(phase.dateDebutPrevue);
|
||||
const fin = new Date(phase.dateFinPrevue);
|
||||
if (debut >= fin) {
|
||||
errors.push('La date de fin doit être postérieure à la date de début');
|
||||
}
|
||||
}
|
||||
|
||||
if (phase.budgetPrevu !== undefined && phase.budgetPrevu < 0) {
|
||||
errors.push('Le budget prévu doit être positif');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculer la durée prévue d'une phase en jours
|
||||
*/
|
||||
calculateDureePrevue(phase: PhaseChantier): number {
|
||||
if (!phase.dateDebutPrevue || !phase.dateFinPrevue) return 0;
|
||||
|
||||
const debut = new Date(phase.dateDebutPrevue);
|
||||
const fin = new Date(phase.dateFinPrevue);
|
||||
const diffTime = Math.abs(fin.getTime() - debut.getTime());
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculer la durée réelle d'une phase en jours
|
||||
*/
|
||||
calculateDureeReelle(phase: PhaseChantier): number {
|
||||
if (!phase.dateDebutReelle) return 0;
|
||||
|
||||
const debut = new Date(phase.dateDebutReelle);
|
||||
const fin = phase.dateFinReelle ? new Date(phase.dateFinReelle) : new Date();
|
||||
const diffTime = Math.abs(fin.getTime() - debut.getTime());
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifier si une phase est en retard
|
||||
*/
|
||||
isEnRetard(phase: PhaseChantier): boolean {
|
||||
if (phase.statut === 'TERMINEE') return false;
|
||||
if (!phase.dateFinPrevue) return false;
|
||||
|
||||
return new Date() > new Date(phase.dateFinPrevue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculer le retard en jours
|
||||
*/
|
||||
calculateRetard(phase: PhaseChantier): number {
|
||||
if (!this.isEnRetard(phase)) return 0;
|
||||
|
||||
const dateFinPrevue = new Date(phase.dateFinPrevue!);
|
||||
const maintenant = new Date();
|
||||
const diffTime = maintenant.getTime() - dateFinPrevue.getTime();
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtenir le libellé d'un statut
|
||||
*/
|
||||
getStatutLabel(statut: StatutPhase): string {
|
||||
const labels: Record<StatutPhase, string> = {
|
||||
PLANIFIEE: 'Planifiée',
|
||||
EN_ATTENTE: 'En attente',
|
||||
EN_COURS: 'En cours',
|
||||
EN_PAUSE: 'En pause',
|
||||
TERMINEE: 'Terminée',
|
||||
ANNULEE: 'Annulée',
|
||||
EN_RETARD: 'En retard'
|
||||
};
|
||||
return labels[statut] || statut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtenir la couleur d'un statut
|
||||
*/
|
||||
getStatutColor(statut: StatutPhase): string {
|
||||
const colors: Record<StatutPhase, string> = {
|
||||
PLANIFIEE: '#6c757d',
|
||||
EN_ATTENTE: '#ffc107',
|
||||
EN_COURS: '#0d6efd',
|
||||
EN_PAUSE: '#fd7e14',
|
||||
TERMINEE: '#198754',
|
||||
ANNULEE: '#dc3545',
|
||||
EN_RETARD: '#dc3545'
|
||||
};
|
||||
return colors[statut] || '#6c757d';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculer les statistiques des phases d'un chantier
|
||||
*/
|
||||
calculateStatistiques(phases: PhaseChantier[]): {
|
||||
total: number;
|
||||
planifiees: number;
|
||||
enCours: number;
|
||||
terminees: number;
|
||||
enRetard: number;
|
||||
avancementMoyen: number;
|
||||
budgetTotal: number;
|
||||
coutTotal: number;
|
||||
} {
|
||||
const stats = {
|
||||
total: phases.length,
|
||||
planifiees: 0,
|
||||
enCours: 0,
|
||||
terminees: 0,
|
||||
enRetard: 0,
|
||||
avancementMoyen: 0,
|
||||
budgetTotal: 0,
|
||||
coutTotal: 0
|
||||
};
|
||||
|
||||
let avancementTotal = 0;
|
||||
|
||||
phases.forEach(phase => {
|
||||
// Compter par statut
|
||||
switch (phase.statut) {
|
||||
case 'PLANIFIEE':
|
||||
case 'EN_ATTENTE':
|
||||
stats.planifiees++;
|
||||
break;
|
||||
case 'EN_COURS':
|
||||
case 'EN_PAUSE':
|
||||
stats.enCours++;
|
||||
break;
|
||||
case 'TERMINEE':
|
||||
stats.terminees++;
|
||||
break;
|
||||
case 'EN_RETARD':
|
||||
stats.enRetard++;
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculer avancement moyen
|
||||
avancementTotal += phase.pourcentageAvancement || 0;
|
||||
|
||||
// Calculer budget et coût
|
||||
stats.budgetTotal += phase.budgetPrevu || 0;
|
||||
stats.coutTotal += phase.coutReel || 0;
|
||||
});
|
||||
|
||||
stats.avancementMoyen = phases.length > 0 ? avancementTotal / phases.length : 0;
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Générer un planning Gantt simple
|
||||
*/
|
||||
generateGanttData(phases: PhaseChantier[]): any[] {
|
||||
return phases.map(phase => ({
|
||||
id: phase.id,
|
||||
name: phase.nom,
|
||||
start: phase.dateDebutPrevue,
|
||||
end: phase.dateFinPrevue,
|
||||
progress: (phase.pourcentageAvancement || 0) / 100,
|
||||
status: phase.statut,
|
||||
parent: phase.phaseParent?.id,
|
||||
dependencies: [], // TODO: Implémenter les dépendances
|
||||
color: this.getStatutColor(phase.statut)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporter les phases au format CSV
|
||||
*/
|
||||
async exportToCsv(chantierId: number): Promise<Blob> {
|
||||
const phases = await this.getByChantier(chantierId);
|
||||
|
||||
const headers = [
|
||||
'ID', 'Nom', 'Statut', 'Date Début Prévue', 'Date Fin Prévue',
|
||||
'Date Début Réelle', 'Date Fin Réelle', 'Avancement (%)',
|
||||
'Budget Prévu', 'Coût Réel', 'Responsable', 'Critique'
|
||||
];
|
||||
|
||||
const csvContent = [
|
||||
headers.join(';'),
|
||||
...phases.map(p => [
|
||||
p.id || '',
|
||||
p.nom || '',
|
||||
this.getStatutLabel(p.statut),
|
||||
p.dateDebutPrevue ? new Date(p.dateDebutPrevue).toLocaleDateString('fr-FR') : '',
|
||||
p.dateFinPrevue ? new Date(p.dateFinPrevue).toLocaleDateString('fr-FR') : '',
|
||||
p.dateDebutReelle ? new Date(p.dateDebutReelle).toLocaleDateString('fr-FR') : '',
|
||||
p.dateFinReelle ? new Date(p.dateFinReelle).toLocaleDateString('fr-FR') : '',
|
||||
p.pourcentageAvancement || 0,
|
||||
p.budgetPrevu || 0,
|
||||
p.coutReel || 0,
|
||||
p.responsable ? `${p.responsable.nom} ${p.responsable.prenom}` : '',
|
||||
p.critique ? 'Oui' : 'Non'
|
||||
].join(';'))
|
||||
].join('\n');
|
||||
|
||||
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
}
|
||||
}
|
||||
|
||||
export default new PhaseChantierService();
|
||||
Reference in New Issue
Block a user