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
+8 -4
View File
@@ -246,7 +246,8 @@ src/
- Ligne par trajet (triée par `Position` croissante au sein du jour) : colonne dédiée **A/R** en début de ligne pour les allers/retours, commentaire (si présent), adresses (taille réduite), distance effective - Ligne par trajet (triée par `Position` croissante au sein du jour) : colonne dédiée **A/R** en début de ligne pour les allers/retours, commentaire (si présent), adresses (taille réduite), distance effective
- Actions par trajet : modifier (→ `TrajetFormComponent` pré-rempli en < 500 ms), supprimer (→ dialog de confirmation), **insérer après** (→ `TrajetFormComponent` en mode insertion avec `adresseDepart` = point de fin du trajet courant — son arrivée, ou son **départ** si le trajet est un aller/retour —, la **date de la journée** du trajet, `positionApres` = Position du trajet courant) - Actions par trajet : modifier (→ `TrajetFormComponent` pré-rempli en < 500 ms), supprimer (→ dialog de confirmation), **insérer après** (→ `TrajetFormComponent` en mode insertion avec `adresseDepart` = point de fin du trajet courant — son arrivée, ou son **départ** si le trajet est un aller/retour —, la **date de la journée** du trajet, `positionApres` = Position du trajet courant)
- Si le trajet n'est pas le dernier de la journée : action **insérer avant le suivant** disponible, pré-remplissant l'adresse de départ avec le point de fin du trajet courant (départ si aller/retour) et l'adresse d'arrivée avec le départ du trajet suivant, en calculant la position d'insertion `(P_courant + P_suivant) / 2` - Si le trajet n'est pas le dernier de la journée : action **insérer avant le suivant** disponible, pré-remplissant l'adresse de départ avec le point de fin du trajet courant (départ si aller/retour) et l'adresse d'arrivée avec le départ du trajet suivant, en calculant la position d'insertion `(P_courant + P_suivant) / 2`
- Méthode publique `ouvrirJour(dateIso)` : déplie une journée donnée (appelée par le conteneur après ajout/insertion pour rendre le trajet visible) - Méthode publique `ouvrirJour(dateIso)` : déplie une journée donnée (appelée par le conteneur après ajout/insertion pour rendre le trajet visible) ; `ouvrirToutesLesJournees()` déplie tous les groupes ; `aDesJourneesFermees()` conditionne l'affichage du bouton « Ouvrir toutes les journées »
- `@Input jourAffiche` : la journée affichée sur la Carte ; le groupe correspondant est mis en surbrillance (`estJourAffiche`)
- **Glisser-déposer** (drag & drop) limité à chaque journée : un `cdkDropList` par jour, listes **non connectées** entre elles (pas de déplacement inter-journées) → recalcul des positions du jour puis `PATCH /api/trajets/reordonner` - **Glisser-déposer** (drag & drop) limité à chaque journée : un `cdkDropList` par jour, listes **non connectées** entre elles (pas de déplacement inter-journées) → recalcul des positions du jour puis `PATCH /api/trajets/reordonner`
- Rafraîchissement du résumé mensuel en < 1 000 ms après modification/suppression/insertion - Rafraîchissement du résumé mensuel en < 1 000 ms après modification/suppression/insertion
@@ -275,16 +276,19 @@ src/
**`SelecteurTemporelComponent`** **`SelecteurTemporelComponent`**
- **Repliable** ; l'en-tête affiche le mois et l'année sélectionnés (ex. « Septembre 2026 ») - **Repliable** ; l'en-tête affiche le mois et l'année sélectionnés (ex. « Septembre 2026 »)
- Sélecteur d'année : liste limitée à l'année courante + années ayant des données (endpoint `/api/annees-disponibles`) - Sélecteur d'année : liste limitée à l'année courante + années ayant des données (endpoint `/api/annees-disponibles`)
- Sélecteur de mois : 12 mois affichés, surbrillance des mois avec données (endpoint `/api/mois-disponibles/{annee}`) - Sélecteur de mois : 12 mois affichés, surbrillance des mois avec données (endpoint `/api/mois-disponibles/{annee}`), avec le même code couleur bleu que la journée mise en avant dans la liste ; le mois sélectionné reste en bleu plein
- Pas de sélecteur de jour (la consultation se fait par mois) ; désactivation de tout mois > mois courant - Pas de sélecteur de jour (la consultation se fait par mois) ; désactivation de tout mois > mois courant
- Émet un événement `moisSelectionne: { annee: number, mois: number }` à chaque changement - Émet un événement `moisSelectionne: { annee: number, mois: number }` à chaque changement
**`ConsultationJourComponent`** (composant conteneur / layout principal) **`ConsultationJourComponent`** (composant conteneur / layout principal)
- Deux états : un signal `mois` (`{ annee, mois }`) pilote la **liste mensuelle** ; un signal `date` (ISO) pilote la **carte** et la date par défaut d'ajout/insertion - Deux états : un signal `mois` (`{ annee, mois }`) pilote la **liste mensuelle** ; un signal `date` (ISO) pilote la **carte** et la date par défaut d'ajout/insertion
- Colonne gauche (fixe) : `SelecteurTemporelComponent`, puis la **bande de navigation par jour** (« ‹ Jour préc. / date / Jour suiv. › »), puis `PanneauTotauxComponent`, puis `CarteTrajetsComponent` - Colonne gauche (fixe) : `SelecteurTemporelComponent`, puis `PanneauTotauxComponent`, puis `CarteTrajetsComponent` (plus de bande de navigation par jour)
- Colonne droite : bande fixe (sélecteur de Véhicule + bouton « Ajouter un trajet »), puis `TrajetListComponent` du mois avec scroll vertical - Colonne droite : bande fixe (sélecteur de Véhicule + bouton « Ajouter un trajet »), puis `TrajetListComponent` du mois avec scroll vertical
- Navigation par jour **bornée aux jours ayant des trajets** dans le mois (exception : le jour courant dans le mois courant reste navigable même sans trajet) ; elle ne recharge que la carte et les totaux du jour - La journée affichée sur la Carte est pilotée par le **survol** des Trajets de la liste : survoler un Trajet bascule la Carte sur sa journée (via un `Subject` debouncé ~80 ms pour éviter les rechargements en rafale) et met le tracé en évidence ; en fin de survol, la Carte **reste** sur la dernière journée survolée (seule la surbrillance du tracé est retirée)
- Au chargement d'un mois, la Carte se positionne par défaut sur la dernière journée du mois comportant des Trajets
- Bouton « Ouvrir toutes les journées » (dans la barre liste, avant « Ajouter un trajet » pour ne pas décaler ce dernier ; affiché seulement s'il reste des journées repliées) → `TrajetListComponent.ouvrirToutesLesJournees()`
- Sélecteur de Véhicule (par défaut : véhicule courant) ; filtre la liste, le Panneau_Totaux et la Carte sur le véhicule sélectionné - Sélecteur de Véhicule (par défaut : véhicule courant) ; filtre la liste, le Panneau_Totaux et la Carte sur le véhicule sélectionné
- Transmet la date affichée à la liste (`[jourAffiche]`) : le groupe de cette journée est mis en surbrillance (en-tête bleu accentué) pour la repérer
- Coordonne le rafraîchissement (< 1 000 ms) ; un mois vide affiche des totaux à 0 et un unique message « Aucun trajet pour ce mois », sans alerte d'erreur - Coordonne le rafraîchissement (< 1 000 ms) ; un mois vide affiche des totaux à 0 et un unique message « Aucun trajet pour ce mois », sans alerte d'erreur
- Après création/modification d'un trajet (`onFormEnregistre(trajet)`), si la date du trajet est dans un autre mois, se repositionne sur ce mois, cale la Carte sur la journée du trajet, puis déplie ce jour dans la liste (`TrajetListComponent.ouvrirJour`, différé au tick suivant pour passer après le recalcul de repliage du setter) - Après création/modification d'un trajet (`onFormEnregistre(trajet)`), si la date du trajet est dans un autre mois, se repositionne sur ce mois, cale la Carte sur la journée du trajet, puis déplie ce jour dans la liste (`TrajetListComponent.ouvrirJour`, différé au tick suivant pour passer après le recalcul de repliage du setter)
@@ -38,7 +38,7 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#.
- **Trajet_Précédent** : Le Trajet immédiatement antérieur dans la liste chronologique d'une journée, dont l'adresse d'arrivée peut servir de départ lors d'une insertion. - **Trajet_Précédent** : Le Trajet immédiatement antérieur dans la liste chronologique d'une journée, dont l'adresse d'arrivée peut servir de départ lors d'une insertion.
- **Trajet_Suivant** : Le Trajet immédiatement postérieur dans la liste chronologique d'une journée, dont l'adresse de départ peut servir d'arrivée lors d'une insertion. - **Trajet_Suivant** : Le Trajet immédiatement postérieur dans la liste chronologique d'une journée, dont l'adresse de départ peut servir d'arrivée lors d'une insertion.
- **Position** : Un entier positif associé à chaque Trajet au sein d'une même journée, déterminant son rang dans la liste affichée. L'ordre d'affichage est toujours trié par Position croissante, indépendamment de l'heure de création du Trajet. - **Position** : Un entier positif associé à chaque Trajet au sein d'une même journée, déterminant son rang dans la liste affichée. L'ordre d'affichage est toujours trié par Position croissante, indépendamment de l'heure de création du Trajet.
- **Sélecteur_Temporel** : Le composant de navigation (repliable) permettant de choisir successivement une année puis un mois pour consulter les Trajets correspondants. La journée ciblée sur la Carte est gérée séparément par une bande de navigation par jour. - **Sélecteur_Temporel** : Le composant de navigation (repliable) permettant de choisir successivement une année puis un mois pour consulter les Trajets correspondants. La journée affichée sur la Carte n'est pas choisie par des boutons de navigation mais suit le survol des Trajets dans la liste.
- **Panneau_Totaux** : La zone latérale, toujours visible, affichant les totaux kilométriques du jour, du mois sélectionné et de l'année sélectionnée (cumul jusqu'au mois sélectionné inclus). - **Panneau_Totaux** : La zone latérale, toujours visible, affichant les totaux kilométriques du jour, du mois sélectionné et de l'année sélectionnée (cumul jusqu'au mois sélectionné inclus).
- **Total_Annuel_Partiel** : Le total kilométrique de l'année sélectionnée cumulant uniquement les Trajets jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs. - **Total_Annuel_Partiel** : Le total kilométrique de l'année sélectionnée cumulant uniquement les Trajets jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs.
- **Véhicule** : Un véhicule de l'utilisateur, caractérisé par une plaque d'immatriculation et une puissance fiscale, avec une date d'effet à partir de laquelle il devient le véhicule courant. L'historique des Véhicules est conservé. - **Véhicule** : Un véhicule de l'utilisateur, caractérisé par une plaque d'immatriculation et une puissance fiscale, avec une date d'effet à partir de laquelle il devient le véhicule courant. L'historique des Véhicules est conservé.
@@ -291,19 +291,19 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#.
1. THE Frontend SHALL afficher un Sélecteur_Temporel repliable permettant de choisir successivement une année puis un mois ; la sélection ne descend pas jusqu'à la journée. Le libellé de l'en-tête indique le mois et l'année sélectionnés (ex. « Septembre 2026 »). 1. THE Frontend SHALL afficher un Sélecteur_Temporel repliable permettant de choisir successivement une année puis un mois ; la sélection ne descend pas jusqu'à la journée. Le libellé de l'en-tête indique le mois et l'année sélectionnés (ex. « Septembre 2026 »).
2. THE Frontend SHALL proposer dans le sélecteur d'année uniquement l'année courante et les années pour lesquelles au moins un Trajet est enregistré. 2. THE Frontend SHALL proposer dans le sélecteur d'année uniquement l'année courante et les années pour lesquelles au moins un Trajet est enregistré.
3. WHEN l'utilisateur sélectionne une année, THE Frontend SHALL afficher les douze mois de cette année et mettre en surbrillance les mois comportant au moins un Trajet. 3. WHEN l'utilisateur sélectionne une année, THE Frontend SHALL afficher les douze mois de cette année et mettre en surbrillance les mois comportant au moins un Trajet, avec le même code couleur que la journée mise en avant dans la liste (cohérence visuelle).
4. THE Frontend SHALL empêcher la sélection de tout mois postérieur au mois courant (aucun mois futur sélectionnable). 4. THE Frontend SHALL empêcher la sélection de tout mois postérieur au mois courant (aucun mois futur sélectionnable).
5. WHEN l'utilisateur sélectionne un mois, THE Frontend SHALL afficher la liste de TOUS les Trajets de ce mois, regroupés par journée et triés par date croissante puis par Position croissante au sein de chaque journée. 5. WHEN l'utilisateur sélectionne un mois, THE Frontend SHALL afficher la liste de TOUS les Trajets de ce mois, regroupés par journée et triés par date croissante puis par Position croissante au sein de chaque journée.
6. THE Frontend SHALL afficher chaque journée de la liste mensuelle comme un groupe repliable ; par défaut toutes les journées sont repliées et, au changement de mois, seule la dernière journée comportant des Trajets est dépliée. 6. THE Frontend SHALL afficher chaque journée de la liste mensuelle comme un groupe repliable ; par défaut toutes les journées sont repliées et, au changement de mois, seule la dernière journée comportant des Trajets est dépliée.
7. THE Frontend SHALL afficher la liste mensuelle des Trajets comme zone principale, avec défilement vertical (scroll) lorsque le nombre de Trajets dépasse la hauteur visible, tandis que le Sélecteur_Temporel, la navigation par jour, le Panneau_Totaux et la Carte restent visibles. 7. THE Frontend SHALL afficher la liste mensuelle des Trajets comme zone principale, avec défilement vertical (scroll) lorsque le nombre de Trajets dépasse la hauteur visible, tandis que le Sélecteur_Temporel, la navigation par jour, le Panneau_Totaux et la Carte restent visibles.
8. THE Frontend SHALL afficher un Panneau_Totaux toujours visible contenant : le total kilométrique du jour ciblé sur la Carte, le total kilométrique du mois sélectionné, et le Total_Annuel_Partiel de l'année sélectionnée. 8. THE Frontend SHALL afficher un Panneau_Totaux toujours visible contenant : le total kilométrique du jour ciblé sur la Carte, le total kilométrique du mois sélectionné, et le Total_Annuel_Partiel de l'année sélectionnée.
9. THE Panneau_Totaux SHALL calculer le Total_Annuel_Partiel en cumulant uniquement les Trajets de l'année sélectionnée jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs au mois sélectionné. 9. THE Panneau_Totaux SHALL calculer le Total_Annuel_Partiel en cumulant uniquement les Trajets de l'année sélectionnée jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs au mois sélectionné.
10. THE Frontend SHALL afficher une bande de navigation par jour (« Jour précédent », date affichée, « Jour suivant ») au-dessus du Panneau_Totaux et de la Carte ; cette date pilote le jour dont les Trajets sont représentés sur la Carte et sert de date par défaut lors de l'ajout ou de l'insertion d'un Trajet. 10. WHEN l'utilisateur survole un Trajet dans la liste, THE Frontend SHALL afficher sur la Carte les Trajets de la journée de ce Trajet et mettre le Trajet survolé en évidence sur la Carte ; la date ainsi affichée sert de date par défaut lors de l'ajout ou de l'insertion d'un Trajet.
11. THE Frontend SHALL afficher la Carte des Trajets de la seule journée ciblée par la bande de navigation par jour. 11. WHEN l'utilisateur cesse de survoler un Trajet, THE Frontend SHALL conserver sur la Carte la dernière journée survolée (avec l'ensemble de ses Trajets), en retirant seulement la mise en évidence du tracé.
12. WHEN l'utilisateur clique sur « Jour précédent » ou « Jour suivant », THE Frontend SHALL cibler respectivement le jour comportant des Trajets immédiatement antérieur ou postérieur DANS LE MOIS sélectionné, et rafraîchir uniquement la Carte et le Panneau_Totaux du jour, sans recharger la liste mensuelle. 12. THE Frontend SHALL mettre en évidence, dans la liste, le groupe correspondant à la journée actuellement affichée sur la Carte (surbrillance de l'en-tête de ce groupe).
13. WHERE le mois sélectionné est le mois courant, THE Frontend SHALL rendre la journée courante navigable et ciblable même si elle ne comporte aucun Trajet (afin de pouvoir y ajouter un Trajet). 13. WHEN un mois est sélectionné, THE Frontend SHALL afficher par défaut sur la Carte la dernière journée du mois comportant des Trajets (ou aucune journée si le mois est vide).
14. IF aucun jour comportant des Trajets n'existe avant (respectivement après) le jour ciblé dans le mois, THEN THE Frontend SHALL désactiver le bouton « Jour précédent » (respectivement « Jour suivant »). 14. THE Frontend SHALL proposer un bouton « Ouvrir toutes les journées » qui déplie tous les groupes de la liste ; ce bouton n'est affiché que lorsqu'au moins une journée est repliée.
15. WHEN l'utilisateur change de mois via le Sélecteur_Temporel, ou de jour via la bande de navigation, ou de Véhicule, THE Frontend SHALL mettre à jour la liste, le Panneau_Totaux et la Carte concernés dans un délai de 1000 ms sans rechargement complet de la page. 15. WHEN l'utilisateur change de mois via le Sélecteur_Temporel, survole un Trajet d'une autre journée, ou change de Véhicule, THE Frontend SHALL mettre à jour la liste, le Panneau_Totaux et la Carte concernés dans un délai de 1000 ms sans rechargement complet de la page.
16. WHEN le Backend reçoit une requête des années disponibles, THE Backend SHALL retourner la liste des années comportant au moins un Trajet, ainsi que l'année courante si elle n'y figure pas déjà. 16. WHEN le Backend reçoit une requête des années disponibles, THE Backend SHALL retourner la liste des années comportant au moins un Trajet, ainsi que l'année courante si elle n'y figure pas déjà.
17. WHEN le Backend reçoit une requête des mois comportant des Trajets pour une année donnée, THE Backend SHALL retourner la liste des mois de cette année pour lesquels au moins un Trajet est enregistré. 17. WHEN le Backend reçoit une requête des mois comportant des Trajets pour une année donnée, THE Backend SHALL retourner la liste des mois de cette année pour lesquels au moins un Trajet est enregistré.
18. IF le mois sélectionné ne comporte aucun Trajet, THEN THE Frontend SHALL afficher un unique message « Aucun trajet pour ce mois » et des totaux à zéro, sans afficher de message d'erreur (un mois vide n'est pas une erreur). 18. IF le mois sélectionné ne comporte aucun Trajet, THEN THE Frontend SHALL afficher un unique message « Aucun trajet pour ce mois » et des totaux à zéro, sans afficher de message d'erreur (un mois vide n'est pas une erreur).
@@ -10,31 +10,6 @@
></app-selecteur-temporel> ></app-selecteur-temporel>
</div> </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 <app-panneau-totaux
[date]="date()" [date]="date()"
[vehiculeId]="vehiculeId()" [vehiculeId]="vehiculeId()"
@@ -64,6 +39,16 @@
</select> </select>
</label> </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()"> <button type="button" class="consultation__ajouter" (click)="ouvrirCreation()">
+ Ajouter un trajet + Ajouter un trajet
</button> </button>
@@ -78,7 +63,9 @@
<p class="consultation__vide">Aucun trajet pour ce mois.</p> <p class="consultation__vide">Aucun trajet pour ce mois.</p>
} }
<app-trajet-list <app-trajet-list
#liste
[trajets]="trajets()" [trajets]="trajets()"
[jourAffiche]="date()"
(edit)="onEdit($event)" (edit)="onEdit($event)"
(insertAfter)="onInsert($event)" (insertAfter)="onInsert($event)"
(insertBetween)="onInsert($event)" (insertBetween)="onInsert($event)"
@@ -20,7 +20,7 @@
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; // aucun scroll global : seule la liste défile (voir &__liste) 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 { &__dates {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -46,38 +46,6 @@
border-radius: 8px; 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 { &__vehicule {
display: flex; display: flex;
flex-direction: column; 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 { &__erreur {
color: #b3261e; color: #b3261e;
margin: 0; margin: 0;
@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild, inject, signal } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild, inject, signal } from '@angular/core';
import { Subscription, forkJoin, of } from 'rxjs'; import { Subject, Subscription, forkJoin, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators'; import { catchError, debounceTime, finalize } from 'rxjs/operators';
import { ResumeMensuel, Trajet, TrajetCarte, Vehicule } from '../../core/models'; import { ResumeMensuel, Trajet, TrajetCarte, Vehicule } from '../../core/models';
import { TrajetService } from '../../core/services/trajet.service'; import { TrajetService } from '../../core/services/trajet.service';
@@ -125,17 +125,35 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
*/ */
private jourAOuvrirApresChargement: string | null = null; 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 { ngOnInit(): void {
// Par défaut : mois courant, et jour = aujourd'hui (contenu dans le mois courant). // Par défaut : mois courant, et jour = aujourd'hui (contenu dans le mois courant).
const auj = new Date(); const auj = new Date();
this.mois.set({ annee: auj.getFullYear(), mois: auj.getMonth() + 1 }); this.mois.set({ annee: auj.getFullYear(), mois: auj.getMonth() + 1 });
this.date.set(this.isoAujourdHui()); this.date.set(this.isoAujourdHui());
this.chargerVehicules(); 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 { ngOnDestroy(): void {
this.jourSub?.unsubscribe(); this.jourSub?.unsubscribe();
this.vehiculesSub?.unsubscribe(); this.vehiculesSub?.unsubscribe();
this.survolSub?.unsubscribe();
this.survolTrajet$.complete();
} }
// --- Sélection date / véhicule --- // --- Sélection date / véhicule ---
@@ -163,45 +181,7 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
this.rafraichirJournee(); this.rafraichirJournee();
} }
// --- Navigation jour, LIMITÉE AUX JOURS AYANT DES TRAJETS --- // --- Sélection de la journée affichée sur la carte (pilotée par le survol) ---
/**
* 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;
}
/** /**
* Jours navigables du mois affiché, triés en ordre croissant (ISO `yyyy-MM-dd`) : * 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(); 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) --- // --- Formulaire (création / édition / insertion) ---
/** Ouvre le formulaire en mode création. */ /** Ouvre le formulaire en mode création. */
@@ -316,9 +267,35 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
this.rafraichirJournee(); 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 { onSurvolTrajet(id: number | null): void {
// Surbrillance immédiate du tracé (ou retrait si null).
this.trajetSurvoleId.set(id); 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 --- // --- Chargement des données ---
@@ -491,19 +468,6 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
// --- Utilitaires de date --- // --- 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`. */ /** Date du jour au format ISO local `yyyy-MM-dd`. */
private isoAujourdHui(): string { private isoAujourdHui(): string {
return this.versIso(new Date()); return this.versIso(new Date());
@@ -94,10 +94,12 @@
cursor: not-allowed; 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) { &.avec-donnees:not(:disabled) {
border-color: #16a34a; border-color: #90c2ff;
background: #ecfdf5; background: #e3f0ff;
color: #0d47a1;
font-weight: 600; font-weight: 600;
} }
@@ -2,7 +2,11 @@
<!-- Un groupe par journée : en-tête de date + liste de dépôt indépendante. <!-- 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. --> Les listes NE sont PAS connectées entre elles → drag limité à chaque jour. -->
@for (groupe of groupes(); track groupe.date) { @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. --> <!-- En-tête repliable : chevron + date + nombre de trajets. -->
<button <button
type="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 { &__chevron {
flex: 0 0 auto; flex: 0 0 auto;
color: #64748b; color: #64748b;
@@ -103,6 +103,15 @@ export class TrajetListComponent {
} }
private _trajets: Trajet[] = []; 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. */ /** Émis lorsque l'utilisateur demande la modification d'un trajet. */
@Output() edit = new EventEmitter<Trajet>(); @Output() edit = new EventEmitter<Trajet>();
@@ -217,6 +226,11 @@ export class TrajetListComponent {
return this.joursOuverts().has(groupe.date); 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). */ /** Bascule le repliage d'une journée (déplie / replie). */
basculerJour(groupe: GroupeJour): void { basculerJour(groupe: GroupeJour): void {
this.joursOuverts.update((ouverts) => { 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 --- // --- Helpers d'affichage ---
/** Vrai si le trajet est le dernier DE SA JOURNÉE (pas d'« insérer avant le suivant »). */ /** Vrai si le trajet est le dernier DE SA JOURNÉE (pas d'« insérer avant le suivant »). */