Files
SchotzIAD/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.ts
T
MadJax f6445d66af Sélecteur de mois : libellés courts et grille de 6 par ligne
Les libellés longs de largeur variable débordaient sur 3 lignes. Les
mois s'affichent désormais en abrégé (Janv., Févr., …), centrés, dans
une grille de 6 colonnes égales (2 rangées de 6, boutons de largeur
constante). Le nom complet du mois reste disponible en infobulle. Le
groupe des années et le mode « année seule » sont inchangés.
2026-10-11 20:51:15 +02:00

237 lines
8.5 KiB
TypeScript

import { CommonModule } from '@angular/common';
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
inject,
signal
} from '@angular/core';
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.
*
* 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)
* - 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.5, 12.14_
*/
@Component({
selector: 'app-selecteur-temporel',
standalone: true,
imports: [CommonModule],
templateUrl: './selecteur-temporel.component.html',
styleUrl: './selecteur-temporel.component.scss'
})
export class SelecteurTemporelComponent implements OnInit, OnChanges {
private readonly trajetService = inject(TrajetService);
/**
* 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;
/**
* Mode « année seule » : masque la grille des mois et limite le sélecteur au
* choix d'une année. La sélection d'une année émet alors `anneeSelectionnee`
* (et non `moisSelectionne`). Par défaut faux (mode année + mois).
*/
@Input() modeAnneeSeule = false;
/** Émis à chaque changement de sélection (année + mois) — mode année + mois. */
@Output() readonly moisSelectionne = new EventEmitter<MoisSelectionne>();
/** Émis à chaque sélection d'année — mode « année seule ». */
@Output() readonly anneeChoisie = new EventEmitter<number>();
/** Libellés des douze mois (index 0 = janvier), utilisés pour l'en-tête « Mois Année ». */
readonly nomsMois = [
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
];
/** Libellés courts des douze mois (index 0 = janvier), pour les pastilles de la grille. */
readonly nomsMoisCourts = [
'Janv.', 'Févr.', 'Mars', 'Avr.', 'Mai', 'Juin',
'Juil.', 'Août', 'Sept.', 'Oct.', 'Nov.', 'Déc.'
];
/** Date du jour, tronquée à minuit, pour borner les sélections futures. */
private readonly aujourdHui = this.debutDeJournee(new Date());
/** Années proposées (année courante + années ayant des données). */
readonly annees = signal<number[]>([]);
/** Les douze mois (1..12), toujours affichés. */
readonly mois: number[] = Array.from({ length: 12 }, (_, i) => i + 1);
/** Mois comportant au moins un trajet pour l'année sélectionnée. */
private readonly moisAvecDonnees = signal<Set<number>>(new Set<number>());
readonly anneeSelectionnee = signal<number | null>(null);
readonly moisSelectionneSignal = signal<number | null>(null);
/** État d'ouverture du conteneur repliable (fermé par défaut). */
readonly ouvert = signal(false);
/**
* Libellé « Mois Année » de la sélection courante (ex. « Septembre 2026 »),
* affiché à droite du bouton d'en-tête. Vide tant qu'aucun mois n'est choisi.
*/
get libelleMoisAnnee(): string {
const annee = this.anneeSelectionnee();
if (this.modeAnneeSeule) {
return annee == null ? '' : `${annee}`;
}
const mois = this.moisSelectionneSignal();
if (annee == null || mois == null) {
return '';
}
return `${this.nomsMois[mois - 1]} ${annee}`;
}
/** Libellé du bouton d'en-tête, selon le mode. */
get libelleEntete(): string {
return this.modeAnneeSeule ? '📅 Choisir une année' : '📅 Choisir un mois';
}
/** Bascule l'ouverture / fermeture du sélecteur repliable. */
basculerOuverture(): void {
this.ouvert.update((v) => !v);
}
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.
// 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);
this.trajetService.getAnneesDisponibles().subscribe({
next: (annees) => {
this.annees.set(this.avecAnneeCourante(annees));
// Réaligner l'affichage sur la date du parent une fois les années connues.
this.synchroniserSurDate(this.date);
},
// En cas d'échec réseau, on conserve l'année courante déjà affichée.
error: () => {}
});
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['date'] && !changes['date'].firstChange) {
this.synchroniserSurDate(this.date);
}
}
/**
* 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] = dateIso.slice(0, 10).split('-').map(Number);
if (!annee || !mois) {
return;
}
// Garantit que l'année affichée figure dans la liste des pastilles.
if (!this.annees().includes(annee)) {
this.annees.set(this.avecAnneeCourante([...this.annees(), annee]));
}
this.anneeSelectionnee.set(annee);
this.moisSelectionneSignal.set(mois);
this.trajetService.getMoisDisponibles(annee).subscribe((m) => {
this.moisAvecDonnees.set(new Set<number>(m));
});
}
/** Sélectionne une année et charge les mois comportant des données. */
selectionnerAnnee(annee: number): void {
if (this.estAnneeFuture(annee)) {
return;
}
this.anneeSelectionnee.set(annee);
this.moisSelectionneSignal.set(null);
this.moisAvecDonnees.set(new Set<number>());
// Mode « année seule » : l'année est le niveau le plus bas → on émet et on
// replie automatiquement le sélecteur. Pas de chargement des mois (non affichés).
if (this.modeAnneeSeule) {
this.anneeChoisie.emit(annee);
this.ouvert.set(false);
return;
}
this.trajetService.getMoisDisponibles(annee).subscribe((mois) => {
this.moisAvecDonnees.set(new Set<number>(mois));
});
}
/** 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.moisSelectionneSignal.set(mois);
this.moisSelectionne.emit({ annee, mois });
// Le mois est le niveau le plus bas en mode année + mois → repli automatique.
this.ouvert.set(false);
}
/** Un mois est en surbrillance s'il comporte au moins un trajet. */
moisADonnees(mois: number): boolean {
return this.moisAvecDonnees().has(mois);
}
/** 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();
}
/** Un mois est désactivé si son premier jour est postérieur à aujourd'hui. */
estMoisFutur(annee: number, mois: number): boolean {
return this.debutDeJournee(new Date(annee, mois - 1, 1)) > 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<number>(annees);
ensemble.add(this.aujourdHui.getFullYear());
return [...ensemble].sort((a, b) => b - a);
}
/** Retourne une copie de la date à minuit (heure locale). */
private debutDeJournee(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
}