UX consultation, trajets aller/retour et rafraîchissement des totaux
- Totaux : rafraîchissement immédiat du panneau (jour/mois/année + montant prévisionnel) après création/modification/suppression/réordonnancement d'un trajet, sans changer de jour (ViewChild + méthode publique rafraichir()). - Trajets Aller/Retour : case à cocher au formulaire ; la distance comptée est doublée (distance simple stockée + booléen AllerRetour, distance effective utilisée dans tous les agrégats : totaux, résumés, barème, régularisation, PDF). Migration TrajetAllerRetour. Badge « A/R » dans une colonne dédiée en début de ligne pour garder date/adresses alignées. - Mise en page de la page Trajets : vue à hauteur fixe où seule la liste défile ; colonne gauche figée (sélecteur de date + navigation, totaux, carte) et bande fixe (véhicule + bouton d'ajout) au-dessus de la liste. Layout applicatif global en pleine hauteur (app.component). - run-dev.sh : chargement de la clé JWT depuis .env (ou secret de dev aléatoire) et Auth__FrontendBaseUrl sur le frontend dev ; rappel du lien de reset journalisé. Build backend (solution) et frontend (production) OK.
This commit is contained in:
@@ -1,4 +1,12 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
@@ -6,8 +14,6 @@
|
||||
padding: 0.5rem 1rem;
|
||||
background: #1f2937;
|
||||
border-bottom: 1px solid #111827;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 500;
|
||||
|
||||
&__lien {
|
||||
@@ -30,3 +36,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Le contenu routé occupe toute la hauteur restante sous le menu.
|
||||
Les vues qui le souhaitent (ex. consultation) peuvent ainsi figer leur mise en page
|
||||
et ne faire défiler qu'une sous-zone interne. */
|
||||
main {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main > * {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface CreateTrajet {
|
||||
arriveeLongitude?: number | null;
|
||||
commentaire?: string | null; // optionnel, max 500 car.
|
||||
vehiculeId?: number | null; // null -> véhicule courant à la date du trajet
|
||||
allerRetour?: boolean; // true -> distance comptée doublée (aller/retour)
|
||||
}
|
||||
|
||||
/** Mise à jour d'un Trajet. */
|
||||
@@ -28,6 +29,7 @@ export interface UpdateTrajet {
|
||||
arriveeLongitude?: number | null;
|
||||
commentaire?: string | null;
|
||||
vehiculeId?: number | null;
|
||||
allerRetour?: boolean; // true -> distance comptée doublée (aller/retour)
|
||||
}
|
||||
|
||||
/** Réponse Trajet. */
|
||||
@@ -36,11 +38,12 @@ export interface Trajet {
|
||||
date: string; // ISO date (yyyy-MM-dd) renvoyée par l'API
|
||||
adresseDepart: string;
|
||||
adresseArrivee: string;
|
||||
distance: number | null; // km, 2 décimales ; null si non calculable (Exigence 2.4)
|
||||
distance: number | null; // DISTANCE EFFECTIVE (déjà doublée si A/R) ; null si non calculable
|
||||
commentaire: string | null;
|
||||
position: number; // rang dans la journée
|
||||
vehiculeId: number;
|
||||
puissanceFiscale: number;
|
||||
allerRetour: boolean; // information aller/retour conservée par trajet
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,6 +62,7 @@ export interface TrajetCarte {
|
||||
arriveeLatitude?: number | null;
|
||||
arriveeLongitude?: number | null;
|
||||
geometrieJson: string | null; // GeoJSON LineString ou null
|
||||
allerRetour?: boolean; // information aller/retour conservée par trajet
|
||||
}
|
||||
|
||||
/** Position d'un trajet lors d'un réordonnancement. */
|
||||
|
||||
@@ -1,58 +1,71 @@
|
||||
<section class="consultation" aria-label="Consultation des trajets d'une journée">
|
||||
<!-- Barre supérieure : sélecteur temporel + navigation jour ± 1 + sélecteur véhicule -->
|
||||
<header class="consultation__barre">
|
||||
<app-selecteur-temporel
|
||||
[date]="date()"
|
||||
(dateSelectionnee)="onDateSelectionnee($event)"
|
||||
></app-selecteur-temporel>
|
||||
|
||||
<div class="consultation__navigation" role="group" aria-label="Navigation par jour">
|
||||
<button
|
||||
type="button"
|
||||
class="consultation__nav-bouton"
|
||||
(click)="jourPrecedent()"
|
||||
>
|
||||
‹ Jour précédent
|
||||
</button>
|
||||
|
||||
<span class="consultation__date" aria-live="polite">
|
||||
{{ dateFormatee }}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="consultation__nav-bouton"
|
||||
(click)="jourSuivant()"
|
||||
[disabled]="jourSuivantDesactive"
|
||||
>
|
||||
Jour suivant ›
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="consultation__vehicule">
|
||||
<span class="consultation__vehicule-libelle">Véhicule</span>
|
||||
<select
|
||||
class="consultation__vehicule-select"
|
||||
[value]="vehiculeId() ?? ''"
|
||||
(change)="onVehiculeChange($any($event.target).value)"
|
||||
>
|
||||
@for (v of vehicules(); track v.id) {
|
||||
<option [value]="v.id">
|
||||
{{ v.denomination }} — {{ v.plaque }} ({{ v.puissanceFiscale }} CV)
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div class="consultation__corps">
|
||||
<!-- Zone principale : bouton d'ajout + liste des trajets (scroll vertical) -->
|
||||
<!-- Colonne GAUCHE fixe (sticky) : sélecteur de date + navigation, puis totaux, puis carte.
|
||||
Reste visible pendant que la liste défile avec la page. -->
|
||||
<aside class="consultation__panneau">
|
||||
<div class="consultation__dates">
|
||||
<app-selecteur-temporel
|
||||
[date]="date()"
|
||||
(dateSelectionnee)="onDateSelectionnee($event)"
|
||||
></app-selecteur-temporel>
|
||||
|
||||
<div class="consultation__navigation" role="group" aria-label="Navigation par jour">
|
||||
<button
|
||||
type="button"
|
||||
class="consultation__nav-bouton"
|
||||
(click)="jourPrecedent()"
|
||||
>
|
||||
‹ 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>
|
||||
</div>
|
||||
|
||||
<app-panneau-totaux
|
||||
[date]="date()"
|
||||
[vehiculeId]="vehiculeId()"
|
||||
></app-panneau-totaux>
|
||||
|
||||
<app-carte-trajets
|
||||
[trajets]="trajetsCarte()"
|
||||
[trajetSurvoleId]="trajetSurvoleId()"
|
||||
></app-carte-trajets>
|
||||
</aside>
|
||||
|
||||
<!-- Colonne DROITE : bande fixe (véhicule + ajout) puis liste défilant avec la page. -->
|
||||
<main class="consultation__principal">
|
||||
<div class="consultation__actions">
|
||||
<header class="consultation__barre-liste">
|
||||
<label class="consultation__vehicule">
|
||||
<span class="consultation__vehicule-libelle">Véhicule</span>
|
||||
<select
|
||||
class="consultation__vehicule-select"
|
||||
[value]="vehiculeId() ?? ''"
|
||||
(change)="onVehiculeChange($any($event.target).value)"
|
||||
>
|
||||
@for (v of vehicules(); track v.id) {
|
||||
<option [value]="v.id">
|
||||
{{ v.denomination }} — {{ v.plaque }} ({{ v.puissanceFiscale }} CV)
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button type="button" class="consultation__ajouter" (click)="ouvrirCreation()">
|
||||
+ Ajouter un trajet
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (erreur()) {
|
||||
<p class="consultation__erreur" role="alert">{{ erreur() }}</p>
|
||||
@@ -76,19 +89,6 @@
|
||||
></app-trajet-list>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Panneau latéral sticky : totaux en haut, carte en dessous -->
|
||||
<aside class="consultation__panneau">
|
||||
<app-panneau-totaux
|
||||
[date]="date()"
|
||||
[vehiculeId]="vehiculeId()"
|
||||
></app-panneau-totaux>
|
||||
|
||||
<app-carte-trajets
|
||||
[trajets]="trajetsCarte()"
|
||||
[trajetSurvoleId]="trajetSurvoleId()"
|
||||
></app-carte-trajets>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire en modale : la liste / carte / totaux restent visibles derrière. -->
|
||||
|
||||
@@ -1,21 +1,42 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.consultation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden; // aucun scroll global : seule la liste défile (voir &__liste)
|
||||
|
||||
&__barre {
|
||||
// Zone dates (colonne gauche) : sélecteur temporel + navigation ± 1 jour, compacte.
|
||||
&__dates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: #f7f8fa;
|
||||
border: 1px solid #e3e6ea;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
// Bande fixe en haut de la colonne DROITE : sélecteur véhicule + bouton d'ajout.
|
||||
// Reste visible pendant le scroll de la page (la liste défile en dessous).
|
||||
&__barre-liste {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 1rem 1.5rem;
|
||||
padding: 1rem;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #f7f8fa;
|
||||
border: 1px solid #e3e6ea;
|
||||
border-radius: 8px;
|
||||
@@ -24,7 +45,8 @@
|
||||
&__navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&__nav-bouton {
|
||||
@@ -56,7 +78,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__vehicule-libelle {
|
||||
@@ -75,16 +96,20 @@
|
||||
// --- Corps deux colonnes ---
|
||||
&__corps {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(380px, 520px);
|
||||
// Colonne GAUCHE = dates + totaux + carte (fixe) ; DROITE = bande + liste (liste défilante).
|
||||
grid-template-columns: minmax(380px, 460px) minmax(0, 1fr);
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
align-items: stretch;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0; // indispensable pour que la colonne droite puisse contraindre la liste
|
||||
}
|
||||
|
||||
&__principal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
min-height: 0; // permet à la liste enfant de scroller au lieu de pousser la hauteur
|
||||
}
|
||||
|
||||
&__formulaire {
|
||||
@@ -100,6 +125,7 @@
|
||||
}
|
||||
|
||||
&__ajouter {
|
||||
align-self: flex-end;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
@@ -107,6 +133,7 @@
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: #135ba1;
|
||||
@@ -123,9 +150,11 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Zone des trajets : scroll vertical quand la liste est haute (Exigence 12.6).
|
||||
// Zone des trajets : SEULE zone défilante de la vue. Elle occupe la hauteur restante
|
||||
// de la colonne droite et fait défiler uniquement la liste (tout le reste est figé).
|
||||
&__liste {
|
||||
max-height: 55vh;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e3e6ea;
|
||||
border-radius: 8px;
|
||||
@@ -139,12 +168,16 @@
|
||||
}
|
||||
|
||||
// Panneau latéral toujours visible (sticky) : totaux + carte (Exigence 12.9).
|
||||
// Il fait partie de la zone FIXE : il se cale près du haut du viewport pendant que
|
||||
// la liste des trajets défile avec la page. `top` réduit pour maximiser la visibilité ;
|
||||
// borné en hauteur avec défilement interne si les totaux + carte dépassent l'écran.
|
||||
&__panneau {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
// Colonne gauche FIGÉE : ne défile jamais. Alignée en haut.
|
||||
align-self: start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,5 +212,7 @@
|
||||
|
||||
.consultation__panneau {
|
||||
position: static;
|
||||
// Sur une colonne, le panneau (totaux + carte) s'affiche en premier, au-dessus de la liste.
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject, signal } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild, inject, signal } from '@angular/core';
|
||||
import { Subscription, forkJoin, of } from 'rxjs';
|
||||
import { catchError, finalize } from 'rxjs/operators';
|
||||
|
||||
@@ -65,6 +65,9 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
|
||||
private readonly vehiculeService = inject(VehiculeService);
|
||||
private readonly cdr = inject(ChangeDetectorRef);
|
||||
|
||||
/** Référence au panneau des totaux, pour forcer son rafraîchissement à la même journée. */
|
||||
@ViewChild(PanneauTotauxComponent) private panneauTotaux?: PanneauTotauxComponent;
|
||||
|
||||
/** Date sélectionnée au format ISO (`yyyy-MM-dd`). `null` tant qu'aucune date n'est choisie. */
|
||||
readonly date = signal<string | null>(null);
|
||||
|
||||
@@ -232,6 +235,10 @@ export class ConsultationJourComponent implements OnInit, OnDestroy {
|
||||
private rafraichirJournee(): void {
|
||||
this.jourSub?.unsubscribe();
|
||||
|
||||
// Force le recalcul des totaux même si la date/le véhicule n'ont pas changé
|
||||
// (ex. ajout/modification/suppression d'un trajet sur la journée affichée).
|
||||
this.panneauTotaux?.rafraichir();
|
||||
|
||||
const date = this.date();
|
||||
if (!date) {
|
||||
this.trajets.set([]);
|
||||
|
||||
@@ -84,8 +84,13 @@ export class PanneauTotauxComponent implements OnChanges, OnDestroy {
|
||||
* Recharge les totaux et le montant prévisionnel pour la date et le véhicule
|
||||
* courants. Ne déclenche aucun appel tant que les deux entrées ne sont pas
|
||||
* disponibles.
|
||||
*
|
||||
* Public afin que le conteneur (`ConsultationJourComponent`) puisse forcer un
|
||||
* rafraîchissement après une création / modification / suppression / réordonnancement
|
||||
* de trajet sur la MÊME journée (cas où ni `date` ni `vehiculeId` ne changent, donc
|
||||
* `ngOnChanges` ne se déclenche pas).
|
||||
*/
|
||||
private rafraichir(): void {
|
||||
rafraichir(): void {
|
||||
this.totauxSub?.unsubscribe();
|
||||
this.montantSub?.unsubscribe();
|
||||
|
||||
|
||||
@@ -214,6 +214,14 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Aller/Retour : double la distance comptée, sans créer de trajet retour. -->
|
||||
<div class="champ champ--aller-retour">
|
||||
<label class="case-aller-retour">
|
||||
<input type="checkbox" formControlName="allerRetour" />
|
||||
<span>Aller/Retour (compte le double de la distance)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn-primaire" [disabled]="soumissionEnCours()">
|
||||
|
||||
@@ -212,3 +212,21 @@
|
||||
background: #f3f5f8;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Case Aller/Retour : compacte, sur une seule ligne pour gagner en hauteur. */
|
||||
.champ--aller-retour {
|
||||
margin: 0.25rem 0 0.5rem;
|
||||
}
|
||||
.case-aller-retour {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.case-aller-retour input {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -371,7 +371,8 @@ export class TrajetFormComponent implements OnInit, OnDestroy {
|
||||
'',
|
||||
[Validators.required, Validators.minLength(1), Validators.maxLength(ADRESSE_MAX)]
|
||||
],
|
||||
commentaire: ['', [Validators.maxLength(COMMENTAIRE_MAX)]]
|
||||
commentaire: ['', [Validators.maxLength(COMMENTAIRE_MAX)]],
|
||||
allerRetour: [false]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -382,7 +383,8 @@ export class TrajetFormComponent implements OnInit, OnDestroy {
|
||||
date: this.versIsoInput(this.trajet.date),
|
||||
adresseDepart: this.trajet.adresseDepart,
|
||||
adresseArrivee: this.trajet.adresseArrivee,
|
||||
commentaire: this.trajet.commentaire ?? ''
|
||||
commentaire: this.trajet.commentaire ?? '',
|
||||
allerRetour: this.trajet.allerRetour ?? false
|
||||
});
|
||||
// En édition, on repart sans coordonnées locales : elles ne seront recalculées
|
||||
// côté backend que si l'adresse est modifiée (Exigence 5.2/5.3). Les adresses du
|
||||
@@ -549,7 +551,8 @@ export class TrajetFormComponent implements OnInit, OnDestroy {
|
||||
arriveeLatitude: coordArrivee?.latitude ?? null,
|
||||
arriveeLongitude: coordArrivee?.longitude ?? null,
|
||||
commentaire: this.normaliserCommentaire(),
|
||||
vehiculeId: this.vehiculeId
|
||||
vehiculeId: this.vehiculeId,
|
||||
allerRetour: !!this.form.get('allerRetour')?.value
|
||||
};
|
||||
}
|
||||
|
||||
@@ -565,7 +568,8 @@ export class TrajetFormComponent implements OnInit, OnDestroy {
|
||||
arriveeLatitude: coordArrivee?.latitude ?? null,
|
||||
arriveeLongitude: coordArrivee?.longitude ?? null,
|
||||
commentaire: this.normaliserCommentaire(),
|
||||
vehiculeId: this.vehiculeId
|
||||
vehiculeId: this.vehiculeId,
|
||||
allerRetour: !!this.form.get('allerRetour')?.value
|
||||
};
|
||||
}
|
||||
|
||||
@@ -618,7 +622,8 @@ export class TrajetFormComponent implements OnInit, OnDestroy {
|
||||
date: '',
|
||||
adresseDepart: '',
|
||||
adresseArrivee: '',
|
||||
commentaire: ''
|
||||
commentaire: '',
|
||||
allerRetour: false
|
||||
});
|
||||
this.coordDepart.set(null);
|
||||
this.coordArrivee.set(null);
|
||||
|
||||
@@ -28,6 +28,13 @@
|
||||
⠿
|
||||
</button>
|
||||
|
||||
<!-- Colonne dédiée A/R (largeur fixe) : garde date/adresses alignées. -->
|
||||
<div class="trajet__ar-col" aria-hidden="false">
|
||||
@if (trajet.allerRetour) {
|
||||
<span class="trajet__ar" title="Aller/Retour : distance comptée doublée">A/R</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="trajet__infos">
|
||||
<div class="trajet__ligne">
|
||||
<span class="trajet__date">{{ trajet.date | date: 'dd/MM/yyyy' }}</span>
|
||||
|
||||
@@ -84,6 +84,29 @@
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// Colonne dédiée A/R en début de ligne (largeur fixe) : maintient l'alignement
|
||||
// des dates/adresses, qu'un trajet soit aller/retour ou non.
|
||||
&__ar-col {
|
||||
flex: 0 0 2.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// Badge « A/R » discret.
|
||||
&__ar {
|
||||
display: inline-block;
|
||||
padding: 0 0.35rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
color: #1565c0;
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
|
||||
Reference in New Issue
Block a user