Consultation : carte pilotée par le survol, plus de navigation jour

Refonte de l'interaction carte/liste suite au retour d'usage :

- Suppression de la bande de navigation « Jour préc. / date / Jour suiv. »
  (boutons, code et styles associés retirés).
- La journée affichée sur la carte suit le SURVOL des trajets : survoler un
  trajet bascule la carte sur sa journée (Subject debouncé ~80 ms) et met le
  tracé en évidence ; en fin de survol, la carte reste sur la dernière journée
  survolée. Au chargement d'un mois, la carte se cale sur la dernière journée
  ayant des trajets.
- Bouton « Ouvrir toutes les journées » (avant « Ajouter un trajet » pour ne pas
  décaler ce dernier ; affiché seulement s'il reste des journées repliées).
- Surbrillance du groupe de la journée affichée sur la carte (en-tête bleu
  accentué), pilotée par le nouvel Input jourAffiche de la liste.
- Harmonisation : les mois « avec données » du sélecteur passent au même bleu
  que la journée mise en avant (le mois sélectionné reste en bleu plein).
- Date par défaut à la création : date de la carte, sinon date du jour (inchangé).

Specs : requirements.md (exigence 12, glossaire Sélecteur_Temporel) et design.md
(ConsultationJour, TrajetList, SelecteurTemporel) mis à jour.
This commit is contained in:
2026-09-20 22:35:34 +02:00
parent 173b123f03
commit 937609a83b
9 changed files with 136 additions and 158 deletions
@@ -10,31 +10,6 @@
></app-selecteur-temporel>
</div>
<!-- Bande de navigation jour (limitée aux jours avec trajets) : la date pilote les totaux et la carte qui suivent. -->
<div class="consultation__navigation" role="group" aria-label="Navigation par jour">
<button
type="button"
class="consultation__nav-bouton"
(click)="jourPrecedent()"
[disabled]="jourPrecedentDesactive"
>
‹ Jour préc.
</button>
<span class="consultation__date" aria-live="polite">
{{ dateFormatee }}
</span>
<button
type="button"
class="consultation__nav-bouton"
(click)="jourSuivant()"
[disabled]="jourSuivantDesactive"
>
Jour suiv. ›
</button>
</div>
<app-panneau-totaux
[date]="date()"
[vehiculeId]="vehiculeId()"
@@ -64,6 +39,16 @@
</select>
</label>
@if (liste.aDesJourneesFermees()) {
<button
type="button"
class="consultation__ouvrir-tout"
(click)="liste.ouvrirToutesLesJournees()"
>
Ouvrir toutes les journées
</button>
}
<button type="button" class="consultation__ajouter" (click)="ouvrirCreation()">
+ Ajouter un trajet
</button>
@@ -78,7 +63,9 @@
<p class="consultation__vide">Aucun trajet pour ce mois.</p>
}
<app-trajet-list
#liste
[trajets]="trajets()"
[jourAffiche]="date()"
(edit)="onEdit($event)"
(insertAfter)="onInsert($event)"
(insertBetween)="onInsert($event)"
@@ -20,7 +20,7 @@
box-sizing: border-box;
overflow: hidden; // aucun scroll global : seule la liste défile (voir &__liste)
// Zone dates (colonne gauche) : sélecteur temporel + navigation ± 1 jour, compacte.
// Zone dates (colonne gauche) : sélecteur temporel (année/mois), compacte.
&__dates {
display: flex;
flex-direction: column;
@@ -46,38 +46,6 @@
border-radius: 8px;
}
&__navigation {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
&__nav-bouton {
padding: 0.4rem 0.9rem;
border: 1px solid #c7ccd3;
border-radius: 6px;
background: #fff;
cursor: pointer;
font-size: 0.9rem;
&:hover:not(:disabled) {
background: #eef1f5;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
&__date {
min-width: 6.5rem;
text-align: center;
font-variant-numeric: tabular-nums;
font-weight: 600;
}
&__vehicule {
display: flex;
flex-direction: column;
@@ -144,6 +112,23 @@
}
}
// Bouton secondaire : déplie toutes les journées de la liste.
&__ouvrir-tout {
align-self: flex-end;
padding: 0.5rem 1rem;
border: 1px solid #c7ccd3;
border-radius: 6px;
background: #fff;
color: #33475b;
cursor: pointer;
font-size: 0.9rem;
white-space: nowrap;
&:hover {
background: #eef1f5;
}
}
&__erreur {
color: #b3261e;
margin: 0;
@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild, inject, signal } from '@angular/core';
import { Subscription, forkJoin, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { Subject, Subscription, forkJoin, of } from 'rxjs';
import { catchError, debounceTime, finalize } from 'rxjs/operators';
import { ResumeMensuel, Trajet, TrajetCarte, Vehicule } from '../../core/models';
import { TrajetService } from '../../core/services/trajet.service';
@@ -125,17 +125,35 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
*/
private jourAOuvrirApresChargement: string | null = null;
/**
* Flux des survols de trajets (id de trajet, ou `null` en fin de survol). Un
* `debounceTime` évite de recharger la carte en rafale lors d'un balayage rapide
* de la liste. Le survol met la carte sur la JOURNÉE du trajet survolé ; en fin de
* survol, la carte reste sur cette dernière journée (on retire seulement la
* surbrillance).
*/
private readonly survolTrajet$ = new Subject<number | null>();
private survolSub?: Subscription;
ngOnInit(): void {
// 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();
// Survol des trajets → bascule la carte sur la journée survolée (avec un léger
// debounce pour ne pas enchaîner les chargements pendant un balayage rapide).
this.survolSub = this.survolTrajet$
.pipe(debounceTime(80))
.subscribe((id) => this.appliquerSurvol(id));
}
ngOnDestroy(): void {
this.jourSub?.unsubscribe();
this.vehiculesSub?.unsubscribe();
this.survolSub?.unsubscribe();
this.survolTrajet$.complete();
}
// --- Sélection date / véhicule ---
@@ -163,45 +181,7 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
this.rafraichirJournee();
}
// --- Navigation jour, LIMITÉE AUX JOURS AYANT DES TRAJETS ---
/**
* Jour affiché sur la carte : navigue vers le jour ayant des trajets précédent
* (dans le mois). Seul `date` change ; la carte est rechargée.
*/
jourPrecedent(): void {
const cible = this.jourNavigableAdjacent(-1);
if (cible == null) {
return;
}
this.date.set(cible);
this.formulaire.set(null);
this.rafraichirCarteJour();
}
/**
* Jour affiché sur la carte : navigue vers le jour ayant des trajets suivant
* (dans le mois). Seul `date` change ; la carte est rechargée.
*/
jourSuivant(): void {
const cible = this.jourNavigableAdjacent(1);
if (cible == null) {
return;
}
this.date.set(cible);
this.formulaire.set(null);
this.rafraichirCarteJour();
}
/** « Jour préc. » désactivé s'il n'existe pas de jour navigable antérieur. */
get jourPrecedentDesactive(): boolean {
return this.jourNavigableAdjacent(-1) == null;
}
/** « Jour suiv. » désactivé s'il n'existe pas de jour navigable postérieur. */
get jourSuivantDesactive(): boolean {
return this.jourNavigableAdjacent(1) == null;
}
// --- Sélection de la journée affichée sur la carte (pilotée par le survol) ---
/**
* Jours navigables du mois affiché, triés en ordre croissant (ISO `yyyy-MM-dd`) :
@@ -225,35 +205,6 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
return [...jours].sort();
}
/**
* Renvoie le jour navigable immédiatement avant (`direction < 0`) ou après
* (`direction > 0`) la date affichée, ou `null` s'il n'existe pas. La date
* courante n'a pas besoin d'appartenir à l'ensemble navigable.
*/
private jourNavigableAdjacent(direction: number): string | null {
const jours = this.joursNavigables();
if (jours.length === 0) {
return null;
}
const courante = this.dateCourante();
if (direction < 0) {
const anterieurs = jours.filter((j) => j < courante);
return anterieurs.length > 0 ? anterieurs[anterieurs.length - 1] : null;
}
const posterieurs = jours.filter((j) => j > courante);
return posterieurs.length > 0 ? posterieurs[0] : null;
}
/** Date courante affichée au format JJ/MM/AAAA (lisible), ou `—` si absente. */
get dateFormatee(): string {
const date = this.date();
if (!date) {
return '—';
}
const [annee, mois, jour] = date.slice(0, 10).split('-');
return `${jour}/${mois}/${annee}`;
}
// --- Formulaire (création / édition / insertion) ---
/** Ouvre le formulaire en mode création. */
@@ -316,9 +267,35 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
this.rafraichirJournee();
}
/** Survol d'une ligne de trajet : met en évidence le tracé correspondant sur la carte. */
/**
* Survol d'une ligne de trajet : la surbrillance du tracé est appliquée
* immédiatement (feedback instantané), tandis que le changement éventuel de
* journée affichée sur la carte est différé (debounce) via `survolTrajet$`.
*/
onSurvolTrajet(id: number | null): void {
// Surbrillance immédiate du tracé (ou retrait si null).
this.trajetSurvoleId.set(id);
// Bascule de journée différée.
this.survolTrajet$.next(id);
}
/**
* Applique l'effet d'un survol sur la carte :
* - `id` non nul → si le trajet appartient à une AUTRE journée que celle affichée,
* bascule la carte (et la date par défaut de création) sur cette journée.
* - `id` nul (fin de survol) → ne change PAS de journée : la carte reste sur la
* dernière journée survolée (seule la surbrillance a déjà été retirée).
*/
private appliquerSurvol(id: number | null): void {
if (id == null) {
return;
}
const trajet = this.trajets().find((t) => t.id === id);
const jour = (trajet?.date ?? '').slice(0, 10);
if (jour && jour !== this.date()) {
this.date.set(jour);
this.rafraichirCarteJour();
}
}
// --- Chargement des données ---
@@ -491,19 +468,6 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
// --- Utilitaires de date ---
/** Date courante de travail (fallback sur aujourd'hui si aucune sélection). */
private dateCourante(): string {
return this.date() ?? this.isoAujourdHui();
}
/** 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);
const d = new Date(annee, mois - 1, jour);
d.setDate(d.getDate() + delta);
return this.versIso(d);
}
/** Date du jour au format ISO local `yyyy-MM-dd`. */
private isoAujourdHui(): string {
return this.versIso(new Date());
@@ -94,10 +94,12 @@
cursor: not-allowed;
}
// Surbrillance des périodes comportant des données (12.3, 12.4).
// Surbrillance des périodes comportant des données (12.3, 12.4) : même code
// couleur bleu que la journée mise en avant dans la liste (cohérence visuelle).
&.avec-donnees:not(:disabled) {
border-color: #16a34a;
background: #ecfdf5;
border-color: #90c2ff;
background: #e3f0ff;
color: #0d47a1;
font-weight: 600;
}
@@ -2,7 +2,11 @@
<!-- Un groupe par journée : en-tête de date + liste de dépôt indépendante.
Les listes NE sont PAS connectées entre elles → drag limité à chaque jour. -->
@for (groupe of groupes(); track groupe.date) {
<section class="jour" aria-label="Trajets d'une journée">
<section
class="jour"
[class.jour--affiche]="estJourAffiche(groupe)"
aria-label="Trajets d'une journée"
>
<!-- En-tête repliable : chevron + date + nombre de trajets. -->
<button
type="button"
@@ -34,6 +34,14 @@
}
}
// Journée affichée sur la carte : en-tête accentué (bleu) pour la repérer.
&--affiche &__entete {
background: #e3f0ff;
border-color: #90c2ff;
color: #0d47a1;
box-shadow: inset 3px 0 0 #1976d2;
}
&__chevron {
flex: 0 0 auto;
color: #64748b;
@@ -103,6 +103,15 @@ export class TrajetListComponent {
}
private _trajets: Trajet[] = [];
/**
* Journée (`yyyy-MM-dd`) actuellement affichée sur la carte : son groupe est mis
* en surbrillance dans la liste pour la repérer visuellement. `null` si aucune.
*/
@Input() set jourAffiche(valeur: string | null) {
this._jourAffiche.set((valeur ?? '').slice(0, 10) || null);
}
private readonly _jourAffiche = signal<string | null>(null);
/** Émis lorsque l'utilisateur demande la modification d'un trajet. */
@Output() edit = new EventEmitter<Trajet>();
@@ -217,6 +226,11 @@ export class TrajetListComponent {
return this.joursOuverts().has(groupe.date);
}
/** Vrai si ce groupe correspond à la journée affichée sur la carte (surbrillance). */
estJourAffiche(groupe: GroupeJour): boolean {
return this._jourAffiche() === groupe.date;
}
/** Bascule le repliage d'une journée (déplie / replie). */
basculerJour(groupe: GroupeJour): void {
this.joursOuverts.update((ouverts) => {
@@ -247,6 +261,16 @@ export class TrajetListComponent {
});
}
/** Déplie TOUTES les journées du mois affiché (bouton « Ouvrir toutes les journées »). */
ouvrirToutesLesJournees(): void {
this.joursOuverts.set(new Set<string>(this.groupes().map((g) => g.date)));
}
/** Vrai si au moins une journée est repliée (pour n'afficher le bouton que si utile). */
aDesJourneesFermees(): boolean {
return this.groupes().some((g) => !this.joursOuverts().has(g.date));
}
// --- Helpers d'affichage ---
/** Vrai si le trajet est le dernier DE SA JOURNÉE (pas d'« insérer avant le suivant »). */