@mostajs/flotte 0.3.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,14 +3,28 @@
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.3.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
7
21
  ### Added
8
22
  - `vehicules({ statut?, type?, site?, q?, reformes? })` : la liste du parc, rangée par code ; `statut` est un code
9
23
  ou une liste ; `q` est cherché dans le code, la plaque, la marque et le modèle, sans casse, espaces et tirets
10
24
  indifférents ; les réformés n'y sont que sur demande. Demandé par le tableau de bord ECR-16 de Guerway, que
11
25
  `@mostajs/flotte-ui` construit sans accéder aux dépôts. Essai T-FLO-17.
12
26
 
13
- ## [0.2.0] — non publiée
27
+ ## [0.2.0] — jamais publiée seule (contenue dans la 0.3.0)
14
28
  ### Added
15
29
  - `prochainsEntretiens(v, { date? })` et `prochainEntretien(v)` : pour chaque plan, l'échéance de chaque pas (km,
16
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
package/llms.txt CHANGED
@@ -25,16 +25,20 @@ EXPORTS
25
25
  API (async ; ctx = { agent })
26
26
  inscrire({ code, immatriculation, type, attributs, marque?, site?, mode? }, ctx) ; lire(id)
27
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
28
31
  changerStatut(id, 'immobiliser'|'atelier'|'remettre'|'reformer', { agent, motif }) motif requis : immobiliser, reformer
29
- 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)
30
34
  echeances(v) ; aEcheance({ dans?, date? }) -> [{ id, vehicule, type, fin, n, expiree }]
31
35
  planifierEntretien({ vehicule, libelle, tousLesKm?, tousLesJours?, toleranceKm?, toleranceJours? }, ctx) ; plans(v) ; entretiensDus({ date? })
32
36
  prochainsEntretiens(v, { date? }) / prochainEntretien(v) (0.2.0) -> { plan, libelle, etat, km: { echeance, restant },
33
37
  jours: { echeance, restant }, le, dans, estime } le pas en km devient une date au rythme relevé (estime: true)
34
38
  ouvrirIntervention({ vehicule, nature, plan?, description?, ouverteLe? }, ctx) corrective => a_l_atelier
35
39
  cloreIntervention(id, { km?, cout?, le? }, ctx) recale le plan ; km => relevé ; close => immuable
36
- relever(v, { km, le?, source?, correction?: { motif, agent } }, ctx) -> relevé + avertissements (SAUT_ANORMAL)
37
- 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)
38
42
  affecter(v, conducteur, ctx) ; liberer(v, ctx) ; affectations(v) ; affectationCourante(v)
39
43
  peutPartir(v, { date?, conducteur?, km? }) -> { ok, motifs[], avertissements[] } TOUS les motifs, en OBJETS
40
44
  disponibles({ date?, type?, attributs? }) ; cout(v, { du, au }) -> { carburant, entretien, echeances, total, km, parKm, nonComptes }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/flotte",
3
- "version": "0.3.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>",
@@ -61,6 +61,7 @@
61
61
  "verbes": [
62
62
  "inscrire",
63
63
  "vehicules",
64
+ "modifier",
64
65
  "changerStatut",
65
66
  "peutPartir",
66
67
  "prochainEntretien",
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
  }
@@ -137,10 +147,8 @@ export function createFlotte({
137
147
  if (!texte(d.immatriculation)) refus('DONNEE', 'immatriculation requise', 400);
138
148
  if (!texte(d.type)) refus('DONNEE', 'type requis', 400);
139
149
  const attributs = validerAttributs(D, d.type, d.attributs);
140
- const code = d.code.trim(), immatriculation = d.immatriculation.trim().toUpperCase();
141
- if (D.immatriculation.length && !D.immatriculation.some((m) => correspond(m, immatriculation))) {
142
- refus('IMMATRICULATION', `immatriculation hors des motifs déclarés : ${immatriculation}`, 400);
143
- }
150
+ const code = d.code.trim(), immatriculation = plaqueValide(d.immatriculation);
151
+ const miseEnService = d.miseEnService === undefined || d.miseEnService === null ? null : exigerJour(d.miseEnService, 'miseEnService');
144
152
  const mode = d.mode ?? 'propre';
145
153
  if (!CODES.mode.includes(mode)) refus('DONNEE', `mode inconnu : ${mode}`, 400);
146
154
  if ((await P.vehicules.find((v) => v.code === code || v.immatriculation === immatriculation)).length) {
@@ -149,7 +157,7 @@ export function createFlotte({
149
157
  const v = await creer('vehicules', {
150
158
  id: randomUUID(), code, immatriculation, type: d.type, marque: d.marque ?? null, modele: d.modele ?? null, annee: d.annee ?? null,
151
159
  couleur: d.couleur ?? null, energie: d.energie ?? null, attributs, site: d.site ?? null, detenteur: d.detenteur ?? null, mode,
152
- 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,
153
161
  });
154
162
  emettre('vehicule.inscrit', { vehicule: v.id, code, type: v.type });
155
163
  return v;
@@ -174,15 +182,42 @@ export function createFlotte({
174
182
  /** Passer une transition du cycle : immobiliser, atelier, remettre, reformer. */
175
183
  async changerStatut(id, transition, ctx = {}) { return transiter(await lire(id), transition, ctx); },
176
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
+
177
211
  // ── Échéances ──
178
- async poserEcheance({ vehicule, type, debut, fin, piece, cout } = {}, { agent } = {}) {
212
+ async poserEcheance({ vehicule, type, debut, fin, piece, cout, reference, organisme } = {}, { agent } = {}) {
179
213
  await lire(vehicule);
180
214
  const d = D.echeances[type] || refus('TYPE', `type d'échéance non déclaré : ${type}`, 400);
181
215
  exigerJour(debut, 'debut');
182
216
  const f = fin === undefined ? R.ajouterJours(debut, d.periodiciteJours) : exigerJour(fin, 'fin');
183
217
  if (f <= debut) refus('DONNEE', 'la fin doit suivre le début', 400);
184
218
  if (cout !== undefined) exigerNombre(cout, 'cout');
185
- 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() });
186
221
  emettre('echeance.posee', { vehicule, echeance: e.id, type, fin: f });
187
222
  return e;
188
223
  },
@@ -298,6 +333,8 @@ export function createFlotte({
298
333
  const r = await ecrireReleve(vehicule, { km, le: jour, source: 'plein' }, avert, agent);
299
334
  return { ...p, avertissements: r.avertissements };
300
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)); },
301
338
  async consommation(vehicule) { await lire(vehicule); return R.consommation(await de('pleins', vehicule)); },
302
339
 
303
340
  // ── Affectations ──
@@ -330,6 +367,8 @@ export function createFlotte({
330
367
  },
331
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)); },
332
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; },
333
372
 
334
373
  // ── Décisions ──
335
374
  peutPartir,
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.2.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
  });