@mostajs/flotte-ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/textes.js CHANGED
@@ -11,13 +11,13 @@ export const TEXTES = Object.freeze({
11
11
  rechercheGlobale: 'Rechercher un véhicule, un chauffeur, une plaque, une tournée…',
12
12
  nouveau: 'Nouveau véhicule',
13
13
  kpi: {
14
- vehicules: 'Véhicules', chauffeurs: 'Chauffeurs', enService: 'En service', enMaintenance: 'En maintenance',
14
+ vehicules: 'Véhicules', chauffeurs: 'Chauffeurs', enService: 'En service', enMaintenance: 'En maintenance', horsService: 'Hors service',
15
15
  documentsExpires: 'Documents expirés', vsPeriode: 'vs période précédente', nouveau: 'nouveau',
16
16
  },
17
17
  liste: {
18
18
  titre: 'Liste des véhicules', recherche: 'Rechercher un véhicule, une plaque, un chauffeur…', tousTypes: 'Tous les types',
19
19
  tousStatuts: 'Tous les statuts', toutesRegions: 'Toutes les régions', filtrer: 'Filtrer', vide: 'Aucun véhicule ne répond à ces filtres.',
20
- exporter: 'Exporter (CSV)', resultat: '{n} véhicule(s)',
20
+ exporter: 'Exporter (CSV)', resultat: '{n} véhicule(s)', tous: 'Tous', puces: 'Filtrer par statut',
21
21
  },
22
22
  col: {
23
23
  numero: '#', plaque: 'Plaque', type: 'Type', capacite: 'Capacité', region: 'Région', station: 'Station', chauffeur: 'Chauffeur',
@@ -25,27 +25,46 @@ export const TEXTES = Object.freeze({
25
25
  },
26
26
  action: {
27
27
  voir: 'Voir', modifier: 'Modifier', affecter: 'Affecter un chauffeur', planifier: 'Planifier un entretien', documents: 'Voir les documents',
28
- tournee: 'Planifier une tournée', historique: 'Consulter l’historique', menu: 'Actions sur {plaque}',
28
+ tournee: 'Planifier une tournée', historique: 'Consulter l’historique', menu: 'Actions sur {plaque}', ouvrir: 'Ouvrir la fiche',
29
+ changerStatut: 'Changer le statut', intervention: 'Ouvrir une intervention', clore: 'Clore', renouveler: 'Renouveler', liberer: 'Libérer le véhicule',
30
+ plein: 'Saisir un plein', releve: 'Ajouter un relevé', relever: 'Relever le compteur', enregistrer: 'Enregistrer', annuler: 'Annuler',
31
+ valider: 'Valider l’affectation', telecharger: 'Télécharger {nom}',
29
32
  },
30
- depart: { oui: 'Peut partir aujourd’hui', non: 'Ne peut pas partir', ouiCourt: 'peut partir', nonCourt: 'ne peut pas partir' },
33
+ depart: { oui: 'Peut partir aujourd’hui', non: 'Ne peut pas partir', ouiCourt: 'peut partir', nonCourt: 'ne peut pas partir', aucunMotif: 'Aucun motif bloquant' },
31
34
  documentExpire: 'document expiré',
32
35
  detail: { titre: 'Détail du véhicule', aucun: 'Choisissez un véhicule dans la liste.' },
33
- onglet: { informations: 'Informations', documents: 'Documents', entretiens: 'Entretiens', affectations: 'Affectations', historique: 'Historique' },
36
+ onglet: {
37
+ informations: 'Informations', documents: 'Documents', entretiens: 'Entretiens', affectations: 'Affectations', pleins: 'Pleins et relevés',
38
+ couts: 'Coûts', historique: 'Historique',
39
+ },
34
40
  info: {
35
41
  plaque: 'Plaque d’immatriculation', type: 'Type', capacite: 'Capacité', region: 'Région', station: 'Station principale', km: 'Kilométrage',
36
42
  miseEnService: 'Date de mise en service', chauffeur: 'Chauffeur actuel', consommation: 'Consommation moyenne', prochainEntretien: 'Prochain entretien',
37
43
  valideJusquau: 'Valide jusqu’au {date}', expireeLe: 'Expirée le {date}', expireBientot: 'Expire le {date}', absent: 'Aucun document',
38
44
  dansJours: 'dans {n} jours', depuisJours: 'depuis {n} jours', aujourdhui: 'aujourd’hui', estimee: 'estimée', aucunPlan: 'Aucun plan d’entretien',
39
- auKm: 'à {km} km', aucunChauffeur: 'Aucun chauffeur', aucuneDonnee: '—',
45
+ auKm: 'à {km} km', aucunChauffeur: 'Aucun chauffeur', aucuneDonnee: '—', chassis: 'Numéro de châssis', mode: 'Mode de détention',
46
+ },
47
+ documents: {
48
+ titre: 'Documents et échéances', type: 'Document', du: 'Du', au: 'Au', cout: 'Coût', piece: 'Pièce', etat: 'État', courante: 'en vigueur',
49
+ ancienne: 'ancienne', vide: 'Aucun document enregistré.', reference: 'Référence', organisme: 'Organisme', empreinte: 'sha256 : {court}',
50
+ deposee: 'déposée par {par} le {date}', precedentes: 'Périodes précédentes ({n})', bloque: 'Le véhicule ne peut pas partir',
51
+ etats: { valide: 'Valide', proche: 'Expire bientôt', expiree: 'Expirée', absente: 'Absente' }, obligatoire: 'obligatoire',
40
52
  },
41
- documents: { type: 'Document', du: 'Du', au: 'Au', cout: 'Coût', piece: 'Pièce', etat: 'État', courante: 'en vigueur', ancienne: 'ancienne', vide: 'Aucun document enregistré.' },
42
53
  entretiens: {
43
54
  plans: 'Plans d’entretien', plan: 'Plan', pas: 'Tous les', prochain: 'Prochain', interventions: 'Interventions', numero: 'N°', nature: 'Nature',
44
55
  ouverte: 'Ouverte le', close: 'Close le', atelier: 'Atelier', cout: 'Coût', enCours: 'En cours', vide: 'Aucune intervention.',
45
- ou: 'ou', km: '{n} km', jours: '{n} jours',
56
+ ou: 'ou', km: '{n} km', jours: '{n} jours', tousLes: 'Tous les {pas}', avanceKm: 'Avancement kilométrique', avanceJours: 'Avancement temporel',
57
+ fait: '{fait} / {pas}', prochaine: 'Prochaine : {date}', aKm: 'à {km} km', rythme: 'au rythme de {n} km/j', auPlusTard: 'au plus tard le {date}',
58
+ kmIntervention: 'Kilométrage', statut: 'Statut', fermee: 'Close', etats: { proche: 'Proche', du: 'Dû', depasse: 'Dépassé' },
46
59
  },
47
- affectations: { chauffeur: 'Chauffeur', du: 'Du', au: 'Au', par: 'Affecté par', enCours: 'en cours', vide: 'Aucune affectation.' },
48
- historique: { le: 'Le', de: 'De', vers: 'Vers', motif: 'Motif', par: 'Par', inscrit: 'Inscription au parc', vide: 'Aucun changement de statut.' },
60
+ affectations: {
61
+ chauffeur: 'Chauffeur', du: 'Du', au: 'Au', par: 'Affecté par', enCours: 'en cours', vide: 'Aucune affectation.', actuel: 'Chauffeur actuel',
62
+ depuis: 'Depuis le {date}', parLe: 'Affecté par {par}', frise: 'Historique des affectations', choisir: 'Sélectionner un chauffeur du bureau',
63
+ categorie: 'Catégorie requise pour ce véhicule', nom: 'Nom', telephone: 'Téléphone', permis: 'Permis', permisDe: 'Permis {p}',
64
+ disponibilite: 'Disponibilité', libre: 'Libre', ceVehicule: 'Ce véhicule', conduit: 'Conduit {plaque}', nonHabilite: 'non habilité',
65
+ aucunCandidat: 'Aucun chauffeur à proposer.',
66
+ },
67
+ historique: { le: 'Le', de: 'De', vers: 'Vers', motif: 'Motif', par: 'Par', inscrit: 'Inscription au parc', modification: 'Modification : {champs}', vide: 'Aucun changement de statut.' },
49
68
  alertes: { titre: 'Alertes et échéances', voirTout: 'Voir tout', vide: 'Aucune échéance dans les {n} jours.', entretien: 'Entretien périodique' },
50
69
  graphe: {
51
70
  types: 'Répartition des véhicules par type', vehicules: 'véhicules', statuts: 'Statut des véhicules', km: 'Évolution du kilométrage',
@@ -53,6 +72,46 @@ export const TEXTES = Object.freeze({
53
72
  top: 'Top 5 des véhicules par activité', kmMois: 'Km (30 j)', tournees: 'Tournées', colis: 'Colis', total: 'Total',
54
73
  },
55
74
  rapides: { titre: 'Actions rapides' },
75
+ pleins: {
76
+ courbe: 'Évolution du kilométrage', kmReleve: 'Kilométrage relevé', au: 'au {date}', rythme: 'Rythme : {n} km/j sur 90 jours',
77
+ releves: 'Relevés du compteur', date: 'Date', km: 'Kilométrage', source: 'Source', saisiPar: 'Saisi par', commentaire: 'Commentaire',
78
+ correction: 'Correction : {motif}', pleins: 'Pleins de carburant', litres: 'Litres', montant: 'Montant', complet: 'Plein complet',
79
+ conso: 'Conso. mesurée', consoMoyenne: 'Consommation moyenne', oui: 'Oui', non: 'Non', aucunReleve: 'Aucun relevé.', aucunPlein: 'Aucun plein.',
80
+ },
81
+ couts: {
82
+ periode: 'Période d’analyse', mois: 'Ce mois-ci', trimestre: '3 derniers mois', annee: 'Cette année', perso: 'Personnalisée', du: 'Du', au: 'Au',
83
+ appliquer: 'Appliquer', carburant: 'Carburant', entretien: 'Entretien', echeances: 'Assurance et documents (prorata)', total: 'Total',
84
+ nonComptes: 'Non comptés : {postes}', km: 'Kilomètres parcourus', parKm: 'Coût au kilomètre', parMois: 'Coûts par mois et par poste',
85
+ parc: 'Coût au km par véhicule', parcSous: '(période sélectionnée)', vehicule: 'Véhicule',
86
+ },
87
+ formulaire: {
88
+ requis: 'Ce champ est requis', nombre: 'Nombre attendu', date: 'Date attendue (JJ/MM/AAAA)', choix: 'Choix invalide',
89
+ identification: 'Identification', rattachement: 'Rattachement', compteur: 'Compteur', documents: 'Documents',
90
+ nouveau: 'Nouveau véhicule', nouveauSous: 'Enregistrer un nouveau véhicule dans la flotte.', modifier: 'Modifier le véhicule {plaque}',
91
+ modifierSous: 'Le code, le type et le statut ne se modifient pas ici.', inscrire: 'Inscrire le véhicule',
92
+ immatriculation: 'Plaque d’immatriculation', type: 'Type de véhicule', choisir: '— Choisir —', categorie: 'Catégorie de permis exigée',
93
+ categorieAide: 'Tirée du type de véhicule : {liste}', region: 'Région', station: 'Station principale', mode: 'Mode de détention',
94
+ kmDepart: 'Kilométrage de départ', kmDepartAide: 'Relevé initial lors de la mise en service du véhicule.', document: 'Type de document',
95
+ attribut: { chargeUtileKg: 'Capacité de charge (kg)', volumeM3: 'Volume utile (m³)' }, dernierReleve: 'Dernier relevé : {km} km',
96
+ statutTitre: '{transition} : {plaque}', motif: 'Motif', motifAide: 'Le motif est écrit tel quel dans l’historique du véhicule.',
97
+ definitif: 'Cette action est définitive', definitifAide: 'Le véhicule sort du parc ; son carnet reste consultable.',
98
+ statutActuel: 'Statut actuel', reformerQuestion: 'Réformer ce véhicule ?',
99
+ plan: 'Planifier un entretien', intervention: 'Ouvrir une intervention', cloture: 'Clore l’intervention', plein: 'Saisir un plein',
100
+ releve: 'Relever le compteur', echeance: 'Renouveler un document',
101
+ champ: {
102
+ libelle: 'Libellé', tousLesKm: 'Tous les (km)', tousLesJours: 'Tous les (jours)', toleranceKm: 'Tolérance (km)', toleranceJours: 'Tolérance (jours)',
103
+ nature: 'Nature', plan: 'Plan d’entretien', prestataire: 'Atelier', description: 'Description', km: 'Kilométrage', cout: 'Coût', le: 'Date',
104
+ litres: 'Litres', montant: 'Montant', complet: 'Plein complet', piece: 'Pièce jointe', ouverteLe: 'Ouverte le', ticket: 'Photo du ticket', type: 'Document',
105
+ debut: 'Du', fin: 'Au', reference: 'Référence', organisme: 'Organisme', aucunPlan: '— Aucun —',
106
+ },
107
+ },
108
+ monVehicule: { titre: 'Mon véhicule', aucun: 'Aucun véhicule ne vous est affecté.', prochains: 'Prochains entretiens', documents: 'Documents' },
109
+ tournee: {
110
+ refus: 'Véhicule refusé par le serveur', refusSous: 'Ce véhicule ne peut pas partir aujourd’hui, avec ce chauffeur.', carnet: 'Ouvrir le carnet du véhicule',
111
+ motifs: '{n} motifs bloquants', motif: '1 motif bloquant', disponibles: 'Autres véhicules disponibles', disponiblesSous: 'Seuls les véhicules pouvant partir sont proposés.',
112
+ aucunDisponible: 'Aucun autre véhicule ne peut partir.', vehicule: 'Véhicule',
113
+ },
114
+ message: { erreur: 'L’opération est refusée' },
56
115
  });
57
116
 
58
117
  const lire = (o, cle) => cle.split('.').reduce((x, k) => (x && typeof x === 'object' ? x[k] : undefined), o);
package/src/vue.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // Lecture seule. Toute RÈGLE (expiré, prochain entretien, peut partir) vient du moteur ; ici, seulement des agrégats
5
5
  // d'affichage : comptes, parts, variations sur la période, km par mois, top 5. Les vue-modèles portent des CODES :
6
6
  // la langue n'intervient qu'au rendu.
7
- import { regles as R } from '@mostajs/flotte';
7
+ import { regles as R, transitionsDepuis } from '@mostajs/flotte';
8
8
 
9
9
  const STATUTS_PARC = ['en_service', 'a_l_atelier', 'immobilise'];
10
10
  const jourDe = (d) => (typeof d === 'string' ? d : d.toISOString().slice(0, 10));
@@ -19,7 +19,8 @@ const nomDe = (conducteurs, id) => {
19
19
  function statutAu(v, jour) {
20
20
  if (v.inscritLe && v.inscritLe > jour) return null;
21
21
  let s = 'en_service';
22
- for (const pas of v.historique || []) if (pas.le <= jour) s = pas.vers;
22
+ // Une entrée sans `vers` (modification descriptive, flotte ≥ 0.4.0) ne change pas le statut.
23
+ for (const pas of v.historique || []) if (pas.vers && pas.le <= jour) s = pas.vers;
23
24
  return s;
24
25
  }
25
26
  /** Une échéance déclarée a-t-elle expiré à `jour` ? On lit celle qui faisait foi à cette date. */
@@ -83,6 +84,7 @@ export async function construireTableau(flotte, opts = {}) {
83
84
  chauffeurs: { n: chauffeurs, variation: variation(chauffeurs, chauffeurs0, true) },
84
85
  enService: { n: parStatut('en_service'), part: pct(parStatut('en_service'), n), variation: variation(parStatut('en_service'), parStatut0('en_service'), true) },
85
86
  enMaintenance: { n: parStatut('a_l_atelier'), part: pct(parStatut('a_l_atelier'), n), variation: variation(parStatut('a_l_atelier'), parStatut0('a_l_atelier'), false) },
87
+ horsService: { n: parStatut('immobilise'), part: pct(parStatut('immobilise'), n), variation: variation(parStatut('immobilise'), parStatut0('immobilise'), false) },
86
88
  documentsExpires: { n: expires, part: pct(expires, n), variation: variation(expires, compte(passe, ({ x }) => expireeAu(x.echeances, avant)), false) },
87
89
  };
88
90
 
@@ -140,48 +142,161 @@ export async function construireTableau(flotte, opts = {}) {
140
142
  const choisi = filtres.v && lus.some((x) => x.v.id === filtres.v) ? filtres.v : liste.lignes[0]?.id;
141
143
  const fiche = choisi ? await construireFiche(flotte, choisi, { ...opts, date: jour, onglet: filtres.onglet }) : null;
142
144
 
145
+ // Les puces de statut (16.29) : l'effectif de chaque statut dans le périmètre, réformés compris.
146
+ const puces = { tous: n, ...Object.fromEntries(STATUTS_PARC.map((st) => [st, parStatut(st)])), reforme: compte(lus, (x) => x.v.statut === 'reforme') };
147
+
143
148
  return {
144
- date: jour, periodeJours, filtres, indicateurs, liste,
149
+ date: jour, periodeJours, filtres, indicateurs, puces, liste,
145
150
  options: { types: Object.keys(declarations.types || {}), statuts: [...STATUTS_PARC, 'reforme'], regions: [...new Set(Object.values(sites).map((s) => s.region).filter(Boolean))].sort() },
146
151
  alertes: { lignes: alertes.slice(0, alertesMax), total: alertes.length, jours: alerteJours },
147
152
  repartition, statuts, km, parSite, top: top.slice(0, 5), avecActivite: typeof activite === 'function', fiche,
148
153
  };
149
154
  }
150
155
 
151
- export const ONGLETS = Object.freeze(['informations', 'documents', 'entretiens', 'affectations', 'historique']);
156
+ export const ONGLETS = Object.freeze(['informations', 'documents', 'entretiens', 'affectations', 'pleins', 'couts', 'historique']);
157
+
158
+ const premierDuMois = (jour, k = 0) => { const d = new Date(`${jour.slice(0, 7)}-01T00:00:00Z`); d.setUTCMonth(d.getUTCMonth() - k); return d.toISOString().slice(0, 10); };
159
+ const finDuMois = (m, jour) => { const d = new Date(`${m}-01T00:00:00Z`); d.setUTCMonth(d.getUTCMonth() + 1); d.setUTCDate(0); const f = d.toISOString().slice(0, 10); return f < jour ? f : jour; };
160
+ /** Les `n` derniers mois (AAAA-MM), le mois de `jour` en dernier. */
161
+ const derniersMois = (jour, n) => Array.from({ length: n }, (_, i) => premierDuMois(jour, n - 1 - i).slice(0, 7));
162
+ const parDateDesc = (cle) => (a, b) => (a[cle] < b[cle] ? 1 : a[cle] > b[cle] ? -1 : 0);
163
+
164
+ /**
165
+ * La période d'analyse des coûts (16.38) : 'mois' (par défaut), 'trimestre' (les 3 derniers mois), 'annee', ou un
166
+ * objet { du, au } (la requête GET elle-même convient : { periode, du, au }). Une période à l'envers vaut le mois.
167
+ */
168
+ export function periodeDe(p, jour) {
169
+ if (p && typeof p === 'object') {
170
+ if (R.estJour(p.du) && R.estJour(p.au) && p.du <= p.au) return { periode: 'perso', du: p.du, au: p.au };
171
+ p = p.periode;
172
+ }
173
+ if (p === 'trimestre') return { periode: 'trimestre', du: premierDuMois(jour, 2), au: jour };
174
+ if (p === 'annee') return { periode: 'annee', du: `${jour.slice(0, 4)}-01-01`, au: jour };
175
+ return { periode: 'mois', du: premierDuMois(jour), au: jour };
176
+ }
152
177
 
153
178
  /**
154
179
  * La fiche d'un véhicule : l'en-tête et l'onglet Informations toujours ; le contenu de l'onglet demandé.
155
- * opts : { declarations, date?, sites?, conducteurs?, onglet? }
180
+ * opts : { declarations, date?, sites?, conducteurs?, agents? (id → { nom }), onglet?, chauffeurs? ([{ id, nom,
181
+ * telephone, permis, habilite? }] du bureau, pour « Affecter »), periode? (onglet Coûts), perimetre? }
156
182
  */
157
183
  export async function construireFiche(flotte, id, opts = {}) {
158
- const { declarations = {}, sites = {}, conducteurs = {} } = opts;
184
+ const { declarations = {}, sites = {}, conducteurs = {}, agents = {}, perimetre = null } = opts;
159
185
  const jour = jourDe(opts.date ?? new Date());
160
186
  const onglet = ONGLETS.includes(opts.onglet) ? opts.onglet : 'informations';
161
187
  const v = await flotte.lire(id);
162
188
  const x = await lireVehicule(flotte, v, jour);
189
+ const agent = (a) => (a ? nomDe(agents, a).nom : null);
163
190
  const alerteJours = declarations.alerteJours ?? 30;
164
191
  const echeances = Object.entries(declarations.echeances || {}).map(([type, d]) => {
165
192
  const courante = R.echeanceCourante(x.echeances.filter((e) => e.debut <= jour), type) || R.echeanceCourante(x.echeances, type);
166
193
  const n = courante ? R.jours(jour, courante.fin) : null;
167
- return { type, obligatoire: Boolean(d.obligatoire), courante, n, etat: !courante ? 'absente' : n < 0 ? 'expiree' : n < alerteJours ? 'proche' : 'valide' };
194
+ return { type, obligatoire: Boolean(d.obligatoire), bloquante: Boolean(d.bloquante), courante, n, etat: !courante ? 'absente' : n < 0 ? 'expiree' : n < alerteJours ? 'proche' : 'valide' };
168
195
  });
169
196
  const fiche = {
170
197
  date: jour, onglet, vehicule: v, plaque: v.immatriculation, site: v.site, siteLibelle: sites[v.site]?.libelle ?? v.site ?? null, region: sites[v.site]?.region ?? null,
171
198
  capacite: { kg: v.attributs?.chargeUtileKg ?? null, m3: v.attributs?.volumeM3 ?? null },
172
199
  conducteur: nomDe(conducteurs, conducteurAu(x.affectations, jour)), km: x.km, depart: x.depart,
173
200
  consommation: (await flotte.consommation(id)).lPour100Km, prochain: await flotte.prochainEntretien(id, { date: jour }), echeances,
174
- miseEnService: v.acquisition?.le ?? v.inscritLe ?? null,
201
+ miseEnService: v.miseEnService ?? v.acquisition?.le ?? v.inscritLe ?? null,
202
+ // 0.2.0 : les changements de statut permis (lus dans le moteur), la catégorie de permis du type, les plans.
203
+ transitions: transitionsDepuis(v.statut), categoriePermis: declarations.types?.[v.type]?.categoriePermis ?? null,
204
+ plans: (await flotte.plans(id)).map((p) => ({ id: p.id, libelle: p.libelle })),
175
205
  };
176
206
  if (onglet === 'documents') {
177
- fiche.documents = [...x.echeances].sort((a, b) => (a.type === b.type ? (a.debut < b.debut ? 1 : -1) : a.type < b.type ? -1 : 1))
178
- .map((e) => ({ ...e, courante: echeances.find((c) => c.type === e.type)?.courante?.id === e.id }));
207
+ // Une carte par échéance déclarée (16.33) : la période qui fait foi, et les autres, de la plus récente à la plus ancienne.
208
+ const avecPar = (e) => (e ? { ...e, par: agent(e.piece?.deposePar ?? e.poseePar), deposeLe: e.piece?.deposeLe ?? e.poseeLe ?? null } : null);
209
+ fiche.documents = echeances.map((e) => ({
210
+ ...e, courante: avecPar(e.courante),
211
+ anciennes: x.echeances.filter((y) => y.type === e.type && y.id !== e.courante?.id).sort(parDateDesc('debut')).map(avecPar),
212
+ }));
179
213
  }
180
214
  if (onglet === 'entretiens') {
181
215
  const [prochains, plans, interventions] = await Promise.all([flotte.prochainsEntretiens(id, { date: jour }), flotte.plans(id), flotte.interventions(id)]);
182
- fiche.entretiens = { prochains: prochains.map((p) => ({ ...p, pas: plans.find((q) => q.id === p.plan) })), interventions: [...interventions].sort((a, b) => (a.ouverteLe < b.ouverteLe ? 1 : -1)) };
216
+ const jauge = (fait, pas) => ({ fait, pas, pct: Math.max(0, Math.min(100, Math.round((100 * fait) / pas))) });
217
+ fiche.entretiens = {
218
+ rythme: R.kmParJour(x.releves, jour),
219
+ plans: prochains.map((p) => {
220
+ const q = plans.find((y) => y.id === p.plan) || {};
221
+ return {
222
+ plan: p.plan, libelle: p.libelle, etat: p.etat, n: p.n ?? null, unite: p.unite ?? null, tousLesKm: q.tousLesKm ?? null, tousLesJours: q.tousLesJours ?? null,
223
+ km: q.tousLesKm > 0 && x.km !== null ? { ...jauge(x.km - (q.dernierKm || 0), q.tousLesKm), echeance: p.km?.echeance ?? null } : null,
224
+ jours: q.tousLesJours > 0 && q.dernierLe ? { ...jauge(R.jours(q.dernierLe, jour), q.tousLesJours), echeance: p.jours?.echeance ?? null } : null,
225
+ le: p.le, dans: p.dans, estime: p.estime, auPlusTard: p.estime ? (p.jours?.echeance ?? null) : null,
226
+ };
227
+ }),
228
+ interventions: [...interventions].sort(parDateDesc('ouverteLe')).map((i) => ({ ...i, close: Boolean(i.closeLe) })),
229
+ };
230
+ }
231
+ if (onglet === 'affectations') {
232
+ const permisDe = (c) => (Array.isArray(opts.chauffeurs) ? opts.chauffeurs.find((y) => y.id === c)?.permis ?? null : null);
233
+ const frise = [...x.affectations].reverse().map((a) => ({ ...a, conducteur: nomDe(conducteurs, a.conducteur), permis: permisDe(a.conducteur), par: agent(a.parAgent) }));
234
+ // La disponibilité de chaque chauffeur proposé est LUE dans le moteur (vehiculeDe) ; le permis et
235
+ // l'habilitation viennent de l'application, qui seule les connaît.
236
+ const candidats = await Promise.all((Array.isArray(opts.chauffeurs) ? opts.chauffeurs : []).map(async (c) => {
237
+ const a = await flotte.vehiculeDe(c.id);
238
+ if (!a) return { ...c, disponibilite: 'libre' };
239
+ if (a.vehicule === id) return { ...c, disponibilite: 'ce_vehicule' };
240
+ return { ...c, disponibilite: 'occupe', plaque: (await flotte.lire(a.vehicule)).immatriculation };
241
+ }));
242
+ fiche.affectations = { courante: frise.find((a) => !a.au) ?? null, frise, candidats, categorie: fiche.categoriePermis };
243
+ }
244
+ if (onglet === 'pleins') {
245
+ const pleins = await flotte.pleins(id);
246
+ const conso = R.consommation(pleins);
247
+ const mois = derniersMois(jour, 7);
248
+ fiche.pleins = {
249
+ km: x.km, rythme: R.kmParJour(x.releves, jour), consommation: conso.lPour100Km,
250
+ courbe: { mois, valeurs: mois.map((m) => R.dernierReleve(x.releves, finDuMois(m, jour))?.km ?? null) },
251
+ releves: [...x.releves].reverse().map((r) => ({ ...r, par: agent(r.saisiPar) })),
252
+ pleins: [...pleins].reverse().map((p) => ({ ...p, par: agent(p.saisiPar), conso: (p.complet && conso.mesures.find((m) => m.au === p.km)?.lPour100Km) || null })),
253
+ };
254
+ }
255
+ if (onglet === 'couts') {
256
+ const P = periodeDe(opts.periode, jour);
257
+ const parMois = await Promise.all(derniersMois(jour, 6).map(async (m) => {
258
+ const c = await flotte.cout(id, { du: `${m}-01`, au: finDuMois(m, jour) });
259
+ return { mois: m, carburant: c.carburant, entretien: c.entretien, echeances: c.echeances, total: c.total };
260
+ }));
261
+ const vs = (await flotte.vehicules()).filter((w) => !perimetre || perimetre.includes(w.site));
262
+ const parc = await Promise.all(vs.map(async (w) => {
263
+ const c = await flotte.cout(w.id, { du: P.du, au: P.au });
264
+ return { vehicule: w.id, code: w.code, plaque: w.immatriculation, type: w.type, km: c.km, carburant: c.carburant, entretien: c.entretien, echeances: c.echeances, total: c.total, parKm: c.parKm, courant: w.id === id };
265
+ }));
266
+ parc.sort((a, b) => (a.parKm === b.parKm ? (a.code < b.code ? -1 : 1) : a.parKm === null ? 1 : b.parKm === null ? -1 : b.parKm - a.parKm));
267
+ const cout = await flotte.cout(id, { du: P.du, au: P.au });
268
+ fiche.couts = { ...P, cout, mois: parMois, parc, nonComptes: cout.nonComptes };
183
269
  }
184
- if (onglet === 'affectations') fiche.affectations = [...x.affectations].reverse().map((a) => ({ ...a, conducteur: nomDe(conducteurs, a.conducteur) }));
185
270
  if (onglet === 'historique') fiche.historique = [...(v.historique || [])].reverse();
186
271
  return fiche;
187
272
  }
273
+
274
+ /**
275
+ * « Mon véhicule » du livreur (16.40) : le véhicule que le conducteur tient (vehiculeDe), sa fiche, ses prochains
276
+ * entretiens et le rythme. { vehicule: null } s'il n'en tient aucun.
277
+ */
278
+ export async function construireMonVehicule(flotte, conducteur, opts = {}) {
279
+ const a = await flotte.vehiculeDe(conducteur);
280
+ if (!a) return { conducteur, vehicule: null, prochains: [], rythme: null };
281
+ const fiche = await construireFiche(flotte, a.vehicule, { ...opts, onglet: 'informations' });
282
+ const [prochains, releves] = await Promise.all([flotte.prochainsEntretiens(a.vehicule, { date: fiche.date }), flotte.releves(a.vehicule)]);
283
+ return { conducteur, vehicule: fiche, prochains, rythme: R.kmParJour(releves, fiche.date) };
284
+ }
285
+
286
+ /**
287
+ * L'aptitude de chaque véhicule du périmètre pour une tournée (16.41) : peutPartir du moteur, avec le conducteur
288
+ * s'il est choisi. Les aptes d'abord, puis par code. Les réformés n'y sont pas.
289
+ * opts : { date?, conducteur?, perimetre?, sites? }
290
+ */
291
+ export async function construireAptitudes(flotte, opts = {}) {
292
+ const { sites = {}, perimetre = null, conducteur } = opts;
293
+ const jour = jourDe(opts.date ?? new Date());
294
+ const vs = (await flotte.vehicules()).filter((w) => !perimetre || perimetre.includes(w.site));
295
+ const lignes = await Promise.all(vs.map(async (w) => ({
296
+ id: w.id, code: w.code, plaque: w.immatriculation, type: w.type, site: w.site, region: sites[w.site]?.region ?? null,
297
+ capacite: { kg: w.attributs?.chargeUtileKg ?? null, m3: w.attributs?.volumeM3 ?? null },
298
+ km: R.dernierReleve(await flotte.releves(w.id), jour)?.km ?? null,
299
+ depart: await flotte.peutPartir(w.id, { date: jour, ...(conducteur ? { conducteur } : {}) }),
300
+ })));
301
+ return lignes.sort((a, b) => (a.depart.ok === b.depart.ok ? (a.code < b.code ? -1 : 1) : a.depart.ok ? -1 : 1));
302
+ }