}
(null);
+
+ /**
+ * Jour affiché au format ISO (`yyyy-MM-dd`) : pilote la CARTE et la date par
+ * défaut d'ajout. Toujours contenu dans le mois sélectionné. `null` tant
+ * qu'aucun jour n'est choisi.
+ */
readonly date = signal(null);
/** Véhicules disponibles pour le sélecteur. */
@@ -99,7 +112,9 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
private vehiculesSub?: Subscription;
ngOnInit(): void {
- // Journée courante par défaut.
+ // Par défaut : mois courant, et jour = aujourd'hui (contenu dans le mois courant).
+ const auj = new Date();
+ this.mois.set({ annee: auj.getFullYear(), mois: auj.getMonth() + 1 });
this.date.set(this.isoAujourdHui());
this.chargerVehicules();
}
@@ -111,9 +126,14 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
// --- Sélection date / véhicule ---
- /** Réagit à la sélection d'une date via le Sélecteur_Temporel (Exigence 12.14). */
- onDateSelectionnee(dateIso: string): void {
- this.date.set(dateIso);
+ /**
+ * Réagit à la sélection d'un mois via le Sélecteur_Temporel (Exigence 12.14).
+ * Met à jour `mois`, positionne `date` sur un jour valide du mois (aujourd'hui si
+ * c'est le mois courant, sinon le 1er jour ; jamais un jour futur) et recharge.
+ */
+ onMoisSelectionne(selection: MoisSelectionne): void {
+ this.mois.set(selection);
+ this.date.set(this.jourParDefautDuMois(selection.annee, selection.mois));
this.formulaire.set(null);
this.rafraichirJournee();
}
@@ -126,28 +146,50 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
this.rafraichirJournee();
}
- // --- Navigation jour ±1 (Exigences 12.10–12.13) ---
+ // --- Navigation jour ±1, BORNÉE AU MOIS sélectionné ---
- /** Sélectionne la journée précédente et rafraîchit l'affichage (Exigence 12.11). */
+ /**
+ * Jour affiché sur la carte : navigue vers le jour précédent DANS LE MOIS. Seul
+ * `date` change (le mois reste inchangé) ; la carte est rechargée.
+ */
jourPrecedent(): void {
+ if (this.jourPrecedentDesactive) {
+ return;
+ }
this.date.set(this.decalerJour(this.dateCourante(), -1));
this.formulaire.set(null);
- this.rafraichirJournee();
+ this.rafraichirCarteJour();
}
- /** Sélectionne la journée suivante et rafraîchit l'affichage (Exigence 12.12). */
+ /**
+ * Jour affiché sur la carte : navigue vers le jour suivant DANS LE MOIS. Seul
+ * `date` change (le mois reste inchangé) ; la carte est rechargée.
+ */
jourSuivant(): void {
if (this.jourSuivantDesactive) {
return;
}
this.date.set(this.decalerJour(this.dateCourante(), 1));
this.formulaire.set(null);
- this.rafraichirJournee();
+ this.rafraichirCarteJour();
}
- /** « Jour suivant » désactivé si le jour suivant est postérieur à aujourd'hui (Exigence 12.13). */
+ /** « Jour préc. » désactivé si la date affichée est le 1er jour du mois sélectionné. */
+ get jourPrecedentDesactive(): boolean {
+ return Number(this.dateCourante().slice(8, 10)) <= 1;
+ }
+
+ /**
+ * « Jour suiv. » désactivé si la date affichée est le dernier jour du mois
+ * sélectionné, OU si le jour suivant serait dans le futur.
+ */
get jourSuivantDesactive(): boolean {
- const suivant = this.decalerJour(this.dateCourante(), 1);
+ const courante = this.dateCourante();
+ const suivant = this.decalerJour(courante, 1);
+ // Sortie du mois affiché (le mois du jour suivant diffère de la date courante).
+ if (suivant.slice(0, 7) !== courante.slice(0, 7)) {
+ return true;
+ }
return suivant > this.isoAujourdHui();
}
@@ -239,16 +281,17 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
// (ex. ajout/modification/suppression d'un trajet sur la journée affichée).
this.panneauTotaux?.rafraichir();
+ const moisCourant = this.mois();
const date = this.date();
- if (!date) {
+ if (!moisCourant || !date) {
this.trajets.set([]);
this.trajetsCarte.set([]);
return;
}
const vehiculeId = this.vehiculeId() ?? undefined;
- const annee = this.anneeDe(date);
- const mois = this.moisDe(date);
+ const annee = moisCourant.annee;
+ const mois = moisCourant.mois;
this.chargement.set(true);
this.erreur.set(null);
@@ -279,8 +322,10 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
)
.subscribe({
next: ({ resume, carte }) => {
- // Liste : trajets du mois restreints au jour sélectionné.
- this.trajets.set((resume.trajets ?? []).filter((t) => this.memeJour(t.date, date)));
+ // Liste : TOUS les trajets du mois (filtre véhicule déjà fait côté API),
+ // sans restriction au jour. Le tri final (date puis position) est assuré
+ // par la liste elle-même.
+ this.trajets.set(resume.trajets ?? []);
// Carte : trajets du jour filtrés par le véhicule sélectionné.
this.trajetsCarte.set(
this.vehiculeId() == null
@@ -288,7 +333,7 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
: this.filtrerCarteParVehicule(carte)
);
this.erreur.set(resumeEnErreur
- ? 'Impossible de charger les trajets de la journée.'
+ ? 'Impossible de charger les trajets du mois.'
: null);
// Force la mise à jour immédiate de la liste, des totaux et de la carte
// après une navigation Jour ± 1, sans attendre une autre interaction.
@@ -299,12 +344,53 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
// robuste : on affiche l'erreur et on rafraîchit la vue.
this.trajets.set([]);
this.trajetsCarte.set([]);
- this.erreur.set('Impossible de charger les trajets de la journée.');
+ this.erreur.set('Impossible de charger les trajets du mois.');
this.cdr.markForCheck();
}
});
}
+ /**
+ * Recharge uniquement la carte pour le jour `date` (navigation jour ± 1 dans le
+ * mois). La liste mensuelle reste inchangée ; seuls la carte et les totaux du
+ * jour sont recalculés.
+ */
+ private rafraichirCarteJour(): void {
+ this.jourSub?.unsubscribe();
+ this.panneauTotaux?.rafraichir();
+
+ const date = this.date();
+ if (!date) {
+ this.trajetsCarte.set([]);
+ return;
+ }
+
+ this.jourSub = this.trajetService
+ .getTrajetsJournee(date)
+ .pipe(
+ catchError(() => of([] as TrajetCarte[])),
+ finalize(() => this.cdr.markForCheck())
+ )
+ .subscribe((carte) => {
+ this.trajetsCarte.set(
+ this.vehiculeId() == null ? carte : this.filtrerCarteParVehicule(carte)
+ );
+ this.cdr.markForCheck();
+ });
+ }
+
+ /**
+ * Jour par défaut d'un mois : aujourd'hui si le mois affiché est le mois courant,
+ * sinon le 1er jour du mois. Jamais un jour futur.
+ */
+ private jourParDefautDuMois(annee: number, mois: number): string {
+ const auj = new Date();
+ if (annee === auj.getFullYear() && mois === auj.getMonth() + 1) {
+ return this.isoAujourdHui();
+ }
+ return this.versIso(new Date(annee, mois - 1, 1));
+ }
+
/**
* Restreint les trajets carte au véhicule sélectionné en s'appuyant sur les ids
* de la liste déjà filtrée par véhicule (le DTO carte ne porte pas le vehiculeId).
@@ -321,11 +407,6 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
return this.date() ?? this.isoAujourdHui();
}
- /** Vrai si deux dates ISO désignent le même jour (tolère un suffixe horaire). */
- private memeJour(a: string, b: string): boolean {
- return (a ?? '').slice(0, 10) === (b ?? '').slice(0, 10);
- }
-
/** Décale une date ISO `yyyy-MM-dd` de `delta` jours, sans dérive de fuseau. */
private decalerJour(dateIso: string, delta: number): string {
const [annee, mois, jour] = dateIso.slice(0, 10).split('-').map(Number);
@@ -345,12 +426,4 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
const dd = String(d.getDate()).padStart(2, '0');
return `${d.getFullYear()}-${mm}-${dd}`;
}
-
- private anneeDe(dateIso: string): number {
- return Number(dateIso.slice(0, 4));
- }
-
- private moisDe(dateIso: string): number {
- return Number(dateIso.slice(5, 7));
- }
}
diff --git a/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.html b/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.html
index 3cdfcf6..a704eef 100644
--- a/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.html
+++ b/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.html
@@ -39,36 +39,14 @@
type="button"
*ngFor="let m of mois"
class="pastille"
- [class.selectionne]="m === moisSelectionne()"
+ [class.selectionne]="m === moisSelectionneSignal()"
[class.avec-donnees]="moisADonnees(m)"
[disabled]="estMoisFutur(anneeSelectionnee()!, m)"
- [attr.aria-pressed]="m === moisSelectionne()"
+ [attr.aria-pressed]="m === moisSelectionneSignal()"
(click)="selectionnerMois(m)">
{{ nomsMois[m - 1] }}
-
-
-
- Jour
-
-
-
-
diff --git a/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.ts b/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.ts
index a28b930..bf85ffe 100644
--- a/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.ts
+++ b/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.ts
@@ -13,22 +13,30 @@ import {
import { TrajetService } from '../../core/services/trajet.service';
+/** Mois sélectionné émis par le sélecteur temporel. */
+export interface MoisSelectionne {
+ annee: number;
+ mois: number;
+}
+
/**
- * Sélecteur temporel : navigation successive année → mois → jour.
+ * Sélecteur temporel : navigation successive année → mois.
+ *
+ * Depuis le passage à une consultation PAR MOIS, la sélection du jour a été
+ * retirée : l'utilisateur choisit une année puis un mois, et la carte reste
+ * pilotée par un jour précis géré par le composant conteneur.
*
* - Sélecteur d'année : année courante + années ayant des données
* (`TrajetService.getAnneesDisponibles`). L'année courante est toujours
* présente afin que le sélecteur ne soit jamais vide. (Exigence 12.2)
* - Sélecteur de mois : les douze mois affichés, surbrillance des mois
* comportant des données (`TrajetService.getMoisDisponibles`). (Exigence 12.3)
- * - Sélecteur de jour : les jours du mois affichés, surbrillance des jours
- * comportant des données (`TrajetService.getJoursDisponibles`). (Exigence 12.4)
- * - Aucune date postérieure à aujourd'hui n'est sélectionnable. (Exigence 12.5)
- * - Émet `dateSelectionnee` (ISO `yyyy-MM-dd`) à chaque sélection utilisateur.
- * - Se synchronise avec la date affichée par le parent via `@Input date` : lors de
- * la navigation ± 1 jour, l'année / mois / jour affichés reflètent cette date.
+ * - Aucun mois postérieur au mois courant n'est sélectionnable. (Exigence 12.5)
+ * - Émet `moisSelectionne` (`{ annee, mois }`) à chaque sélection utilisateur.
+ * - Se synchronise avec le mois affiché par le parent via `@Input date` (jour ISO
+ * affiché sur la carte) : l'année / le mois affichés reflètent ce jour.
*
- * _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.14_
+ * _Requirements: 12.1, 12.2, 12.3, 12.5, 12.14_
*/
@Component({
selector: 'app-selecteur-temporel',
@@ -41,14 +49,14 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
private readonly trajetService = inject(TrajetService);
/**
- * Date affichée par le parent (ISO `yyyy-MM-dd`). Quand elle change (navigation
- * ± 1 jour ou sélection), le sélecteur pré-sélectionne l'année, le mois et le
- * jour correspondants sans réémettre `dateSelectionnee`.
+ * Jour affiché par le parent (ISO `yyyy-MM-dd`). Quand il change (navigation
+ * jour ± 1 ou sélection d'un mois), le sélecteur pré-sélectionne l'année et le
+ * mois correspondants sans réémettre `moisSelectionne`.
*/
@Input() date: string | null = null;
- /** Émis à chaque changement de sélection complète (année + mois + jour). */
- @Output() readonly dateSelectionnee = new EventEmitter();
+ /** Émis à chaque changement de sélection (année + mois). */
+ @Output() readonly moisSelectionne = new EventEmitter();
/** Libellés des douze mois (index 0 = janvier). */
readonly nomsMois = [
@@ -63,17 +71,12 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
readonly annees = signal([]);
/** Les douze mois (1..12), toujours affichés. */
readonly mois: number[] = Array.from({ length: 12 }, (_, i) => i + 1);
- /** Jours du mois sélectionné (1..N). */
- readonly jours = signal([]);
/** Mois comportant au moins un trajet pour l'année sélectionnée. */
private readonly moisAvecDonnees = signal>(new Set());
- /** Jours comportant au moins un trajet pour le mois sélectionné. */
- private readonly joursAvecDonnees = signal>(new Set());
readonly anneeSelectionnee = signal(null);
- readonly moisSelectionne = signal(null);
- readonly jourSelectionne = signal(null);
+ readonly moisSelectionneSignal = signal(null);
/** État d'ouverture du conteneur repliable (fermé par défaut). */
readonly ouvert = signal(false);
@@ -85,7 +88,7 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
ngOnInit(): void {
// Affichage garanti dès le premier rendu : au minimum l'année courante et,
- // si le parent a déjà fourni une date, la pré-sélection année/mois/jour.
+ // si le parent a déjà fourni une date, la pré-sélection année/mois.
// Ainsi le sélecteur n'est jamais vide même si l'appel réseau tarde ou échoue.
this.annees.set(this.avecAnneeCourante([]));
this.synchroniserSurDate(this.date);
@@ -108,15 +111,15 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
}
/**
- * Aligne la sélection (année, mois, jour) sur la date fournie par le parent,
- * sans réémettre d'évènement. Charge les mois / jours avec données associés.
+ * Aligne la sélection (année, mois) sur le jour fourni par le parent, sans
+ * réémettre d'évènement. Charge les mois comportant des données associés.
*/
private synchroniserSurDate(dateIso: string | null): void {
if (!dateIso) {
return;
}
- const [annee, mois, jour] = dateIso.slice(0, 10).split('-').map(Number);
- if (!annee || !mois || !jour) {
+ const [annee, mois] = dateIso.slice(0, 10).split('-').map(Number);
+ if (!annee || !mois) {
return;
}
@@ -126,16 +129,11 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
}
this.anneeSelectionnee.set(annee);
- this.moisSelectionne.set(mois);
- this.jours.set(this.genererJours(annee, mois));
- this.jourSelectionne.set(jour);
+ this.moisSelectionneSignal.set(mois);
this.trajetService.getMoisDisponibles(annee).subscribe((m) => {
this.moisAvecDonnees.set(new Set(m));
});
- this.trajetService.getJoursDisponibles(annee, mois).subscribe((j) => {
- this.joursAvecDonnees.set(new Set(j));
- });
}
/** Sélectionne une année et charge les mois comportant des données. */
@@ -144,10 +142,7 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
return;
}
this.anneeSelectionnee.set(annee);
- this.moisSelectionne.set(null);
- this.jourSelectionne.set(null);
- this.jours.set([]);
- this.joursAvecDonnees.set(new Set());
+ this.moisSelectionneSignal.set(null);
this.moisAvecDonnees.set(new Set());
this.trajetService.getMoisDisponibles(annee).subscribe((mois) => {
@@ -155,37 +150,14 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
});
}
- /** Sélectionne un mois et charge les jours comportant des données. */
+ /** Sélectionne un mois et émet `{ annee, mois }`. */
selectionnerMois(mois: number): void {
const annee = this.anneeSelectionnee();
if (annee == null || this.estMoisFutur(annee, mois)) {
return;
}
- this.moisSelectionne.set(mois);
- this.jourSelectionne.set(null);
- this.jours.set(this.genererJours(annee, mois));
- this.joursAvecDonnees.set(new Set());
-
- this.trajetService.getJoursDisponibles(annee, mois).subscribe((jours) => {
- this.joursAvecDonnees.set(new Set(jours));
- });
- }
-
- /** Sélectionne un jour et émet la date ISO correspondante. */
- selectionnerJour(jour: number): void {
- const annee = this.anneeSelectionnee();
- const mois = this.moisSelectionne();
- if (
- annee == null ||
- mois == null ||
- this.estJourFutur(annee, mois, jour)
- ) {
- return;
- }
- this.jourSelectionne.set(jour);
- this.dateSelectionnee.emit(
- this.versIso(annee, mois, jour)
- );
+ this.moisSelectionneSignal.set(mois);
+ this.moisSelectionne.emit({ annee, mois });
}
/** Un mois est en surbrillance s'il comporte au moins un trajet. */
@@ -193,11 +165,6 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
return this.moisAvecDonnees().has(mois);
}
- /** Un jour est en surbrillance s'il comporte au moins un trajet. */
- jourADonnees(jour: number): boolean {
- return this.joursAvecDonnees().has(jour);
- }
-
/** Une année est désactivée si elle est postérieure à l'année courante. */
estAnneeFuture(annee: number): boolean {
return annee > this.aujourdHui.getFullYear();
@@ -208,11 +175,6 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
return this.debutDeJournee(new Date(annee, mois - 1, 1)) > this.aujourdHui;
}
- /** Un jour est désactivé s'il est postérieur à la date courante. */
- estJourFutur(annee: number, mois: number, jour: number): boolean {
- return this.debutDeJournee(new Date(annee, mois - 1, jour)) > this.aujourdHui;
- }
-
/** Complète la liste avec l'année courante et trie en ordre décroissant, sans doublon. */
private avecAnneeCourante(annees: number[]): number[] {
const ensemble = new Set(annees);
@@ -220,19 +182,6 @@ export class SelecteurTemporelComponent implements OnInit, OnChanges {
return [...ensemble].sort((a, b) => b - a);
}
- /** Génère la liste 1..N des jours du mois donné. */
- private genererJours(annee: number, mois: number): number[] {
- const nbJours = new Date(annee, mois, 0).getDate();
- return Array.from({ length: nbJours }, (_, i) => i + 1);
- }
-
- /** Formate une date en ISO `yyyy-MM-dd` sans dérive de fuseau horaire. */
- private versIso(annee: number, mois: number, jour: number): string {
- const mm = String(mois).padStart(2, '0');
- const dd = String(jour).padStart(2, '0');
- return `${annee}-${mm}-${dd}`;
- }
-
/** Retourne une copie de la date à minuit (heure locale). */
private debutDeJournee(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
diff --git a/frontend/src/app/features/trajet-list/trajet-list.component.html b/frontend/src/app/features/trajet-list/trajet-list.component.html
index afc7046..f8fc233 100644
--- a/frontend/src/app/features/trajet-list/trajet-list.component.html
+++ b/frontend/src/app/features/trajet-list/trajet-list.component.html
@@ -1,107 +1,117 @@
-@if (trajetsTries().length === 0) {
-