@mostajs/flotte-ui 0.0.0-stage → 0.1.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/CHANGELOG.md +14 -0
- package/LICENSE +15 -0
- package/README.md +82 -2
- package/llms.txt +42 -0
- package/package.json +25 -4
- package/src/index.js +9 -0
- package/src/kit.js +118 -0
- package/src/rendu.js +268 -0
- package/src/textes.js +73 -0
- package/src/vue.js +187 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog — @mostajs/flotte-ui
|
|
2
|
+
|
|
3
|
+
Format [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) · [SemVer](https://semver.org/lang/fr/).
|
|
4
|
+
Auteur : Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
|
|
6
|
+
## [0.1.0] — non publiée
|
|
7
|
+
### Added
|
|
8
|
+
- `construireTableau` et `rendreTableau` : l'écran ECR-16 de Guerway (indicateurs et variation, liste filtrée et
|
|
9
|
+
paginée, alertes et échéances, répartition par type, statuts, kilométrage sur six mois, véhicules par station,
|
|
10
|
+
top 5, actions rapides), sans script.
|
|
11
|
+
- `construireFiche` et `rendreFiche` : fiche du véhicule, bandeau « peut partir », onglets en liens GET.
|
|
12
|
+
- `exporterCsv`, `texte` et `TEXTES` (clés `flotte.ecran.*`), `KIT_DEFAUT` et `FLOTTE_CSS`. Le kit de
|
|
13
|
+
l'application, c'est-à-dire sa charte, remplace celui du module membre à membre.
|
|
14
|
+
- Essais T-FUI-1 à 10 ; exemple `examples/verifier.mjs`. Rendu vérifié avec la charte de Guerway.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@mostajs/flotte-ui
|
|
2
|
+
Copyright (C) 2026 Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
|
|
4
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
5
|
+
the terms of the GNU Affero General Public License as published by the Free
|
|
6
|
+
Software Foundation, either version 3 of the License, or (at your option) any
|
|
7
|
+
later version.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
10
|
+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
11
|
+
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
12
|
+
details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.
|
package/README.md
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mostajs/flotte-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · Licence AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
Les écrans du carnet du parc ([`@mostajs/flotte`](../mosta-flotte)), en HTML rendu par le serveur, **sans React ni
|
|
6
|
+
script**. Le module sert le même écran à Guerway et au SaaS « Carnet du parc ». Chacun l'habille de sa propre charte.
|
|
7
|
+
|
|
8
|
+
- **Le tableau de bord** (écran ECR-16 de Guerway) :
|
|
9
|
+
- cinq indicateurs avec leur variation sur la période ;
|
|
10
|
+
- la liste filtrée et paginée, avec le statut, l'indicateur « document expiré » et la réponse « peut partir » du
|
|
11
|
+
jour ;
|
|
12
|
+
- les alertes et échéances ;
|
|
13
|
+
- la répartition par type, les statuts et le kilométrage sur six mois ;
|
|
14
|
+
- les véhicules par station, le top 5 et les actions rapides.
|
|
15
|
+
- **La fiche du véhicule** :
|
|
16
|
+
- le bandeau « peut partir », ou ses motifs ;
|
|
17
|
+
- les onglets Informations, Documents, Entretiens, Affectations et Historique, chacun en lien GET ;
|
|
18
|
+
- le prochain entretien daté, y compris quand cette date est estimée au rythme du véhicule.
|
|
19
|
+
|
|
20
|
+
L'écran se fait en deux temps :
|
|
21
|
+
1. **construire** : lit l'API de `flotte` et produit une vue-modèle faite de codes ;
|
|
22
|
+
2. **rendre** : une fonction pure qui produit le HTML.
|
|
23
|
+
|
|
24
|
+
L'application garde son gabarit, son menu, ses routes et ses droits.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { construireTableau, rendreTableau, FLOTTE_CSS } from '@mostajs/flotte-ui';
|
|
28
|
+
|
|
29
|
+
const vm = await construireTableau(flotte, {
|
|
30
|
+
declarations, // celles données à createFlotte
|
|
31
|
+
date: '2026-10-24', filtres: req.query, // q, type, statut, region, page, v (véhicule choisi), onglet
|
|
32
|
+
sites: { 'ALG-01': { libelle: 'Alger Centre', region: 'Centre' } },
|
|
33
|
+
conducteurs: (id) => ({ nom, telephone }), // l'identifiant du conducteur est opaque pour flotte
|
|
34
|
+
perimetre: ['ALG-01'], // la console d'un bureau ; absent : tout le parc
|
|
35
|
+
});
|
|
36
|
+
const { titre, sousTitre, outils, corps } = rendreTableau(vm, {
|
|
37
|
+
kit, // la charte de l'application ; absent : KIT_DEFAUT
|
|
38
|
+
t, // sa traduction : clés flotte.ecran.* et flotte.<famille>.<code>
|
|
39
|
+
liens: { base: '/flotte', tournee: '/tournees/nouvelle' },
|
|
40
|
+
droits: { gerer: peut('flotte:gerer') },
|
|
41
|
+
});
|
|
42
|
+
// La page : votre gabarit (titre, sousTitre, outils) + `<style>${FLOTTE_CSS}</style>${corps}`
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Le kit
|
|
46
|
+
|
|
47
|
+
Le kit reprend les signatures des composants de Guerway (`src/charte.mjs`), qui se branchent tels quels :
|
|
48
|
+
|
|
49
|
+
| membre | signature |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `kpi` | `({ teinte, icone, valeur, libelle, sous })` |
|
|
52
|
+
| `badge` | `(texte, teinte, icone)` |
|
|
53
|
+
| `carte` | `(titre, icone, corps, { droite, id })` |
|
|
54
|
+
| `anneau` | `(segments, { unite })` |
|
|
55
|
+
| `courbes` | `(libelles, series)` |
|
|
56
|
+
| `icone` | `(nom, taille)` |
|
|
57
|
+
| `alerte` | `(genre, texte)` |
|
|
58
|
+
| `pagination` | `({ total, page, parPage, lien })` |
|
|
59
|
+
| `classes` | `{ tableau, onglets, rapides, rapide, btn }` |
|
|
60
|
+
|
|
61
|
+
Un membre absent est pris dans `KIT_DEFAUT`.
|
|
62
|
+
|
|
63
|
+
## Langues
|
|
64
|
+
|
|
65
|
+
Le module ne contient aucune chaîne en dur.
|
|
66
|
+
- **Les textes de l'écran** passent par `texte(cle, t)` : l'application reçoit d'abord la clé `flotte.ecran.<cle>`.
|
|
67
|
+
Si elle ne la connaît pas, le français du module s'affiche.
|
|
68
|
+
- **Les codes de la flotte** (statut, motif, type, échéance) passent par `libelle()` de `@mostajs/flotte`.
|
|
69
|
+
|
|
70
|
+
## Ce que le module ne fait pas
|
|
71
|
+
|
|
72
|
+
- Écrire ou décider : c'est `@mostajs/flotte`, qui porte toutes les règles.
|
|
73
|
+
- Router, poser le menu ou vérifier les droits : c'est l'application. `droits.gerer` ne fait que masquer, le serveur
|
|
74
|
+
refuse.
|
|
75
|
+
- Les formulaires d'écriture (nouveau véhicule, affectation, entretien, plein) : ils viendront en 0.2.0, une fois
|
|
76
|
+
les maquettes reçues (`Entreprise/GUERWAY/docs/L0/PROMPT-MAQUETTE-FLOTTE.md`).
|
|
77
|
+
|
|
78
|
+
## Vérifier
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm test # T-FUI-1 à 10
|
|
82
|
+
node examples/verifier.mjs # écrit test-scripts/.out/exemple-flotte-ui.html (le parc de la maquette)
|
|
83
|
+
```
|
package/llms.txt
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @mostajs/flotte-ui — llms.txt
|
|
2
|
+
|
|
3
|
+
RÔLE
|
|
4
|
+
Écrans HTML du carnet du parc (@mostajs/flotte), sans React ni script : tableau de bord ECR-16 de Guerway et fiche
|
|
5
|
+
du véhicule à onglets. Deux temps : construire (async, lit l'API de flotte → vue-modèle de CODES) puis rendre (pur,
|
|
6
|
+
vue-modèle → HTML, avec le kit et la traduction de l'application). N3, métier, forme « vue ».
|
|
7
|
+
|
|
8
|
+
EXPORTS
|
|
9
|
+
construireTableau(flotte, opts) -> vm opts : { declarations, date?, filtres?: { q, type, statut, region, page, v, onglet },
|
|
10
|
+
sites?: { code: { libelle, region } }, conducteurs?: objet|fonction id → { nom, telephone },
|
|
11
|
+
activite?: async (id, { du, au }) → { tournees, colis }, perimetre?: [sites], periodeJours? (30), parPage? (10 ; Infinity = tout),
|
|
12
|
+
alertesMax? (5) }
|
|
13
|
+
vm = { date, filtres, indicateurs: { vehicules, chauffeurs, enService, enMaintenance, documentsExpires } (n, part?, variation
|
|
14
|
+
{ sens, pct|null, favorable }), liste: { lignes, total, page, pages, parPage }, options, alertes: { lignes, total, jours },
|
|
15
|
+
repartition, statuts, km: { mois, valeurs }, parSite, top, avecActivite, fiche }
|
|
16
|
+
ligne = { id, code, plaque, type, capacite: { kg, m3 }, site, region, conducteur, km, statut, documentExpire, depart }
|
|
17
|
+
construireFiche(flotte, id, { declarations, date?, sites?, conducteurs?, onglet? }) -> fiche
|
|
18
|
+
onglet ∈ ONGLETS (informations, documents, entretiens, affectations, historique) ; inconnu → informations
|
|
19
|
+
rendreTableau(vm, { kit?, t?, locale?, liens?: { base, nouveau?, modifier?(id), affecter?(id), planifier?(id), tournee?,
|
|
20
|
+
alertes?, export? }, droits?: { gerer }, declarations? }) -> { titre, sousTitre, outils, corps }
|
|
21
|
+
rendreFiche(fiche, mêmes opts + filtres?) -> HTML
|
|
22
|
+
exporterCsv(vm, opts?) -> CSV « ; » (construire avec parPage: Infinity)
|
|
23
|
+
TEXTES, texte(cle, t?, vars?) t('flotte.ecran.<cle>') puis repli français
|
|
24
|
+
KIT_DEFAUT { icone, kpi, badge, carte, alerte, anneau, courbes, pagination, classes } ; FLOTTE_CSS (classes fl-*) ; COULEURS
|
|
25
|
+
|
|
26
|
+
RÈGLES
|
|
27
|
+
- Aucune règle de flotte ici : expiré, prochain entretien, peut partir viennent du moteur. Seuls des agrégats
|
|
28
|
+
d'affichage (comptes, parts, variation à date − periodeJours rejouée sur l'historique, km par mois, top 5).
|
|
29
|
+
- Le texte cherche aussi le NOM du chauffeur (le moteur ne connaît que l'identifiant).
|
|
30
|
+
- « Document expiré » est un indicateur (pastille), jamais un statut.
|
|
31
|
+
- Une action n'apparaît que si son lien est fourni ; une action d'écriture, que si droits.gerer.
|
|
32
|
+
|
|
33
|
+
PIÈGES
|
|
34
|
+
- Toute valeur est échappée ; titre et corps des cartes du kit reçoivent du HTML déjà sûr.
|
|
35
|
+
- Les nombres sont formatés par Intl (fr-FR : espace fine insécable U+202F) : en test, accepter [\s ].
|
|
36
|
+
- Dépend de @mostajs/flotte ^0.3.0 (vehicules()) en peerDependency : l'application fournit l'instance.
|
|
37
|
+
|
|
38
|
+
EXEMPLE / TEST
|
|
39
|
+
examples/verifier.mjs le parc de la maquette, page complète → test-scripts/.out/exemple-flotte-ui.html
|
|
40
|
+
test-scripts/unit/flotte-ui.test.mjs T-FUI-1 à 10
|
|
41
|
+
|
|
42
|
+
LICENCE AGPL-3.0-or-later · Auteur Dr Hamid MADANI <drmdh@msn.com>
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/flotte-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Écrans HTML du carnet du parc (@mostajs/flotte), sans React ni script : tableau de bord (indicateurs, liste filtrée, alertes et échéances, répartitions, kilométrage, véhicules par station, top 5, actions rapides) et fiche du véhicule à onglets. Vue-modèle lue dans l'API de flotte, rendue avec le kit de composants de l'application (sa charte) ou un kit sobre par défaut.",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": { ".": "./src/index.js" },
|
|
10
|
+
"files": ["src", "llms.txt", "README.md", "CHANGELOG.md"],
|
|
11
|
+
"engines": { "node": ">=20" },
|
|
12
|
+
"scripts": { "test": "bash test-scripts/run-tests.sh" },
|
|
13
|
+
"keywords": ["mostajs", "flotte", "fleet", "fleet-management", "html", "server-side", "no-react", "dashboard"],
|
|
14
|
+
"repository": { "type": "git", "url": "https://github.com/apolocine/mosta-flotte-ui" },
|
|
15
|
+
"dependencies": { "@mostajs/app-shell-ui": "^0.3.0" },
|
|
16
|
+
"peerDependencies": { "@mostajs/flotte": "^0.3.0" },
|
|
17
|
+
"devDependencies": { "@mostajs/mjs-unit": "0.10.0" },
|
|
18
|
+
"mostajs": {
|
|
19
|
+
"niveau": "N3", "nature": "metier", "reutilisabilite": "mono-domaine", "depend_de": ["@mostajs/flotte", "@mostajs/app-shell-ui"],
|
|
20
|
+
"question": "Comment le chef de parc voit-il son parc, et chaque véhicule, sans React ?",
|
|
21
|
+
"forme": "vue",
|
|
22
|
+
"entites": [],
|
|
23
|
+
"verbes": ["construireTableau", "rendreTableau", "construireFiche", "rendreFiche", "exporterCsv"],
|
|
24
|
+
"interop": { "moteur": "@mostajs/flotte (vehicules, peutPartir, prochainsEntretiens, echeances, releves, affectations)", "primitives": "@mostajs/app-shell-ui (esc, pagination)", "charte": "kit de composants injecté par l'application" },
|
|
25
|
+
"ne_fait_pas": { "stocker, calculer une règle de flotte": "@mostajs/flotte", "gabarit, menu, routes": "l'application (ou @mostajs/app-shell-ui)", "droits": "l'application (droits.gerer)" }
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @mostajs/flotte-ui — écrans HTML du carnet du parc (@mostajs/flotte), sans React ni script.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// Deux temps : CONSTRUIRE (async, lit l'API de flotte → vue-modèle de codes) puis RENDRE (pur, vue-modèle → HTML,
|
|
5
|
+
// avec le kit et la langue de l'application). L'application garde son gabarit, son menu, ses routes et ses droits.
|
|
6
|
+
export { construireTableau, construireFiche, ONGLETS } from './vue.js';
|
|
7
|
+
export { rendreTableau, rendreFiche, exporterCsv } from './rendu.js';
|
|
8
|
+
export { TEXTES, texte } from './textes.js';
|
|
9
|
+
export { KIT_DEFAUT, FLOTTE_CSS, COULEURS } from './kit.js';
|
package/src/kit.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @mostajs/flotte-ui — le kit de composants par défaut, et sa feuille de style.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// L'application passe SON kit (sa charte : Guerway fournit kpi, badge, carte, anneau, courbes, icone, alerte de
|
|
5
|
+
// src/charte.mjs, et les noms de ses classes). Ce kit-ci, sobre, sert à défaut : un SaaS sans charte, un essai, un
|
|
6
|
+
// exemple. Mêmes signatures que la charte de Guerway, pour que l'une remplace l'autre membre à membre.
|
|
7
|
+
import { esc, pagination } from '@mostajs/app-shell-ui';
|
|
8
|
+
|
|
9
|
+
const nf = (x) => new Intl.NumberFormat('fr-FR').format(x);
|
|
10
|
+
|
|
11
|
+
/** Icônes pleines 24 × 24, sans ressource externe. */
|
|
12
|
+
const ICONES = {
|
|
13
|
+
camion: '<path d="M3 6h11v9H3zM14 9h4l3 3v3h-7zM6.5 18.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm11 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/>',
|
|
14
|
+
utilisateur: '<path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zm-7 8a7 7 0 0 1 14 0z"/>',
|
|
15
|
+
coche: '<path d="M9.5 16.2 5.3 12l-1.4 1.4 5.6 5.6L20.1 8.4 18.7 7z"/>',
|
|
16
|
+
croix: '<path d="M6.4 5 5 6.4 10.6 12 5 17.6 6.4 19l5.6-5.6 5.6 5.6 1.4-1.4-5.6-5.6L19 6.4 17.6 5 12 10.6z"/>',
|
|
17
|
+
cle: '<path d="M21 4.5 19.5 3l-8.6 8.6a5 5 0 1 0 1.5 1.5L15 10.5l2 2 1.5-1.5-2-2L18 7.5l2 2 1.5-1.5-2-2zM7 18a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/>',
|
|
18
|
+
document: '<path d="M6 2h8l6 6v14H6zm7 1.5V9h5.5z"/>',
|
|
19
|
+
alerte: '<path d="M12 2 1 21h22zm-1 7h2v6h-2zm0 8h2v2h-2z"/>',
|
|
20
|
+
plus: '<path d="M11 5h2v6h6v2h-6v6h-2v-6H5v-2h6z"/>',
|
|
21
|
+
calendrier: '<path d="M7 2h2v2h6V2h2v2h3v18H4V4h3zm-1 8v10h12V10z"/>',
|
|
22
|
+
epingle: '<path d="M12 2a7 7 0 0 0-7 7c0 5 7 13 7 13s7-8 7-13a7 7 0 0 0-7-7zm0 9.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/>',
|
|
23
|
+
barres: '<path d="M4 20V10h3v10zm6.5 0V4h3v16zM17 20v-7h3v7z"/>',
|
|
24
|
+
oeil: '<path d="M12 5C6 5 2 12 2 12s4 7 10 7 10-7 10-7-4-7-10-7zm0 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/>',
|
|
25
|
+
horloge: '<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm1 10.4 3.6 2.1-.8 1.3L11 13V6h2z"/>',
|
|
26
|
+
fleche: '<path d="M4 11h12.2l-5.6-5.6L12 4l8 8-8 8-1.4-1.4 5.6-5.6H4z"/>',
|
|
27
|
+
liste: '<path d="M3 5h2v2H3zm4 0h14v2H7zM3 11h2v2H3zm4 0h14v2H7zM3 17h2v2H3zm4 0h14v2H7z"/>',
|
|
28
|
+
menu: '<path d="M12 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/>',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const KIT_DEFAUT = Object.freeze({
|
|
32
|
+
icone: (nom, taille = 20) => `<svg class="fl-ic" width="${taille}" height="${taille}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">${ICONES[nom] || ''}</svg>`,
|
|
33
|
+
kpi: ({ teinte, icone, valeur, libelle, sous = '' }) => `<div class="fl-kpi fl-${esc(teinte)}"><span class="fl-pastille">${KIT_DEFAUT.icone(icone, 28)}</span>`
|
|
34
|
+
+ `<div><div class="fl-valeur">${esc(valeur)}</div><div class="fl-libelle">${esc(libelle)}</div>${sous ? `<div class="fl-sous">${sous}</div>` : ''}</div></div>`,
|
|
35
|
+
badge: (texte, teinte, icone) => `<span class="fl-badge fl-${esc(teinte)}">${icone ? KIT_DEFAUT.icone(icone, 14) : ''}${esc(texte)}</span>`,
|
|
36
|
+
carte: (titre, icone, corps, { droite = '', classe = '', id = '' } = {}) => `<section class="fl-carte ${esc(classe)}"${id ? ` id="${esc(id)}"` : ''}>`
|
|
37
|
+
+ `<header>${icone ? KIT_DEFAUT.icone(icone, 20) : ''}<h2>${titre}</h2>${droite ? `<div class="fl-droite">${droite}</div>` : ''}</header>${corps}</section>`,
|
|
38
|
+
alerte: (genre, texte) => `<div class="fl-alerte fl-${esc(genre)}" role="${genre === 'erreur' ? 'alert' : 'status'}">${esc(texte)}</div>`,
|
|
39
|
+
/** Anneau : segments [{ libelle, valeur, couleur }], total au centre, légende chiffrée. */
|
|
40
|
+
anneau(segments, { unite = '' } = {}) {
|
|
41
|
+
const total = segments.reduce((s, x) => s + x.valeur, 0);
|
|
42
|
+
let decal = 25;
|
|
43
|
+
const arcs = segments.filter((s) => s.valeur > 0).map((s) => {
|
|
44
|
+
const l = (100 * s.valeur) / total;
|
|
45
|
+
const a = `<circle cx="21" cy="21" r="15.9155" fill="none" stroke="${esc(s.couleur)}" stroke-width="6" stroke-dasharray="${l.toFixed(3)} ${(100 - l).toFixed(3)}" stroke-dashoffset="${decal.toFixed(3)}"/>`;
|
|
46
|
+
decal -= l; return a;
|
|
47
|
+
}).join('');
|
|
48
|
+
return `<div class="fl-duo"><svg width="150" height="150" viewBox="0 0 42 42" role="img" aria-label="${esc(`${nf(total)} ${unite}`)}">${arcs}`
|
|
49
|
+
+ `<text x="21" y="22" text-anchor="middle" font-size="7" font-weight="700" fill="currentColor">${nf(total)}</text><text x="21" y="27" text-anchor="middle" font-size="3" fill="currentColor">${esc(unite)}</text></svg>`
|
|
50
|
+
+ `<table class="fl-legende"><tbody>${segments.map((s) => `<tr><td><span class="fl-point" style="background:${esc(s.couleur)}"></span></td><td>${esc(s.libelle)}</td><td>${nf(s.valeur)}</td><td>(${total ? Math.round((100 * s.valeur) / total) : 0} %)</td></tr>`).join('')}</tbody></table></div>`;
|
|
51
|
+
},
|
|
52
|
+
/** Courbe : libellés en abscisse, series [{ libelle, couleur, valeurs }] ; aire sous la première. */
|
|
53
|
+
courbes(libelles, series) {
|
|
54
|
+
const L = 520, H = 170, g = 40, bas = H - 22, utile = bas - 12;
|
|
55
|
+
const max = Math.max(1, ...series.flatMap((s) => s.valeurs));
|
|
56
|
+
const x = (i) => g + 10 + ((L - g - 20) * i) / Math.max(1, libelles.length - 1), y = (v) => bas - (utile * v) / max;
|
|
57
|
+
let s = `<line x1="${g}" x2="${L}" y1="${bas}" y2="${bas}" stroke="currentColor" opacity=".2"/>`;
|
|
58
|
+
series.forEach((se, k) => {
|
|
59
|
+
const pts = se.valeurs.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ');
|
|
60
|
+
if (k === 0) s += `<polygon points="${x(0)},${bas} ${pts} ${x(se.valeurs.length - 1)},${bas}" fill="${esc(se.couleur)}" opacity=".12"/>`;
|
|
61
|
+
s += `<polyline points="${pts}" fill="none" stroke="${esc(se.couleur)}" stroke-width="2.2"/>`;
|
|
62
|
+
});
|
|
63
|
+
libelles.forEach((l, i) => { s += `<text x="${x(i)}" y="${H - 5}" text-anchor="middle" font-size="11" fill="currentColor">${esc(l)}</text>`; });
|
|
64
|
+
return `<svg viewBox="0 0 ${L} ${H}" width="100%" role="img" aria-hidden="true">${s}</svg>`;
|
|
65
|
+
},
|
|
66
|
+
pagination,
|
|
67
|
+
classes: Object.freeze({ grille: 'fl-grille', tableau: 'fl-tableau', onglets: 'fl-onglets', rapides: 'fl-rapides', rapide: 'fl-rapide', fiche: 'fl-fiche', btn: 'fl-btn' }),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** Les couleurs des types (anneau) et des statuts : celles de la maquette d'ECR-16. */
|
|
71
|
+
export const COULEURS = Object.freeze({
|
|
72
|
+
types: ['#2878E0', '#FD8A2B', '#E5484D', '#8EA3C0', '#7C3AED', '#18A048'],
|
|
73
|
+
statut: { en_service: '#18A048', a_l_atelier: '#FD8A2B', immobilise: '#E5484D', reforme: '#8EA3C0' },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Feuille de style : les classes fl-* sur des variables --fl-* à valeurs de repli. Une application qui passe sa
|
|
78
|
+
* charte n'en a besoin que pour ce que sa charte n'a pas (grille à cinq indicateurs, barres de statut, menu « ⋮ »).
|
|
79
|
+
*/
|
|
80
|
+
export const FLOTTE_CSS = `
|
|
81
|
+
.fl-ecran{--fl-bleu:var(--gw-bleu,#0757ca);--fl-texte:var(--gw-texte,#182040);--fl-doux:var(--gw-texte-doux,#586880);--fl-ligne:var(--gw-piste,#e1e6ed);--fl-carte:var(--gw-carte,#fff);--fl-vert:#18a048;--fl-orange:#c06000;--fl-rouge:#d01010;display:grid;gap:16px;color:var(--fl-texte)}
|
|
82
|
+
.fl-k5{display:grid;gap:16px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
|
83
|
+
.fl-c21{display:grid;gap:16px;grid-template-columns:minmax(0,2fr) minmax(0,1fr);align-items:start}
|
|
84
|
+
.fl-large{display:grid;gap:16px;grid-template-columns:minmax(0,2.4fr) minmax(0,1fr);align-items:start}
|
|
85
|
+
.fl-c3{display:grid;gap:16px;grid-template-columns:repeat(3,minmax(0,1fr));align-items:start}
|
|
86
|
+
@media (max-width:1100px){.fl-k5{grid-template-columns:repeat(2,minmax(0,1fr))}.fl-c3{grid-template-columns:1fr}}
|
|
87
|
+
@media (max-width:900px){.fl-c21,.fl-large{grid-template-columns:1fr}}
|
|
88
|
+
.fl-c2{display:grid;gap:16px;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}@media (max-width:900px){.fl-c2{grid-template-columns:1fr}}
|
|
89
|
+
.fl-plaque,.fl-nowrap{white-space:nowrap}
|
|
90
|
+
.fl-ecran nav.fl-serres a{padding-inline:8px}.fl-menu>summary{font-size:20px;line-height:1;font-weight:700}
|
|
91
|
+
.fl-pile{display:grid;gap:16px;align-content:start}
|
|
92
|
+
.fl-carte{background:var(--fl-carte);border:1px solid var(--fl-ligne);border-radius:12px;padding:18px}.fl-carte>header{display:flex;align-items:center;gap:8px;margin-bottom:12px}.fl-carte h2{font-size:16px;margin:0}.fl-droite{margin-inline-start:auto}
|
|
93
|
+
.fl-kpi{display:flex;gap:12px;align-items:center;border:1px solid var(--fl-ligne);border-radius:12px;padding:14px;background:var(--fl-carte)}.fl-pastille{display:grid;place-items:center;width:52px;height:52px;border-radius:50%;background:#eef3fb}
|
|
94
|
+
.fl-valeur{font-size:26px;font-weight:700}.fl-libelle{font-weight:500}.fl-sous{font-size:12.5px;color:var(--fl-doux)}
|
|
95
|
+
.fl-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 9px;border-radius:6px;font-size:12.5px;font-weight:500;white-space:nowrap}
|
|
96
|
+
.fl-vert{background:#e1f7eb;color:#086840}.fl-orange{background:#fef1d4;color:#a05000}.fl-rouge{background:#fdecec;color:#b00}.fl-gris{background:#e5e9ee;color:#445}.fl-bleu{background:#e3f0fe;color:#0757ca}.fl-violet{background:#ede7fd;color:#5b21b6}
|
|
97
|
+
.fl-tableau{width:100%;border-collapse:collapse;font-size:13.5px}.fl-tableau th{text-align:start;font-weight:600;padding:8px;background:#eff2f5}.fl-tableau td{padding:8px;border-bottom:1px solid var(--fl-ligne)}
|
|
98
|
+
.fl-defile{overflow-x:auto}
|
|
99
|
+
.fl-filtres{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}.fl-filtres input[type=search]{flex:1 1 220px}.fl-filtres select,.fl-filtres input,.fl-filtres button{height:38px;border:1px solid var(--fl-ligne);border-radius:8px;padding:0 10px;font:inherit;background:var(--fl-carte);color:inherit}
|
|
100
|
+
.fl-btn{display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 16px;border-radius:8px;background:var(--fl-bleu);color:#fff;text-decoration:none;font-weight:500;border:0}
|
|
101
|
+
.fl-depart{display:inline-grid;place-items:center;width:24px;height:24px;border-radius:50%}.fl-depart.ok{color:var(--fl-vert)}.fl-depart.ko{color:var(--fl-rouge)}
|
|
102
|
+
.fl-marque{display:inline-block;margin-inline-start:6px;padding:0 7px;border-radius:9px;background:#fdecec;color:#b00;font-size:11.5px;font-weight:600;white-space:nowrap}
|
|
103
|
+
.fl-menu{position:relative}.fl-menu>summary{list-style:none;cursor:pointer;padding:2px 6px;border-radius:6px}.fl-menu>summary::-webkit-details-marker{display:none}
|
|
104
|
+
.fl-menu[open]>div{position:absolute;inset-inline-end:0;z-index:5;display:grid;min-width:200px;background:var(--fl-carte);border:1px solid var(--fl-ligne);border-radius:8px;box-shadow:0 6px 18px rgb(16 40 66/12%);padding:6px}
|
|
105
|
+
.fl-menu[open]>div a{padding:7px 10px;border-radius:6px;text-decoration:none;color:inherit}.fl-menu[open]>div a:hover{background:#eff2f5}
|
|
106
|
+
.fl-bandeau{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;border-radius:8px;font-weight:500;margin:10px 0}.fl-bandeau.ok{background:#ebf9f2;color:#086840}.fl-bandeau.ko{background:#fdecec;color:#9b1c1c}.fl-bandeau ul{margin:4px 0 0;padding-inline-start:18px;font-weight:400}
|
|
107
|
+
.fl-entete-fiche{display:flex;gap:14px;align-items:center}.fl-entete-fiche b{font-size:20px}.fl-vignette{display:grid;place-items:center;width:84px;height:60px;border-radius:10px;background:#eef3fb;color:#5a7194}
|
|
108
|
+
.fl-onglets{display:flex;flex-wrap:wrap;gap:2px;border-bottom:1px solid var(--fl-ligne);margin:12px 0}.fl-onglets a{padding:8px 10px;text-decoration:none;color:var(--fl-doux);border-bottom:2px solid transparent}.fl-onglets a[aria-current]{color:var(--fl-bleu);border-bottom-color:currentColor;font-weight:600}
|
|
109
|
+
.fl-infos{display:grid;grid-template-columns:minmax(120px,170px) 1fr;gap:7px 12px;margin:0;font-size:13.5px}.fl-infos dt{color:var(--fl-doux)}.fl-infos dd{margin:0;font-weight:500}
|
|
110
|
+
.fl-delai{color:var(--fl-orange);font-weight:500}.fl-retard{color:var(--fl-rouge);font-weight:600}.fl-ok{color:var(--fl-vert)}.fl-doux{color:var(--fl-doux);font-size:12px;font-weight:400}
|
|
111
|
+
.fl-alertes{list-style:none;margin:0;padding:0;display:grid;gap:8px;font-size:13.5px}.fl-alertes li{display:grid;grid-template-columns:20px auto 1fr auto;gap:8px;align-items:center}.fl-alertes small{display:block;color:var(--fl-doux);font-weight:400}.fl-alertes li.fl-retard small{color:inherit}
|
|
112
|
+
.fl-barres{display:grid;gap:12px;font-size:13.5px}.fl-barre{display:grid;grid-template-columns:110px 1fr auto;gap:10px;align-items:center}.fl-barre>span:nth-child(2){height:12px;border-radius:6px;background:var(--fl-ligne);overflow:hidden}.fl-barre i{display:block;height:100%;border-radius:6px}
|
|
113
|
+
.fl-donnees{width:100%;border-collapse:collapse;font-size:12px;margin-top:8px;color:var(--fl-doux)}.fl-donnees th,.fl-donnees td{padding:3px 6px;text-align:center;border-top:1px solid var(--fl-ligne)}
|
|
114
|
+
.fl-duo{display:flex;align-items:center;gap:16px;flex-wrap:wrap}.fl-legende td{padding:3px 6px}.fl-point{display:inline-block;width:10px;height:10px;border-radius:50%}
|
|
115
|
+
.fl-rapides{display:grid;grid-template-columns:1fr 1fr;gap:10px}.fl-rapide{display:flex;align-items:center;gap:10px;min-height:44px;padding:8px 14px;border-radius:8px;text-decoration:none;font-weight:500;font-size:14px}.fl-rapide.plein{background:var(--fl-bleu);color:#fff}
|
|
116
|
+
.fl-vide{color:var(--fl-doux);margin:8px 0}
|
|
117
|
+
.fl-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
|
|
118
|
+
`;
|
package/src/rendu.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// @mostajs/flotte-ui — le rendu : vue-modèle → HTML, sans script (DEC-UI-13 de Guerway). Fonctions PURES.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
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.
|
|
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';
|
|
11
|
+
import { ONGLETS } from './vue.js';
|
|
12
|
+
|
|
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(' / ') || '—';
|
|
57
|
+
|
|
58
|
+
/** La variation d'un indicateur : flèche selon le sens, couleur selon le sens favorable. */
|
|
59
|
+
function variationHtml(x, v) {
|
|
60
|
+
if (!v) return '';
|
|
61
|
+
const fl = v.sens === 'hausse' ? '↑' : v.sens === 'baisse' ? '↓' : '→';
|
|
62
|
+
const cl = v.favorable === null ? 'fl-doux' : v.favorable ? 'fl-ok' : 'fl-retard';
|
|
63
|
+
const p = v.pct === null ? x.tx('kpi.nouveau') : `${v.pct > 0 ? '+' : ''}${x.nf(v.pct)} %`;
|
|
64
|
+
return `<span class="${cl}">${fl} ${esc(p)}</span> <span class="fl-doux">${esc(x.tx('kpi.vsPeriode'))}</span>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function indicateurs(x, I) {
|
|
68
|
+
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>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Les actions d'une ligne : « Voir » toujours ; les autres si l'application en donne le lien (et le droit, pour écrire). */
|
|
75
|
+
function actionsLigne(x, l, filtres) {
|
|
76
|
+
const L = x.liens, base = L.base || '';
|
|
77
|
+
const a = [[url(base, { ...garder(filtres), v: l.id }), 'voir']];
|
|
78
|
+
if (x.peutGerer && L.modifier) a.push([L.modifier(l.id), 'modifier']);
|
|
79
|
+
if (x.peutGerer && L.affecter) a.push([L.affecter(l.id), 'affecter']);
|
|
80
|
+
if (x.peutGerer && L.planifier) a.push([L.planifier(l.id), 'planifier']);
|
|
81
|
+
return a;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function liste(x, vm) {
|
|
85
|
+
const { filtres, liste: L, options } = vm;
|
|
86
|
+
const opt = (v, l, cour) => `<option value="${esc(v)}"${v === cour ? ' selected' : ''}>${esc(l)}</option>`;
|
|
87
|
+
const form = `<form class="fl-filtres" method="get" action="${esc(x.liens.base || '')}" role="search">`
|
|
88
|
+
+ `<input type="search" name="q" value="${esc(filtres.q)}" placeholder="${esc(x.tx('liste.recherche'))}" aria-label="${esc(x.tx('liste.recherche'))}">`
|
|
89
|
+
+ `<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
|
+
+ (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>`;
|
|
93
|
+
if (!L.lignes.length) return `${form}<p class="fl-vide">${esc(x.tx('liste.vide'))}</p>`;
|
|
94
|
+
const dep = (d) => (d.ok
|
|
95
|
+
? `<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
|
+
: `<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
|
+
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) => {
|
|
100
|
+
const acts = actionsLigne(x, l, filtres);
|
|
101
|
+
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>`;
|
|
106
|
+
}).join('');
|
|
107
|
+
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>`;
|
|
109
|
+
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
|
+
const exp = x.liens.export ? ` <a href="${esc(url(x.liens.export, garder(filtres)))}">${esc(x.tx('liste.exporter'))}</a>` : '';
|
|
111
|
+
return `${form}${table}<div class="fl-pied">${pages}${exp}</div>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function alertesHtml(x, vm) {
|
|
115
|
+
const A = vm.alertes;
|
|
116
|
+
if (!A.lignes.length) return `<p class="fl-vide">${esc(x.tx('alertes.vide', { n: x.nf(A.jours) }))}</p>`;
|
|
117
|
+
const ico = (a) => (a.genre === 'entretien' ? 'cle' : a.type === 'assurance' ? 'document' : 'calendrier');
|
|
118
|
+
return `<ul class="fl-alertes">${A.lignes.map((a) => `<li class="${a.n < 0 ? 'fl-retard' : ''}">${x.k.icone(ico(a), 18)}<span>${esc(x.jj(a.date))}</span>`
|
|
119
|
+
+ `<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
|
+
}
|
|
121
|
+
|
|
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
|
+
}
|
|
128
|
+
|
|
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); });
|
|
131
|
+
return x.k.courbes(mois, [{ libelle: x.tx('graphe.kmSerie'), couleur: '#2878E0', valeurs: km.valeurs }])
|
|
132
|
+
+ `<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
|
+
+ `<tbody><tr><th scope="row">km</th>${km.valeurs.map((v) => `<td>${x.nf(v)}</td>`).join('')}</tr></tbody></table>`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
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>`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function topHtml(x, vm) {
|
|
143
|
+
const act = vm.avecActivite;
|
|
144
|
+
return `<div class="fl-defile"><table class="${esc(x.c.tableau)}"><thead><tr><th scope="col">#</th><th scope="col">${esc(x.tx('col.plaque'))}</th><th scope="col">${esc(x.tx('col.type'))}</th><th scope="col">${esc(x.tx('graphe.kmMois'))}</th>`
|
|
145
|
+
+ `${act ? `<th scope="col">${esc(x.tx('graphe.tournees'))}</th><th scope="col">${esc(x.tx('graphe.colis'))}</th>` : ''}</tr></thead><tbody>`
|
|
146
|
+
+ vm.top.map((t, i) => `<tr><td class="num">${i + 1}</td><td class="fl-plaque">${esc(t.plaque)}</td><td class="fl-nowrap">${esc(x.lib('type', t.type))}</td><td>${x.nf(t.km)}</td>${act ? `<td>${x.nf(t.tournees ?? 0)}</td><td>${x.nf(t.colis ?? 0)}</td>` : ''}</tr>`).join('')
|
|
147
|
+
+ '</tbody></table></div>';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function rapidesHtml(x, vm) {
|
|
151
|
+
const L = x.liens, id = vm.fiche?.vehicule.id, base = L.base || '';
|
|
152
|
+
const items = [
|
|
153
|
+
x.peutGerer && L.nouveau && [L.nouveau, 'nouveau', 'plein', 'plus'],
|
|
154
|
+
x.peutGerer && L.affecter && id && [L.affecter(id), 'action.affecter', 'vert', 'utilisateur'],
|
|
155
|
+
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'],
|
|
157
|
+
L.tournee && [L.tournee, 'action.tournee', 'bleu', 'calendrier'],
|
|
158
|
+
id && [url(base, { ...garder(vm.filtres), v: id, onglet: 'historique' }), 'action.historique', 'rouge', 'horloge'],
|
|
159
|
+
].filter(Boolean);
|
|
160
|
+
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
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── La fiche ────────────────────────────────────────────────────────────────────────────────────────────────
|
|
164
|
+
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>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function informations(x, f) {
|
|
170
|
+
const p = f.prochain;
|
|
171
|
+
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
|
+
: `${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'));
|
|
181
|
+
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))],
|
|
183
|
+
[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
|
+
[x.tx('info.miseEnService'), esc(x.jj(f.miseEnService))], [x.tx('info.chauffeur'), ch],
|
|
185
|
+
[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]),
|
|
187
|
+
];
|
|
188
|
+
return `<dl class="fl-infos ${esc(x.c.fiche)}">${lignes.map(([l, v]) => `<dt>${esc(l)}</dt><dd>${v}</dd>`).join('')}</dl>`;
|
|
189
|
+
}
|
|
190
|
+
|
|
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>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
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) => [esc(x.jj(h.le)), badgeStatut(x, h.de), badgeStatut(x, h.vers), esc(h.motif ?? '—'), esc(h.agent ?? '—')]);
|
|
223
|
+
lignes.push([esc(x.jj(f.vehicule.inscritLe)), '—', badgeStatut(x, 'en_service'), esc(T('historique.inscrit')), esc(f.vehicule.inscritPar ?? '—')]);
|
|
224
|
+
return tableau(x, [T('historique.le'), T('historique.de'), T('historique.vers'), T('historique.motif'), T('historique.par')], lignes, T('historique.vide'));
|
|
225
|
+
}
|
|
226
|
+
return informations(x, f);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** La fiche d'un véhicule : en-tête, bandeau « peut partir », onglets en liens GET, contenu de l'onglet. */
|
|
230
|
+
export function rendreFiche(f, opts = {}) {
|
|
231
|
+
const x = contexte(opts);
|
|
232
|
+
if (!f) return `<p class="fl-vide">${esc(x.tx('detail.aucun'))}</p>`;
|
|
233
|
+
const base = x.liens.base || '', keep = garder(opts.filtres || {});
|
|
234
|
+
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>`;
|
|
235
|
+
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>` : '';
|
|
236
|
+
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>`;
|
|
237
|
+
return `${tete}${bandeau(x, f.depart)}${onglets}${contenuOnglet(x, f)}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Le tableau de bord d'ECR-16. Rend { titre, sousTitre, outils, corps } : l'application les pose dans son gabarit
|
|
242
|
+
* (zone de titre et corps). opts : { kit?, t?, locale?, liens?: { base, nouveau?, modifier?(id), affecter?(id),
|
|
243
|
+
* planifier?(id), tournee?, alertes?, export? }, droits?: { gerer }, declarations? }
|
|
244
|
+
*/
|
|
245
|
+
export function rendreTableau(vm, opts = {}) {
|
|
246
|
+
const x = contexte(opts);
|
|
247
|
+
const locale = opts.locale || 'fr-FR';
|
|
248
|
+
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>` : '';
|
|
249
|
+
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);
|
|
250
|
+
const voirTout = x.liens.alertes ? `<a href="${esc(x.liens.alertes)}">${esc(x.tx('alertes.voirTout'))}</a>` : '';
|
|
251
|
+
const segments = vm.repartition.map((r, i) => ({ libelle: x.lib('type', r.type), valeur: r.n, couleur: COULEURS.types[i % COULEURS.types.length] }));
|
|
252
|
+
const corps = `<div class="fl-ecran">${indicateurs(x, vm.indicateurs)}
|
|
253
|
+
<div class="fl-large"><div class="fl-pile">${carte('liste.titre', 'liste', liste(x, vm))}
|
|
254
|
+
<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>
|
|
255
|
+
<div class="fl-c2">${carte('graphe.carte', 'epingle', parSiteHtml(x, vm), { sous: 'graphe.carteSous' })}${carte('graphe.top', 'camion', topHtml(x, vm))}</div></div>
|
|
256
|
+
<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>`;
|
|
257
|
+
return { titre: x.tx('titre'), sousTitre: x.tx('sousTitre'), outils, corps };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** La liste en CSV (séparateur « ; », guillemets doublés) : construire le vue-modèle avec parPage: Infinity. */
|
|
261
|
+
export function exporterCsv(vm, opts = {}) {
|
|
262
|
+
const x = contexte(opts);
|
|
263
|
+
const champ = (v) => { const s = String(v ?? ''); return /[;"\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; };
|
|
264
|
+
const ent = ['plaque', 'type', 'capacite', 'region', 'station', 'chauffeur', 'km', 'statut', 'depart'].map((c) => x.tx(`col.${c}`));
|
|
265
|
+
const lignes = vm.liste.lignes.map((l) => [l.plaque, x.lib('type', l.type), l.capacite.kg ?? '', l.region ?? '', l.site ?? '', l.conducteur?.nom ?? '', l.km ?? '',
|
|
266
|
+
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(' ; ')]);
|
|
267
|
+
return `${[ent, ...lignes].map((l) => l.map(champ).join(';')).join('\n')}\n`;
|
|
268
|
+
}
|
package/src/textes.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @mostajs/flotte-ui — les textes des écrans : le français de référence, traduit par l'application.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// Aucune chaîne en dur dans le rendu (DEVRULES §11.5) : tout passe par texte(cle, t), qui demande d'abord
|
|
5
|
+
// `flotte.ecran.<cle>` à la traduction de l'application et se replie sur ce catalogue. Les CODES de la flotte
|
|
6
|
+
// (statut, motif…) ne sont pas ici : ils passent par libelle() de @mostajs/flotte (`flotte.<famille>.<code>`).
|
|
7
|
+
|
|
8
|
+
export const TEXTES = Object.freeze({
|
|
9
|
+
titre: 'Véhicules et gestion de la flotte',
|
|
10
|
+
sousTitre: 'Gérez vos véhicules, chauffeurs, entretiens, documents et leur affectation aux tournées et transferts.',
|
|
11
|
+
rechercheGlobale: 'Rechercher un véhicule, un chauffeur, une plaque, une tournée…',
|
|
12
|
+
nouveau: 'Nouveau véhicule',
|
|
13
|
+
kpi: {
|
|
14
|
+
vehicules: 'Véhicules', chauffeurs: 'Chauffeurs', enService: 'En service', enMaintenance: 'En maintenance',
|
|
15
|
+
documentsExpires: 'Documents expirés', vsPeriode: 'vs période précédente', nouveau: 'nouveau',
|
|
16
|
+
},
|
|
17
|
+
liste: {
|
|
18
|
+
titre: 'Liste des véhicules', recherche: 'Rechercher un véhicule, une plaque, un chauffeur…', tousTypes: 'Tous les types',
|
|
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)',
|
|
21
|
+
},
|
|
22
|
+
col: {
|
|
23
|
+
numero: '#', plaque: 'Plaque', type: 'Type', capacite: 'Capacité', region: 'Région', station: 'Station', chauffeur: 'Chauffeur',
|
|
24
|
+
km: 'Kilométrage', statut: 'Statut', depart: 'Départ', actions: 'Actions',
|
|
25
|
+
},
|
|
26
|
+
action: {
|
|
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}',
|
|
29
|
+
},
|
|
30
|
+
depart: { oui: 'Peut partir aujourd’hui', non: 'Ne peut pas partir', ouiCourt: 'peut partir', nonCourt: 'ne peut pas partir' },
|
|
31
|
+
documentExpire: 'document expiré',
|
|
32
|
+
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' },
|
|
34
|
+
info: {
|
|
35
|
+
plaque: 'Plaque d’immatriculation', type: 'Type', capacite: 'Capacité', region: 'Région', station: 'Station principale', km: 'Kilométrage',
|
|
36
|
+
miseEnService: 'Date de mise en service', chauffeur: 'Chauffeur actuel', consommation: 'Consommation moyenne', prochainEntretien: 'Prochain entretien',
|
|
37
|
+
valideJusquau: 'Valide jusqu’au {date}', expireeLe: 'Expirée le {date}', expireBientot: 'Expire le {date}', absent: 'Aucun document',
|
|
38
|
+
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: '—',
|
|
40
|
+
},
|
|
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
|
+
entretiens: {
|
|
43
|
+
plans: 'Plans d’entretien', plan: 'Plan', pas: 'Tous les', prochain: 'Prochain', interventions: 'Interventions', numero: 'N°', nature: 'Nature',
|
|
44
|
+
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',
|
|
46
|
+
},
|
|
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.' },
|
|
49
|
+
alertes: { titre: 'Alertes et échéances', voirTout: 'Voir tout', vide: 'Aucune échéance dans les {n} jours.', entretien: 'Entretien périodique' },
|
|
50
|
+
graphe: {
|
|
51
|
+
types: 'Répartition des véhicules par type', vehicules: 'véhicules', statuts: 'Statut des véhicules', km: 'Évolution du kilométrage',
|
|
52
|
+
kmSous: '(6 derniers mois)', kmSerie: 'Kilomètres parcourus', mois: 'Mois', carte: 'Véhicules par station', carteSous: '(station de rattachement)',
|
|
53
|
+
top: 'Top 5 des véhicules par activité', kmMois: 'Km (30 j)', tournees: 'Tournées', colis: 'Colis', total: 'Total',
|
|
54
|
+
},
|
|
55
|
+
rapides: { titre: 'Actions rapides' },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const lire = (o, cle) => cle.split('.').reduce((x, k) => (x && typeof x === 'object' ? x[k] : undefined), o);
|
|
59
|
+
const remplir = (s, vars) => s.replace(/\{(\w+)\}/g, (m, k) => (vars && vars[k] !== undefined && vars[k] !== null ? String(vars[k]) : m));
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Le texte d'une clé de l'écran. `t` (facultative) est la traduction de l'application, appelée avec
|
|
63
|
+
* `flotte.ecran.<cle>` ; si elle rend la clé nue, ou rien, le français du module s'applique.
|
|
64
|
+
*/
|
|
65
|
+
export function texte(cle, t, vars) {
|
|
66
|
+
const pleine = `flotte.ecran.${cle}`;
|
|
67
|
+
if (typeof t === 'function') {
|
|
68
|
+
const tr = t(pleine, vars);
|
|
69
|
+
if (typeof tr === 'string' && tr && tr !== pleine) return tr;
|
|
70
|
+
}
|
|
71
|
+
const s = lire(TEXTES, cle);
|
|
72
|
+
return typeof s === 'string' ? remplir(s, vars) : cle;
|
|
73
|
+
}
|
package/src/vue.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// @mostajs/flotte-ui — les vue-modèles : l'état du parc LU dans l'API de @mostajs/flotte, mis en forme pour l'écran.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// Lecture seule. Toute RÈGLE (expiré, prochain entretien, peut partir) vient du moteur ; ici, seulement des agrégats
|
|
5
|
+
// d'affichage : comptes, parts, variations sur la période, km par mois, top 5. Les vue-modèles portent des CODES :
|
|
6
|
+
// la langue n'intervient qu'au rendu.
|
|
7
|
+
import { regles as R } from '@mostajs/flotte';
|
|
8
|
+
|
|
9
|
+
const STATUTS_PARC = ['en_service', 'a_l_atelier', 'immobilise'];
|
|
10
|
+
const jourDe = (d) => (typeof d === 'string' ? d : d.toISOString().slice(0, 10));
|
|
11
|
+
const pct = (n, total) => (total ? Math.round((100 * n) / total) : 0);
|
|
12
|
+
const nomDe = (conducteurs, id) => {
|
|
13
|
+
if (!id) return null;
|
|
14
|
+
const c = typeof conducteurs === 'function' ? conducteurs(id) : conducteurs?.[id];
|
|
15
|
+
return { id, nom: c?.nom ?? id, telephone: c?.telephone ?? null };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Le statut d'un véhicule à une date passée, rejoué sur son historique ; null s'il n'était pas encore inscrit. */
|
|
19
|
+
function statutAu(v, jour) {
|
|
20
|
+
if (v.inscritLe && v.inscritLe > jour) return null;
|
|
21
|
+
let s = 'en_service';
|
|
22
|
+
for (const pas of v.historique || []) if (pas.le <= jour) s = pas.vers;
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
/** Une échéance déclarée a-t-elle expiré à `jour` ? On lit celle qui faisait foi à cette date. */
|
|
26
|
+
const expireeAu = (echeances, jour) => [...new Set(echeances.map((e) => e.type))]
|
|
27
|
+
.some((type) => { const e = R.echeanceCourante(echeances.filter((x) => x.debut <= jour), type); return Boolean(e && e.fin < jour); });
|
|
28
|
+
const conducteurAu = (affectations, jour) => affectations.find((a) => a.du <= jour && (!a.au || a.au > jour))?.conducteur ?? null;
|
|
29
|
+
/** Les km parcourus de `du` (exclu) à `au` : différence des derniers relevés, ou depuis le premier relevé de la période. */
|
|
30
|
+
function kmEntre(releves, du, au) {
|
|
31
|
+
const fin = R.dernierReleve(releves, au);
|
|
32
|
+
const debut = R.dernierReleve(releves, du) || releves.find((r) => r.le > du && r.le <= au);
|
|
33
|
+
return fin && debut ? Math.max(0, fin.km - debut.km) : 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** La variation d'un effectif ; `pct` null quand la base est nulle (pas de pourcentage depuis zéro). */
|
|
37
|
+
function variation(n, avant, hausseFavorable) {
|
|
38
|
+
if (n === avant) return { sens: 'stable', pct: 0, favorable: null };
|
|
39
|
+
const sens = n > avant ? 'hausse' : 'baisse';
|
|
40
|
+
return { sens, pct: avant ? Math.round((100 * (n - avant)) / avant) : null, favorable: (sens === 'hausse') === hausseFavorable };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Tout ce que l'écran lit d'un véhicule, une fois. */
|
|
44
|
+
async function lireVehicule(flotte, v, jour) {
|
|
45
|
+
const [releves, echeances, affectations, depart] = await Promise.all([
|
|
46
|
+
flotte.releves(v.id), flotte.echeances(v.id), flotte.affectations(v.id), flotte.peutPartir(v.id, { date: jour }),
|
|
47
|
+
]);
|
|
48
|
+
return { v, releves, echeances, affectations, depart, km: R.dernierReleve(releves, jour)?.km ?? null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const plat = (s) => String(s ?? '').toLowerCase().replace(/[\s-]+/g, '');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Le tableau de bord d'ECR-16.
|
|
55
|
+
* opts : { declarations, date?, filtres?: { q, type, statut, region, page, v, onglet }, sites?: { code: { libelle, region } },
|
|
56
|
+
* conducteurs?: objet ou fonction id → { nom, telephone }, activite?: async (id, { du, au }) → { tournees, colis },
|
|
57
|
+
* perimetre?: [codes de site], periodeJours? (30), parPage? (10), alertesMax? (5) }
|
|
58
|
+
*/
|
|
59
|
+
export async function construireTableau(flotte, opts = {}) {
|
|
60
|
+
const { declarations = {}, sites = {}, conducteurs = {}, activite, perimetre = null, periodeJours = 30, parPage = 10, alertesMax = 5 } = opts;
|
|
61
|
+
const jour = jourDe(opts.date ?? new Date());
|
|
62
|
+
const avant = R.ajouterJours(jour, -periodeJours);
|
|
63
|
+
const f0 = opts.filtres || {};
|
|
64
|
+
const filtres = { q: String(f0.q ?? '').trim(), type: f0.type || '', statut: f0.statut || '', region: f0.region || '', page: Math.max(1, Number.parseInt(f0.page, 10) || 1), v: f0.v || '', onglet: f0.onglet || '' };
|
|
65
|
+
const dansPerimetre = (v) => !perimetre || perimetre.includes(v.site);
|
|
66
|
+
const regionDe = (v) => sites[v.site]?.region ?? null;
|
|
67
|
+
|
|
68
|
+
// Le parc du périmètre, réformés compris : il faut connaître le parc d'il y a `periodeJours` jours.
|
|
69
|
+
const tous = (await flotte.vehicules({ reformes: true })).filter(dansPerimetre);
|
|
70
|
+
const lus = await Promise.all(tous.map((v) => lireVehicule(flotte, v, jour)));
|
|
71
|
+
const parc = lus.filter((x) => x.v.statut !== 'reforme');
|
|
72
|
+
|
|
73
|
+
// ── Indicateurs (16.04–16.08) ──
|
|
74
|
+
const passe = lus.map((x) => ({ x, s: statutAu(x.v, avant) })).filter(({ s }) => s && s !== 'reforme');
|
|
75
|
+
const compte = (liste, pred) => liste.filter(pred).length;
|
|
76
|
+
const n = parc.length, n0 = passe.length;
|
|
77
|
+
const expires = compte(parc, (x) => expireeAu(x.echeances, jour));
|
|
78
|
+
const chauffeurs = new Set(parc.map((x) => conducteurAu(x.affectations, jour)).filter(Boolean)).size;
|
|
79
|
+
const chauffeurs0 = new Set(passe.map(({ x }) => conducteurAu(x.affectations, avant)).filter(Boolean)).size;
|
|
80
|
+
const parStatut = (s) => compte(parc, (x) => x.v.statut === s), parStatut0 = (s) => compte(passe, (p) => p.s === s);
|
|
81
|
+
const indicateurs = {
|
|
82
|
+
vehicules: { n, variation: variation(n, n0, true) },
|
|
83
|
+
chauffeurs: { n: chauffeurs, variation: variation(chauffeurs, chauffeurs0, true) },
|
|
84
|
+
enService: { n: parStatut('en_service'), part: pct(parStatut('en_service'), n), variation: variation(parStatut('en_service'), parStatut0('en_service'), true) },
|
|
85
|
+
enMaintenance: { n: parStatut('a_l_atelier'), part: pct(parStatut('a_l_atelier'), n), variation: variation(parStatut('a_l_atelier'), parStatut0('a_l_atelier'), false) },
|
|
86
|
+
documentsExpires: { n: expires, part: pct(expires, n), variation: variation(expires, compte(passe, ({ x }) => expireeAu(x.echeances, avant)), false) },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// ── Liste (16.09–16.11) : le texte cherche aussi le NOM du chauffeur, que le moteur ne connaît pas ──
|
|
90
|
+
const parTexte = filtres.q ? new Set((await flotte.vehicules({ q: filtres.q, reformes: true })).map((v) => v.id)) : null;
|
|
91
|
+
const ligne = (x) => {
|
|
92
|
+
const c = nomDe(conducteurs, conducteurAu(x.affectations, jour));
|
|
93
|
+
return {
|
|
94
|
+
id: x.v.id, code: x.v.code, plaque: x.v.immatriculation, type: x.v.type, site: x.v.site, region: regionDe(x.v),
|
|
95
|
+
capacite: { kg: x.v.attributs?.chargeUtileKg ?? null, m3: x.v.attributs?.volumeM3 ?? null }, conducteur: c, km: x.km, statut: x.v.statut,
|
|
96
|
+
documentExpire: expireeAu(x.echeances, jour), depart: x.depart,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const retenus = (filtres.statut === 'reforme' ? lus : parc).filter((x) => (!filtres.statut || x.v.statut === filtres.statut)
|
|
100
|
+
&& (!filtres.type || x.v.type === filtres.type) && (!filtres.region || regionDe(x.v) === filtres.region)
|
|
101
|
+
&& (!parTexte || parTexte.has(x.v.id) || plat(nomDe(conducteurs, conducteurAu(x.affectations, jour))?.nom).includes(plat(filtres.q))));
|
|
102
|
+
const pages = Number.isFinite(parPage) ? Math.max(1, Math.ceil(retenus.length / parPage)) : 1;
|
|
103
|
+
const page = Math.min(filtres.page, pages);
|
|
104
|
+
const tranche = Number.isFinite(parPage) ? retenus.slice((page - 1) * parPage, page * parPage) : retenus;
|
|
105
|
+
const liste = { lignes: tranche.map(ligne), total: retenus.length, page, pages, parPage };
|
|
106
|
+
|
|
107
|
+
// ── Alertes (16.19) : échéances et prochains entretiens à moins de alerteJours, expirés compris ──
|
|
108
|
+
const alerteJours = declarations.alerteJours ?? 30, limite = R.ajouterJours(jour, alerteJours);
|
|
109
|
+
const ids = new Set(parc.map((x) => x.v.id)), plaque = Object.fromEntries(parc.map((x) => [x.v.id, x.v.immatriculation]));
|
|
110
|
+
const alertes = (await flotte.aEcheance({ dans: alerteJours, date: jour })).filter((e) => ids.has(e.vehicule))
|
|
111
|
+
.map((e) => ({ date: e.fin, n: e.n, genre: 'echeance', type: e.type, vehicule: e.vehicule, plaque: plaque[e.vehicule], expiree: e.expiree }));
|
|
112
|
+
for (const x of parc) {
|
|
113
|
+
for (const p of await flotte.prochainsEntretiens(x.v.id, { date: jour })) {
|
|
114
|
+
if (p.le && p.le <= limite) alertes.push({ date: p.le, n: p.dans, genre: 'entretien', type: p.libelle, vehicule: x.v.id, plaque: x.v.immatriculation, expiree: p.dans < 0, estime: p.estime });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
alertes.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
|
|
118
|
+
|
|
119
|
+
// ── Répartitions, kilométrage, stations, top 5 (16.20–16.24) ──
|
|
120
|
+
const repartition = Object.keys(declarations.types || {}).map((type) => ({ type, n: compte(parc, (x) => x.v.type === type) })).filter((r) => r.n);
|
|
121
|
+
const statuts = { ...Object.fromEntries(STATUTS_PARC.map((s) => [s, parStatut(s)])), total: n };
|
|
122
|
+
const mois = [];
|
|
123
|
+
for (let i = 5; i >= 0; i--) { const d = new Date(`${jour.slice(0, 7)}-01T00:00:00Z`); d.setUTCMonth(d.getUTCMonth() - i); mois.push(d.toISOString().slice(0, 7)); }
|
|
124
|
+
const fin = (m) => { 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; };
|
|
125
|
+
const veille = (m) => R.ajouterJours(`${m}-01`, -1);
|
|
126
|
+
const km = { mois, valeurs: mois.map((m) => parc.reduce((s, x) => s + kmEntre(x.releves, veille(m), fin(m)), 0)) };
|
|
127
|
+
const parSiteMap = new Map();
|
|
128
|
+
for (const x of parc) {
|
|
129
|
+
const k = x.v.site ?? '';
|
|
130
|
+
if (!parSiteMap.has(k)) parSiteMap.set(k, { site: x.v.site ?? null, libelle: sites[k]?.libelle ?? x.v.site ?? null, region: sites[k]?.region ?? null, en_service: 0, a_l_atelier: 0, immobilise: 0, total: 0 });
|
|
131
|
+
const s = parSiteMap.get(k); s[x.v.statut] += 1; s.total += 1;
|
|
132
|
+
}
|
|
133
|
+
const parSite = [...parSiteMap.values()].sort((a, b) => String(a.site).localeCompare(String(b.site)));
|
|
134
|
+
const top = await Promise.all(parc.map(async (x) => ({
|
|
135
|
+
vehicule: x.v.id, code: x.v.code, plaque: x.v.immatriculation, type: x.v.type, km: kmEntre(x.releves, avant, jour),
|
|
136
|
+
...(activite ? await activite(x.v.id, { du: avant, au: jour }) : {}),
|
|
137
|
+
})));
|
|
138
|
+
top.sort((a, b) => b.km - a.km || (a.code < b.code ? -1 : 1));
|
|
139
|
+
|
|
140
|
+
const choisi = filtres.v && lus.some((x) => x.v.id === filtres.v) ? filtres.v : liste.lignes[0]?.id;
|
|
141
|
+
const fiche = choisi ? await construireFiche(flotte, choisi, { ...opts, date: jour, onglet: filtres.onglet }) : null;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
date: jour, periodeJours, filtres, indicateurs, liste,
|
|
145
|
+
options: { types: Object.keys(declarations.types || {}), statuts: [...STATUTS_PARC, 'reforme'], regions: [...new Set(Object.values(sites).map((s) => s.region).filter(Boolean))].sort() },
|
|
146
|
+
alertes: { lignes: alertes.slice(0, alertesMax), total: alertes.length, jours: alerteJours },
|
|
147
|
+
repartition, statuts, km, parSite, top: top.slice(0, 5), avecActivite: typeof activite === 'function', fiche,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const ONGLETS = Object.freeze(['informations', 'documents', 'entretiens', 'affectations', 'historique']);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 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? }
|
|
156
|
+
*/
|
|
157
|
+
export async function construireFiche(flotte, id, opts = {}) {
|
|
158
|
+
const { declarations = {}, sites = {}, conducteurs = {} } = opts;
|
|
159
|
+
const jour = jourDe(opts.date ?? new Date());
|
|
160
|
+
const onglet = ONGLETS.includes(opts.onglet) ? opts.onglet : 'informations';
|
|
161
|
+
const v = await flotte.lire(id);
|
|
162
|
+
const x = await lireVehicule(flotte, v, jour);
|
|
163
|
+
const alerteJours = declarations.alerteJours ?? 30;
|
|
164
|
+
const echeances = Object.entries(declarations.echeances || {}).map(([type, d]) => {
|
|
165
|
+
const courante = R.echeanceCourante(x.echeances.filter((e) => e.debut <= jour), type) || R.echeanceCourante(x.echeances, type);
|
|
166
|
+
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' };
|
|
168
|
+
});
|
|
169
|
+
const fiche = {
|
|
170
|
+
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
|
+
capacite: { kg: v.attributs?.chargeUtileKg ?? null, m3: v.attributs?.volumeM3 ?? null },
|
|
172
|
+
conducteur: nomDe(conducteurs, conducteurAu(x.affectations, jour)), km: x.km, depart: x.depart,
|
|
173
|
+
consommation: (await flotte.consommation(id)).lPour100Km, prochain: await flotte.prochainEntretien(id, { date: jour }), echeances,
|
|
174
|
+
miseEnService: v.acquisition?.le ?? v.inscritLe ?? null,
|
|
175
|
+
};
|
|
176
|
+
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 }));
|
|
179
|
+
}
|
|
180
|
+
if (onglet === 'entretiens') {
|
|
181
|
+
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)) };
|
|
183
|
+
}
|
|
184
|
+
if (onglet === 'affectations') fiche.affectations = [...x.affectations].reverse().map((a) => ({ ...a, conducteur: nomDe(conducteurs, a.conducteur) }));
|
|
185
|
+
if (onglet === 'historique') fiche.historique = [...(v.historique || [])].reverse();
|
|
186
|
+
return fiche;
|
|
187
|
+
}
|