@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 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.1.0] — non publiée
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 à 15 (@mostajs/mjs-unit)
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 à 15 ; exemple : node examples/verifier.mjs
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.1.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.trim().toUpperCase();
131
- if (D.immatriculation.length && !D.immatriculation.some((m) => correspond(m, immatriculation))) {
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 e = await creer('echeances', { id: randomUUID(), vehicule, type, debut, fin: f, piece: piece ?? null, cout: cout ?? null, poseePar: agent ?? null, poseeLe: aujourdhui() });
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, piece: T, cout: N, poseePar: T, poseeLe: T,
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.1.0', type: 'business', priority: 45, displayName: 'Flotte (carnet du parc)',
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
  });