@mostajs/flotte-ui 0.1.1 → 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/rendu.js CHANGED
@@ -1,59 +1,24 @@
1
1
  // @mostajs/flotte-ui — le rendu : vue-modèle → HTML, sans script (DEC-UI-13 de Guerway). Fonctions PURES.
2
2
  // Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
3
3
  //
4
- // Chaque filtre est un formulaire GET, chaque onglet un lien GET, le menu « ⋮ » un <details>. Toute valeur passe par
5
- // esc(). Les composants viennent du kit (celui de l'application, sinon KIT_DEFAUT) ; les textes, de texte() ; les
6
- // codes de la flotte, de libelle() de @mostajs/flotte.
4
+ // Chaque filtre est un formulaire GET, chaque onglet un lien GET, chaque menu un <details>, chaque écriture un
5
+ // formulaire POST vers un lien que l'application fournit. Toute valeur passe par esc(). Les composants viennent du
6
+ // kit (celui de l'application, sinon KIT_DEFAUT) ; les textes, de texte() ; les codes de la flotte, de libelle().
7
7
  import { esc } from '@mostajs/app-shell-ui';
8
- import { libelle } from '@mostajs/flotte';
9
- import { texte } from './textes.js';
10
- import { KIT_DEFAUT, COULEURS } from './kit.js';
8
+ import { COULEURS } from './kit.js';
11
9
  import { ONGLETS } from './vue.js';
10
+ import {
11
+ contexte, url, caches, garder, lienFiche, delai, badgeStatut, capacite, tableau, bandeauOk, bandeauKo, texteRefus, bouton,
12
+ } from './commun.js';
12
13
 
13
- const BADGE = { en_service: ['vert', 'coche'], a_l_atelier: ['orange', 'cle'], immobilise: ['rouge', 'croix'], reforme: ['gris', 'document'] };
14
-
15
- /** Le contexte de rendu : kit complété, traduction, formats. */
16
- function contexte({ kit = {}, t, locale = 'fr-FR', liens = {}, droits = {}, declarations = {} } = {}) {
17
- const k = { ...KIT_DEFAUT, ...kit, classes: { ...KIT_DEFAUT.classes, ...(kit.classes || {}) } };
18
- const nf = (x) => new Intl.NumberFormat(locale).format(x);
19
- const tx = (cle, vars) => texte(cle, t, vars);
20
- // Un code de la flotte ; à défaut de traduction, le libellé déclaré, sinon le code.
21
- const lib = (famille, code, vars) => {
22
- const l = libelle(famille, code, t, vars);
23
- if (l !== code) return l;
24
- const d = famille === 'type' ? declarations.types?.[code] : famille === 'echeance' ? declarations.echeances?.[code] : null;
25
- return d?.libelle ?? code;
26
- };
27
- const jj = (d) => (d ? d.split('-').reverse().join('/') : '—');
28
- /** Une phrase de motif ou d'avertissement : les valeurs qui sont des codes sont traduites, les dates mises en forme. */
29
- const phrase = (famille, m) => {
30
- const v = { ...m };
31
- if (v.statut) v.statut = lib('statut', v.statut);
32
- if (v.type) v.type = lib('echeance', v.type);
33
- if (v.unite) v.unite = lib('unite', v.unite);
34
- if (v.date) v.date = jj(v.date);
35
- if (Number.isFinite(v.n)) v.n = nf(v.n);
36
- return lib(famille, m.code, v);
37
- };
38
- return { k, c: k.classes, nf, tx, lib, jj, phrase, liens, droits, peutGerer: Boolean(droits.gerer) };
39
- }
40
-
41
- /** Une URL GET : la base, puis les paramètres non vides, dans l'ordre donné. */
42
- function url(base, params) {
43
- const q = Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== '')
44
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
45
- return q ? `${base}?${q}` : base;
46
- }
47
- const garder = (f = {}) => ({ q: f.q, type: f.type, statut: f.statut, region: f.region, ...(f.page > 1 ? { page: f.page } : {}) });
48
-
49
- function delai(x, n) {
50
- if (n === null || n === undefined) return '';
51
- if (n < 0) return `<span class="fl-retard">(${esc(x.tx('info.depuisJours', { n: x.nf(-n) }))})</span>`;
52
- if (n === 0) return `<span class="fl-delai">(${esc(x.tx('info.aujourdhui'))})</span>`;
53
- return `<span class="fl-delai">(${esc(x.tx('info.dansJours', { n: x.nf(n) }))})</span>`;
54
- }
55
- const badgeStatut = (x, s) => x.k.badge(x.lib('statut', s), ...(BADGE[s] || ['gris']));
56
- const capacite = (x, c) => [c.kg !== null ? `${x.nf(c.kg)} kg` : null, c.m3 !== null ? `${x.nf(c.m3)} m³` : null].filter(Boolean).join(' / ') || '—';
14
+ const STATUTS_PARC = ['en_service', 'a_l_atelier', 'immobilise'];
15
+ const TEINTE_DOC = { valide: 'vert', proche: 'orange', expiree: 'rouge', absente: 'gris' };
16
+ const TEINTE_ENT = { proche: 'orange', du: 'orange', depasse: 'rouge' };
17
+ const COULEURS_POSTES = { carburant: '#2878E0', entretien: '#FD8A2B', echeances: '#8B5CF6' };
18
+ const nomMois = (x, m, annee = false) => {
19
+ const s = new Intl.DateTimeFormat(x.locale, { month: 'short', ...(annee ? { year: 'numeric' } : {}), timeZone: 'UTC' }).format(new Date(`${m}-15T00:00:00Z`));
20
+ return s.charAt(0).toUpperCase() + s.slice(1);
21
+ };
57
22
 
58
23
  /** La variation d'un indicateur : flèche selon le sens, couleur selon le sens favorable. */
59
24
  function variationHtml(x, v) {
@@ -66,46 +31,59 @@ function variationHtml(x, v) {
66
31
 
67
32
  function indicateurs(x, I) {
68
33
  const part = (o) => (o.part !== undefined ? `<span class="fl-doux">(${x.nf(o.part)} %)</span> ` : '');
69
- const un = (o, cle, teinte, icone) => x.k.kpi({ teinte, icone, valeur: x.nf(o.n), libelle: x.tx(`kpi.${cle}`), sous: `${part(o)}${variationHtml(x, o.variation)}` });
70
- return `<div class="fl-k5">${un(I.vehicules, 'vehicules', 'bleu', 'camion')}${un(I.chauffeurs, 'chauffeurs', 'vert', 'utilisateur')}${un(I.enService, 'enService', 'orange', 'coche')}`
71
- + `${un(I.enMaintenance, 'enMaintenance', 'rouge', 'cle')}${un(I.documentsExpires, 'documentsExpires', 'rouge', 'document')}</div>`;
34
+ const un = (o, cle, teinte, icone) => (o ? x.k.kpi({ teinte, icone, valeur: x.nf(o.n), libelle: x.tx(`kpi.${cle}`), sous: `${part(o)}${variationHtml(x, o.variation)}` }) : '');
35
+ return `<div class="fl-k6">${un(I.vehicules, 'vehicules', 'bleu', 'camion')}${un(I.chauffeurs, 'chauffeurs', 'vert', 'utilisateur')}${un(I.enService, 'enService', 'vert', 'coche')}`
36
+ + `${un(I.enMaintenance, 'enMaintenance', 'orange', 'cle')}${un(I.horsService, 'horsService', 'rouge', 'croix')}${un(I.documentsExpires, 'documentsExpires', 'rouge', 'document')}</div>`;
72
37
  }
73
38
 
74
39
  /** Les actions d'une ligne : « Voir » toujours ; les autres si l'application en donne le lien (et le droit, pour écrire). */
75
40
  function actionsLigne(x, l, filtres) {
76
41
  const L = x.liens, base = L.base || '';
77
42
  const a = [[url(base, { ...garder(filtres), v: l.id }), 'voir']];
43
+ if (L.fiche) a.push([L.fiche(l.id, ''), 'ouvrir']);
78
44
  if (x.peutGerer && L.modifier) a.push([L.modifier(l.id), 'modifier']);
79
45
  if (x.peutGerer && L.affecter) a.push([L.affecter(l.id), 'affecter']);
80
46
  if (x.peutGerer && L.planifier) a.push([L.planifier(l.id), 'planifier']);
81
47
  return a;
82
48
  }
83
49
 
50
+ /** Les puces de statut (16.29) : un lien GET par statut, avec l'effectif ; la recherche est gardée. */
51
+ function puces(x, vm) {
52
+ const P = vm.puces, f = vm.filtres, base = x.liens.base || '';
53
+ const items = [['', x.tx('liste.tous'), P.tous], ...[...STATUTS_PARC, 'reforme'].map((s) => [s, x.lib('statut', s), P[s]])];
54
+ return `<nav class="fl-puces" aria-label="${esc(x.tx('liste.puces'))}">${items.map(([s, l, n]) => {
55
+ const actif = (f.statut || '') === s;
56
+ return `<a class="fl-puce${actif ? ' actif' : ''}" href="${esc(url(base, garder({ ...f, statut: s, page: 1 })))}"${actif ? ' aria-current="page"' : ''}>${esc(l)} <span class="fl-n">(${x.nf(n)})</span></a>`;
57
+ }).join('')}</nav>`;
58
+ }
59
+
84
60
  function liste(x, vm) {
85
61
  const { filtres, liste: L, options } = vm;
86
62
  const opt = (v, l, cour) => `<option value="${esc(v)}"${v === cour ? ' selected' : ''}>${esc(l)}</option>`;
87
63
  const form = `<form class="fl-filtres" method="get" action="${esc(x.liens.base || '')}" role="search">`
88
64
  + `<input type="search" name="q" value="${esc(filtres.q)}" placeholder="${esc(x.tx('liste.recherche'))}" aria-label="${esc(x.tx('liste.recherche'))}">`
89
65
  + `<select name="type" aria-label="${esc(x.tx('col.type'))}">${opt('', x.tx('liste.tousTypes'), filtres.type)}${options.types.map((c) => opt(c, x.lib('type', c), filtres.type)).join('')}</select>`
90
- + `<select name="statut" aria-label="${esc(x.tx('col.statut'))}">${opt('', x.tx('liste.tousStatuts'), filtres.statut)}${options.statuts.map((c) => opt(c, x.lib('statut', c), filtres.statut)).join('')}</select>`
91
66
  + (options.regions.length ? `<select name="region" aria-label="${esc(x.tx('col.region'))}">${opt('', x.tx('liste.toutesRegions'), filtres.region)}${options.regions.map((c) => opt(c, c, filtres.region)).join('')}</select>` : '')
92
- + `<button type="submit">${esc(x.tx('liste.filtrer'))}</button></form>`;
67
+ + (filtres.statut ? `<input type="hidden" name="statut" value="${esc(filtres.statut)}">` : '')
68
+ + `<button type="submit">${esc(x.tx('liste.filtrer'))}</button></form>${vm.puces ? puces(x, vm) : ''}`;
93
69
  if (!L.lignes.length) return `${form}<p class="fl-vide">${esc(x.tx('liste.vide'))}</p>`;
94
70
  const dep = (d) => (d.ok
95
71
  ? `<span class="fl-depart ok" title="${esc(x.tx('depart.ouiCourt'))}">${x.k.icone('coche', 18)}<span class="fl-sr">${esc(x.tx('depart.ouiCourt'))}</span></span>`
96
72
  : `<span class="fl-depart ko" title="${esc(d.motifs.map((m) => x.phrase('motif', m)).join(' ; '))}">${x.k.icone('croix', 18)}<span class="fl-sr">${esc(x.tx('depart.nonCourt'))}</span></span>`);
97
73
  const choisi = vm.fiche?.vehicule.id;
98
- const debut = (L.page - 1) * (Number.isFinite(L.parPage) ? L.parPage : 0);
99
- const lignes = L.lignes.map((l, i) => {
74
+ const lignes = L.lignes.map((l) => {
100
75
  const acts = actionsLigne(x, l, filtres);
101
76
  const menu = `<details class="fl-menu"><summary aria-label="${esc(x.tx('action.menu', { plaque: l.plaque }))}"><span aria-hidden="true">⋮</span></summary><div>${acts.map(([h, cle]) => `<a href="${esc(h)}">${esc(x.tx(`action.${cle}`))}</a>`).join('')}</div></details>`;
102
- return `<tr${l.id === choisi ? ' class="choisi"' : ''}><td class="num">${x.nf(debut + i + 1)}</td><td class="fl-plaque"><a href="${esc(acts[0][0])}">${esc(l.plaque)}</a></td><td class="fl-nowrap">${esc(x.lib('type', l.type))}</td>`
103
- + `<td class="fl-nowrap">${l.capacite.kg !== null ? `${x.nf(l.capacite.kg)} kg` : '—'}</td><td>${esc(l.region ?? '—')}</td><td>${esc(l.conducteur?.nom ?? '—')}</td>`
104
- + `<td class="fl-nowrap">${l.km !== null ? `${x.nf(l.km)} km` : '—'}</td><td>${badgeStatut(x, l.statut)}${l.documentExpire ? `<span class="fl-marque">${esc(x.tx('documentExpire'))}</span>` : ''}</td>`
105
- + `<td>${dep(l.depart)}</td><td>${menu}</td></tr>`;
77
+ const cap = l.capacite.kg !== null ? capacite(x, l.capacite) : '';
78
+ return `<tr${l.id === choisi ? ' class="choisi"' : ''}><td class="fl-plaque"><span class="fl-mini">${x.k.icone('camion', 18)}</span><a href="${esc(acts[0][0])}" dir="ltr">${esc(l.plaque)}</a></td>`
79
+ + `<td class="fl-nowrap">${esc(x.lib('type', l.type))}${cap ? `<br><small class="fl-doux">${esc(cap)}</small>` : ''}</td>`
80
+ + `<td class="fl-nowrap">${esc(l.site ?? '—')}${l.region ? `<br><small class="fl-doux">(${esc(l.region)})</small>` : ''}</td>`
81
+ + `<td>${l.conducteur ? esc(l.conducteur.nom) : `<span class="fl-doux">${esc(x.tx('info.aucunChauffeur'))}</span>`}</td>`
82
+ + `<td>${badgeStatut(x, l.statut)}${l.documentExpire ? `<br><span class="fl-marque">${esc(x.tx('documentExpire'))}</span>` : ''}</td>`
83
+ + `<td>${dep(l.depart)}</td><td class="fl-nowrap">${l.km !== null ? `${x.nf(l.km)} km` : '—'}</td><td>${menu}</td></tr>`;
106
84
  }).join('');
107
85
  const C = (cle) => `<th scope="col">${esc(x.tx(`col.${cle}`))}</th>`;
108
- const table = `<div class="fl-defile"><table class="${esc(x.c.tableau)}"><thead><tr>${['numero', 'plaque', 'type', 'capacite', 'region', 'chauffeur', 'km', 'statut', 'depart', 'actions'].map(C).join('')}</tr></thead><tbody>${lignes}</tbody></table></div>`;
86
+ const table = `<div class="fl-defile"><table class="${esc(x.c.tableau)}"><thead><tr>${['plaque', 'type', 'station', 'chauffeur', 'statut', 'depart', 'km', 'actions'].map(C).join('')}</tr></thead><tbody>${lignes}</tbody></table></div>`;
109
87
  const pages = Number.isFinite(L.parPage) ? x.k.pagination({ total: L.total, page: L.page, parPage: L.parPage, lien: (p) => url(x.liens.base || '', { ...garder(filtres), page: p > 1 ? p : '' }) }) : '';
110
88
  const exp = x.liens.export ? ` <a href="${esc(url(x.liens.export, garder(filtres)))}">${esc(x.tx('liste.exporter'))}</a>` : '';
111
89
  return `${form}${table}<div class="fl-pied">${pages}${exp}</div>`;
@@ -119,25 +97,28 @@ function alertesHtml(x, vm) {
119
97
  + `<span>${esc(a.genre === 'entretien' ? `${x.tx('alertes.entretien')} — ${a.type}` : x.lib('echeance', a.type))}<small class="fl-plaque">${esc(a.plaque)}</small></span><span>${delai(x, a.n)}</span></li>`).join('')}</ul>`;
120
98
  }
121
99
 
122
- function statutsHtml(x, S) {
123
- return `<div class="fl-barres">${['en_service', 'a_l_atelier', 'immobilise'].map((s) => {
124
- const p = S.total ? Math.round((100 * S[s]) / S.total) : 0;
125
- return `<div class="fl-barre"><span>${esc(x.lib('statut', s))}</span><span role="img" aria-label="${p} %"><i style="width:${p}%;background:${COULEURS.statut[s]}"></i></span><span><b>${x.nf(S[s])}</b> <span class="fl-doux">(${x.nf(p)} %)</span></span></div>`;
126
- }).join('')}</div>`;
127
- }
100
+ const statutsHtml = (x, S) => x.k.anneau(STATUTS_PARC.map((s) => ({ libelle: x.lib('statut', s), valeur: S[s], couleur: COULEURS.statut[s] })), { unite: x.tx('graphe.vehicules') });
128
101
 
129
- function kmHtml(x, km, locale) {
130
- const mois = km.mois.map((m) => { const s = new Intl.DateTimeFormat(locale, { month: 'short', timeZone: 'UTC' }).format(new Date(`${m}-15T00:00:00Z`)); return s.charAt(0).toUpperCase() + s.slice(1); });
102
+ function kmHtml(x, km) {
103
+ const mois = km.mois.map((m) => nomMois(x, m));
131
104
  return x.k.courbes(mois, [{ libelle: x.tx('graphe.kmSerie'), couleur: '#2878E0', valeurs: km.valeurs }])
132
105
  + `<table class="fl-donnees"><caption class="fl-sr">${esc(x.tx('graphe.kmSerie'))}</caption><thead><tr><th scope="row">${esc(x.tx('graphe.mois'))}</th>${mois.map((m) => `<th scope="col">${esc(m)}</th>`).join('')}</tr></thead>`
133
106
  + `<tbody><tr><th scope="row">km</th>${km.valeurs.map((v) => `<td>${x.nf(v)}</td>`).join('')}</tr></tbody></table>`;
134
107
  }
135
108
 
109
+ /** Véhicules par station (16.30) : une barre empilée par statut, à l'échelle de la plus grande station. */
136
110
  function parSiteHtml(x, vm) {
137
- if (!vm.parSite.length) return `<p class="fl-vide">—</p>`;
138
- return `<div class="fl-defile"><table class="${esc(x.c.tableau)}"><thead><tr><th scope="col">${esc(x.tx('col.station'))}</th><th scope="col">${esc(x.tx('col.region'))}</th>${['en_service', 'a_l_atelier', 'immobilise'].map((s) => `<th scope="col"><span class="fl-point" style="background:${COULEURS.statut[s]}"></span> ${esc(x.lib('statut', s))}</th>`).join('')}</tr></thead>`
139
- + `<tbody>${vm.parSite.map((s) => `<tr><td>${esc(s.libelle ?? '—')}</td><td>${esc(s.region ?? '—')}</td><td>${x.nf(s.en_service)}</td><td>${x.nf(s.a_l_atelier)}</td><td>${x.nf(s.immobilise)}</td></tr>`).join('')}</tbody></table></div>`;
111
+ if (!vm.parSite.length) return '<p class="fl-vide">—</p>';
112
+ const max = Math.max(1, ...vm.parSite.map((s) => s.total));
113
+ const lignes = vm.parSite.map((s) => {
114
+ const parts = STATUTS_PARC.filter((k) => s[k]).map((k) => `<i style="width:${((100 * s[k]) / max).toFixed(1)}%;background:${COULEURS.statut[k]}"></i>`).join('');
115
+ const detail = STATUTS_PARC.map((k) => `${x.lib('statut', k)} ${x.nf(s[k])}`).join(', ');
116
+ return `<div class="fl-ligne-empile"><span title="${esc(s.libelle ?? '')}">${esc(s.site ?? '—')}${s.region ? ` <small class="fl-doux">(${esc(s.region)})</small>` : ''}</span><b>${x.nf(s.total)}</b>`
117
+ + `<span class="fl-piste" role="img" aria-label="${esc(detail)}">${parts}</span></div>`;
118
+ }).join('');
119
+ return `<div class="fl-empile">${lignes}</div>${legende(x, STATUTS_PARC.map((k) => [x.lib('statut', k), COULEURS.statut[k]]))}`;
140
120
  }
121
+ const legende = (x, items) => `<p class="fl-legende-ligne">${items.map(([l, c]) => `<span><span class="fl-point" style="background:${c}"></span> ${esc(l)}</span>`).join('')}</p>`;
141
122
 
142
123
  function topHtml(x, vm) {
143
124
  const act = vm.avecActivite;
@@ -148,113 +129,278 @@ function topHtml(x, vm) {
148
129
  }
149
130
 
150
131
  function rapidesHtml(x, vm) {
151
- const L = x.liens, id = vm.fiche?.vehicule.id, base = L.base || '';
132
+ const L = x.liens, id = vm.fiche?.vehicule.id;
152
133
  const items = [
153
134
  x.peutGerer && L.nouveau && [L.nouveau, 'nouveau', 'plein', 'plus'],
154
135
  x.peutGerer && L.affecter && id && [L.affecter(id), 'action.affecter', 'vert', 'utilisateur'],
155
136
  x.peutGerer && L.planifier && id && [L.planifier(id), 'action.planifier', 'orange', 'cle'],
156
- id && [url(base, { ...garder(vm.filtres), v: id, onglet: 'documents' }), 'action.documents', 'violet', 'document'],
137
+ id && [lienFiche(x, id, 'documents', {}, vm.filtres), 'action.documents', 'violet', 'document'],
157
138
  L.tournee && [L.tournee, 'action.tournee', 'bleu', 'calendrier'],
158
- id && [url(base, { ...garder(vm.filtres), v: id, onglet: 'historique' }), 'action.historique', 'rouge', 'horloge'],
139
+ id && [lienFiche(x, id, 'historique', {}, vm.filtres), 'action.historique', 'rouge', 'horloge'],
159
140
  ].filter(Boolean);
160
141
  return `<div class="${esc(x.c.rapides)}">${items.map(([h, cle, teinte, ic]) => `<a class="${esc(x.c.rapide)} ${teinte}" href="${esc(h)}">${x.k.icone(ic, 18)}${esc(x.tx(cle))}</a>`).join('')}</div>`;
161
142
  }
162
143
 
163
- // ── La fiche ────────────────────────────────────────────────────────────────────────────────────────────────
144
+ // ── La fiche : bandeau, informations, onglets ───────────────────────────────────────────────────────────────
164
145
  function bandeau(x, d) {
165
- if (d.ok) return `<div class="fl-bandeau ok" role="status">${x.k.icone('coche', 18)}<div>${esc(x.tx('depart.oui'))}${d.avertissements.length ? `<ul>${d.avertissements.map((a) => `<li>${esc(x.phrase('avertissement', a))}</li>`).join('')}</ul>` : ''}</div></div>`;
166
- return `<div class="fl-bandeau ko" role="alert">${x.k.icone('croix', 18)}<div>${esc(x.tx('depart.non'))}<ul>${d.motifs.map((m) => `<li>${esc(x.phrase('motif', m))}</li>`).join('')}</ul></div></div>`;
146
+ if (d.ok) return bandeauOk(x, `${esc(x.tx('depart.oui'))}${d.avertissements.length ? `<ul>${d.avertissements.map((a) => `<li>${esc(x.phrase('avertissement', a))}</li>`).join('')}</ul>` : `<br><small>${esc(x.tx('depart.aucunMotif'))}</small>`}`);
147
+ return bandeauKo(x, `${esc(x.tx('depart.non'))}<ul>${d.motifs.map((m) => `<li>${esc(x.phrase('motif', m))}</li>`).join('')}</ul>`);
148
+ }
149
+
150
+ /** L'état d'une échéance en une ligne : « Valide jusqu'au », « Expire le … (dans N jours) », « Expirée le ». */
151
+ function etatEcheance(x, e) {
152
+ if (e.etat === 'absente') return `<span class="${e.obligatoire ? 'fl-retard' : 'fl-doux'}">${esc(x.tx('info.absent'))}</span>`;
153
+ if (e.etat === 'expiree') return `<span class="fl-retard">${x.k.icone('croix', 16)} ${esc(x.tx('info.expireeLe', { date: x.jj(e.courante.fin) }))}</span>`;
154
+ if (e.etat === 'proche') return `<span class="fl-delai">${x.k.icone('alerte', 16)} ${esc(x.tx('info.expireBientot', { date: x.jj(e.courante.fin) }))}</span> ${delai(x, e.n)}`;
155
+ return `<span class="fl-ok">${x.k.icone('coche', 16)}</span> ${esc(x.tx('info.valideJusquau', { date: x.jj(e.courante.fin) }))}`;
167
156
  }
168
157
 
169
158
  function informations(x, f) {
170
159
  const p = f.prochain;
171
160
  const prochain = p ? (p.le ? `${esc(x.jj(p.le))} ${delai(x, p.dans)}${p.estime ? ` <span class="fl-doux">${esc(x.tx('info.estimee'))}</span>` : ''}<br><span class="fl-doux">${esc(p.libelle)}</span>`
172
161
  : `${esc(p.libelle)}${p.km ? ` <span class="fl-doux">${esc(x.tx('info.auKm', { km: x.nf(p.km.echeance) }))}</span>` : ''}`) : esc(x.tx('info.aucunPlan'));
173
- const ech = f.echeances.map((e) => {
174
- const v = e.etat === 'absente' ? `<span class="${e.obligatoire ? 'fl-retard' : 'fl-doux'}">${esc(x.tx('info.absent'))}</span>`
175
- : e.etat === 'expiree' ? `<span class="fl-retard">${x.k.icone('croix', 16)} ${esc(x.tx('info.expireeLe', { date: x.jj(e.courante.fin) }))}</span>`
176
- : e.etat === 'proche' ? `<span class="fl-delai">${x.k.icone('alerte', 16)} ${esc(x.tx('info.expireBientot', { date: x.jj(e.courante.fin) }))}</span> ${delai(x, e.n)}`
177
- : `<span class="fl-ok">${x.k.icone('coche', 16)}</span> ${esc(x.tx('info.valideJusquau', { date: x.jj(e.courante.fin) }))}`;
178
- return [x.lib('echeance', e.type), v];
179
- });
180
- const ch = f.conducteur ? `${esc(f.conducteur.nom)}${f.conducteur.telephone ? ` <span class="fl-doux">(${esc(f.conducteur.telephone)})</span>` : ''}` : esc(x.tx('info.aucunChauffeur'));
162
+ const ch = f.conducteur ? `${esc(f.conducteur.nom)}${f.conducteur.telephone ? ` <span class="fl-doux" dir="ltr">(${esc(f.conducteur.telephone)})</span>` : ''}` : esc(x.tx('info.aucunChauffeur'));
163
+ const v = f.vehicule;
181
164
  const lignes = [
182
- [x.tx('info.plaque'), esc(f.plaque)], [x.tx('info.type'), esc(x.lib('type', f.vehicule.type))], [x.tx('info.capacite'), esc(capacite(x, f.capacite))],
165
+ [x.tx('info.plaque'), `<span dir="ltr">${esc(f.plaque)}</span>`], [x.tx('info.type'), esc(x.lib('type', v.type))], [x.tx('info.capacite'), esc(capacite(x, f.capacite))],
183
166
  [x.tx('info.region'), esc(f.region ?? '—')], [x.tx('info.station'), esc(f.siteLibelle ?? '—')], [x.tx('info.km'), f.km !== null ? `${x.nf(f.km)} km` : '—'],
184
167
  [x.tx('info.miseEnService'), esc(x.jj(f.miseEnService))], [x.tx('info.chauffeur'), ch],
185
168
  [x.tx('info.consommation'), f.consommation !== null && f.consommation !== undefined ? `${x.nf(f.consommation)} L / 100 km` : '—'],
186
- [x.tx('info.prochainEntretien'), prochain], ...ech.map(([l, v]) => [l, v]),
169
+ [x.tx('info.prochainEntretien'), prochain], ...f.echeances.map((e) => [x.lib('echeance', e.type), etatEcheance(x, e)]),
170
+ ...(v.chassis ? [[x.tx('info.chassis'), `<span dir="ltr">${esc(v.chassis)}</span>`]] : []),
171
+ ...(v.mode ? [[x.tx('info.mode'), esc(x.lib('mode', v.mode))]] : []),
187
172
  ];
188
- return `<dl class="fl-infos ${esc(x.c.fiche)}">${lignes.map(([l, v]) => `<dt>${esc(l)}</dt><dd>${v}</dd>`).join('')}</dl>`;
173
+ return `<dl class="fl-infos ${esc(x.c.fiche)}">${lignes.map(([l, val]) => `<dt>${esc(l)}</dt><dd>${val}</dd>`).join('')}</dl>`;
189
174
  }
190
175
 
191
- function tableau(x, entetes, lignes, vide) {
192
- if (!lignes.length) return `<p class="fl-vide">${esc(vide)}</p>`;
193
- return `<div class="fl-defile"><table class="${esc(x.c.tableau)}"><thead><tr>${entetes.map((e) => `<th scope="col">${esc(e)}</th>`).join('')}</tr></thead><tbody>${lignes.map((l) => `<tr>${l.map((c) => `<td>${c}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`;
176
+ // ── Documents (16.33) : une carte par échéance déclarée ──
177
+ function documentsHtml(x, f) {
178
+ const id = f.vehicule.id;
179
+ const piece = (e) => {
180
+ if (!e?.piece) return '';
181
+ const nom = typeof e.piece === 'string' ? e.piece : e.piece.nom ?? '';
182
+ const emp = typeof e.piece === 'object' && e.piece.empreinte ? String(e.piece.empreinte) : '';
183
+ const lien = x.liens.piece ? `<a href="${esc(x.liens.piece(id, e.id))}">${esc(nom)}</a>` : esc(nom);
184
+ return `<div class="fl-piece">${x.k.icone('document', 22)}<div>${lien}${emp ? `<br><small class="fl-doux" dir="ltr">${esc(x.tx('documents.empreinte', { court: `${emp.slice(0, 4)}…${emp.slice(-4)}` }))}</small>` : ''}`
185
+ + `${e.par ? `<br><small class="fl-doux">${esc(x.tx('documents.deposee', { par: e.par, date: x.jj(e.deposeLe) }))}</small>` : ''}</div></div>`;
186
+ };
187
+ const cartes = f.documents.map((d) => {
188
+ const c = d.courante;
189
+ const bloque = (d.etat === 'expiree' && d.bloquante) || (d.etat === 'absente' && d.obligatoire);
190
+ const dl = c ? `<dl class="fl-infos">${[[x.tx('documents.reference'), c.reference], [x.tx('documents.organisme'), c.organisme], [x.tx('documents.du'), x.jj(c.debut)], [x.tx('documents.au'), x.jj(c.fin)], [x.tx('documents.cout'), c.cout ? x.argent(c.cout) : null]]
191
+ .filter(([, v]) => v).map(([l, v]) => `<dt>${esc(l)}</dt><dd>${esc(v)}</dd>`).join('')}</dl>` : '';
192
+ const renouveler = x.peutGerer && x.liens.renouveler ? `<a class="${bouton(x, 'fl-secondaire fl-bloc')}" href="${esc(x.liens.renouveler(id, d.type))}">${esc(x.tx('action.renouveler'))}</a>` : '';
193
+ const anciennes = d.anciennes.length ? `<details class="fl-replie"><summary>${esc(x.tx('documents.precedentes', { n: x.nf(d.anciennes.length) }))}</summary><ul>${d.anciennes.map((a) => `<li>${esc(x.jj(a.debut))} – ${esc(x.jj(a.fin))}${a.reference ? ` · <span dir="ltr">${esc(a.reference)}</span>` : ''}${a.cout ? ` · ${esc(x.argent(a.cout))}` : ''}</li>`).join('')}</ul></details>` : '';
194
+ return `<article class="fl-doc${bloque ? ' fl-ko' : ''}"><header><span class="fl-pastille-doc">${x.k.icone('document', 22)}</span><div><b>${esc(x.lib('echeance', d.type))}</b>`
195
+ + `${d.obligatoire ? ` <small class="fl-doux">(${esc(x.tx('documents.obligatoire'))})</small>` : ''}<div>${etatEcheance(x, d)}</div></div>`
196
+ + `<span class="fl-droite">${x.k.badge(x.tx(`documents.etats.${d.etat}`), TEINTE_DOC[d.etat])}</span></header>${dl}${piece(c)}`
197
+ + `${bloque ? `<p class="fl-retard">${esc(x.tx('documents.bloque'))}</p>` : ''}${renouveler}${anciennes}</article>`;
198
+ }).join('');
199
+ return `<h3>${esc(x.tx('documents.titre'))}</h3><div class="fl-docs">${cartes}</div>`;
194
200
  }
195
201
 
196
- function contenuOnglet(x, f) {
197
- const T = (c) => x.tx(c);
198
- if (f.onglet === 'documents') {
199
- return tableau(x, [T('documents.type'), T('documents.du'), T('documents.au'), T('documents.cout'), T('documents.piece'), T('documents.etat')],
200
- (f.documents || []).map((d) => [esc(x.lib('echeance', d.type)), esc(x.jj(d.debut)), esc(x.jj(d.fin)), d.cout ? esc(x.nf(d.cout)) : '—',
201
- d.piece ? `<code>${esc(String(d.piece.empreinte ?? d.piece.nom ?? d.piece).slice(0, 16))}</code>` : '—',
202
- d.courante ? (d.fin < f.date ? `<span class="fl-retard">${esc(x.tx('info.expireeLe', { date: x.jj(d.fin) }))}</span>` : esc(T('documents.courante'))) : `<span class="fl-doux">${esc(T('documents.ancienne'))}</span>`]),
203
- T('documents.vide'));
204
- }
205
- if (f.onglet === 'entretiens') {
206
- const E = f.entretiens;
207
- const pas = (q) => [q?.tousLesKm ? x.tx('entretiens.km', { n: x.nf(q.tousLesKm) }) : null, q?.tousLesJours ? x.tx('entretiens.jours', { n: x.nf(q.tousLesJours) }) : null].filter(Boolean).join(` ${T('entretiens.ou')} `);
208
- const plans = tableau(x, [T('entretiens.plan'), T('entretiens.pas'), T('entretiens.prochain'), T('col.statut')],
209
- E.prochains.map((p) => [esc(p.libelle), esc(pas(p.pas)), p.le ? `${esc(x.jj(p.le))} ${delai(x, p.dans)}${p.estime ? ` <span class="fl-doux">${esc(T('info.estimee'))}</span>` : ''}` : (p.km ? esc(x.tx('info.auKm', { km: x.nf(p.km.echeance) })) : '—'),
210
- p.etat === 'ok' ? '' : x.k.badge(p.etat === 'proche' ? x.lib('avertissement', 'ENTRETIEN_PROCHE', { plan: p.libelle, n: x.nf(p.n), unite: x.lib('unite', p.unite) }) : x.lib('motif', 'ENTRETIEN_DEPASSE', { plan: p.libelle, n: x.nf(p.n), unite: x.lib('unite', p.unite) }), p.etat === 'proche' ? 'orange' : 'rouge')]),
211
- T('info.aucunPlan'));
212
- const inter = tableau(x, [T('entretiens.numero'), T('entretiens.nature'), T('entretiens.ouverte'), T('entretiens.close'), T('entretiens.atelier'), T('entretiens.cout')],
213
- E.interventions.map((i) => [`<code>${esc(i.numero)}</code>`, esc(x.lib('nature', i.nature)), esc(x.jj(i.ouverteLe)), i.closeLe ? esc(x.jj(i.closeLe)) : x.k.badge(T('entretiens.enCours'), 'orange', 'horloge'),
214
- esc(i.prestataire ?? '—'), i.cout ? esc(x.nf(i.cout)) : '—']), T('entretiens.vide'));
215
- return `<h3>${esc(T('entretiens.plans'))}</h3>${plans}<h3>${esc(T('entretiens.interventions'))}</h3>${inter}`;
216
- }
217
- if (f.onglet === 'affectations') {
218
- return tableau(x, [T('affectations.chauffeur'), T('affectations.du'), T('affectations.au'), T('affectations.par')],
219
- f.affectations.map((a) => [esc(a.conducteur?.nom ?? '—'), esc(x.jj(a.du)), a.au ? esc(x.jj(a.au)) : x.k.badge(T('affectations.enCours'), 'vert'), esc(a.parAgent ?? '—')]), T('affectations.vide'));
220
- }
221
- if (f.onglet === 'historique') {
222
- const lignes = f.historique.map((h) => (h.vers ? [esc(x.jj(h.le)), badgeStatut(x, h.de), badgeStatut(x, h.vers), esc(h.motif ?? '—'), esc(h.agent ?? '—')]
223
- : [esc(x.jj(h.le)), '—', '—', esc(x.tx('historique.modification', { champs: (h.modification?.champs || []).join(', ') })), esc(h.agent ?? '—')]));
224
- lignes.push([esc(x.jj(f.vehicule.inscritLe)), '—', badgeStatut(x, 'en_service'), esc(T('historique.inscrit')), esc(f.vehicule.inscritPar ?? '—')]);
225
- return tableau(x, [T('historique.le'), T('historique.de'), T('historique.vers'), T('historique.motif'), T('historique.par')], lignes, T('historique.vide'));
202
+ // ── Entretiens (16.34) : plans à double jauge, interventions ──
203
+ function entretiensHtml(x, f) {
204
+ const E = f.entretiens, id = f.vehicule.id, T = (c, v) => x.tx(c, v);
205
+ const pas = (p) => [p.tousLesKm ? T('entretiens.km', { n: x.nf(p.tousLesKm) }) : null, p.tousLesJours ? T('entretiens.jours', { n: x.nf(p.tousLesJours) }) : null]
206
+ .filter(Boolean).join(` ${T('entretiens.ou')} `);
207
+ const jauge = (cle, j, unite) => `<div class="fl-jauge"><span>${esc(T(cle))}</span><b>${x.nf(j.pct)} %</b><span class="fl-piste" role="img" aria-label="${x.nf(j.pct)} %"><i style="width:${j.pct}%"></i></span>`
208
+ + `<span class="fl-doux">${esc(T('entretiens.fait', { fait: x.nf(j.fait), pas: `${x.nf(j.pas)} ${x.lib('unite', unite)}` }))}</span></div>`;
209
+ const prochaine = (p) => {
210
+ const l = [];
211
+ if (p.le) l.push(`<b>${esc(T('entretiens.prochaine', { date: x.jj(p.le) }))}</b> ${p.estime ? `<span class="fl-doux">(${esc(T('info.estimee'))})</span> ` : ''}${delai(x, p.dans)}`);
212
+ if (p.km?.echeance) l.push(esc(T('entretiens.aKm', { km: x.nf(p.km.echeance) })));
213
+ if (p.estime && E.rythme) l.push(esc(T('entretiens.rythme', { n: x.nf(E.rythme) })));
214
+ if (p.auPlusTard) l.push(esc(T('entretiens.auPlusTard', { date: x.jj(p.auPlusTard) })));
215
+ return `<div class="fl-prochaine">${x.k.icone('calendrier', 18)}<div>${l.join('<br>')}</div></div>`;
216
+ };
217
+ const planifier = x.peutGerer && x.liens.planifier ? `<a class="${bouton(x)}" href="${esc(x.liens.planifier(id))}">${x.k.icone('plus', 18)}${esc(T('action.planifier'))}</a>` : '';
218
+ const plans = E.plans.length ? `<div class="fl-plans">${E.plans.map((p) => `<article class="fl-plan"><header><span class="fl-pastille-doc">${x.k.icone('cle', 22)}</span><div><b>${esc(p.libelle)}</b>`
219
+ + `<div class="fl-doux">${esc(T('entretiens.tousLes', { pas: pas(p) }))}</div></div>${p.etat !== 'ok' ? `<span class="fl-droite">${x.k.badge(T(`entretiens.etats.${p.etat}`), TEINTE_ENT[p.etat] || 'gris')}</span>` : ''}</header>`
220
+ + `<div class="fl-jauges">${p.km ? jauge('entretiens.avanceKm', p.km, 'km') : ''}${p.jours ? jauge('entretiens.avanceJours', p.jours, 'jours') : ''}</div>${prochaine(p)}</article>`).join('')}</div>`
221
+ : `<p class="fl-vide">${esc(T('info.aucunPlan'))}</p>`;
222
+ const ouvrir = x.peutGerer && x.liens.intervention ? `<a class="${bouton(x)}" href="${esc(x.liens.intervention(id))}">${x.k.icone('plus', 18)}${esc(T('action.intervention'))}</a>` : '';
223
+ const inter = tableau(x, [T('entretiens.numero'), T('entretiens.nature'), T('entretiens.ouverte'), T('entretiens.close'), T('entretiens.atelier'), T('entretiens.kmIntervention'), T('entretiens.cout'), T('entretiens.statut'), T('col.actions')],
224
+ E.interventions.map((i) => [`<code>${esc(i.numero)}</code>`, x.k.badge(x.lib('nature', i.nature), i.nature === 'corrective' ? 'orange' : i.nature === 'inspection' ? 'bleu' : 'vert'), esc(x.jj(i.ouverteLe)),
225
+ i.closeLe ? esc(x.jj(i.closeLe)) : '—', esc(i.prestataire ?? '—'), i.km !== null && i.km !== undefined ? `${x.nf(i.km)} km` : '—', i.cout ? esc(x.argent(i.cout)) : '—',
226
+ i.close ? `<span class="fl-cadenas">${x.k.icone('cadenas', 14)} ${esc(T('entretiens.fermee'))}</span>` : x.k.badge(T('entretiens.enCours'), 'orange', 'horloge'),
227
+ !i.close && x.peutGerer && x.liens.clore ? `<a href="${esc(x.liens.clore(id, i.id))}">${esc(T('action.clore'))}</a>` : '']), T('entretiens.vide'));
228
+ return `<div class="fl-titre-section"><h3>${esc(T('entretiens.plans'))}</h3>${planifier}</div>${plans}<div class="fl-titre-section"><h3>${esc(T('entretiens.interventions'))}</h3>${ouvrir}</div>${inter}`;
229
+ }
230
+
231
+ // ── Affectations (16.35) : chauffeur actuel, frise, panneau « Affecter un chauffeur » ──
232
+ function affectationsHtml(x, f) {
233
+ const A = f.affectations, id = f.vehicule.id, T = (c, v) => x.tx(c, v);
234
+ const qui = (a) => `<b>${esc(a.conducteur?.nom ?? '—')}</b>${a.conducteur?.telephone ? `<br><span class="fl-doux" dir="ltr">${esc(a.conducteur.telephone)}</span>` : ''}${a.permis ? ` <span class="fl-doux">· ${esc(T('affectations.permisDe', { p: a.permis }))}</span>` : ''}`;
235
+ const liberer = x.peutGerer && x.liens.liberer && A.courante
236
+ ? `<form method="post" action="${esc(x.liens.liberer(id))}" class="fl-en-ligne"><button type="submit" class="${bouton(x, 'fl-secondaire')}">${esc(T('action.liberer'))}</button></form>` : '';
237
+ const actuel = `<div class="fl-titre-section"><h3>${esc(T('affectations.actuel'))}</h3>${liberer}</div>`
238
+ + (A.courante ? `<div class="fl-actuel">${x.k.icone('utilisateur', 32)}<div>${qui(A.courante)}</div><div class="fl-droite">${esc(T('affectations.depuis', { date: x.jj(A.courante.du) }))}`
239
+ + `${A.courante.par ? `<br><span class="fl-doux">${esc(T('affectations.parLe', { par: A.courante.par }))}</span>` : ''}</div></div>` : `<p class="fl-vide">${esc(T('info.aucunChauffeur'))}</p>`);
240
+ const frise = A.frise.length ? `<h3>${esc(T('affectations.frise'))}</h3><ol class="fl-frise">${A.frise.map((a) => `<li${a.au ? '' : ' class="actif"'}><div><b>${esc(x.jj(a.du))}</b> → ${a.au ? `<b>${esc(x.jj(a.au))}</b>` : `<b class="fl-ok">${esc(T('affectations.enCours'))}</b>`}</div>`
241
+ + `<div>${qui(a)}</div>${a.par ? `<div class="fl-doux">${esc(T('affectations.parLe', { par: a.par }))}</div>` : ''}</li>`).join('')}</ol>` : `<p class="fl-vide">${esc(T('affectations.vide'))}</p>`;
242
+ let form = '';
243
+ if (x.peutGerer && x.liens.affecter && A.candidats.length) {
244
+ const dispo = (c) => (c.disponibilite === 'libre' ? `<span class="fl-ok">●</span> ${esc(T('affectations.libre'))}`
245
+ : c.disponibilite === 'ce_vehicule' ? `<span class="fl-ok">●</span> ${esc(T('affectations.ceVehicule'))}` : `<span class="fl-retard">●</span> ${esc(T('affectations.conduit', { plaque: c.plaque }))}`);
246
+ const lignes = A.candidats.map((c) => [`<label><input type="radio" name="conducteur" value="${esc(c.id)}"${c.disponibilite === 'ce_vehicule' ? ' checked' : ''}> ${esc(c.nom ?? c.id)}</label>`,
247
+ `<span dir="ltr">${esc(c.telephone ?? '—')}</span>`, `${c.permis ? esc(T('affectations.permisDe', { p: c.permis })) : '—'}${c.habilite === false ? ` ${x.k.badge(T('affectations.nonHabilite'), 'rouge')}` : ''}`, dispo(c)]);
248
+ form = `<form method="post" action="${esc(x.liens.affecter(id))}" class="fl-panneau"><h3>${esc(T('action.affecter'))}</h3>`
249
+ + `<p>${esc(T('affectations.choisir'))}${A.categorie ? ` — ${esc(T('affectations.categorie'))} : <b>${esc(A.categorie)}</b>` : ''}</p>`
250
+ + `${tableau(x, [T('affectations.nom'), T('affectations.telephone'), T('affectations.permis'), T('affectations.disponibilite')], lignes, T('affectations.aucunCandidat'))}`
251
+ + `<div class="fl-boutons"><a class="${bouton(x, 'fl-secondaire')}" href="${esc(lienFiche(x, id, 'affectations'))}">${esc(T('action.annuler'))}</a><button type="submit" class="${bouton(x)}">${esc(T('action.valider'))}</button></div></form>`;
226
252
  }
253
+ return `${actuel}${frise}${form}`;
254
+ }
255
+
256
+ // ── Pleins et relevés (16.37) ──
257
+ function pleinsHtml(x, f) {
258
+ const P = f.pleins, id = f.vehicule.id, T = (c, v) => x.tx(c, v);
259
+ const debut = P.courbe.valeurs.findIndex((v) => v !== null);
260
+ const mois = debut < 0 ? [] : P.courbe.mois.slice(debut).map((m) => nomMois(x, m, true));
261
+ // Un mois sans relevé garde le kilométrage du mois précédent : le compteur ne redescend pas.
262
+ const vals = debut < 0 ? [] : P.courbe.valeurs.slice(debut).reduce((a, v) => [...a, v ?? a.at(-1)], []);
263
+ const courbe = `<div class="fl-titre-section"><h3>${esc(T('pleins.courbe'))}</h3><div class="fl-droite"><b>${P.km !== null ? `${x.nf(P.km)} km` : '—'}</b><br><small class="fl-doux">${esc(T('pleins.au', { date: x.jj(f.date) }))}</small></div></div>`
264
+ + (vals.length ? courbeKm(x, mois, vals) : '')
265
+ + (P.rythme ? `<p class="fl-doux">${esc(T('pleins.rythme', { n: x.nf(P.rythme) }))}</p>` : '')
266
+ + (vals.length ? `<table class="fl-donnees"><caption class="fl-sr">${esc(T('pleins.kmReleve'))}</caption><thead><tr><th scope="row">${esc(T('graphe.mois'))}</th>${mois.map((m) => `<th scope="col">${esc(m)}</th>`).join('')}</tr></thead>`
267
+ + `<tbody><tr><th scope="row">km</th>${vals.map((v) => `<td>${x.nf(v)}</td>`).join('')}</tr></tbody></table>` : '');
268
+ const ajouter = x.peutGerer && x.liens.releve ? `<a class="${bouton(x, 'fl-secondaire')}" href="${esc(x.liens.releve(id))}">${x.k.icone('plus', 18)}${esc(T('action.releve'))}</a>` : '';
269
+ const commentaire = (r) => [...(r.avertissements || []).map((a) => `<span class="fl-avert">${x.k.icone('alerte', 14)} ${esc(x.phrase('avertissement', a))}</span>`),
270
+ ...(r.correction?.motif ? [esc(T('pleins.correction', { motif: r.correction.motif }))] : [])].join('<br>') || '—';
271
+ const releves = tableau(x, [T('pleins.date'), T('pleins.km'), T('pleins.source'), T('pleins.saisiPar'), T('pleins.commentaire')],
272
+ P.releves.map((r) => [esc(x.jj(r.le)), `${x.nf(r.km)} km`, x.k.badge(x.lib('source', r.source), r.source === 'plein' ? 'vert' : r.source === 'intervention' ? 'violet' : 'bleu'), esc(r.par ?? '—'), commentaire(r)]), T('pleins.aucunReleve'));
273
+ const saisir = x.peutGerer && x.liens.plein ? `<a class="${bouton(x, 'fl-secondaire')}" href="${esc(x.liens.plein(id))}">${x.k.icone('plus', 18)}${esc(T('action.plein'))}</a>` : '';
274
+ const moyenne = P.consommation !== null && P.consommation !== undefined ? `<span class="fl-doux">${esc(T('pleins.consoMoyenne'))} : <b>${x.nf(P.consommation)} L/100 km</b></span>` : '';
275
+ const pleins = tableau(x, [T('pleins.date'), T('pleins.km'), T('pleins.litres'), T('pleins.montant'), T('pleins.complet'), T('pleins.conso')],
276
+ P.pleins.map((p) => [esc(x.jj(p.le)), `${x.nf(p.km)} km`, `${x.nf(p.litres)} L`, esc(x.argent(p.montant)), p.complet ? x.k.badge(T('pleins.oui'), 'vert') : x.k.badge(T('pleins.non'), 'orange'),
277
+ p.conso !== null ? `${x.nf(p.conso)} L/100 km` : '—']), T('pleins.aucunPlein'));
278
+ return `${courbe}<div class="fl-titre-section"><h3>${esc(T('pleins.releves'))}</h3>${ajouter}</div>${releves}`
279
+ + `<div class="fl-titre-section"><h3>${esc(T('pleins.pleins'))}</h3>${moyenne}${saisir}</div>${pleins}`;
280
+ }
281
+
282
+ /**
283
+ * La courbe du compteur (16.37), dessinée ici : un compteur se lit entre son minimum et son maximum, pas depuis zéro
284
+ * (la courbe du kit, faite pour des effectifs, écraserait la pente). Graduations arrondies, aire sous la courbe.
285
+ */
286
+ function courbeKm(x, mois, vals) {
287
+ const L = 640, H = 220, g = 64, bas = H - 26, haut = 12;
288
+ const pas = 10 ** Math.max(3, Math.floor(Math.log10(Math.max(1, Math.max(...vals) - Math.min(...vals)))));
289
+ const min = Math.floor(Math.min(...vals) / pas) * pas, max = Math.max(min + pas, Math.ceil(Math.max(...vals) / pas) * pas);
290
+ const X = (i) => g + ((L - g - 16) * i) / Math.max(1, vals.length - 1), Y = (v) => bas - ((bas - haut) * (v - min)) / (max - min);
291
+ const graduations = [0, 1, 2, 3, 4].map((k) => min + ((max - min) * k) / 4);
292
+ const pts = vals.map((v, i) => `${X(i).toFixed(1)},${Y(v).toFixed(1)}`).join(' ');
293
+ return `<svg class="fl-courbe" viewBox="0 0 ${L} ${H}" width="100%" role="img" aria-label="${esc(x.tx('pleins.kmReleve'))}">`
294
+ + graduations.map((v) => `<line x1="${g}" x2="${L - 8}" y1="${Y(v).toFixed(1)}" y2="${Y(v).toFixed(1)}" stroke="currentColor" opacity=".12"/><text x="${g - 8}" y="${(Y(v) + 4).toFixed(1)}" text-anchor="end" font-size="11" fill="currentColor" opacity=".7">${esc(x.nf(Math.round(v)))}</text>`).join('')
295
+ + `<polygon points="${X(0).toFixed(1)},${bas} ${pts} ${X(vals.length - 1).toFixed(1)},${bas}" fill="#2878E0" opacity=".1"/><polyline points="${pts}" fill="none" stroke="#2878E0" stroke-width="2.5"/>`
296
+ + vals.map((v, i) => `<circle cx="${X(i).toFixed(1)}" cy="${Y(v).toFixed(1)}" r="3.5" fill="#2878E0"/>`).join('')
297
+ + mois.map((m, i) => `<text x="${X(i).toFixed(1)}" y="${H - 6}" text-anchor="middle" font-size="11" fill="currentColor" opacity=".7">${esc(m)}</text>`).join('') + '</svg>';
298
+ }
299
+
300
+ // ── Coûts (16.38) ──
301
+ function coutsHtml(x, f) {
302
+ const C = f.couts, c = C.cout, id = f.vehicule.id, T = (k, v) => x.tx(k, v);
303
+ const chips = ['mois', 'trimestre', 'annee'].map((p) => {
304
+ const actif = C.periode === p;
305
+ return `<a class="fl-puce${actif ? ' actif' : ''}" href="${esc(lienFiche(x, id, 'couts', { periode: p === 'mois' ? '' : p }))}"${actif ? ' aria-current="page"' : ''}>${esc(T(`couts.${p}`))}</a>`;
306
+ }).join('');
307
+ const action = caches(lienFiche(x, id, 'couts'));
308
+ const perso = `<form class="fl-filtres" method="get" action="${esc(action.chemin)}">${action.html}<input type="hidden" name="periode" value="perso">`
309
+ + `<label>${esc(T('couts.du'))} <input type="date" name="du" value="${esc(C.du)}"></label><label>${esc(T('couts.au'))} <input type="date" name="au" value="${esc(C.au)}"></label>`
310
+ + `<button type="submit">${esc(T(C.periode === 'perso' ? 'couts.perso' : 'couts.appliquer'))}</button></form>`;
311
+ const parKm = (n) => (n !== null && n !== undefined ? `${x.nf2(n)}${x.devise ? ` ${x.devise}` : ''}/km` : '—');
312
+ const kpis = `<div class="fl-k4">${x.k.kpi({ teinte: 'bleu', icone: 'pieces', valeur: x.argent(c.carburant), libelle: T('couts.carburant') })}`
313
+ + `${x.k.kpi({ teinte: 'orange', icone: 'cle', valeur: x.argent(c.entretien), libelle: T('couts.entretien') })}`
314
+ + `${x.k.kpi({ teinte: 'violet', icone: 'document', valeur: x.argent(c.echeances), libelle: T('couts.echeances') })}`
315
+ + `${x.k.kpi({ teinte: 'bleu', icone: 'barres', valeur: x.argent(c.total), libelle: T('couts.total'), sous: esc(T('couts.nonComptes', { postes: C.nonComptes.map((p) => x.lib('poste', p)).join(', ') })) })}</div>`
316
+ + `<div class="fl-k2">${x.k.kpi({ teinte: 'gris', icone: 'fleche', valeur: `${x.nf(c.km)} km`, libelle: T('couts.km') })}${x.k.kpi({ teinte: 'bleu', icone: 'barres', valeur: parKm(c.parKm), libelle: T('couts.parKm') })}</div>`;
317
+ const max = Math.max(1, ...C.mois.map((m) => m.total));
318
+ const postes = ['carburant', 'entretien', 'echeances'];
319
+ const colonnes = `<div class="fl-colonnes">${C.mois.map((m) => `<div class="fl-colonne"><span class="fl-total">${x.nf(Math.round(m.total))}</span><span class="fl-pile-col" role="img" aria-label="${esc(`${nomMois(x, m.mois)} : ${x.argent(m.total)}`)}">`
320
+ + `${postes.filter((p) => m[p] > 0).map((p) => `<i style="height:${((100 * m[p]) / max).toFixed(1)}%;background:${COULEURS_POSTES[p]}"></i>`).join('')}</span><span>${esc(nomMois(x, m.mois))}</span></div>`).join('')}</div>`
321
+ + legende(x, postes.map((p) => [T(`couts.${p}`), COULEURS_POSTES[p]]))
322
+ + `<table class="fl-donnees"><thead><tr><th scope="row">${esc(T('graphe.mois'))}</th>${C.mois.map((m) => `<th scope="col">${esc(nomMois(x, m.mois))}</th>`).join('')}</tr></thead><tbody>`
323
+ + postes.map((p) => `<tr><th scope="row">${esc(T(`couts.${p}`))}</th>${C.mois.map((m) => `<td>${x.nf(Math.round(m[p]))}</td>`).join('')}</tr>`).join('') + '</tbody></table>';
324
+ const parc = tableau(x, ['#', T('couts.vehicule'), T('col.type'), T('couts.km'), T('couts.carburant'), T('couts.entretien'), T('couts.echeances'), T('couts.total'), T('couts.parKm')],
325
+ C.parc.map((v, i) => [x.nf(i + 1), `<span dir="ltr">${esc(v.plaque)}</span>`, esc(x.lib('type', v.type)), `${x.nf(v.km)} km`, esc(x.nf2(v.carburant)), esc(x.nf2(v.entretien)), esc(x.nf2(v.echeances)), esc(x.nf2(v.total)),
326
+ esc(parKm(v.parKm))]), '—', { classeLigne: (i) => (C.parc[i].courant ? 'choisi' : '') });
327
+ return `<h3>${esc(T('couts.periode'))}</h3><div class="fl-puces">${chips}</div>${perso}${kpis}<h3>${esc(T('couts.parMois'))}</h3>${colonnes}`
328
+ + `<h3>${esc(T('couts.parc'))} <span class="fl-doux">${esc(T('couts.parcSous'))}</span></h3>${parc}`;
329
+ }
330
+
331
+ function historiqueHtml(x, f) {
332
+ const T = (c) => x.tx(c);
333
+ const lignes = f.historique.map((h) => (h.vers ? [esc(x.jj(h.le)), badgeStatut(x, h.de), badgeStatut(x, h.vers), esc(h.motif ?? '—'), esc(h.agent ?? '—')]
334
+ : [esc(x.jj(h.le)), '—', '—', esc(x.tx('historique.modification', { champs: (h.modification?.champs || []).join(', ') })), esc(h.agent ?? '—')]));
335
+ lignes.push([esc(x.jj(f.vehicule.inscritLe)), '—', badgeStatut(x, 'en_service'), esc(T('historique.inscrit')), esc(f.vehicule.inscritPar ?? '—')]);
336
+ return tableau(x, [T('historique.le'), T('historique.de'), T('historique.vers'), T('historique.motif'), T('historique.par')], lignes, T('historique.vide'));
337
+ }
338
+
339
+ function contenuOnglet(x, f) {
340
+ if (f.onglet === 'documents' && f.documents) return documentsHtml(x, f);
341
+ if (f.onglet === 'entretiens' && f.entretiens) return entretiensHtml(x, f);
342
+ if (f.onglet === 'affectations' && f.affectations) return affectationsHtml(x, f);
343
+ if (f.onglet === 'pleins' && f.pleins) return pleinsHtml(x, f);
344
+ if (f.onglet === 'couts' && f.couts) return coutsHtml(x, f);
345
+ if (f.onglet === 'historique' && f.historique) return historiqueHtml(x, f);
227
346
  return informations(x, f);
228
347
  }
229
348
 
230
- /** La fiche d'un véhicule : en-tête, bandeau « peut partir », onglets en liens GET, contenu de l'onglet. */
349
+ const ongletsHtml = (x, f, filtres) => `<nav class="${esc(x.c.onglets)} fl-serres" aria-label="${esc(x.tx('detail.titre'))}">${ONGLETS.map((o) => `<a href="${esc(lienFiche(x, f.vehicule.id, o, {}, filtres))}"${o === f.onglet ? ' aria-current="page" class="actif"' : ''}>${esc(x.tx(`onglet.${o}`))}</a>`).join('')}</nav>`;
350
+
351
+ /** La fiche d'un véhicule, en panneau (tableau de bord) : en-tête, bandeau « peut partir », onglets, contenu. */
231
352
  export function rendreFiche(f, opts = {}) {
232
353
  const x = contexte(opts);
233
354
  if (!f) return `<p class="fl-vide">${esc(x.tx('detail.aucun'))}</p>`;
234
- const base = x.liens.base || '', keep = garder(opts.filtres || {});
235
- const onglets = `<nav class="${esc(x.c.onglets)} fl-serres" aria-label="${esc(x.tx('detail.titre'))}">${ONGLETS.map((o) => `<a href="${esc(url(base, { ...keep, v: f.vehicule.id, onglet: o === 'informations' ? '' : o }))}"${o === f.onglet ? ' aria-current="page" class="actif"' : ''}>${esc(x.tx(`onglet.${o}`))}</a>`).join('')}</nav>`;
236
- const modifier = x.peutGerer && x.liens.modifier ? `<a class="${esc(x.c.btn)} petit" href="${esc(x.liens.modifier(f.vehicule.id))}">${esc(x.tx('action.modifier'))}</a>` : '';
237
- const tete = `<div class="fl-entete-fiche"><span class="fl-vignette">${x.k.icone('camion', 40)}</span><div><b>${esc(f.plaque)}</b><div>${esc(x.lib('type', f.vehicule.type))}</div></div><div>${badgeStatut(x, f.vehicule.statut)}</div>${modifier ? `<div class="fl-droite">${modifier}</div>` : ''}</div>`;
238
- return `${tete}${bandeau(x, f.depart)}${onglets}${contenuOnglet(x, f)}`;
355
+ const modifier = x.peutGerer && x.liens.modifier ? `<a class="${bouton(x, 'petit')}" href="${esc(x.liens.modifier(f.vehicule.id))}">${esc(x.tx('action.modifier'))}</a>` : '';
356
+ const ouvrir = x.liens.fiche ? `<a class="${bouton(x, 'petit fl-secondaire')}" href="${esc(x.liens.fiche(f.vehicule.id, ''))}">${esc(x.tx('action.ouvrir'))}</a>` : '';
357
+ const tete = `<div class="fl-entete-fiche"><span class="fl-vignette">${x.k.icone('camion', 40)}</span><div><b dir="ltr">${esc(f.plaque)}</b><div>${esc(x.lib('type', f.vehicule.type))} · ${esc(capacite(x, f.capacite))}</div></div><div>${badgeStatut(x, f.vehicule.statut)}</div></div>`
358
+ + `${modifier || ouvrir ? `<div class="fl-boutons-fiche">${ouvrir}${modifier}</div>` : ''}`;
359
+ return `${tete}${bandeau(x, f.depart)}${ongletsHtml(x, f, opts.filtres || {})}${contenuOnglet(x, f)}`;
360
+ }
361
+
362
+ /**
363
+ * La fiche pleine page (16.31, 16.32). Rend { titre, sousTitre, fil, outils, corps } : `fil` est le fil d'Ariane
364
+ * que l'application pose dans son gabarit. opts : ceux de rendreTableau, plus { message?, refus? (une ErreurFlotte
365
+ * du moteur, rendue par son code), devise? } ; liens : { base, fiche(id, onglet), modifier, statut(id, transition),
366
+ * renouveler(id, type), piece(id, echeance), planifier, intervention, clore(id, intervention), affecter, liberer,
367
+ * plein, releve }.
368
+ */
369
+ export function rendrePageVehicule(f, opts = {}) {
370
+ const x = contexte(opts);
371
+ const v = f.vehicule, id = v.id, T = (c, vars) => x.tx(c, vars);
372
+ const modifier = x.peutGerer && x.liens.modifier ? `<a class="${bouton(x, 'fl-secondaire')}" href="${esc(x.liens.modifier(id))}">${esc(T('action.modifier'))}</a>` : '';
373
+ const statut = x.peutGerer && x.liens.statut && f.transitions.length
374
+ ? `<details class="fl-menu fl-statut"><summary class="${bouton(x)}">${esc(T('action.changerStatut'))} ▾</summary><div>${f.transitions.map((t) => `<a href="${esc(x.liens.statut(id, t.id))}">${esc(x.lib('transition', t.id))}</a>`).join('')}</div></details>` : '';
375
+ const ch = f.conducteur;
376
+ const tete = `<div class="fl-tete-page"><span class="fl-vignette">${x.k.icone('camion', 48)}</span>`
377
+ + `<div class="fl-ident"><div><b class="fl-grande" dir="ltr">${esc(f.plaque)}</b> ${badgeStatut(x, v.statut)}</div><div>${esc(x.lib('type', v.type))} · ${esc(capacite(x, f.capacite))}</div>`
378
+ + `<div class="fl-doux">${x.k.icone('epingle', 16)} ${esc(f.site ?? '—')}${f.region ? ` · ${esc(f.region)}` : ''} · ${f.km !== null ? `${x.nf(f.km)} km` : '—'}</div></div>`
379
+ + `<div class="fl-qui">${x.k.icone('utilisateur', 20)} <div>${ch ? `${esc(ch.nom)}${ch.telephone ? `<br><span class="fl-doux" dir="ltr">${esc(ch.telephone)}</span>` : ''}` : `<span class="fl-doux">${esc(T('info.aucunChauffeur'))}</span>`}</div></div>`
380
+ + `<div class="fl-actions">${modifier}${statut}</div></div>`;
381
+ const msg = opts.message ? bandeauOk(x, esc(opts.message)) : '';
382
+ const refus = opts.refus ? bandeauKo(x, esc(texteRefus(x, opts.refus))) : '';
383
+ const corps = `<div class="fl-ecran fl-page">${refus}${msg}<div class="fl-carte">${tete}${bandeau(x, f.depart)}</div>`
384
+ + `<div class="fl-carte">${ongletsHtml(x, f)}${contenuOnglet(x, f)}</div></div>`;
385
+ return { titre: f.plaque, sousTitre: `${x.lib('type', v.type)} · ${capacite(x, f.capacite)}`, fil: [{ libelle: T('titre'), href: x.liens.base || null }, { libelle: f.plaque, href: null }], outils: '', corps };
239
386
  }
240
387
 
241
388
  /**
242
- * Le tableau de bord d'ECR-16. Rend { titre, sousTitre, outils, corps } : l'application les pose dans son gabarit
243
- * (zone de titre et corps). opts : { kit?, t?, locale?, liens?: { base, nouveau?, modifier?(id), affecter?(id),
244
- * planifier?(id), tournee?, alertes?, export? }, droits?: { gerer }, declarations? }
389
+ * Le tableau de bord d'ECR-16 (planche A). Rend { titre, sousTitre, outils, corps } : l'application les pose dans
390
+ * son gabarit (zone de titre et corps). opts : { kit?, t?, locale?, liens?: { base, fiche?(id, onglet), nouveau?,
391
+ * modifier?(id), affecter?(id), planifier?(id), tournee?, alertes?, export? }, droits?: { gerer }, declarations? }
245
392
  */
246
393
  export function rendreTableau(vm, opts = {}) {
247
394
  const x = contexte(opts);
248
- const locale = opts.locale || 'fr-FR';
249
395
  const outils = x.peutGerer && x.liens.nouveau ? `<a class="${esc(x.c.btn)}" href="${esc(x.liens.nouveau)}">${x.k.icone('plus', 18)}${esc(x.tx('nouveau'))}</a>` : '';
250
396
  const carte = (cle, ic, corps, o = {}) => x.k.carte(esc(x.tx(cle)) + (o.sous ? ` <span class="fl-doux">${esc(x.tx(o.sous))}</span>` : ''), ic, corps, o);
251
397
  const voirTout = x.liens.alertes ? `<a href="${esc(x.liens.alertes)}">${esc(x.tx('alertes.voirTout'))}</a>` : '';
252
398
  const segments = vm.repartition.map((r, i) => ({ libelle: x.lib('type', r.type), valeur: r.n, couleur: COULEURS.types[i % COULEURS.types.length] }));
253
399
  const corps = `<div class="fl-ecran">${indicateurs(x, vm.indicateurs)}
254
- <div class="fl-large"><div class="fl-pile">${carte('liste.titre', 'liste', liste(x, vm))}
255
- <div class="fl-c3">${carte('graphe.types', 'barres', x.k.anneau(segments, { unite: x.tx('graphe.vehicules') }))}${carte('graphe.statuts', 'barres', statutsHtml(x, vm.statuts))}${carte('graphe.km', 'barres', kmHtml(x, vm.km, locale), { sous: 'graphe.kmSous' })}</div>
256
- <div class="fl-c2">${carte('graphe.carte', 'epingle', parSiteHtml(x, vm), { sous: 'graphe.carteSous' })}${carte('graphe.top', 'camion', topHtml(x, vm))}</div></div>
257
- <div class="fl-pile">${carte('detail.titre', 'utilisateur', rendreFiche(vm.fiche, { ...opts, filtres: vm.filtres }), { id: 'detail' })}${carte('alertes.titre', 'calendrier', alertesHtml(x, vm), { droite: voirTout })}${carte('rapides.titre', 'fleche', rapidesHtml(x, vm))}</div></div></div>`;
400
+ <div class="fl-large"><div class="fl-pile">${carte('liste.titre', 'liste', liste(x, vm))}</div>
401
+ <div class="fl-pile">${carte('detail.titre', 'utilisateur', rendreFiche(vm.fiche, { ...opts, filtres: vm.filtres }), { id: 'detail' })}${carte('alertes.titre', 'calendrier', alertesHtml(x, vm), { droite: voirTout })}${carte('rapides.titre', 'fleche', rapidesHtml(x, vm))}</div></div>
402
+ <div class="fl-c3">${carte('graphe.statuts', 'barres', statutsHtml(x, vm.statuts))}${carte('graphe.carte', 'epingle', parSiteHtml(x, vm), { sous: 'graphe.carteSous' })}${carte('graphe.top', 'camion', topHtml(x, vm))}</div>
403
+ <div class="fl-c2">${carte('graphe.types', 'barres', x.k.anneau(segments, { unite: x.tx('graphe.vehicules') }))}${carte('graphe.km', 'barres', kmHtml(x, vm.km), { sous: 'graphe.kmSous' })}</div></div>`;
258
404
  return { titre: x.tx('titre'), sousTitre: x.tx('sousTitre'), outils, corps };
259
405
  }
260
406
 
@@ -267,3 +413,65 @@ export function exporterCsv(vm, opts = {}) {
267
413
  x.lib('statut', l.statut) + (l.documentExpire ? ` (${x.tx('documentExpire')})` : ''), l.depart.ok ? x.tx('depart.ouiCourt') : l.depart.motifs.map((m) => x.phrase('motif', m)).join(' ; ')]);
268
414
  return `${[ent, ...lignes].map((l) => l.map(champ).join(';')).join('\n')}\n`;
269
415
  }
416
+
417
+ // ── « Mon véhicule » du livreur (16.40) ─────────────────────────────────────────────────────────────────────
418
+ /** opts.liens : { plein, releve } (des URL : le livreur n'a qu'un véhicule). Rend { titre, corps }. */
419
+ export function rendreMonVehicule(vm, opts = {}) {
420
+ const x = contexte(opts), T = (c, v) => x.tx(c, v);
421
+ const f = vm.vehicule;
422
+ if (!f) return { titre: T('monVehicule.titre'), corps: `<div class="fl-ecran fl-mobile"><p class="fl-vide">${esc(T('monVehicule.aucun'))}</p></div>` };
423
+ const ch = f.conducteur;
424
+ const tete = `<div class="fl-carte"><div class="fl-entete-fiche"><span class="fl-vignette">${x.k.icone('camion', 40)}</span><div><b dir="ltr">${esc(f.plaque)}</b><div>${esc(x.lib('type', f.vehicule.type))}</div><div class="fl-doux">${esc(capacite(x, f.capacite))}</div></div></div>`
425
+ + `${bandeau(x, f.depart)}<ul class="fl-liste-infos"><li>${x.k.icone('epingle', 18)} ${esc(f.site ?? '—')}${f.region ? ` — ${esc(f.region)}` : ''}</li>`
426
+ + `${ch ? `<li>${x.k.icone('utilisateur', 18)} ${esc(ch.nom)}${ch.telephone ? ` <span class="fl-doux" dir="ltr">${esc(ch.telephone)}</span>` : ''}</li>` : ''}`
427
+ + `<li>${x.k.icone('horloge', 18)} ${f.km !== null ? `${x.nf(f.km)} km` : '—'}</li>${f.consommation !== null && f.consommation !== undefined ? `<li>${x.k.icone('barres', 18)} ${x.nf(f.consommation)} L/100 km</li>` : ''}</ul></div>`;
428
+ const boutons = `<div class="fl-boutons-pleins">${x.liens.plein ? `<a class="${bouton(x, 'fl-bloc')}" href="${esc(x.liens.plein)}">${x.k.icone('plus', 20)}${esc(T('action.plein'))}</a>` : ''}`
429
+ + `${x.liens.releve ? `<a class="${bouton(x, 'fl-bloc fl-secondaire')}" href="${esc(x.liens.releve)}">${x.k.icone('horloge', 20)}${esc(T('action.relever'))}</a>` : ''}</div>`;
430
+ const prochains = vm.prochains.length ? `<ul class="fl-liste-infos">${vm.prochains.map((p) => {
431
+ const l = [];
432
+ if (p.le) l.push(`${esc(x.jj(p.le))}${p.estime ? ` (${esc(T('info.estimee'))})` : ''}`);
433
+ if (p.km?.echeance) l.push(esc(T('entretiens.aKm', { km: x.nf(p.km.echeance) })));
434
+ if (p.estime && vm.rythme) l.push(esc(T('entretiens.rythme', { n: x.nf(vm.rythme) })));
435
+ if (p.estime && p.jours?.echeance) l.push(`(${esc(T('entretiens.auPlusTard', { date: x.jj(p.jours.echeance) }))})`);
436
+ const badge = p.etat !== 'ok' ? x.k.badge(T(`entretiens.etats.${p.etat}`), TEINTE_ENT[p.etat] || 'gris') : p.dans !== null && p.dans !== undefined ? x.k.badge(T('info.dansJours', { n: x.nf(p.dans) }), 'orange') : '';
437
+ return `<li>${x.k.icone('cle', 18)} <div><b>${esc(p.libelle)}</b> ${badge}<br>${l.join('<br>')}</div></li>`;
438
+ }).join('')}</ul>` : `<p class="fl-vide">${esc(T('info.aucunPlan'))}</p>`;
439
+ const docs = `<ul class="fl-liste-infos">${f.echeances.map((e) => `<li><div><b>${esc(x.lib('echeance', e.type))}</b><br>${etatEcheance(x, e)}</div></li>`).join('')}</ul>`;
440
+ const corps = `<div class="fl-ecran fl-mobile">${tete}${boutons}<section class="fl-carte"><h2>${esc(T('monVehicule.prochains'))}</h2>${prochains}</section>`
441
+ + `<section class="fl-carte"><h2>${esc(T('monVehicule.documents'))}</h2>${docs}</section></div>`;
442
+ return { titre: T('monVehicule.titre'), corps };
443
+ }
444
+
445
+ // ── Tournée : aptitude des véhicules (16.41) ────────────────────────────────────────────────────────────────
446
+ /** La liste des véhicules d'une tournée, chacun avec son aptitude : un bouton radio par véhicule (le formulaire est celui de l'application). */
447
+ export function rendreChoixVehicule(aptitudes, opts = {}) {
448
+ const x = contexte(opts), nom = opts.nom || 'vehicule';
449
+ return `<div class="fl-choix" role="radiogroup" aria-label="${esc(x.tx('tournee.vehicule'))}">${aptitudes.map((a) => {
450
+ const etat = a.depart.ok
451
+ ? (a.depart.avertissements.length ? `<span class="fl-avert">${x.k.icone('alerte', 16)} ${esc(x.phrase('avertissement', a.depart.avertissements[0]))}</span>` : '')
452
+ : x.k.badge(a.depart.motifs.length === 1 ? x.tx('tournee.motif') : x.tx('tournee.motifs', { n: x.nf(a.depart.motifs.length) }), 'rouge', 'croix');
453
+ return `<label class="fl-option${a.depart.ok ? '' : ' ko'}"><input type="radio" name="${esc(nom)}" value="${esc(a.id)}"${a.id === opts.choisi ? ' checked' : ''}>`
454
+ + `<span class="fl-depart ${a.depart.ok ? 'ok' : 'ko'}">${x.k.icone(a.depart.ok ? 'coche' : 'croix', 18)}</span>`
455
+ + `<span><b dir="ltr">${esc(a.plaque)}</b><br><small class="fl-doux">${esc(x.lib('type', a.type))} (${esc(capacite(x, a.capacite))})</small></span>`
456
+ + `<span>${a.km !== null ? `${x.nf(a.km)} km` : '—'}</span><span>${esc(a.site ?? '—')}</span><span>${etat}</span></label>`;
457
+ }).join('')}</div>`;
458
+ }
459
+
460
+ /** Le refus de départ (côté serveur) : TOUS les motifs, et le lien vers le carnet du véhicule. '' si le départ est permis. */
461
+ export function rendreRefusDepart(depart, opts = {}) {
462
+ if (!depart || depart.ok) return '';
463
+ const x = contexte(opts);
464
+ return `<div class="fl-refus" role="alert">${x.k.icone('croix', 28)}<div><b>${esc(x.tx('tournee.refus'))}</b><p>${esc(x.tx('tournee.refusSous'))}</p>`
465
+ + `<ul>${depart.motifs.map((m) => `<li>${esc(x.phrase('motif', m))}</li>`).join('')}</ul>`
466
+ + `${opts.lien ? `<a class="${bouton(x, 'fl-secondaire')}" href="${esc(opts.lien)}">${esc(x.tx('tournee.carnet'))}</a>` : ''}</div></div>`;
467
+ }
468
+
469
+ /** Les autres véhicules qui peuvent partir (sauf `exclure`), dans une carte du kit. */
470
+ export function rendreDisponibles(aptitudes, opts = {}) {
471
+ const x = contexte(opts);
472
+ const ok = aptitudes.filter((a) => a.depart.ok && a.id !== opts.exclure);
473
+ const corps = ok.length ? `<ul class="fl-liste-infos">${ok.map((a) => `<li><span class="fl-depart ok">${x.k.icone('coche', 18)}</span><div><b dir="ltr">${esc(a.plaque)}</b><br><small class="fl-doux">${esc(x.lib('type', a.type))} (${esc(capacite(x, a.capacite))})`
474
+ + ` · ${a.km !== null ? `${x.nf(a.km)} km` : '—'}${a.region ? ` · ${esc(a.region)}` : ''}</small>${a.depart.avertissements.length ? `<br><span class="fl-avert">${esc(x.phrase('avertissement', a.depart.avertissements[0]))}</span>` : ''}</div></li>`).join('')}</ul>`
475
+ : `<p class="fl-vide">${esc(x.tx('tournee.aucunDisponible'))}</p>`;
476
+ return x.k.carte(esc(x.tx('tournee.disponibles')), 'camion', `<p class="fl-doux">${esc(x.tx('tournee.disponiblesSous'))}</p>${corps}`);
477
+ }