- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts - Ajout des propriétés manquantes aux objets User mockés - Conversion des dates de string vers objets Date - Correction des appels asynchrones et des types incompatibles - Ajout de dynamic rendering pour résoudre les erreurs useSearchParams - Enveloppement de useSearchParams dans Suspense boundary - Configuration de force-dynamic au niveau du layout principal Build réussi: 126 pages générées avec succès 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
351 lines
11 KiB
TypeScript
351 lines
11 KiB
TypeScript
import { chantierService as apiChantierService, apiService } from './api';
|
|
import { Chantier } from '../types/btp';
|
|
import { ChantierFormData } from '../types/chantier-form';
|
|
|
|
class ChantierService {
|
|
/**
|
|
* Récupérer tous les chantiers
|
|
*/
|
|
async getAll(): Promise<Chantier[]> {
|
|
return await apiChantierService.getAll();
|
|
}
|
|
|
|
/**
|
|
* Récupérer un chantier par ID
|
|
*/
|
|
async getById(id: string): Promise<Chantier> {
|
|
return await apiChantierService.getById(id);
|
|
}
|
|
|
|
/**
|
|
* Créer un nouveau chantier
|
|
*/
|
|
async create(chantier: ChantierFormData): Promise<Chantier> {
|
|
return await apiChantierService.create(chantier as any);
|
|
}
|
|
|
|
/**
|
|
* Modifier un chantier existant
|
|
*/
|
|
async update(id: string, chantier: ChantierFormData): Promise<Chantier> {
|
|
return await apiChantierService.update(id, chantier as any);
|
|
}
|
|
|
|
/**
|
|
* Supprimer un chantier
|
|
* @param id - ID du chantier
|
|
* @param permanent - Si true, suppression physique définitive. Si false, suppression logique (défaut)
|
|
*/
|
|
async delete(id: string, permanent: boolean = false): Promise<void> {
|
|
await apiChantierService.delete(id, permanent);
|
|
}
|
|
|
|
/**
|
|
* Récupérer les chantiers d'un client
|
|
*/
|
|
async getByClient(clientId: string): Promise<Chantier[]> {
|
|
try {
|
|
return await apiChantierService.getByClient(clientId);
|
|
} catch (error: any) {
|
|
// Si l'endpoint spécifique n'existe pas (404), essayer de filtrer tous les chantiers
|
|
if (error.status === 404 || error.response?.status === 404) {
|
|
try {
|
|
const tousLesChantiers = await this.getAll();
|
|
return tousLesChantiers.filter(chantier =>
|
|
(chantier.client as any)?.id === clientId || (chantier as any).clientId === clientId
|
|
);
|
|
} catch (fallbackError) {
|
|
console.debug('Fallback sur filtrage côté client également impossible, retour liste vide');
|
|
// Retourner une liste vide plutôt qu'une erreur si pas de chantiers
|
|
return [];
|
|
}
|
|
}
|
|
// Pour toute autre erreur, la remonter
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupérer les chantiers récents
|
|
*/
|
|
async getRecents(): Promise<Chantier[]> {
|
|
return await apiChantierService.getRecents() as any;
|
|
}
|
|
|
|
/**
|
|
* Récupérer les chantiers par statut
|
|
*/
|
|
async getByStatut(statut: string): Promise<Chantier[]> {
|
|
const allChantiers = await this.getAll();
|
|
return allChantiers.filter(c => c.statut === statut);
|
|
}
|
|
|
|
/**
|
|
* Récupérer les chantiers en retard
|
|
*/
|
|
async getEnRetard(): Promise<Chantier[]> {
|
|
const allChantiers = await this.getAll();
|
|
return allChantiers.filter(c => this.isEnRetard(c));
|
|
}
|
|
|
|
/**
|
|
* Récupérer les chantiers actifs
|
|
*/
|
|
async getActifs(): Promise<Chantier[]> {
|
|
return await this.getByStatut('EN_COURS');
|
|
}
|
|
|
|
/**
|
|
* Actions sur les chantiers (nécessitent des endpoints spécifiques)
|
|
*/
|
|
async demarrer(id: string): Promise<void> {
|
|
// Ces méthodes nécessiteraient des endpoints spécifiques dans l'API
|
|
throw new Error('Méthode non implémentée côté API');
|
|
}
|
|
|
|
async terminer(id: string): Promise<void> {
|
|
throw new Error('Méthode non implémentée côté API');
|
|
}
|
|
|
|
async suspendre(id: string): Promise<void> {
|
|
throw new Error('Méthode non implémentée côté API');
|
|
}
|
|
|
|
async reprendre(id: string): Promise<void> {
|
|
throw new Error('Méthode non implémentée côté API');
|
|
}
|
|
|
|
async annuler(id: string): Promise<void> {
|
|
throw new Error('Méthode non implémentée côté API');
|
|
}
|
|
|
|
/**
|
|
* Valider les données d'un chantier
|
|
*/
|
|
validateChantier(chantier: ChantierFormData): string[] {
|
|
const errors: string[] = [];
|
|
|
|
if (!chantier.nom || chantier.nom.trim().length === 0) {
|
|
errors.push('Le nom du chantier est obligatoire');
|
|
}
|
|
|
|
if (!chantier.adresse || chantier.adresse.trim().length === 0) {
|
|
errors.push('L\'adresse est obligatoire');
|
|
}
|
|
|
|
if (!chantier.clientId) {
|
|
errors.push('Le client est obligatoire');
|
|
}
|
|
|
|
if (!chantier.dateDebut) {
|
|
errors.push('La date de début est obligatoire');
|
|
}
|
|
|
|
if (!chantier.dateFinPrevue) {
|
|
errors.push('La date de fin prévue est obligatoire');
|
|
}
|
|
|
|
if (chantier.dateDebut && chantier.dateFinPrevue) {
|
|
const debut = new Date(chantier.dateDebut);
|
|
const fin = new Date(chantier.dateFinPrevue);
|
|
if (debut >= fin) {
|
|
errors.push('La date de fin doit être postérieure à la date de début');
|
|
}
|
|
}
|
|
|
|
if (chantier.montantPrevu !== undefined && chantier.montantPrevu < 0) {
|
|
errors.push('Le montant prévu doit être positif');
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Calculer la durée prévue du chantier en jours
|
|
*/
|
|
calculateDureePrevue(chantier: Chantier): number {
|
|
if (!chantier.dateDebut || !chantier.dateFinPrevue) return 0;
|
|
|
|
const debut = new Date(chantier.dateDebut);
|
|
const fin = new Date(chantier.dateFinPrevue);
|
|
const diffTime = Math.abs(fin.getTime() - debut.getTime());
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
}
|
|
|
|
/**
|
|
* Calculer la durée réelle du chantier en jours
|
|
*/
|
|
calculateDureeReelle(chantier: Chantier): number {
|
|
if (!chantier.dateDebut) return 0;
|
|
|
|
const debut = new Date(chantier.dateDebut);
|
|
const fin = chantier.dateFinReelle ? new Date(chantier.dateFinReelle) : new Date();
|
|
const diffTime = Math.abs(fin.getTime() - debut.getTime());
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
}
|
|
|
|
/**
|
|
* Vérifier si un chantier est en retard
|
|
*/
|
|
isEnRetard(chantier: Chantier): boolean {
|
|
if (chantier.statut === 'TERMINE') return false;
|
|
if (!chantier.dateFinPrevue) return false;
|
|
|
|
return new Date() > new Date(chantier.dateFinPrevue);
|
|
}
|
|
|
|
/**
|
|
* Calculer le retard en jours
|
|
*/
|
|
calculateRetard(chantier: Chantier): number {
|
|
if (!this.isEnRetard(chantier)) return 0;
|
|
|
|
const dateFinPrevue = new Date(chantier.dateFinPrevue!);
|
|
const maintenant = new Date();
|
|
const diffTime = maintenant.getTime() - dateFinPrevue.getTime();
|
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
}
|
|
|
|
/**
|
|
* Calculer l'avancement du chantier en pourcentage
|
|
*/
|
|
calculateAvancement(chantier: Chantier): number {
|
|
if (chantier.statut === 'TERMINE') return 100;
|
|
if (chantier.statut === 'ANNULE') return 0;
|
|
if (!chantier.dateDebut || !chantier.dateFinPrevue) return 0;
|
|
|
|
const now = new Date();
|
|
const start = new Date(chantier.dateDebut);
|
|
const end = new Date(chantier.dateFinPrevue);
|
|
|
|
if (now < start) return 0;
|
|
if (now > end) return 100;
|
|
|
|
const totalDays = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
|
const elapsedDays = (now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
|
|
|
return Math.min(Math.max((elapsedDays / totalDays) * 100, 0), 100);
|
|
}
|
|
|
|
/**
|
|
* Obtenir le libellé d'un statut
|
|
*/
|
|
getStatutLabel(statut: string): string {
|
|
const labels: Record<string, string> = {
|
|
PLANIFIE: 'Planifié',
|
|
EN_COURS: 'En cours',
|
|
TERMINE: 'Terminé',
|
|
ANNULE: 'Annulé',
|
|
SUSPENDU: 'Suspendu'
|
|
};
|
|
return labels[statut] || statut;
|
|
}
|
|
|
|
/**
|
|
* Obtenir la couleur d'un statut
|
|
*/
|
|
getStatutColor(statut: string): string {
|
|
const colors: Record<string, string> = {
|
|
PLANIFIE: '#6c757d',
|
|
EN_COURS: '#0d6efd',
|
|
TERMINE: '#198754',
|
|
ANNULE: '#dc3545',
|
|
SUSPENDU: '#fd7e14'
|
|
};
|
|
return colors[statut] || '#6c757d';
|
|
}
|
|
|
|
/**
|
|
* Calculer les statistiques des chantiers
|
|
*/
|
|
calculateStatistiques(chantiers: Chantier[]): {
|
|
total: number;
|
|
planifies: number;
|
|
enCours: number;
|
|
termines: number;
|
|
annules: number;
|
|
suspendus: number;
|
|
enRetard: number;
|
|
montantTotal: number;
|
|
coutTotal: number;
|
|
} {
|
|
const stats = {
|
|
total: chantiers.length,
|
|
planifies: 0,
|
|
enCours: 0,
|
|
termines: 0,
|
|
annules: 0,
|
|
suspendus: 0,
|
|
enRetard: 0,
|
|
montantTotal: 0,
|
|
coutTotal: 0
|
|
};
|
|
|
|
chantiers.forEach(chantier => {
|
|
// Compter par statut
|
|
switch (chantier.statut) {
|
|
case 'PLANIFIE':
|
|
stats.planifies++;
|
|
break;
|
|
case 'EN_COURS':
|
|
stats.enCours++;
|
|
break;
|
|
case 'TERMINE':
|
|
stats.termines++;
|
|
break;
|
|
case 'ANNULE':
|
|
stats.annules++;
|
|
break;
|
|
case 'SUSPENDU':
|
|
stats.suspendus++;
|
|
break;
|
|
}
|
|
|
|
// Vérifier les retards
|
|
if (this.isEnRetard(chantier)) {
|
|
stats.enRetard++;
|
|
}
|
|
|
|
// Calculer montants
|
|
stats.montantTotal += chantier.montantPrevu || 0;
|
|
stats.coutTotal += chantier.montantReel || 0;
|
|
});
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Exporter les chantiers au format CSV
|
|
*/
|
|
async exportToCsv(): Promise<Blob> {
|
|
const chantiers = await this.getAll();
|
|
|
|
const headers = [
|
|
'ID', 'Nom', 'Description', 'Adresse', 'Client', 'Statut',
|
|
'Date Début', 'Date Fin Prévue', 'Date Fin Réelle',
|
|
'Montant Prévu', 'Montant Réel', 'Actif'
|
|
];
|
|
|
|
const csvContent = [
|
|
headers.join(';'),
|
|
...chantiers.map(c => [
|
|
c.id || '',
|
|
c.nom || '',
|
|
c.description || '',
|
|
c.adresse || '',
|
|
c.client ? `${c.client.prenom} ${c.client.nom}` : '',
|
|
this.getStatutLabel(c.statut),
|
|
c.dateDebut ? new Date(c.dateDebut).toLocaleDateString('fr-FR') : '',
|
|
c.dateFinPrevue ? new Date(c.dateFinPrevue).toLocaleDateString('fr-FR') : '',
|
|
c.dateFinReelle ? new Date(c.dateFinReelle).toLocaleDateString('fr-FR') : '',
|
|
c.montantPrevu || 0,
|
|
c.montantReel || 0,
|
|
c.actif ? 'Oui' : 'Non'
|
|
].join(';'))
|
|
].join('\n');
|
|
|
|
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
}
|
|
}
|
|
|
|
export default new ChantierService(); |