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.
237 lines
8.5 KiB
TypeScript
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());
|
|
}
|
|
}
|