@mostajs/flotte 0.1.0 → 0.4.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 +29 -1
- package/README.md +5 -3
- package/llms.txt +12 -5
- package/package.json +4 -1
- package/src/flotte.js +76 -8
- package/src/regles.js +28 -0
- package/src/schemas.js +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,35 @@
|
|
|
3
3
|
Format [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) · [SemVer](https://semver.org/lang/fr/).
|
|
4
4
|
Auteur : Dr Hamid MADANI <drmdh@msn.com>
|
|
5
5
|
|
|
6
|
-
## [0.
|
|
6
|
+
## [0.4.0] — non publiée
|
|
7
|
+
### Added
|
|
8
|
+
- `poserEcheance` garde `reference` (n° de police, de procès-verbal) et `organisme` ; `piece` accepte un objet
|
|
9
|
+
(`{ nom, empreinte, deposePar, deposeLe }`) : colonne JSON. Essai T-FLO-18.
|
|
10
|
+
- Un relevé garde ses `avertissements` (saut anormal), lisibles dans son historique. Essai T-FLO-19.
|
|
11
|
+
- `modifier(id, champs)` : les champs descriptifs (plaque contrôlée, attributs validés par le type, site, mode,
|
|
12
|
+
`chassis`, `miseEnService`…), jamais le code, le type ni le statut ; inscrit à l'historique
|
|
13
|
+
(`{ le, agent, modification: { champs } }`). `inscrire` accepte `chassis` et `miseEnService`. Essai T-FLO-20.
|
|
14
|
+
- `vehiculeDe(conducteur)` : l'affectation en cours d'un conducteur. Essai T-FLO-21.
|
|
15
|
+
- `pleins(v)` : la liste des pleins, par date (onglet « Pleins et relevés »). Essai T-FLO-22.
|
|
16
|
+
Demandés par les planches A à F de Guerway (`GUERWAY/docs/L0/maquette-flotte/`, inventaire partie 9).
|
|
17
|
+
### Fixed
|
|
18
|
+
- Le manifeste de `register` annonçait la version 0.2.0 dans la 0.3.0 publiée ; il suit désormais la version.
|
|
19
|
+
|
|
20
|
+
## [0.3.0] — 2026-10-24
|
|
21
|
+
### Added
|
|
22
|
+
- `vehicules({ statut?, type?, site?, q?, reformes? })` : la liste du parc, rangée par code ; `statut` est un code
|
|
23
|
+
ou une liste ; `q` est cherché dans le code, la plaque, la marque et le modèle, sans casse, espaces et tirets
|
|
24
|
+
indifférents ; les réformés n'y sont que sur demande. Demandé par le tableau de bord ECR-16 de Guerway, que
|
|
25
|
+
`@mostajs/flotte-ui` construit sans accéder aux dépôts. Essai T-FLO-17.
|
|
26
|
+
|
|
27
|
+
## [0.2.0] — jamais publiée seule (contenue dans la 0.3.0)
|
|
28
|
+
### Added
|
|
29
|
+
- `prochainsEntretiens(v, { date? })` et `prochainEntretien(v)` : pour chaque plan, l'échéance de chaque pas (km,
|
|
30
|
+
jours) et la date du premier atteint, rangés du plus urgent au moins urgent. Un pas en km devient une date au rythme
|
|
31
|
+
relevé (`estime: true`). Demandé par l'écran ECR-16 de Guerway (« Prochain entretien : date, dans N jours »).
|
|
32
|
+
- Règles pures `regles.prochainEntretien` et `regles.kmParJour` (rythme sur les 90 derniers jours). Essai T-FLO-16.
|
|
33
|
+
|
|
34
|
+
## [0.1.0] — 2026-10-24
|
|
7
35
|
### Added
|
|
8
36
|
- `createFlotte` : registre par types déclarés, cycle en service / immobilisé / à l'atelier / réformé
|
|
9
37
|
(`@mostajs/statemachine`), échéances typées, plans d'entretien au km et à la date, interventions numérotées,
|
package/README.md
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
Le carnet du parc. Il répond à une question : **ce véhicule peut-il partir aujourd'hui, avec ce conducteur, et
|
|
6
6
|
combien coûte-t-il ?**
|
|
7
7
|
|
|
8
|
-
- un registre de véhicules, chacun d'un **type déclaré** par l'application (fourgon, moto, autocar…)
|
|
8
|
+
- un registre de véhicules, chacun d'un **type déclaré** par l'application (fourgon, moto, autocar…), et sa liste
|
|
9
|
+
filtrée par statut, type, site ou texte ;
|
|
9
10
|
- un cycle : en service, immobilisé, à l'atelier, réformé ; chaque passage est tracé, avec son motif ;
|
|
10
11
|
- des échéances (assurance, contrôle technique…) ; la plus récente fait foi, les anciennes restent lisibles ;
|
|
11
|
-
- l'entretien au kilométrage **et** à la date, dû au premier des deux atteint, avec une tolérance ;
|
|
12
|
+
- l'entretien au kilométrage **et** à la date, dû au premier des deux atteint, avec une tolérance ; le prochain
|
|
13
|
+
entretien est daté, et un pas en km devient une date au rythme relevé ;
|
|
12
14
|
- des relevés de compteur : un recul est refusé, sauf correction motivée ; un saut anormal est signalé ;
|
|
13
15
|
- des pleins, et la consommation mesurée entre deux pleins complets ;
|
|
14
16
|
- des affectations de conducteurs (identifiant opaque) ;
|
|
@@ -47,7 +49,7 @@ alertes (`notifications`), fichiers scannés (`blob-store`). `peutPartir` dit **
|
|
|
47
49
|
## Vérifier
|
|
48
50
|
|
|
49
51
|
```bash
|
|
50
|
-
npm test # T-FLO-1 à
|
|
52
|
+
npm test # T-FLO-1 à 16 (@mostajs/mjs-unit)
|
|
51
53
|
node examples/verifier.mjs # un fourgon, de l'inscription au coût au km
|
|
52
54
|
```
|
|
53
55
|
|
package/llms.txt
CHANGED
|
@@ -19,19 +19,26 @@ EXPORTS
|
|
|
19
19
|
createMemoryRepositories() 7 dépôts : vehicules, echeances, plans, interventions, releves, pleins, affectations
|
|
20
20
|
ErreurFlotte { code, statut } (400 donnée, 404 inconnu, 409 doublon/recul/close/ouverte/occupé, 422 transition)
|
|
21
21
|
CODES, LIBELLES, libelle(famille, code, t?, vars?) ; STATUTS, MOTIFS ; PERMISSIONS (flotte:domaine:action)
|
|
22
|
-
regles (pur) : etatEntretien, consommation, evaluerDepart, cout, echeanceCourante, jours, ajouterJours
|
|
22
|
+
regles (pur) : etatEntretien, prochainEntretien, kmParJour, consommation, evaluerDepart, cout, echeanceCourante, jours, ajouterJours
|
|
23
23
|
register (7 tables ; aussi './register'), SCHEMAS, validerDeclarations
|
|
24
24
|
|
|
25
25
|
API (async ; ctx = { agent })
|
|
26
26
|
inscrire({ code, immatriculation, type, attributs, marque?, site?, mode? }, ctx) ; lire(id)
|
|
27
|
+
vehicules({ statut?, type?, site?, q?, reformes? }) (0.3.0) -> copies rangées par code ; réformés exclus sauf demande
|
|
28
|
+
modifier(id, { immatriculation?, marque?, attributs?, site?, mode?, chassis?, miseEnService?, … }, ctx) (0.4.0) jamais code/type/statut ;
|
|
29
|
+
historique += { le, agent, modification: { champs } } (une entrée SANS `vers` : ce n'est pas un changement de statut)
|
|
30
|
+
vehiculeDe(conducteur) (0.4.0) -> affectation en cours ou null
|
|
27
31
|
changerStatut(id, 'immobiliser'|'atelier'|'remettre'|'reformer', { agent, motif }) motif requis : immobiliser, reformer
|
|
28
|
-
poserEcheance({ vehicule, type, debut, fin?, cout?, piece? }, ctx) fin absente = debut + periodiciteJours
|
|
32
|
+
poserEcheance({ vehicule, type, debut, fin?, cout?, piece?, reference?, organisme? }, ctx) fin absente = debut + periodiciteJours ;
|
|
33
|
+
piece : chaîne ou { nom, empreinte, deposePar, deposeLe } (0.4.0)
|
|
29
34
|
echeances(v) ; aEcheance({ dans?, date? }) -> [{ id, vehicule, type, fin, n, expiree }]
|
|
30
35
|
planifierEntretien({ vehicule, libelle, tousLesKm?, tousLesJours?, toleranceKm?, toleranceJours? }, ctx) ; plans(v) ; entretiensDus({ date? })
|
|
36
|
+
prochainsEntretiens(v, { date? }) / prochainEntretien(v) (0.2.0) -> { plan, libelle, etat, km: { echeance, restant },
|
|
37
|
+
jours: { echeance, restant }, le, dans, estime } le pas en km devient une date au rythme relevé (estime: true)
|
|
31
38
|
ouvrirIntervention({ vehicule, nature, plan?, description?, ouverteLe? }, ctx) corrective => a_l_atelier
|
|
32
39
|
cloreIntervention(id, { km?, cout?, le? }, ctx) recale le plan ; km => relevé ; close => immuable
|
|
33
|
-
relever(v, { km, le?, source?, correction?: { motif, agent } }, ctx) -> relevé + avertissements (SAUT_ANORMAL)
|
|
34
|
-
saisirPlein({ vehicule, le, litres, montant, km, complet }, ctx) vaut relevé ; consommation(v)
|
|
40
|
+
relever(v, { km, le?, source?, correction?: { motif, agent } }, ctx) -> relevé + avertissements (SAUT_ANORMAL), conservés sur le relevé (0.4.0)
|
|
41
|
+
saisirPlein({ vehicule, le, litres, montant, km, complet }, ctx) vaut relevé ; pleins(v) (0.4.0) ; consommation(v)
|
|
35
42
|
affecter(v, conducteur, ctx) ; liberer(v, ctx) ; affectations(v) ; affectationCourante(v)
|
|
36
43
|
peutPartir(v, { date?, conducteur?, km? }) -> { ok, motifs[], avertissements[] } TOUS les motifs, en OBJETS
|
|
37
44
|
disponibles({ date?, type?, attributs? }) ; cout(v, { du, au }) -> { carburant, entretien, echeances, total, km, parKm, nonComptes }
|
|
@@ -46,5 +53,5 @@ PIÈGES
|
|
|
46
53
|
- L'entité de relevé s'appelle ReleveCompteur (Releve appartient à @mostajs/recette).
|
|
47
54
|
- L'échéance qui fait foi est la plus récente par début ; les anciennes restent lisibles.
|
|
48
55
|
|
|
49
|
-
ESSAIS test-scripts/unit/flotte.test.mjs — T-FLO-1 à
|
|
56
|
+
ESSAIS test-scripts/unit/flotte.test.mjs — T-FLO-1 à 16 ; exemple : node examples/verifier.mjs
|
|
50
57
|
LICENCE AGPL-3.0-or-later · Auteur Dr Hamid MADANI <drmdh@msn.com>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/flotte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Le carnet du parc : registre des véhicules par type déclaré, cycle d'état, échéances, entretien au kilométrage et à la date, relevés, pleins, affectations de conducteurs, coût au km, et la réponse « ce véhicule peut-il partir ? ».",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
@@ -60,8 +60,11 @@
|
|
|
60
60
|
],
|
|
61
61
|
"verbes": [
|
|
62
62
|
"inscrire",
|
|
63
|
+
"vehicules",
|
|
64
|
+
"modifier",
|
|
63
65
|
"changerStatut",
|
|
64
66
|
"peutPartir",
|
|
67
|
+
"prochainEntretien",
|
|
65
68
|
"aEcheance",
|
|
66
69
|
"entretiensDus",
|
|
67
70
|
"saisirPlein",
|
package/src/flotte.js
CHANGED
|
@@ -20,6 +20,10 @@ const exigerNombre = (n, quoi, { min = 0, strict = false } = {}) => {
|
|
|
20
20
|
if (!Number.isFinite(n) || (strict ? n <= min : n < min)) refus('DONNEE', `${quoi} : nombre ${strict ? '>' : '≥'} ${min} attendu`, 400);
|
|
21
21
|
return n;
|
|
22
22
|
};
|
|
23
|
+
/** Un texte facultatif : absent → null, chaîne → nettoyée, autre chose → refus. */
|
|
24
|
+
const texteOuNul = (x, quoi) => { if (x === undefined || x === null || x === '') return null; if (typeof x !== 'string') refus('DONNEE', `${quoi} : texte attendu`, 400); return x.trim() || null; };
|
|
25
|
+
/** Les champs descriptifs d'un véhicule, modifiables par `modifier` (ni le code, ni le type, ni le statut). */
|
|
26
|
+
const MODIFIABLES = ['immatriculation', 'marque', 'modele', 'annee', 'couleur', 'energie', 'attributs', 'site', 'detenteur', 'mode', 'acquisition', 'chassis', 'miseEnService'];
|
|
23
27
|
const correspond = (motif, valeur) => new RegExp(motif.source, motif.flags.replace(/[gy]/g, '')).test(valeur);
|
|
24
28
|
|
|
25
29
|
/**
|
|
@@ -50,6 +54,12 @@ export function createFlotte({
|
|
|
50
54
|
const maj = async (table, doc, champs) => { await P[table].update(doc.id, champs); return { ...doc, ...structuredClone(champs) }; };
|
|
51
55
|
const de = (table, vehicule) => P[table].find((x) => x.vehicule === vehicule);
|
|
52
56
|
|
|
57
|
+
function plaqueValide(saisie) {
|
|
58
|
+
const p = String(saisie).trim().toUpperCase();
|
|
59
|
+
if (D.immatriculation.length && !D.immatriculation.some((m) => correspond(m, p))) refus('IMMATRICULATION', `immatriculation hors des motifs déclarés : ${p}`, 400);
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
async function lire(id) {
|
|
54
64
|
return (await P.vehicules.find((v) => v.id === id))[0] || refus('INCONNU', `véhicule inconnu : ${id}`, 404);
|
|
55
65
|
}
|
|
@@ -79,7 +89,7 @@ export function createFlotte({
|
|
|
79
89
|
return [];
|
|
80
90
|
}
|
|
81
91
|
async function ecrireReleve(vehicule, { km, le, source, correction }, avertissements, agent) {
|
|
82
|
-
const r = await creer('releves', { id: randomUUID(), vehicule, km, le, source, correction: correction ? { motif: correction.motif, agent: correction.agent } : null, saisiPar: agent ?? null });
|
|
92
|
+
const r = await creer('releves', { id: randomUUID(), vehicule, km, le, source, correction: correction ? { motif: correction.motif, agent: correction.agent } : null, saisiPar: agent ?? null, avertissements });
|
|
83
93
|
emettre('releve.saisi', { vehicule, releve: r.id, km, source });
|
|
84
94
|
return { ...r, avertissements };
|
|
85
95
|
}
|
|
@@ -102,6 +112,16 @@ export function createFlotte({
|
|
|
102
112
|
return r;
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
/** Les prochains entretiens d'un véhicule, du plus urgent au moins urgent (sans date : en dernier). */
|
|
116
|
+
async function prochainsEntretiens(vehicule, { date } = {}) {
|
|
117
|
+
await lire(vehicule);
|
|
118
|
+
const jour = date ? exigerJour(date, 'date') : aujourdhui();
|
|
119
|
+
const tous = await releves(vehicule);
|
|
120
|
+
const km = R.dernierReleve(tous, jour)?.km, rythme = R.kmParJour(tous, jour);
|
|
121
|
+
return (await de('plans', vehicule)).map((p) => R.prochainEntretien(p, { km, date: jour, kmParJour: rythme }))
|
|
122
|
+
.sort((a, b) => (a.le === b.le ? 0 : a.le === null ? 1 : b.le === null ? -1 : a.le < b.le ? -1 : 1));
|
|
123
|
+
}
|
|
124
|
+
|
|
105
125
|
async function peutPartir(id, { date, conducteur, km } = {}) {
|
|
106
126
|
const v = await lire(id);
|
|
107
127
|
const jour = date ? exigerJour(date, 'date') : aujourdhui();
|
|
@@ -127,10 +147,8 @@ export function createFlotte({
|
|
|
127
147
|
if (!texte(d.immatriculation)) refus('DONNEE', 'immatriculation requise', 400);
|
|
128
148
|
if (!texte(d.type)) refus('DONNEE', 'type requis', 400);
|
|
129
149
|
const attributs = validerAttributs(D, d.type, d.attributs);
|
|
130
|
-
const code = d.code.trim(), immatriculation = d.immatriculation
|
|
131
|
-
|
|
132
|
-
refus('IMMATRICULATION', `immatriculation hors des motifs déclarés : ${immatriculation}`, 400);
|
|
133
|
-
}
|
|
150
|
+
const code = d.code.trim(), immatriculation = plaqueValide(d.immatriculation);
|
|
151
|
+
const miseEnService = d.miseEnService === undefined || d.miseEnService === null ? null : exigerJour(d.miseEnService, 'miseEnService');
|
|
134
152
|
const mode = d.mode ?? 'propre';
|
|
135
153
|
if (!CODES.mode.includes(mode)) refus('DONNEE', `mode inconnu : ${mode}`, 400);
|
|
136
154
|
if ((await P.vehicules.find((v) => v.code === code || v.immatriculation === immatriculation)).length) {
|
|
@@ -139,24 +157,67 @@ export function createFlotte({
|
|
|
139
157
|
const v = await creer('vehicules', {
|
|
140
158
|
id: randomUUID(), code, immatriculation, type: d.type, marque: d.marque ?? null, modele: d.modele ?? null, annee: d.annee ?? null,
|
|
141
159
|
couleur: d.couleur ?? null, energie: d.energie ?? null, attributs, site: d.site ?? null, detenteur: d.detenteur ?? null, mode,
|
|
142
|
-
acquisition: d.acquisition ?? null, statut: cycle.initial, historique: [], inscritLe: aujourdhui(), inscritPar: agent ?? null,
|
|
160
|
+
acquisition: d.acquisition ?? null, chassis: texteOuNul(d.chassis, 'chassis'), miseEnService, statut: cycle.initial, historique: [], inscritLe: aujourdhui(), inscritPar: agent ?? null,
|
|
143
161
|
});
|
|
144
162
|
emettre('vehicule.inscrit', { vehicule: v.id, code, type: v.type });
|
|
145
163
|
return v;
|
|
146
164
|
},
|
|
147
165
|
|
|
166
|
+
/**
|
|
167
|
+
* La liste du parc, rangée par code : `statut` (un code ou une liste), `type`, `site`, et `q` cherché dans le code,
|
|
168
|
+
* la plaque, la marque et le modèle (sans casse ; espaces et tirets indifférents). Sans `statut`, les réformés
|
|
169
|
+
* n'y sont que si `reformes` est vrai.
|
|
170
|
+
*/
|
|
171
|
+
async vehicules({ statut, type, site, q, reformes = false } = {}) {
|
|
172
|
+
const statuts = statut === undefined ? null : [statut].flat();
|
|
173
|
+
for (const s of statuts || []) if (!CODES.statut.includes(s)) refus('DONNEE', `statut inconnu : ${s}`, 400);
|
|
174
|
+
const plat = (s) => String(s ?? '').toLowerCase().replace(/[\s-]+/g, '');
|
|
175
|
+
const cherche = texte(q) ? plat(q) : null;
|
|
176
|
+
return (await P.vehicules.find((v) => (statuts ? statuts.includes(v.statut) : reformes || v.statut !== 'reforme')
|
|
177
|
+
&& (!type || v.type === type) && (!site || v.site === site)
|
|
178
|
+
&& (!cherche || [v.code, v.immatriculation, v.marque, v.modele].some((c) => plat(c).includes(cherche)))))
|
|
179
|
+
.sort((a, b) => (a.code < b.code ? -1 : a.code > b.code ? 1 : 0)).map((v) => structuredClone(v));
|
|
180
|
+
},
|
|
181
|
+
|
|
148
182
|
/** Passer une transition du cycle : immobiliser, atelier, remettre, reformer. */
|
|
149
183
|
async changerStatut(id, transition, ctx = {}) { return transiter(await lire(id), transition, ctx); },
|
|
150
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Modifier les champs DESCRIPTIFS d'un véhicule (MODIFIABLES) ; ni le code, ni le type, ni le statut (→ changerStatut).
|
|
187
|
+
* La plaque reste contrôlée (motif, unicité), les attributs par le type. La modification est inscrite à
|
|
188
|
+
* l'historique : { le, agent, modification: { champs } }. Un véhicule réformé ne se modifie plus.
|
|
189
|
+
*/
|
|
190
|
+
async modifier(id, champs = {}, { agent } = {}) {
|
|
191
|
+
const v = await lire(id);
|
|
192
|
+
const cles = Object.keys(champs);
|
|
193
|
+
const interdites = cles.filter((k) => !MODIFIABLES.includes(k));
|
|
194
|
+
if (interdites.length) refus('DONNEE', `champ non modifiable : ${interdites.join(', ')}`, 400);
|
|
195
|
+
if (v.statut === 'reforme') refus('TRANSITION', 'un véhicule réformé ne se modifie plus', 422);
|
|
196
|
+
const m = { ...champs };
|
|
197
|
+
if ('immatriculation' in m) {
|
|
198
|
+
m.immatriculation = plaqueValide(m.immatriculation);
|
|
199
|
+
if ((await P.vehicules.find((x) => x.id !== id && x.immatriculation === m.immatriculation)).length) refus('DOUBLON', `immatriculation déjà prise : ${m.immatriculation}`, 409);
|
|
200
|
+
}
|
|
201
|
+
if ('attributs' in m) m.attributs = validerAttributs(D, v.type, { ...v.attributs, ...m.attributs });
|
|
202
|
+
if ('mode' in m && !CODES.mode.includes(m.mode)) refus('DONNEE', `mode inconnu : ${m.mode}`, 400);
|
|
203
|
+
if ('miseEnService' in m && m.miseEnService !== null) exigerJour(m.miseEnService, 'miseEnService');
|
|
204
|
+
if ('chassis' in m) m.chassis = texteOuNul(m.chassis, 'chassis');
|
|
205
|
+
if (!cles.length) return v;
|
|
206
|
+
const r = await maj('vehicules', v, { ...m, historique: [...v.historique, { le: aujourdhui(), agent: agent ?? null, modification: { champs: cles } }] });
|
|
207
|
+
emettre('vehicule.modifie', { vehicule: id, champs: cles });
|
|
208
|
+
return r;
|
|
209
|
+
},
|
|
210
|
+
|
|
151
211
|
// ── Échéances ──
|
|
152
|
-
async poserEcheance({ vehicule, type, debut, fin, piece, cout } = {}, { agent } = {}) {
|
|
212
|
+
async poserEcheance({ vehicule, type, debut, fin, piece, cout, reference, organisme } = {}, { agent } = {}) {
|
|
153
213
|
await lire(vehicule);
|
|
154
214
|
const d = D.echeances[type] || refus('TYPE', `type d'échéance non déclaré : ${type}`, 400);
|
|
155
215
|
exigerJour(debut, 'debut');
|
|
156
216
|
const f = fin === undefined ? R.ajouterJours(debut, d.periodiciteJours) : exigerJour(fin, 'fin');
|
|
157
217
|
if (f <= debut) refus('DONNEE', 'la fin doit suivre le début', 400);
|
|
158
218
|
if (cout !== undefined) exigerNombre(cout, 'cout');
|
|
159
|
-
const
|
|
219
|
+
const ref = texteOuNul(reference, 'reference'), org = texteOuNul(organisme, 'organisme');
|
|
220
|
+
const e = await creer('echeances', { id: randomUUID(), vehicule, type, debut, fin: f, reference: ref, organisme: org, piece: piece ?? null, cout: cout ?? null, poseePar: agent ?? null, poseeLe: aujourdhui() });
|
|
160
221
|
emettre('echeance.posee', { vehicule, echeance: e.id, type, fin: f });
|
|
161
222
|
return e;
|
|
162
223
|
},
|
|
@@ -191,6 +252,9 @@ export function createFlotte({
|
|
|
191
252
|
return p;
|
|
192
253
|
},
|
|
193
254
|
async plans(vehicule) { await lire(vehicule); return de('plans', vehicule); },
|
|
255
|
+
prochainsEntretiens,
|
|
256
|
+
/** Le plus urgent des prochains entretiens, ou null sans plan (ECR-16.17 : « Prochain entretien »). */
|
|
257
|
+
async prochainEntretien(vehicule, opts = {}) { return (await prochainsEntretiens(vehicule, opts))[0] || null; },
|
|
194
258
|
/** Les plans dus ou dépassés à `date`, sur les véhicules non réformés. */
|
|
195
259
|
async entretiensDus({ date } = {}) {
|
|
196
260
|
const jour = date ? exigerJour(date, 'date') : aujourdhui();
|
|
@@ -269,6 +333,8 @@ export function createFlotte({
|
|
|
269
333
|
const r = await ecrireReleve(vehicule, { km, le: jour, source: 'plein' }, avert, agent);
|
|
270
334
|
return { ...p, avertissements: r.avertissements };
|
|
271
335
|
},
|
|
336
|
+
/** Les pleins d'un véhicule, par date. */
|
|
337
|
+
async pleins(vehicule) { await lire(vehicule); return (await de('pleins', vehicule)).sort((a, b) => (a.le < b.le ? -1 : a.le > b.le ? 1 : 0)); },
|
|
272
338
|
async consommation(vehicule) { await lire(vehicule); return R.consommation(await de('pleins', vehicule)); },
|
|
273
339
|
|
|
274
340
|
// ── Affectations ──
|
|
@@ -301,6 +367,8 @@ export function createFlotte({
|
|
|
301
367
|
},
|
|
302
368
|
async affectations(vehicule) { await lire(vehicule); return (await de('affectations', vehicule)).sort((a, b) => (a.du < b.du ? -1 : a.du > b.du ? 1 : 0)); },
|
|
303
369
|
async affectationCourante(vehicule) { await lire(vehicule); return courante(vehicule); },
|
|
370
|
+
/** L'affectation en cours d'un conducteur (le véhicule qu'il tient), ou null. */
|
|
371
|
+
async vehiculeDe(conducteur) { return (await P.affectations.find((a) => a.conducteur === conducteur && !a.au))[0] || null; },
|
|
304
372
|
|
|
305
373
|
// ── Décisions ──
|
|
306
374
|
peutPartir,
|
package/src/regles.js
CHANGED
|
@@ -40,6 +40,34 @@ export function etatEntretien(plan, { km, date }) {
|
|
|
40
40
|
return { etat: pire.etat, unite: pire.unite, n: pire.n };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Le rythme du véhicule, en km par jour, mesuré sur les relevés des `fenetre` derniers jours avant `date`.
|
|
45
|
+
* Null sans deux relevés de dates différentes, ou si le compteur n'a pas avancé.
|
|
46
|
+
*/
|
|
47
|
+
export function kmParJour(releves, date, fenetre = 90) {
|
|
48
|
+
const debut = ajouterJours(date, -fenetre);
|
|
49
|
+
const dans = [...releves].sort(parDate).filter((r) => r.le >= debut && r.le <= date);
|
|
50
|
+
if (dans.length < 2) return null;
|
|
51
|
+
const a = dans[0], b = dans.at(-1), j = jours(a.le, b.le);
|
|
52
|
+
return j > 0 && b.km > a.km ? arrondi((b.km - a.km) / j) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Le PROCHAIN entretien d'un plan : l'échéance de chaque pas (km, jours), et la date du premier atteint. Un pas en
|
|
57
|
+
* km devient une date au rythme relevé (`kmParJour`) : la date est alors ESTIMÉE. Sans rythme, un plan au km seul
|
|
58
|
+
* n'a pas de date (`le: null`), seulement ses km restants. `dans` est négatif quand l'échéance est passée.
|
|
59
|
+
*/
|
|
60
|
+
export function prochainEntretien(plan, { km, date, kmParJour: rythme = null }) {
|
|
61
|
+
const etat = etatEntretien(plan, { km, date });
|
|
62
|
+
const parKm = plan.tousLesKm > 0 && Number.isFinite(km)
|
|
63
|
+
? { echeance: (plan.dernierKm || 0) + plan.tousLesKm, restant: (plan.dernierKm || 0) + plan.tousLesKm - km } : null;
|
|
64
|
+
const parJours = plan.tousLesJours > 0 && plan.dernierLe
|
|
65
|
+
? { echeance: ajouterJours(plan.dernierLe, plan.tousLesJours), restant: jours(date, ajouterJours(plan.dernierLe, plan.tousLesJours)) } : null;
|
|
66
|
+
const leKm = parKm && rythme > 0 ? ajouterJours(date, Math.ceil(parKm.restant / rythme)) : null;
|
|
67
|
+
const le = [leKm, parJours?.echeance].filter(Boolean).sort()[0] || null;
|
|
68
|
+
return { plan: plan.id, libelle: plan.libelle, ...etat, km: parKm, jours: parJours, le, dans: le ? jours(date, le) : null, estime: Boolean(le && le === leKm && le !== parJours?.echeance) };
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
/** L'échéance qui fait foi pour un type : la plus récente par début (à début égal, la dernière posée). */
|
|
44
72
|
export function echeanceCourante(echeances, type) {
|
|
45
73
|
return echeances.filter((e) => e.type === type).sort((a, b) => (a.debut < b.debut ? -1 : a.debut > b.debut ? 1 : 0)).at(-1) || null;
|
package/src/schemas.js
CHANGED
|
@@ -4,10 +4,10 @@ const T = { type: 'string' }, TR = { type: 'string', required: true }, N = { typ
|
|
|
4
4
|
|
|
5
5
|
export const VehiculeSchema = s('Vehicule', 'flotte_vehicules', {
|
|
6
6
|
code: { ...TR, unique: true }, immatriculation: { ...TR, unique: true }, type: TR, marque: T, modele: T, annee: N, couleur: T, energie: T,
|
|
7
|
-
attributs: J, site: T, detenteur: T, mode: TR, acquisition: J, statut: TR, historique: J, inscritLe: T, inscritPar: T,
|
|
7
|
+
attributs: J, site: T, detenteur: T, mode: TR, acquisition: J, chassis: T, miseEnService: T, statut: TR, historique: J, inscritLe: T, inscritPar: T,
|
|
8
8
|
}, [{ fields: { code: 'asc' }, unique: true }, { fields: { immatriculation: 'asc' }, unique: true }, { fields: { statut: 'asc', type: 'asc' } }]);
|
|
9
9
|
export const EcheanceSchema = s('Echeance', 'flotte_echeances', {
|
|
10
|
-
vehicule: TR, type: TR, debut: TR, fin: TR,
|
|
10
|
+
vehicule: TR, type: TR, debut: TR, fin: TR, reference: T, organisme: T, piece: J, cout: N, poseePar: T, poseeLe: T,
|
|
11
11
|
}, [{ fields: { vehicule: 'asc', type: 'asc' } }, { fields: { fin: 'asc' } }]);
|
|
12
12
|
export const PlanEntretienSchema = s('PlanEntretien', 'flotte_plans', {
|
|
13
13
|
vehicule: TR, libelle: TR, tousLesKm: N, tousLesJours: N, toleranceKm: N, toleranceJours: N, dernierKm: N, dernierLe: T, creePar: T,
|
|
@@ -17,7 +17,7 @@ export const InterventionSchema = s('Intervention', 'flotte_interventions', {
|
|
|
17
17
|
ouvertePar: T, closePar: T, modifieePar: T,
|
|
18
18
|
}, [{ fields: { numero: 'asc' }, unique: true }, { fields: { vehicule: 'asc', closeLe: 'asc' } }]);
|
|
19
19
|
export const ReleveCompteurSchema = s('ReleveCompteur', 'flotte_releves', {
|
|
20
|
-
vehicule: TR, km: NR, le: TR, source: TR, correction: J, saisiPar: T,
|
|
20
|
+
vehicule: TR, km: NR, le: TR, source: TR, correction: J, saisiPar: T, avertissements: J,
|
|
21
21
|
}, [{ fields: { vehicule: 'asc', le: 'asc' } }]);
|
|
22
22
|
export const PleinSchema = s('Plein', 'flotte_pleins', {
|
|
23
23
|
vehicule: TR, le: TR, litres: NR, montant: NR, km: NR, complet: B, carte: T, saisiPar: T,
|
|
@@ -30,7 +30,7 @@ export const SCHEMAS = Object.freeze([VehiculeSchema, EcheanceSchema, PlanEntret
|
|
|
30
30
|
|
|
31
31
|
export function register(registry) {
|
|
32
32
|
registry.register({
|
|
33
|
-
manifest: { name: 'flotte', package: '@mostajs/flotte', version: '0.
|
|
33
|
+
manifest: { name: 'flotte', package: '@mostajs/flotte', version: '0.4.0', type: 'business', priority: 45, displayName: 'Flotte (carnet du parc)',
|
|
34
34
|
description: 'Registre des véhicules, cycle, échéances, entretien, relevés, pleins, affectations, coût au km, peutPartir.', register: './src/schemas.js' },
|
|
35
35
|
schemas: SCHEMAS.map((x) => ({ name: x.name, schema: x })),
|
|
36
36
|
});
|