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:
@@ -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 »). */
|
||||
|
||||
Reference in New Issue
Block a user