diff --git a/.kiro/specs/schotziad-km-tracker/design.md b/.kiro/specs/schotziad-km-tracker/design.md index 429fcfd..6cf755a 100644 --- a/.kiro/specs/schotziad-km-tracker/design.md +++ b/.kiro/specs/schotziad-km-tracker/design.md @@ -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 - 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` -- 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` - Rafraîchissement du résumé mensuel en < 1 000 ms après modification/suppression/insertion @@ -275,16 +276,19 @@ src/ **`SelecteurTemporelComponent`** - **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 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 - Émet un événement `moisSelectionne: { annee: number, mois: number }` à chaque changement **`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 -- 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 -- 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é +- 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 - 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) diff --git a/.kiro/specs/schotziad-km-tracker/requirements.md b/.kiro/specs/schotziad-km-tracker/requirements.md index 78ee7ef..c548136 100644 --- a/.kiro/specs/schotziad-km-tracker/requirements.md +++ b/.kiro/specs/schotziad-km-tracker/requirements.md @@ -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_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. -- **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). - **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é. @@ -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 »). 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). 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. 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. 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. -11. THE Frontend SHALL afficher la Carte des Trajets de la seule journée ciblée par la bande de navigation par jour. -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. -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). -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 »). -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. +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. 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. 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. 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. 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, 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à. 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). diff --git a/frontend/src/app/features/consultation-jour/consultation-jour.component.html b/frontend/src/app/features/consultation-jour/consultation-jour.component.html index 20935b9..2d00927 100644 --- a/frontend/src/app/features/consultation-jour/consultation-jour.component.html +++ b/frontend/src/app/features/consultation-jour/consultation-jour.component.html @@ -10,31 +10,6 @@ > - -
- - - - {{ dateFormatee }} - - - -
- + @if (liste.aDesJourneesFermees()) { + + } + @@ -78,7 +63,9 @@

Aucun trajet pour ce mois.

} (); + 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()); diff --git a/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.scss b/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.scss index a5b455e..53c9d51 100644 --- a/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.scss +++ b/frontend/src/app/features/navigation-temporelle/selecteur-temporel.component.scss @@ -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; } diff --git a/frontend/src/app/features/trajet-list/trajet-list.component.html b/frontend/src/app/features/trajet-list/trajet-list.component.html index 4bb648d..32f6d70 100644 --- a/frontend/src/app/features/trajet-list/trajet-list.component.html +++ b/frontend/src/app/features/trajet-list/trajet-list.component.html @@ -2,7 +2,11 @@ @for (groupe of groupes(); track groupe.date) { -
+