- Export PDF: nouveau nommage AAAAMM/AAAA_Trajets_MATRICULE_PLAQUE.pdf, logo carré embarqué en en-tête, km formatés selon Precision_Distance, restructuration en-têtes (Domicile déplacé, période/année fusionnées au titre) - Correction bug: le nom de fichier suggéré au téléchargement ne remontait pas au frontend (CORS WithExposedHeaders manquant + composants résumé ne lisaient pas Content-Disposition) - Résumé mensuel: colonne Commentaire, sélecteur temporel unifié (auto-chargement), km selon précision - Résumé annuel: bouton Export PDF repositionné, km selon précision - Panneau totaux: renommage "Remboursement indicatif" + infobulles d'aide (icône) au lieu du texte permanent - Ajout de PdfServiceIntegrationTests (3 tests), 65/65 tests backend OK - Mise à jour requirements.md et design.md en conséquence
162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
|
import { Injectable, inject } from '@angular/core';
|
|
import { Observable } from 'rxjs';
|
|
|
|
import { environment } from '../../../environments/environment';
|
|
import {
|
|
CreateTrajet,
|
|
ReordonnerTrajets,
|
|
ResumeAnnuel,
|
|
ResumeMensuel,
|
|
Totaux,
|
|
Trajet,
|
|
TrajetCarte,
|
|
UpdateTrajet
|
|
} from '../models';
|
|
|
|
/**
|
|
* Service HTTP des trajets, résumés, exports PDF et navigation temporelle.
|
|
*
|
|
* Toutes les routes sont relatives à `environment.apiUrl` (`/api` en prod,
|
|
* `http://localhost:8080/api` en dev). Les appels sont soumis au
|
|
* `timeoutInterceptor` (10 s, Exigence 1.9).
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class TrajetService {
|
|
private readonly http = inject(HttpClient);
|
|
private readonly baseUrl = environment.apiUrl;
|
|
|
|
/** Crée un trajet (POST /api/trajets). */
|
|
createTrajet(dto: CreateTrajet): Observable<Trajet> {
|
|
return this.http.post<Trajet>(`${this.baseUrl}/trajets`, dto);
|
|
}
|
|
|
|
/** Modifie un trajet (PUT /api/trajets/{id}). */
|
|
updateTrajet(id: number, dto: UpdateTrajet): Observable<Trajet> {
|
|
return this.http.put<Trajet>(`${this.baseUrl}/trajets/${id}`, dto);
|
|
}
|
|
|
|
/** Supprime un trajet (DELETE /api/trajets/{id}). */
|
|
deleteTrajet(id: number): Observable<void> {
|
|
return this.http.delete<void>(`${this.baseUrl}/trajets/${id}`);
|
|
}
|
|
|
|
/** Réordonne les trajets d'une journée (PATCH /api/trajets/reordonner). */
|
|
reordonner(dto: ReordonnerTrajets): Observable<void> {
|
|
return this.http.patch<void>(`${this.baseUrl}/trajets/reordonner`, dto);
|
|
}
|
|
|
|
/**
|
|
* Recalcule la distance de tous les trajets via ORS
|
|
* (POST /api/trajets/recalculer-distances). Retourne le nombre de trajets recalculés.
|
|
*/
|
|
recalculerDistances(): Observable<{ trajetsRecalcules: number }> {
|
|
return this.http.post<{ trajetsRecalcules: number }>(
|
|
`${this.baseUrl}/trajets/recalculer-distances`,
|
|
{}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Résumé mensuel (GET /api/resumes/mensuel/{annee}/{mois}).
|
|
* Filtre optionnel par véhicule ; véhicule courant si omis.
|
|
*/
|
|
getResumeMensuel(annee: number, mois: number, vehiculeId?: number): Observable<ResumeMensuel> {
|
|
return this.http.get<ResumeMensuel>(`${this.baseUrl}/resumes/mensuel/${annee}/${mois}`, {
|
|
params: this.vehiculeParams(vehiculeId)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Résumé annuel (GET /api/resumes/annuel/{annee}).
|
|
* Filtre optionnel par véhicule ; véhicule courant si omis.
|
|
*/
|
|
getResumeAnnuel(annee: number, vehiculeId?: number): Observable<ResumeAnnuel> {
|
|
return this.http.get<ResumeAnnuel>(`${this.baseUrl}/resumes/annuel/${annee}`, {
|
|
params: this.vehiculeParams(vehiculeId)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Export PDF mensuel (GET /api/exports/mensuel/{annee}/{mois}). Réponse complète
|
|
* (et non le seul Blob) afin que l'appelant puisse lire le nom de fichier suggéré
|
|
* par le backend depuis l'en-tête `Content-Disposition`.
|
|
*/
|
|
exportPdfMensuel(annee: number, mois: number, vehiculeId?: number): Observable<HttpResponse<Blob>> {
|
|
return this.http.get(`${this.baseUrl}/exports/mensuel/${annee}/${mois}`, {
|
|
params: this.vehiculeParams(vehiculeId),
|
|
responseType: 'blob',
|
|
observe: 'response'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Export PDF annuel (GET /api/exports/annuel/{annee}). Réponse complète (et non le
|
|
* seul Blob) afin que l'appelant puisse lire le nom de fichier suggéré par le backend
|
|
* depuis l'en-tête `Content-Disposition`.
|
|
*/
|
|
exportPdfAnnuel(annee: number, vehiculeId?: number): Observable<HttpResponse<Blob>> {
|
|
return this.http.get(`${this.baseUrl}/exports/annuel/${annee}`, {
|
|
params: this.vehiculeParams(vehiculeId),
|
|
responseType: 'blob',
|
|
observe: 'response'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Extrait le nom de fichier suggéré par le backend depuis l'en-tête
|
|
* `Content-Disposition: attachment; filename="..."` d'une réponse HTTP.
|
|
* Retourne `null` si l'en-tête est absent ou ne contient pas de nom de fichier.
|
|
*/
|
|
extraireNomFichier(reponse: HttpResponse<Blob>): string | null {
|
|
const entete = reponse.headers.get('Content-Disposition');
|
|
if (!entete) {
|
|
return null;
|
|
}
|
|
const correspondance = /filename\*?=(?:UTF-8'')?"?([^;"]+)"?/i.exec(entete);
|
|
return correspondance ? decodeURIComponent(correspondance[1]) : null;
|
|
}
|
|
|
|
/**
|
|
* Trajets d'une journée avec géométries pour la carte
|
|
* (GET /api/trajets/journee/{date}). La date est au format ISO (yyyy-MM-dd).
|
|
*/
|
|
getTrajetsJournee(date: string): Observable<TrajetCarte[]> {
|
|
return this.http.get<TrajetCarte[]>(`${this.baseUrl}/trajets/journee/${date}`);
|
|
}
|
|
|
|
/** Années disponibles pour la navigation (GET /api/annees-disponibles). */
|
|
getAnneesDisponibles(): Observable<number[]> {
|
|
return this.http.get<number[]>(`${this.baseUrl}/annees-disponibles`);
|
|
}
|
|
|
|
/** Mois disponibles d'une année (GET /api/mois-disponibles/{annee}). */
|
|
getMoisDisponibles(annee: number): Observable<number[]> {
|
|
return this.http.get<number[]>(`${this.baseUrl}/mois-disponibles/${annee}`);
|
|
}
|
|
|
|
/** Jours disponibles d'un mois (GET /api/jours-disponibles/{annee}/{mois}). */
|
|
getJoursDisponibles(annee: number, mois: number): Observable<number[]> {
|
|
return this.http.get<number[]>(`${this.baseUrl}/jours-disponibles/${annee}/${mois}`);
|
|
}
|
|
|
|
/**
|
|
* Totaux jour / mois / année partiel d'un véhicule à une date donnée
|
|
* (GET /api/totaux/{date}?vehiculeId={id}). La date est au format ISO.
|
|
*/
|
|
getTotauxJour(date: string, vehiculeId: number): Observable<Totaux> {
|
|
return this.http.get<Totaux>(`${this.baseUrl}/totaux/${date}`, {
|
|
params: new HttpParams().set('vehiculeId', vehiculeId)
|
|
});
|
|
}
|
|
|
|
/** Construit les paramètres de requête avec un `vehiculeId` optionnel. */
|
|
private vehiculeParams(vehiculeId?: number): HttpParams {
|
|
let params = new HttpParams();
|
|
if (vehiculeId != null) {
|
|
params = params.set('vehiculeId', vehiculeId);
|
|
}
|
|
return params;
|
|
}
|
|
}
|