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(); /** Émis à chaque sélection d'année — mode « année seule ». */ @Output() readonly anneeChoisie = new EventEmitter(); /** 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([]); /** 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>(new Set()); readonly anneeSelectionnee = signal(null); readonly moisSelectionneSignal = signal(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(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()); // 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(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(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()); } }