@mostajs/flotte 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 +53 -2
- package/llms.txt +50 -0
- package/package.json +85 -4
- package/src/cycle.js +22 -0
- package/src/declarations.js +65 -0
- package/src/flotte.js +326 -0
- package/src/index.js +15 -0
- package/src/libelles.js +69 -0
- package/src/memoire.js +16 -0
- package/src/regles.js +129 -0
- package/src/schemas.js +37 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog — @mostajs/flotte
|
|
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
|
+
- `createFlotte` : registre par types déclarés, cycle en service / immobilisé / à l'atelier / réformé
|
|
9
|
+
(`@mostajs/statemachine`), échéances typées, plans d'entretien au km et à la date, interventions numérotées,
|
|
10
|
+
relevés avec correction motivée, pleins et consommation, affectations, `peutPartir`, `disponibles`, `cout`.
|
|
11
|
+
- Port `habilitations` facultatif (`@mostajs/staff`) ; numérotation injectée (`@mostajs/numbering`).
|
|
12
|
+
- Codes et libellés français canoniques, `libelle(famille, code, t)` pour les sept langues de Guerway.
|
|
13
|
+
- Sept tables déclarées par `register` ; permissions `flotte:domaine:action` ; événements.
|
|
14
|
+
- Essais T-FLO-1 à 15 ; exemple `examples/verifier.mjs`. Premier consommateur prévu : Guerway.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@mostajs/flotte
|
|
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,54 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mostajs/flotte
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · Licence AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
Le carnet du parc. Il répond à une question : **ce véhicule peut-il partir aujourd'hui, avec ce conducteur, et
|
|
6
|
+
combien coûte-t-il ?**
|
|
7
|
+
|
|
8
|
+
- un registre de véhicules, chacun d'un **type déclaré** par l'application (fourgon, moto, autocar…) ;
|
|
9
|
+
- un cycle : en service, immobilisé, à l'atelier, réformé ; chaque passage est tracé, avec son motif ;
|
|
10
|
+
- 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
|
+
- des relevés de compteur : un recul est refusé, sauf correction motivée ; un saut anormal est signalé ;
|
|
13
|
+
- des pleins, et la consommation mesurée entre deux pleins complets ;
|
|
14
|
+
- des affectations de conducteurs (identifiant opaque) ;
|
|
15
|
+
- `peutPartir` rend **tous** les motifs de refus, pas seulement le premier ;
|
|
16
|
+
- `cout` rend le coût sur une période et au km, et dit ce qu'il ne compte pas.
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { createFlotte, createMemoryRepositories, libelle } from '@mostajs/flotte';
|
|
20
|
+
|
|
21
|
+
const flotte = createFlotte({
|
|
22
|
+
repositories: createMemoryRepositories(), // en production : les dépôts data-plug de l'application
|
|
23
|
+
declarations: {
|
|
24
|
+
types: { fourgon: { categoriePermis: 'B', attributs: { chargeUtileKg: 'nombre' } } },
|
|
25
|
+
echeances: { assurance: { bloquante: true, obligatoire: true, periodiciteJours: 365 } },
|
|
26
|
+
immatriculation: [/^\d{5,6}-\d{3}-\d{2}$/],
|
|
27
|
+
},
|
|
28
|
+
numeroter: async ({ nature }) => `INT-${Date.now()}`, // en production : @mostajs/numbering
|
|
29
|
+
});
|
|
30
|
+
const v = await flotte.inscrire({ code: 'GW-F01', immatriculation: '12345-116-16', type: 'fourgon' }, { agent: 'chef-parc' });
|
|
31
|
+
const r = await flotte.peutPartir(v.id);
|
|
32
|
+
r.motifs.map((m) => libelle('motif', m.code, t, m)); // t : la traduction de l'application
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Langues
|
|
36
|
+
|
|
37
|
+
Le module rend des **codes** (`r.motifs = [{ code: 'ECHEANCE_ABSENTE', type: 'assurance' }]`) et un libellé
|
|
38
|
+
français canonique. L'application traduit la clé `flotte.<famille>.<code>`. Guerway le fait dans ses sept
|
|
39
|
+
catalogues. Sans traduction, le libellé du module s'affiche. Le motif saisi par un agent reste tel quel.
|
|
40
|
+
|
|
41
|
+
## Ce que le module ne fait pas
|
|
42
|
+
|
|
43
|
+
Position (`geo-tracker`), réservation (`booking`), déplacement de billets ou de colis (`tournees`), personne du
|
|
44
|
+
conducteur et permis (`staff`, par le port `habilitations`), pièces en stock (`stocks`), comptabilité (`compta`),
|
|
45
|
+
alertes (`notifications`), fichiers scannés (`blob-store`). `peutPartir` dit **apte**, jamais **libre**.
|
|
46
|
+
|
|
47
|
+
## Vérifier
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm test # T-FLO-1 à 15 (@mostajs/mjs-unit)
|
|
51
|
+
node examples/verifier.mjs # un fourgon, de l'inscription au coût au km
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Plan : `docs/03-PLAN-DEV-FLOTTE.md`. Fiche pour les assistants : `llms.txt`.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @mostajs/flotte — fiche LLM
|
|
2
|
+
|
|
3
|
+
RÔLE
|
|
4
|
+
Le carnet du parc (N3, forme annuaire) : registre des véhicules par TYPE DÉCLARÉ, cycle d'état, échéances
|
|
5
|
+
(assurance, contrôle technique…), entretien au km ET à la date, relevés de compteur, pleins, affectations de
|
|
6
|
+
conducteurs, coût au km, et la réponse « ce véhicule peut-il partir aujourd'hui, avec ce conducteur ? ».
|
|
7
|
+
NE FAIT PAS : position (→ geo-tracker) ; réservation (→ booking) ; déplacer billets ou colis (→ tournees) ;
|
|
8
|
+
personne du conducteur et permis (→ staff, par le PORT habilitations) ; stock de pièces (→ stocks) ; écriture
|
|
9
|
+
comptable (→ compta) ; alerte (→ notifications) ; fichier scanné (→ blob-store, on garde sa référence) ;
|
|
10
|
+
numéroter (→ numbering, INJECTÉ). `peutPartir` dit APTE, jamais LIBRE.
|
|
11
|
+
|
|
12
|
+
EXPORTS
|
|
13
|
+
createFlotte({ repositories, declarations, numeroter({ vehicule, nature, date }), habilitations?, audit?,
|
|
14
|
+
plusieursVehiculesParConducteur?, exigerAffectation?, maintenant? }) -> api
|
|
15
|
+
declarations = { types: { <type>: { categoriePermis?, attributs: { <nom>: 'nombre'|'booleen'|'texte' } } },
|
|
16
|
+
echeances: { <type>: { bloquante?, obligatoire?, periodiciteJours } },
|
|
17
|
+
immatriculation: [RegExp…], alerteJours? (30), sautAnormalKmParJour? (2000) }
|
|
18
|
+
PORT habilitations : peutConduire(conducteur, categorie, date) -> booléen
|
|
19
|
+
createMemoryRepositories() 7 dépôts : vehicules, echeances, plans, interventions, releves, pleins, affectations
|
|
20
|
+
ErreurFlotte { code, statut } (400 donnée, 404 inconnu, 409 doublon/recul/close/ouverte/occupé, 422 transition)
|
|
21
|
+
CODES, LIBELLES, libelle(famille, code, t?, vars?) ; STATUTS, MOTIFS ; PERMISSIONS (flotte:domaine:action)
|
|
22
|
+
regles (pur) : etatEntretien, consommation, evaluerDepart, cout, echeanceCourante, jours, ajouterJours
|
|
23
|
+
register (7 tables ; aussi './register'), SCHEMAS, validerDeclarations
|
|
24
|
+
|
|
25
|
+
API (async ; ctx = { agent })
|
|
26
|
+
inscrire({ code, immatriculation, type, attributs, marque?, site?, mode? }, ctx) ; lire(id)
|
|
27
|
+
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
|
|
29
|
+
echeances(v) ; aEcheance({ dans?, date? }) -> [{ id, vehicule, type, fin, n, expiree }]
|
|
30
|
+
planifierEntretien({ vehicule, libelle, tousLesKm?, tousLesJours?, toleranceKm?, toleranceJours? }, ctx) ; plans(v) ; entretiensDus({ date? })
|
|
31
|
+
ouvrirIntervention({ vehicule, nature, plan?, description?, ouverteLe? }, ctx) corrective => a_l_atelier
|
|
32
|
+
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)
|
|
35
|
+
affecter(v, conducteur, ctx) ; liberer(v, ctx) ; affectations(v) ; affectationCourante(v)
|
|
36
|
+
peutPartir(v, { date?, conducteur?, km? }) -> { ok, motifs[], avertissements[] } TOUS les motifs, en OBJETS
|
|
37
|
+
disponibles({ date?, type?, attributs? }) ; cout(v, { du, au }) -> { carburant, entretien, echeances, total, km, parKm, nonComptes }
|
|
38
|
+
onEvenement((type, donnees) => …) -> désabonnement
|
|
39
|
+
|
|
40
|
+
LANGUES
|
|
41
|
+
Le module rend des CODES et un libellé français canonique. L'application traduit `flotte.<famille>.<code>`
|
|
42
|
+
(Guerway : ses 7 catalogues) et passe son `t` à libelle(). Le motif saisi par un agent n'est jamais traduit.
|
|
43
|
+
|
|
44
|
+
PIÈGES
|
|
45
|
+
- Dates en jours 'AAAA-MM-JJ'. Un relevé doit être cohérent avec ceux qui l'entourent dans le temps.
|
|
46
|
+
- L'entité de relevé s'appelle ReleveCompteur (Releve appartient à @mostajs/recette).
|
|
47
|
+
- L'échéance qui fait foi est la plus récente par début ; les anciennes restent lisibles.
|
|
48
|
+
|
|
49
|
+
ESSAIS test-scripts/unit/flotte.test.mjs — T-FLO-1 à 15 ; exemple : node examples/verifier.mjs
|
|
50
|
+
LICENCE AGPL-3.0-or-later · Auteur Dr Hamid MADANI <drmdh@msn.com>
|
package/package.json
CHANGED
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/flotte",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./register": "./src/schemas.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"llms.txt",
|
|
16
|
+
"README.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "bash test-scripts/run-tests.sh"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"mostajs",
|
|
28
|
+
"flotte",
|
|
29
|
+
"fleet",
|
|
30
|
+
"vehicule",
|
|
31
|
+
"entretien",
|
|
32
|
+
"maintenance",
|
|
33
|
+
"carburant",
|
|
34
|
+
"echeances",
|
|
35
|
+
"parc-automobile"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@mostajs/statemachine": "0.0.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@mostajs/mjs-unit": "0.10.0"
|
|
42
|
+
},
|
|
43
|
+
"mostajs": {
|
|
44
|
+
"niveau": "N3",
|
|
45
|
+
"nature": "metier",
|
|
46
|
+
"reutilisabilite": "mono-domaine",
|
|
47
|
+
"depend_de": [
|
|
48
|
+
"@mostajs/statemachine"
|
|
49
|
+
],
|
|
50
|
+
"question": "Ce véhicule peut-il partir aujourd'hui, avec ce conducteur, et combien coûte-t-il ?",
|
|
51
|
+
"forme": "annuaire",
|
|
52
|
+
"entites": [
|
|
53
|
+
"Vehicule",
|
|
54
|
+
"Echeance",
|
|
55
|
+
"PlanEntretien",
|
|
56
|
+
"Intervention",
|
|
57
|
+
"ReleveCompteur",
|
|
58
|
+
"Plein",
|
|
59
|
+
"Affectation"
|
|
60
|
+
],
|
|
61
|
+
"verbes": [
|
|
62
|
+
"inscrire",
|
|
63
|
+
"changerStatut",
|
|
64
|
+
"peutPartir",
|
|
65
|
+
"aEcheance",
|
|
66
|
+
"entretiensDus",
|
|
67
|
+
"saisirPlein",
|
|
68
|
+
"affecter",
|
|
69
|
+
"cout"
|
|
70
|
+
],
|
|
71
|
+
"interop": [
|
|
72
|
+
"ISO/TS 15143-3 (forme des relevés cumulés)",
|
|
73
|
+
"ISO 39001 (indicateurs d'exposition)"
|
|
74
|
+
],
|
|
75
|
+
"ne_fait_pas": {
|
|
76
|
+
"suivre une position": "@mostajs/geo-tracker",
|
|
77
|
+
"réserver un véhicule sur un créneau": "@mostajs/booking",
|
|
78
|
+
"déplacer billets ou colis d'un véhicule à un autre": "@mostajs/tournees",
|
|
79
|
+
"tenir la personne du conducteur et ses permis": "@mostajs/staff",
|
|
80
|
+
"tenir le stock de pièces": "@mostajs/stocks",
|
|
81
|
+
"passer l'écriture comptable": "@mostajs/compta",
|
|
82
|
+
"envoyer une alerte": "@mostajs/notifications",
|
|
83
|
+
"ranger les pièces scannées": "@mostajs/blob-store",
|
|
84
|
+
"numéroter": "@mostajs/numbering"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/cycle.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @mostajs/flotte — le cycle du véhicule, par @mostajs/statemachine (plan de dev §3.3).
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
import { defineMachine } from '@mostajs/statemachine';
|
|
4
|
+
|
|
5
|
+
/** Les transitions : `motif` requis ou non ; `remettre` est gardée (aucune intervention ouverte). */
|
|
6
|
+
export const TRANSITIONS = Object.freeze([
|
|
7
|
+
{ id: 'immobiliser', from: ['en_service', 'a_l_atelier'], to: 'immobilise', motif: true },
|
|
8
|
+
{ id: 'atelier', from: ['en_service', 'immobilise'], to: 'a_l_atelier', motif: false },
|
|
9
|
+
{ id: 'remettre', from: ['a_l_atelier', 'immobilise'], to: 'en_service', motif: false, guard: (c) => c.data?.interventionsOuvertes === 0 },
|
|
10
|
+
{ id: 'reformer', from: ['en_service', 'immobilise', 'a_l_atelier'], to: 'reforme', motif: true },
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* La machine d'une instance. `audit` (AuditSink de statemachine) est celui de l'application quand elle en
|
|
15
|
+
* fournit un ; sinon la machine garde son journal en mémoire.
|
|
16
|
+
*/
|
|
17
|
+
export function creerCycle({ audit, maintenant }) {
|
|
18
|
+
return defineMachine({
|
|
19
|
+
states: [{ id: 'en_service', initial: true }, { id: 'immobilise' }, { id: 'a_l_atelier' }, { id: 'reforme', terminal: true }],
|
|
20
|
+
transitions: TRANSITIONS.map(({ id, from, to, guard }) => (guard ? { id, from, to, guard } : { id, from, to })),
|
|
21
|
+
}, { ...(audit ? { audit } : {}), now: () => new Date(`${maintenant()}T00:00:00Z`) });
|
|
22
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @mostajs/flotte — validation des déclarations de l'application : types, échéances, motifs d'immatriculation.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// Les déclarations sont des DONNÉES (R-K) : Guerway les tient dans son paramétrage. Une déclaration invalide
|
|
5
|
+
// refuse la création de l'instance : mieux vaut ne pas démarrer que bloquer une tournée à tort.
|
|
6
|
+
|
|
7
|
+
export const FORMES = Object.freeze(['nombre', 'booleen', 'texte']);
|
|
8
|
+
|
|
9
|
+
export class ErreurFlotte extends Error {
|
|
10
|
+
/** @param {string} code un code de LIBELLES.erreur ; @param {number} statut 400, 404, 409 ou 422 */
|
|
11
|
+
constructor(code, message, statut = 400) { super(message); this.name = 'ErreurFlotte'; this.code = code; this.statut = statut; }
|
|
12
|
+
}
|
|
13
|
+
export const refus = (code, message, statut = 400) => { throw new ErreurFlotte(code, message, statut); };
|
|
14
|
+
|
|
15
|
+
const objet = (x) => x !== null && typeof x === 'object' && !Array.isArray(x);
|
|
16
|
+
const decl = (message) => refus('DECLARATION', `flotte : déclaration invalide — ${message}`, 400);
|
|
17
|
+
|
|
18
|
+
/** Valide et normalise les déclarations ; rend une copie figée. */
|
|
19
|
+
export function validerDeclarations(d = {}) {
|
|
20
|
+
if (!objet(d)) decl('un objet est attendu');
|
|
21
|
+
const types = d.types ?? {};
|
|
22
|
+
if (!objet(types)) decl('types doit être un objet');
|
|
23
|
+
for (const [nom, t] of Object.entries(types)) {
|
|
24
|
+
if (!objet(t)) decl(`type ${nom} : un objet est attendu`);
|
|
25
|
+
if (t.categoriePermis !== undefined && typeof t.categoriePermis !== 'string') decl(`type ${nom} : categoriePermis doit être un texte`);
|
|
26
|
+
if (t.attributs !== undefined && !objet(t.attributs)) decl(`type ${nom} : attributs doit être un objet`);
|
|
27
|
+
for (const [a, forme] of Object.entries(t.attributs || {})) {
|
|
28
|
+
if (!FORMES.includes(forme)) decl(`type ${nom}, attribut ${a} : forme « ${forme} » inconnue (${FORMES.join(', ')})`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const echeances = d.echeances ?? {};
|
|
32
|
+
if (!objet(echeances)) decl('echeances doit être un objet');
|
|
33
|
+
for (const [nom, e] of Object.entries(echeances)) {
|
|
34
|
+
if (!objet(e)) decl(`échéance ${nom} : un objet est attendu`);
|
|
35
|
+
if (!(Number.isInteger(e.periodiciteJours) && e.periodiciteJours > 0)) decl(`échéance ${nom} : periodiciteJours (entier > 0) requis`);
|
|
36
|
+
for (const b of ['bloquante', 'obligatoire']) if (e[b] !== undefined && typeof e[b] !== 'boolean') decl(`échéance ${nom} : ${b} doit être un booléen`);
|
|
37
|
+
}
|
|
38
|
+
const immatriculation = d.immatriculation ?? [];
|
|
39
|
+
if (!Array.isArray(immatriculation) || !immatriculation.every((m) => m instanceof RegExp)) {
|
|
40
|
+
decl('immatriculation doit être une liste de RegExp fournies par l’application (jamais construites depuis une saisie)');
|
|
41
|
+
}
|
|
42
|
+
for (const k of ['alerteJours', 'sautAnormalKmParJour']) {
|
|
43
|
+
if (d[k] !== undefined && !(Number.isFinite(d[k]) && d[k] > 0)) decl(`${k} doit être un nombre > 0`);
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
types: structuredClone(types),
|
|
47
|
+
echeances: structuredClone(echeances),
|
|
48
|
+
immatriculation: [...immatriculation],
|
|
49
|
+
alerteJours: d.alerteJours ?? 30,
|
|
50
|
+
sautAnormalKmParJour: d.sautAnormalKmParJour ?? 2000,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Valide les attributs d'un véhicule selon son type ; rend une copie. */
|
|
55
|
+
export function validerAttributs(declarations, type, attributs = {}) {
|
|
56
|
+
const t = declarations.types[type] || refus('TYPE', `type non déclaré : ${type}`, 400);
|
|
57
|
+
if (!objet(attributs)) refus('ATTRIBUT', 'attributs : un objet est attendu', 400);
|
|
58
|
+
for (const [a, v] of Object.entries(attributs)) {
|
|
59
|
+
const forme = t.attributs?.[a];
|
|
60
|
+
if (!forme) refus('ATTRIBUT', `attribut ${a} non déclaré pour le type ${type}`, 400);
|
|
61
|
+
const bon = forme === 'nombre' ? Number.isFinite(v) : forme === 'booleen' ? typeof v === 'boolean' : typeof v === 'string';
|
|
62
|
+
if (!bon) refus('ATTRIBUT', `attribut ${a} : ${forme} attendu`, 400);
|
|
63
|
+
}
|
|
64
|
+
return { ...attributs };
|
|
65
|
+
}
|
package/src/flotte.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// @mostajs/flotte — le carnet du parc : registre, cycle, échéances, entretien, relevés, pleins, affectations, coût,
|
|
2
|
+
// et la réponse « ce véhicule peut-il partir ? ».
|
|
3
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
4
|
+
//
|
|
5
|
+
// NE FAIT PAS : suivre une position (→ geo-tracker), réserver (→ booking), déplacer billets ou colis (→ tournees),
|
|
6
|
+
// tenir la personne du conducteur et ses permis (→ staff, par le PORT `habilitations`), envoyer une alerte
|
|
7
|
+
// (→ notifications), ranger une pièce scannée (→ blob-store : on garde sa référence), numéroter (→ numbering, INJECTÉ).
|
|
8
|
+
// Couche service PURE (R-A, R-L) : dépôts, déclarations, ports et horloge sont injectés.
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { ErreurFlotte, refus, validerDeclarations, validerAttributs } from './declarations.js';
|
|
11
|
+
import { TRANSITIONS, creerCycle } from './cycle.js';
|
|
12
|
+
import { CODES } from './libelles.js';
|
|
13
|
+
import * as R from './regles.js';
|
|
14
|
+
|
|
15
|
+
const TABLES = ['vehicules', 'echeances', 'plans', 'interventions', 'releves', 'pleins', 'affectations'];
|
|
16
|
+
const jourDe = (x) => (x instanceof Date ? x.toISOString().slice(0, 10) : String(x).slice(0, 10));
|
|
17
|
+
const texte = (x) => typeof x === 'string' && x.trim().length > 0;
|
|
18
|
+
const exigerJour = (d, quoi) => { if (!R.estJour(d)) refus('DONNEE', `${quoi} : date AAAA-MM-JJ attendue`, 400); return d; };
|
|
19
|
+
const exigerNombre = (n, quoi, { min = 0, strict = false } = {}) => {
|
|
20
|
+
if (!Number.isFinite(n) || (strict ? n <= min : n < min)) refus('DONNEE', `${quoi} : nombre ${strict ? '>' : '≥'} ${min} attendu`, 400);
|
|
21
|
+
return n;
|
|
22
|
+
};
|
|
23
|
+
const correspond = (motif, valeur) => new RegExp(motif.source, motif.flags.replace(/[gy]/g, '')).test(valeur);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* repositories : { vehicules, echeances, plans, interventions, releves, pleins, affectations } — chacun
|
|
27
|
+
* { find(prédicat), create(doc), update(id, champs) } (data-plug en production, createMemoryRepositories en essai).
|
|
28
|
+
* declarations : { types, echeances, immatriculation, alerteJours?, sautAnormalKmParJour? } (plan §3.2).
|
|
29
|
+
* numeroter({ vehicule, nature, date }) → numéro d'intervention (injecté : @mostajs/numbering).
|
|
30
|
+
* habilitations : PORT facultatif { peutConduire(conducteur, categorie, date) → booléen } (@mostajs/staff).
|
|
31
|
+
* plusieursVehiculesParConducteur, exigerAffectation : options (faux par défaut).
|
|
32
|
+
* audit : AuditSink de @mostajs/statemachine, facultatif. maintenant() → 'AAAA-MM-JJ' ou Date.
|
|
33
|
+
*/
|
|
34
|
+
export function createFlotte({
|
|
35
|
+
repositories, declarations, numeroter, habilitations, audit,
|
|
36
|
+
plusieursVehiculesParConducteur = false, exigerAffectation = false,
|
|
37
|
+
maintenant = () => new Date(),
|
|
38
|
+
} = {}) {
|
|
39
|
+
const D = validerDeclarations(declarations);
|
|
40
|
+
for (const t of TABLES) if (!repositories?.[t]) throw new Error(`flotte : repositories.${t} requis`);
|
|
41
|
+
if (typeof numeroter !== 'function') throw new Error('flotte : numeroter() requis (@mostajs/numbering)');
|
|
42
|
+
if (habilitations !== undefined && typeof habilitations?.peutConduire !== 'function') throw new Error('flotte : habilitations.peutConduire() requis');
|
|
43
|
+
const P = repositories;
|
|
44
|
+
const aujourdhui = () => jourDe(maintenant());
|
|
45
|
+
const cycle = creerCycle({ audit, maintenant: aujourdhui });
|
|
46
|
+
|
|
47
|
+
const abonnes = new Set();
|
|
48
|
+
const emettre = (type, d) => { for (const f of abonnes) { try { f(type, d); } catch (e) { console.error('[flotte] abonné :', e.message); } } };
|
|
49
|
+
const creer = async (table, doc) => { await P[table].create(doc); return structuredClone(doc); };
|
|
50
|
+
const maj = async (table, doc, champs) => { await P[table].update(doc.id, champs); return { ...doc, ...structuredClone(champs) }; };
|
|
51
|
+
const de = (table, vehicule) => P[table].find((x) => x.vehicule === vehicule);
|
|
52
|
+
|
|
53
|
+
async function lire(id) {
|
|
54
|
+
return (await P.vehicules.find((v) => v.id === id))[0] || refus('INCONNU', `véhicule inconnu : ${id}`, 404);
|
|
55
|
+
}
|
|
56
|
+
async function intervention(id) {
|
|
57
|
+
return (await P.interventions.find((i) => i.id === id))[0] || refus('INCONNU', `intervention inconnue : ${id}`, 404);
|
|
58
|
+
}
|
|
59
|
+
const releves = async (vehicule) => (await de('releves', vehicule)).sort((a, b) => (a.le < b.le ? -1 : a.le > b.le ? 1 : 0));
|
|
60
|
+
const ouvertes = async (vehicule) => (await de('interventions', vehicule)).filter((i) => !i.closeLe);
|
|
61
|
+
const courante = async (vehicule) => (await de('affectations', vehicule)).find((a) => !a.au) || null;
|
|
62
|
+
|
|
63
|
+
// ── Relevés : vérifier AVANT d'écrire quoi que ce soit (un plein refusé ne laisse pas de plein orphelin) ──
|
|
64
|
+
async function verifierReleve(vehicule, { km, le, correction }) {
|
|
65
|
+
exigerNombre(km, 'km'); exigerJour(le, 'le');
|
|
66
|
+
if (correction !== undefined) {
|
|
67
|
+
if (!texte(correction?.motif)) refus('MOTIF', 'une correction de compteur exige un motif', 400);
|
|
68
|
+
if (!texte(correction?.agent)) refus('DONNEE', 'une correction de compteur exige un agent', 400);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const tous = await releves(vehicule);
|
|
72
|
+
const avant = tous.filter((r) => r.le <= le).at(-1), apres = tous.find((r) => r.le > le);
|
|
73
|
+
if (avant && km < avant.km) refus('RECUL', `le compteur recule : ${km} km après ${avant.km} km`, 409);
|
|
74
|
+
if (apres && km > apres.km) refus('RECUL', `le compteur dépasse le relevé suivant : ${km} km avant ${apres.km} km`, 409);
|
|
75
|
+
if (avant) {
|
|
76
|
+
const j = Math.max(1, R.jours(avant.le, le)), n = km - avant.km;
|
|
77
|
+
if (n / j > D.sautAnormalKmParJour) return [{ code: 'SAUT_ANORMAL', n, jours: j }];
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
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 });
|
|
83
|
+
emettre('releve.saisi', { vehicule, releve: r.id, km, source });
|
|
84
|
+
return { ...r, avertissements };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function transiter(v, transition, { motif, agent } = {}) {
|
|
88
|
+
const def = TRANSITIONS.find((t) => t.id === transition);
|
|
89
|
+
if (!def || !def.from.includes(v.statut)) refus('TRANSITION', `« ${transition} » est impossible depuis ${v.statut}`, 422);
|
|
90
|
+
if (def.motif && !texte(motif)) refus('MOTIF', `« ${transition} » exige un motif`, 400);
|
|
91
|
+
const data = { interventionsOuvertes: (await ouvertes(v.id)).length };
|
|
92
|
+
const ok = await cycle.canTransition(v.statut, def.to, { actorId: agent, data });
|
|
93
|
+
if (!ok.allowed) {
|
|
94
|
+
if (ok.reason === 'guard') refus('INTERVENTION_OUVERTE', 'une intervention est encore ouverte : la clore avant de remettre en service', 409);
|
|
95
|
+
refus('TRANSITION', `« ${transition} » refusée (${ok.reason})`, 422);
|
|
96
|
+
}
|
|
97
|
+
await cycle.transition({ entityType: 'vehicule', entityId: v.id }, v.statut, def.to, { actorId: agent, data, comment: motif ?? undefined });
|
|
98
|
+
// Le motif est écrit TEL QUEL : il est saisi par un agent, il ne se traduit pas.
|
|
99
|
+
const pas = { de: v.statut, vers: def.to, motif: def.motif ? motif : (texte(motif) ? motif : null), agent: agent ?? null, le: aujourdhui() };
|
|
100
|
+
const r = await maj('vehicules', v, { statut: def.to, historique: [...v.historique, pas] });
|
|
101
|
+
emettre('vehicule.statut', { vehicule: v.id, transition, ...pas });
|
|
102
|
+
return r;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function peutPartir(id, { date, conducteur, km } = {}) {
|
|
106
|
+
const v = await lire(id);
|
|
107
|
+
const jour = date ? exigerJour(date, 'date') : aujourdhui();
|
|
108
|
+
const kmActuel = Number.isFinite(km) ? km : R.dernierReleve(await releves(id), jour)?.km;
|
|
109
|
+
let habilitation;
|
|
110
|
+
if (conducteur && habilitations) {
|
|
111
|
+
habilitation = Boolean(await habilitations.peutConduire(conducteur, D.types[v.type]?.categoriePermis ?? null, jour));
|
|
112
|
+
}
|
|
113
|
+
return R.evaluerDepart({
|
|
114
|
+
vehicule: v, declarations: D, echeances: await de('echeances', id), plans: await de('plans', id), km: kmActuel, date: jour,
|
|
115
|
+
conducteur, affectation: await courante(id), exigerAffectation, habilitation,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
CODES,
|
|
121
|
+
onEvenement(f) { abonnes.add(f); return () => abonnes.delete(f); },
|
|
122
|
+
lire,
|
|
123
|
+
|
|
124
|
+
/** Inscrire un véhicule d'un type déclaré. */
|
|
125
|
+
async inscrire(d = {}, { agent } = {}) {
|
|
126
|
+
if (!texte(d.code)) refus('DONNEE', 'code requis', 400);
|
|
127
|
+
if (!texte(d.immatriculation)) refus('DONNEE', 'immatriculation requise', 400);
|
|
128
|
+
if (!texte(d.type)) refus('DONNEE', 'type requis', 400);
|
|
129
|
+
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
|
+
}
|
|
134
|
+
const mode = d.mode ?? 'propre';
|
|
135
|
+
if (!CODES.mode.includes(mode)) refus('DONNEE', `mode inconnu : ${mode}`, 400);
|
|
136
|
+
if ((await P.vehicules.find((v) => v.code === code || v.immatriculation === immatriculation)).length) {
|
|
137
|
+
refus('DOUBLON', `code ou immatriculation déjà pris : ${code} / ${immatriculation}`, 409);
|
|
138
|
+
}
|
|
139
|
+
const v = await creer('vehicules', {
|
|
140
|
+
id: randomUUID(), code, immatriculation, type: d.type, marque: d.marque ?? null, modele: d.modele ?? null, annee: d.annee ?? null,
|
|
141
|
+
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,
|
|
143
|
+
});
|
|
144
|
+
emettre('vehicule.inscrit', { vehicule: v.id, code, type: v.type });
|
|
145
|
+
return v;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/** Passer une transition du cycle : immobiliser, atelier, remettre, reformer. */
|
|
149
|
+
async changerStatut(id, transition, ctx = {}) { return transiter(await lire(id), transition, ctx); },
|
|
150
|
+
|
|
151
|
+
// ── Échéances ──
|
|
152
|
+
async poserEcheance({ vehicule, type, debut, fin, piece, cout } = {}, { agent } = {}) {
|
|
153
|
+
await lire(vehicule);
|
|
154
|
+
const d = D.echeances[type] || refus('TYPE', `type d'échéance non déclaré : ${type}`, 400);
|
|
155
|
+
exigerJour(debut, 'debut');
|
|
156
|
+
const f = fin === undefined ? R.ajouterJours(debut, d.periodiciteJours) : exigerJour(fin, 'fin');
|
|
157
|
+
if (f <= debut) refus('DONNEE', 'la fin doit suivre le début', 400);
|
|
158
|
+
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() });
|
|
160
|
+
emettre('echeance.posee', { vehicule, echeance: e.id, type, fin: f });
|
|
161
|
+
return e;
|
|
162
|
+
},
|
|
163
|
+
async echeances(vehicule) { await lire(vehicule); return de('echeances', vehicule); },
|
|
164
|
+
/** Les échéances qui font foi et finissent au plus tard dans `dans` jours (les expirées comprises). */
|
|
165
|
+
async aEcheance({ dans = D.alerteJours, date } = {}) {
|
|
166
|
+
const jour = date ? exigerJour(date, 'date') : aujourdhui(), limite = R.ajouterJours(jour, dans);
|
|
167
|
+
const sortie = [];
|
|
168
|
+
for (const v of await P.vehicules.find((x) => x.statut !== 'reforme')) {
|
|
169
|
+
const toutes = await de('echeances', v.id);
|
|
170
|
+
for (const type of new Set(toutes.map((e) => e.type))) {
|
|
171
|
+
const e = R.echeanceCourante(toutes, type);
|
|
172
|
+
if (e.fin <= limite) sortie.push({ id: e.id, vehicule: v.id, code: v.code, type, fin: e.fin, n: R.jours(jour, e.fin), expiree: e.fin < jour });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return sortie.sort((a, b) => (a.fin < b.fin ? -1 : a.fin > b.fin ? 1 : 0));
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// ── Entretien ──
|
|
179
|
+
async planifierEntretien({ vehicule, libelle, tousLesKm, tousLesJours, toleranceKm = 0, toleranceJours = 0, dernierKm, dernierLe } = {}, { agent } = {}) {
|
|
180
|
+
await lire(vehicule);
|
|
181
|
+
if (!texte(libelle)) refus('DONNEE', 'libelle requis', 400);
|
|
182
|
+
if (!(tousLesKm > 0) && !(tousLesJours > 0)) refus('DONNEE', 'un plan exige un pas en km, en jours, ou les deux', 400);
|
|
183
|
+
for (const [n, q] of [[tousLesKm, 'tousLesKm'], [tousLesJours, 'tousLesJours']]) if (n !== undefined) exigerNombre(n, q, { strict: true });
|
|
184
|
+
exigerNombre(toleranceKm, 'toleranceKm'); exigerNombre(toleranceJours, 'toleranceJours');
|
|
185
|
+
const km = dernierKm ?? R.dernierReleve(await releves(vehicule))?.km ?? 0;
|
|
186
|
+
const p = await creer('plans', {
|
|
187
|
+
id: randomUUID(), vehicule, libelle: libelle.trim(), tousLesKm: tousLesKm ?? null, tousLesJours: tousLesJours ?? null, toleranceKm, toleranceJours,
|
|
188
|
+
dernierKm: exigerNombre(km, 'dernierKm'), dernierLe: dernierLe ? exigerJour(dernierLe, 'dernierLe') : aujourdhui(), creePar: agent ?? null,
|
|
189
|
+
});
|
|
190
|
+
emettre('entretien.planifie', { vehicule, plan: p.id });
|
|
191
|
+
return p;
|
|
192
|
+
},
|
|
193
|
+
async plans(vehicule) { await lire(vehicule); return de('plans', vehicule); },
|
|
194
|
+
/** Les plans dus ou dépassés à `date`, sur les véhicules non réformés. */
|
|
195
|
+
async entretiensDus({ date } = {}) {
|
|
196
|
+
const jour = date ? exigerJour(date, 'date') : aujourdhui();
|
|
197
|
+
const sortie = [];
|
|
198
|
+
for (const v of await P.vehicules.find((x) => x.statut !== 'reforme')) {
|
|
199
|
+
const km = R.dernierReleve(await releves(v.id), jour)?.km;
|
|
200
|
+
for (const p of await de('plans', v.id)) {
|
|
201
|
+
const s = R.etatEntretien(p, { km, date: jour });
|
|
202
|
+
if (s.etat === 'du' || s.etat === 'depasse') sortie.push({ plan: p.id, vehicule: v.id, code: v.code, libelle: p.libelle, ...s });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return sortie;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
// ── Interventions ──
|
|
209
|
+
async ouvrirIntervention({ vehicule, nature, plan, description, prestataire, ouverteLe } = {}, { agent } = {}) {
|
|
210
|
+
const v = await lire(vehicule);
|
|
211
|
+
if (!CODES.nature.includes(nature)) refus('DONNEE', `nature inconnue : ${nature}`, 400);
|
|
212
|
+
if (v.statut === 'reforme') refus('TRANSITION', 'un véhicule réformé ne reçoit plus d’intervention', 422);
|
|
213
|
+
if (plan !== undefined && !(await de('plans', vehicule)).some((p) => p.id === plan)) refus('INCONNU', `plan inconnu pour ce véhicule : ${plan}`, 404);
|
|
214
|
+
const jour = ouverteLe ? exigerJour(ouverteLe, 'ouverteLe') : aujourdhui(); // une saisie a posteriori garde sa date
|
|
215
|
+
const i = await creer('interventions', {
|
|
216
|
+
id: randomUUID(), numero: await numeroter({ vehicule, nature, date: jour }), vehicule, nature, plan: plan ?? null, ouverteLe: jour, closeLe: null,
|
|
217
|
+
km: null, cout: null, prestataire: prestataire ?? null, description: description ?? '', ouvertePar: agent ?? null,
|
|
218
|
+
});
|
|
219
|
+
emettre('intervention.ouverte', { vehicule, intervention: i.id, numero: i.numero, nature });
|
|
220
|
+
// Une corrective arrête le véhicule : règle du service (MostaGare le faisait dans une route).
|
|
221
|
+
if (nature === 'corrective' && v.statut === 'en_service') await transiter(v, 'atelier', { agent });
|
|
222
|
+
return i;
|
|
223
|
+
},
|
|
224
|
+
async modifierIntervention(id, champs = {}, { agent } = {}) {
|
|
225
|
+
const i = await intervention(id);
|
|
226
|
+
if (i.closeLe) refus('CLOSE', `intervention ${i.numero} close : elle ne se modifie plus`, 409);
|
|
227
|
+
const permis = {};
|
|
228
|
+
for (const k of ['description', 'prestataire']) if (champs[k] !== undefined) permis[k] = champs[k];
|
|
229
|
+
if (champs.cout !== undefined) permis.cout = exigerNombre(champs.cout, 'cout');
|
|
230
|
+
const r = await maj('interventions', i, { ...permis, modifieePar: agent ?? null });
|
|
231
|
+
emettre('intervention.modifiee', { vehicule: i.vehicule, intervention: i.id });
|
|
232
|
+
return r;
|
|
233
|
+
},
|
|
234
|
+
/** Clore : recale le plan lié, et le kilométrage vaut relevé. */
|
|
235
|
+
async cloreIntervention(id, { km, cout, le } = {}, { agent } = {}) {
|
|
236
|
+
const i = await intervention(id);
|
|
237
|
+
if (i.closeLe) refus('CLOSE', `intervention ${i.numero} déjà close`, 409);
|
|
238
|
+
const jour = le ? exigerJour(le, 'le') : aujourdhui();
|
|
239
|
+
if (jour < i.ouverteLe) refus('DONNEE', 'la clôture précède l’ouverture', 400);
|
|
240
|
+
if (cout !== undefined) exigerNombre(cout, 'cout');
|
|
241
|
+
const avert = km !== undefined ? await verifierReleve(i.vehicule, { km, le: jour }) : [];
|
|
242
|
+
const r = await maj('interventions', i, { closeLe: jour, km: km ?? null, cout: cout ?? null, closePar: agent ?? null });
|
|
243
|
+
emettre('intervention.close', { vehicule: i.vehicule, intervention: i.id, numero: i.numero });
|
|
244
|
+
if (km !== undefined) await ecrireReleve(i.vehicule, { km, le: jour, source: 'intervention' }, avert, agent);
|
|
245
|
+
if (i.plan) {
|
|
246
|
+
const p = (await de('plans', i.vehicule)).find((x) => x.id === i.plan);
|
|
247
|
+
if (p) await maj('plans', p, { dernierKm: km ?? p.dernierKm, dernierLe: jour });
|
|
248
|
+
}
|
|
249
|
+
return r;
|
|
250
|
+
},
|
|
251
|
+
async interventions(vehicule) { await lire(vehicule); return de('interventions', vehicule); },
|
|
252
|
+
|
|
253
|
+
// ── Relevés et pleins ──
|
|
254
|
+
async relever(vehicule, { km, le, source = 'saisie', correction } = {}, { agent } = {}) {
|
|
255
|
+
await lire(vehicule);
|
|
256
|
+
if (!CODES.source.includes(source)) refus('DONNEE', `source inconnue : ${source}`, 400);
|
|
257
|
+
const jour = le ?? aujourdhui();
|
|
258
|
+
const avert = await verifierReleve(vehicule, { km, le: jour, correction });
|
|
259
|
+
return ecrireReleve(vehicule, { km, le: jour, source, correction }, avert, agent);
|
|
260
|
+
},
|
|
261
|
+
async releves(vehicule) { await lire(vehicule); return releves(vehicule); },
|
|
262
|
+
async saisirPlein({ vehicule, le, litres, montant, km, complet = false, carte } = {}, { agent } = {}) {
|
|
263
|
+
await lire(vehicule);
|
|
264
|
+
const jour = le ?? aujourdhui();
|
|
265
|
+
exigerNombre(litres, 'litres', { strict: true }); exigerNombre(montant, 'montant');
|
|
266
|
+
const avert = await verifierReleve(vehicule, { km, le: jour });
|
|
267
|
+
const p = await creer('pleins', { id: randomUUID(), vehicule, le: jour, litres, montant, km, complet: Boolean(complet), carte: carte ?? null, saisiPar: agent ?? null });
|
|
268
|
+
emettre('plein.saisi', { vehicule, plein: p.id, litres, montant });
|
|
269
|
+
const r = await ecrireReleve(vehicule, { km, le: jour, source: 'plein' }, avert, agent);
|
|
270
|
+
return { ...p, avertissements: r.avertissements };
|
|
271
|
+
},
|
|
272
|
+
async consommation(vehicule) { await lire(vehicule); return R.consommation(await de('pleins', vehicule)); },
|
|
273
|
+
|
|
274
|
+
// ── Affectations ──
|
|
275
|
+
async affecter(vehicule, conducteur, { agent } = {}) {
|
|
276
|
+
const v = await lire(vehicule);
|
|
277
|
+
if (!texte(conducteur)) refus('DONNEE', 'conducteur requis (identifiant opaque)', 400);
|
|
278
|
+
if (v.statut === 'reforme') refus('TRANSITION', 'un véhicule réformé ne s’affecte plus', 422);
|
|
279
|
+
const actuelle = await courante(vehicule);
|
|
280
|
+
if (actuelle?.conducteur === conducteur) return actuelle;
|
|
281
|
+
if (!plusieursVehiculesParConducteur) {
|
|
282
|
+
const ailleurs = (await P.affectations.find((a) => a.conducteur === conducteur && !a.au && a.vehicule !== vehicule))[0];
|
|
283
|
+
if (ailleurs) refus('CONDUCTEUR_OCCUPE', `le conducteur ${conducteur} tient déjà un autre véhicule`, 409);
|
|
284
|
+
}
|
|
285
|
+
const jour = aujourdhui();
|
|
286
|
+
if (actuelle) {
|
|
287
|
+
await maj('affectations', actuelle, { au: jour, closePar: agent ?? null });
|
|
288
|
+
emettre('affectation.fin', { vehicule, affectation: actuelle.id, conducteur: actuelle.conducteur });
|
|
289
|
+
}
|
|
290
|
+
const a = await creer('affectations', { id: randomUUID(), vehicule, conducteur, du: jour, au: null, parAgent: agent ?? null });
|
|
291
|
+
emettre('affectation.debut', { vehicule, affectation: a.id, conducteur });
|
|
292
|
+
return a;
|
|
293
|
+
},
|
|
294
|
+
async liberer(vehicule, { agent } = {}) {
|
|
295
|
+
await lire(vehicule);
|
|
296
|
+
const actuelle = await courante(vehicule);
|
|
297
|
+
if (!actuelle) return null;
|
|
298
|
+
const r = await maj('affectations', actuelle, { au: aujourdhui(), closePar: agent ?? null });
|
|
299
|
+
emettre('affectation.fin', { vehicule, affectation: actuelle.id, conducteur: actuelle.conducteur });
|
|
300
|
+
return r;
|
|
301
|
+
},
|
|
302
|
+
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
|
+
async affectationCourante(vehicule) { await lire(vehicule); return courante(vehicule); },
|
|
304
|
+
|
|
305
|
+
// ── Décisions ──
|
|
306
|
+
peutPartir,
|
|
307
|
+
/** Les véhicules aptes à `date`, d'un type, avec des attributs minimaux (nombre : au moins ; sinon : égal). */
|
|
308
|
+
async disponibles({ date, type, attributs = {} } = {}) {
|
|
309
|
+
const jour = date ? exigerJour(date, 'date') : aujourdhui();
|
|
310
|
+
const sortie = [];
|
|
311
|
+
for (const v of await P.vehicules.find((x) => x.statut !== 'reforme' && (!type || x.type === type))) {
|
|
312
|
+
const assez = Object.entries(attributs).every(([k, min]) => (typeof min === 'number' ? v.attributs?.[k] >= min : v.attributs?.[k] === min));
|
|
313
|
+
if (assez && (await peutPartir(v.id, { date: jour })).ok) sortie.push(v);
|
|
314
|
+
}
|
|
315
|
+
return sortie.sort((a, b) => (a.code < b.code ? -1 : a.code > b.code ? 1 : 0));
|
|
316
|
+
},
|
|
317
|
+
async cout(vehicule, { du, au } = {}) {
|
|
318
|
+
await lire(vehicule);
|
|
319
|
+
exigerJour(du, 'du'); exigerJour(au, 'au');
|
|
320
|
+
if (au < du) refus('DONNEE', 'la période finit avant de commencer', 400);
|
|
321
|
+
return R.cout({ pleins: await de('pleins', vehicule), interventions: await de('interventions', vehicule), echeances: await de('echeances', vehicule), releves: await releves(vehicule), du, au });
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export { ErreurFlotte };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @mostajs/flotte — point d'entrée. Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
2
|
+
import { CODES } from './libelles.js';
|
|
3
|
+
export { createFlotte, ErreurFlotte } from './flotte.js';
|
|
4
|
+
export { createMemoryRepositories } from './memoire.js';
|
|
5
|
+
export { CODES, LIBELLES, libelle } from './libelles.js';
|
|
6
|
+
export { register, SCHEMAS } from './schemas.js';
|
|
7
|
+
export { validerDeclarations, FORMES } from './declarations.js';
|
|
8
|
+
export * as regles from './regles.js';
|
|
9
|
+
export const STATUTS = CODES.statut;
|
|
10
|
+
export const MOTIFS = CODES.motif;
|
|
11
|
+
/** Les permissions, au format domaine:action (§10 #5) : le module les nomme, rbac-tenant les vérifie. */
|
|
12
|
+
export const PERMISSIONS = Object.freeze([
|
|
13
|
+
'flotte:vehicules:inscrire', 'flotte:vehicules:statut', 'flotte:vehicules:reformer', 'flotte:echeances:gerer',
|
|
14
|
+
'flotte:entretien:saisir', 'flotte:carburant:saisir', 'flotte:affectations:gerer', 'flotte:couts:lire',
|
|
15
|
+
]);
|
package/src/libelles.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @mostajs/flotte — les codes de tout ce qui s'affiche, et leur libellé français canonique.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// Règle des libellés d'un module (mosta-i18n, méthode §7) : le module POSSÈDE les codes et le libellé canonique ;
|
|
5
|
+
// la langue d'affichage appartient à l'application. Guerway traduit la clé `flotte.<famille>.<code>` dans ses
|
|
6
|
+
// sept catalogues ; sans traduction, le libellé ci-dessous s'affiche. Les codes sont une API publique (SemVer).
|
|
7
|
+
|
|
8
|
+
export const LIBELLES = Object.freeze({
|
|
9
|
+
statut: { en_service: 'En service', immobilise: 'Immobilisé', a_l_atelier: 'À l’atelier', reforme: 'Réformé' },
|
|
10
|
+
transition: { immobiliser: 'Immobiliser', atelier: 'Envoyer à l’atelier', remettre: 'Remettre en service', reformer: 'Réformer' },
|
|
11
|
+
motif: {
|
|
12
|
+
STATUT: 'Le véhicule n’est pas en service ({statut})',
|
|
13
|
+
ECHEANCE_ABSENTE: 'Échéance absente : {type}',
|
|
14
|
+
ECHEANCE_EXPIREE: 'Échéance expirée : {type}, le {date}',
|
|
15
|
+
ENTRETIEN_DEPASSE: 'Entretien « {plan} » dépassé de {n} {unite}',
|
|
16
|
+
CONDUCTEUR: 'Le conducteur {conducteur} n’est pas affecté à ce véhicule',
|
|
17
|
+
HABILITATION: 'Le conducteur {conducteur} n’est pas habilité (catégorie {categorie})',
|
|
18
|
+
},
|
|
19
|
+
avertissement: {
|
|
20
|
+
ECHEANCE_PROCHE: 'Échéance proche : {type}, le {date}, dans {n} jours',
|
|
21
|
+
ENTRETIEN_PROCHE: 'Entretien « {plan} » dû dans {n} {unite}',
|
|
22
|
+
ENTRETIEN_DU: 'Entretien « {plan} » dû depuis {n} {unite}, dans la tolérance',
|
|
23
|
+
SAUT_ANORMAL: 'Saut anormal du compteur : {n} km en {jours} jours',
|
|
24
|
+
},
|
|
25
|
+
erreur: {
|
|
26
|
+
DECLARATION: 'Déclaration invalide',
|
|
27
|
+
DONNEE: 'Donnée invalide',
|
|
28
|
+
TYPE: 'Type non déclaré',
|
|
29
|
+
ATTRIBUT: 'Attribut invalide pour ce type',
|
|
30
|
+
IMMATRICULATION: 'Immatriculation hors des motifs déclarés',
|
|
31
|
+
DOUBLON: 'Code ou immatriculation déjà pris',
|
|
32
|
+
INCONNU: 'Élément inconnu',
|
|
33
|
+
MOTIF: 'Motif requis',
|
|
34
|
+
TRANSITION: 'Changement d’état refusé',
|
|
35
|
+
INTERVENTION_OUVERTE: 'Une intervention est encore ouverte',
|
|
36
|
+
CLOSE: 'Intervention close : elle ne se modifie plus',
|
|
37
|
+
RECUL: 'Le compteur recule',
|
|
38
|
+
CONDUCTEUR_OCCUPE: 'Le conducteur tient déjà un autre véhicule',
|
|
39
|
+
},
|
|
40
|
+
nature: { preventive: 'Préventive', corrective: 'Corrective', inspection: 'Inspection' },
|
|
41
|
+
source: { saisie: 'Saisie', plein: 'Plein', intervention: 'Intervention', gps: 'GPS', constructeur: 'Constructeur' },
|
|
42
|
+
mode: { propre: 'En propre', location: 'En location', sous_traitant: 'Sous-traitant' },
|
|
43
|
+
unite: { km: 'km', jours: 'jours' },
|
|
44
|
+
poste: { carburant: 'Carburant', entretien: 'Entretien', echeances: 'Échéances', amortissement: 'Amortissement', sinistres: 'Sinistres', pieces_hors_intervention: 'Pièces hors intervention' },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Les codes de chaque famille, dans l'ordre des libellés. */
|
|
48
|
+
export const CODES = Object.freeze(Object.fromEntries(Object.entries(LIBELLES).map(([f, l]) => [f, Object.freeze(Object.keys(l))])));
|
|
49
|
+
|
|
50
|
+
const remplir = (texte, vars) => texte.replace(/\{(\w+)\}/g, (m, k) => (vars && vars[k] !== undefined && vars[k] !== null ? String(vars[k]) : m));
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Le libellé à afficher. `t` est la fonction de traduction de la requête (Guerway : `t(cle, vars)`) ; si elle ne
|
|
54
|
+
* connaît pas la clé (elle rend la clé nue, ou rien), on se replie sur le libellé du module. Une FONCTION, appelée
|
|
55
|
+
* dans la requête : jamais une constante évaluée hors contexte de langue.
|
|
56
|
+
* @param {string} famille statut, transition, motif, avertissement, erreur, nature, source, mode, unite, poste
|
|
57
|
+
* @param {string} code
|
|
58
|
+
* @param {(cle: string, vars?: object) => string} [t]
|
|
59
|
+
* @param {object} [vars] les valeurs d'un motif ou d'un avertissement ({ plan, n, unite… })
|
|
60
|
+
*/
|
|
61
|
+
export function libelle(famille, code, t, vars) {
|
|
62
|
+
const cle = `flotte.${famille}.${code}`;
|
|
63
|
+
if (typeof t === 'function') {
|
|
64
|
+
const tr = t(cle, vars);
|
|
65
|
+
if (typeof tr === 'string' && tr && tr !== cle) return tr;
|
|
66
|
+
}
|
|
67
|
+
const canon = LIBELLES[famille]?.[code];
|
|
68
|
+
return canon === undefined ? code : remplir(canon, vars);
|
|
69
|
+
}
|
package/src/memoire.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @mostajs/flotte — dépôts en mémoire, pour les essais et la démonstration (en production : data-plug, §10.bis).
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
const TABLES = ['vehicules', 'echeances', 'plans', 'interventions', 'releves', 'pleins', 'affectations'];
|
|
4
|
+
|
|
5
|
+
/** Un dépôt par table : { find(prédicat), create(doc), update(id, champs) }, copies à l'entrée et à la sortie. */
|
|
6
|
+
export function createMemoryRepositories() {
|
|
7
|
+
const depot = () => {
|
|
8
|
+
const m = new Map();
|
|
9
|
+
return {
|
|
10
|
+
async create(d) { m.set(d.id, structuredClone(d)); return structuredClone(d); },
|
|
11
|
+
async update(id, p) { if (!m.has(id)) throw new Error(`flotte (mémoire) : ${id} inconnu`); Object.assign(m.get(id), structuredClone(p)); return structuredClone(m.get(id)); },
|
|
12
|
+
async find(f = () => true) { return [...m.values()].map((r) => structuredClone(r)).filter(f); },
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
return Object.fromEntries(TABLES.map((t) => [t, depot()]));
|
|
16
|
+
}
|
package/src/regles.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @mostajs/flotte — le cœur PUR : des valeurs en entrée, une réponse en sortie, aucun dépôt, aucune horloge.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// Les dates sont des jours 'AAAA-MM-JJ'. Motifs et avertissements sont des OBJETS { code, … } : la phrase
|
|
5
|
+
// appartient au catalogue de l'application (libelles.js), jamais au calcul.
|
|
6
|
+
|
|
7
|
+
const JOUR = 86_400_000;
|
|
8
|
+
const t0 = (d) => Date.parse(`${d}T00:00:00Z`);
|
|
9
|
+
/** Nombre de jours de `a` à `b` (négatif si `b` précède `a`). */
|
|
10
|
+
export const jours = (a, b) => Math.round((t0(b) - t0(a)) / JOUR);
|
|
11
|
+
export const ajouterJours = (d, n) => new Date(t0(d) + n * JOUR).toISOString().slice(0, 10);
|
|
12
|
+
export const estJour = (d) => typeof d === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(d) && !Number.isNaN(t0(d)) && ajouterJours(d, 0) === d;
|
|
13
|
+
const arrondi = (x) => Math.round(x * 100) / 100;
|
|
14
|
+
const parDate = (a, b) => (a.le < b.le ? -1 : a.le > b.le ? 1 : 0); // stable : l'ordre d'entrée départage
|
|
15
|
+
|
|
16
|
+
const RANG = { ok: 0, proche: 1, du: 2, depasse: 3 };
|
|
17
|
+
function selonPas(pas, tolerance, ecart) { // ecart = valeur atteinte − échéance du pas
|
|
18
|
+
if (ecart > tolerance) return { etat: 'depasse', n: ecart };
|
|
19
|
+
if (ecart >= 0) return { etat: 'du', n: ecart };
|
|
20
|
+
if (-ecart <= pas * 0.1) return { etat: 'proche', n: -ecart };
|
|
21
|
+
return { etat: 'ok', n: -ecart };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* L'état d'un plan d'entretien : dû au PREMIER atteint du kilométrage ou de la date.
|
|
26
|
+
* `proche` : moins de 10 % du pas avant l'échéance ; `du` : échéance passée, dans la tolérance ;
|
|
27
|
+
* `depasse` : au-delà de la tolérance. À gravité égale, le kilométrage l'emporte.
|
|
28
|
+
* @returns {{ etat: 'ok'|'proche'|'du'|'depasse', unite?: 'km'|'jours', n?: number }}
|
|
29
|
+
*/
|
|
30
|
+
export function etatEntretien(plan, { km, date }) {
|
|
31
|
+
const cands = [];
|
|
32
|
+
if (plan.tousLesKm > 0 && Number.isFinite(km)) {
|
|
33
|
+
cands.push({ unite: 'km', ...selonPas(plan.tousLesKm, plan.toleranceKm || 0, km - ((plan.dernierKm || 0) + plan.tousLesKm)) });
|
|
34
|
+
}
|
|
35
|
+
if (plan.tousLesJours > 0 && plan.dernierLe && date) {
|
|
36
|
+
cands.push({ unite: 'jours', ...selonPas(plan.tousLesJours, plan.toleranceJours || 0, jours(plan.dernierLe, date) - plan.tousLesJours) });
|
|
37
|
+
}
|
|
38
|
+
const pire = cands.reduce((m, c) => (!m || RANG[c.etat] > RANG[m.etat] ? c : m), null);
|
|
39
|
+
if (!pire || pire.etat === 'ok') return { etat: 'ok' };
|
|
40
|
+
return { etat: pire.etat, unite: pire.unite, n: pire.n };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 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
|
+
export function echeanceCourante(echeances, type) {
|
|
45
|
+
return echeances.filter((e) => e.type === type).sort((a, b) => (a.debut < b.debut ? -1 : a.debut > b.debut ? 1 : 0)).at(-1) || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Le dernier relevé au plus tard à `date` (tous si `date` est absente). */
|
|
49
|
+
export function dernierReleve(releves, date) {
|
|
50
|
+
return [...releves].sort(parDate).filter((r) => !date || r.le <= date).at(-1) || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* La consommation, mesurée entre deux pleins COMPLETS : les litres des pleins qui suivent le premier, jusqu'au
|
|
55
|
+
* second inclus, sur les kilomètres parcourus entre les deux. Un plein partiel seul ne mesure rien.
|
|
56
|
+
*/
|
|
57
|
+
export function consommation(pleins) {
|
|
58
|
+
const tries = [...pleins].sort((a, b) => a.km - b.km);
|
|
59
|
+
const mesures = [];
|
|
60
|
+
let depart = null, litres = 0;
|
|
61
|
+
for (const p of tries) {
|
|
62
|
+
if (depart) litres += p.litres;
|
|
63
|
+
if (p.complet) {
|
|
64
|
+
if (depart && p.km > depart.km) {
|
|
65
|
+
const km = p.km - depart.km;
|
|
66
|
+
mesures.push({ du: depart.km, au: p.km, litres: arrondi(litres), km, lPour100Km: arrondi((litres / km) * 100) });
|
|
67
|
+
}
|
|
68
|
+
depart = p; litres = 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const L = mesures.reduce((s, m) => s + m.litres, 0), K = mesures.reduce((s, m) => s + m.km, 0);
|
|
72
|
+
return { lPour100Km: K > 0 ? arrondi((L / K) * 100) : null, mesures };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* « Ce véhicule peut-il partir ? » : TOUS les motifs bloquants, et les avertissements.
|
|
77
|
+
* L'habilitation est résolue avant (le port est asynchrone) et arrive ici en valeur : `habilitation` vaut
|
|
78
|
+
* `true`, `false`, ou `undefined` quand elle n'est pas vérifiée.
|
|
79
|
+
*/
|
|
80
|
+
export function evaluerDepart({ vehicule, declarations, echeances, plans, km, date, conducteur, affectation, exigerAffectation, habilitation }) {
|
|
81
|
+
const motifs = [], avertissements = [];
|
|
82
|
+
if (vehicule.statut !== 'en_service') motifs.push({ code: 'STATUT', statut: vehicule.statut });
|
|
83
|
+
for (const [type, d] of Object.entries(declarations.echeances || {})) {
|
|
84
|
+
const e = echeanceCourante(echeances, type);
|
|
85
|
+
if (!e) { if (d.obligatoire) motifs.push({ code: 'ECHEANCE_ABSENTE', type }); continue; }
|
|
86
|
+
if (!d.bloquante) continue;
|
|
87
|
+
if (e.fin < date) motifs.push({ code: 'ECHEANCE_EXPIREE', type, date: e.fin });
|
|
88
|
+
else if (jours(date, e.fin) < (declarations.alerteJours ?? 30)) avertissements.push({ code: 'ECHEANCE_PROCHE', type, date: e.fin, n: jours(date, e.fin) });
|
|
89
|
+
}
|
|
90
|
+
for (const p of plans) {
|
|
91
|
+
const s = etatEntretien(p, { km, date });
|
|
92
|
+
if (s.etat === 'depasse') motifs.push({ code: 'ENTRETIEN_DEPASSE', plan: p.libelle, n: s.n, unite: s.unite });
|
|
93
|
+
else if (s.etat === 'du') avertissements.push({ code: 'ENTRETIEN_DU', plan: p.libelle, n: s.n, unite: s.unite });
|
|
94
|
+
else if (s.etat === 'proche') avertissements.push({ code: 'ENTRETIEN_PROCHE', plan: p.libelle, n: s.n, unite: s.unite });
|
|
95
|
+
}
|
|
96
|
+
if (conducteur && exigerAffectation && affectation?.conducteur !== conducteur) motifs.push({ code: 'CONDUCTEUR', conducteur });
|
|
97
|
+
if (conducteur && habilitation === false) {
|
|
98
|
+
motifs.push({ code: 'HABILITATION', conducteur, categorie: declarations.types?.[vehicule.type]?.categoriePermis ?? null });
|
|
99
|
+
}
|
|
100
|
+
return { ok: motifs.length === 0, motifs, avertissements };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Les postes que `cout` ne compte pas en 0.1.0 : le coût ne se lit pas comme complet. */
|
|
104
|
+
export const NON_COMPTES = Object.freeze(['amortissement', 'sinistres', 'pieces_hors_intervention']);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Le coût d'un véhicule sur [du, au] : carburant et entretien de la période, échéances au prorata de leur
|
|
108
|
+
* recouvrement avec la période, kilomètres entre le dernier relevé ≤ du (ou le premier de la période) et le
|
|
109
|
+
* dernier relevé ≤ au.
|
|
110
|
+
*/
|
|
111
|
+
export function cout({ pleins, interventions, echeances, releves, du, au }) {
|
|
112
|
+
const dans = (d) => d && d >= du && d <= au;
|
|
113
|
+
const carburant = pleins.filter((p) => dans(p.le)).reduce((s, p) => s + (p.montant || 0), 0);
|
|
114
|
+
const entretien = interventions.filter((i) => dans(i.closeLe)).reduce((s, i) => s + (i.cout || 0), 0);
|
|
115
|
+
let ech = 0;
|
|
116
|
+
for (const e of echeances) {
|
|
117
|
+
if (!e.cout) continue;
|
|
118
|
+
const duree = jours(e.debut, e.fin);
|
|
119
|
+
const recouvre = jours(e.debut > du ? e.debut : du, e.fin < au ? e.fin : au);
|
|
120
|
+
if (duree > 0 && recouvre > 0) ech += (e.cout * recouvre) / duree;
|
|
121
|
+
}
|
|
122
|
+
const echeancesPeriode = arrondi(ech);
|
|
123
|
+
const tries = [...releves].sort(parDate);
|
|
124
|
+
const fin = tries.filter((r) => r.le <= au).at(-1);
|
|
125
|
+
const debut = tries.filter((r) => r.le <= du).at(-1) || tries.find((r) => dans(r.le));
|
|
126
|
+
const km = fin && debut ? Math.max(0, fin.km - debut.km) : 0;
|
|
127
|
+
const total = arrondi(carburant + entretien + echeancesPeriode);
|
|
128
|
+
return { du, au, carburant, entretien, echeances: echeancesPeriode, total, km, parKm: km > 0 ? total / km : null, nonComptes: [...NON_COMPTES] };
|
|
129
|
+
}
|
package/src/schemas.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @mostajs/flotte — les sept tables, déclarées au socle par register (R-P). Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
const s = (name, collection, fields, indexes = []) => ({ name, collection, timestamps: false, relations: {}, fields, indexes });
|
|
3
|
+
const T = { type: 'string' }, TR = { type: 'string', required: true }, N = { type: 'number' }, NR = { type: 'number', required: true }, J = { type: 'json' }, B = { type: 'boolean' };
|
|
4
|
+
|
|
5
|
+
export const VehiculeSchema = s('Vehicule', 'flotte_vehicules', {
|
|
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,
|
|
8
|
+
}, [{ fields: { code: 'asc' }, unique: true }, { fields: { immatriculation: 'asc' }, unique: true }, { fields: { statut: 'asc', type: 'asc' } }]);
|
|
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,
|
|
11
|
+
}, [{ fields: { vehicule: 'asc', type: 'asc' } }, { fields: { fin: 'asc' } }]);
|
|
12
|
+
export const PlanEntretienSchema = s('PlanEntretien', 'flotte_plans', {
|
|
13
|
+
vehicule: TR, libelle: TR, tousLesKm: N, tousLesJours: N, toleranceKm: N, toleranceJours: N, dernierKm: N, dernierLe: T, creePar: T,
|
|
14
|
+
}, [{ fields: { vehicule: 'asc' } }]);
|
|
15
|
+
export const InterventionSchema = s('Intervention', 'flotte_interventions', {
|
|
16
|
+
numero: { ...TR, unique: true }, vehicule: TR, nature: TR, plan: T, ouverteLe: TR, closeLe: T, km: N, cout: N, prestataire: T, description: T,
|
|
17
|
+
ouvertePar: T, closePar: T, modifieePar: T,
|
|
18
|
+
}, [{ fields: { numero: 'asc' }, unique: true }, { fields: { vehicule: 'asc', closeLe: 'asc' } }]);
|
|
19
|
+
export const ReleveCompteurSchema = s('ReleveCompteur', 'flotte_releves', {
|
|
20
|
+
vehicule: TR, km: NR, le: TR, source: TR, correction: J, saisiPar: T,
|
|
21
|
+
}, [{ fields: { vehicule: 'asc', le: 'asc' } }]);
|
|
22
|
+
export const PleinSchema = s('Plein', 'flotte_pleins', {
|
|
23
|
+
vehicule: TR, le: TR, litres: NR, montant: NR, km: NR, complet: B, carte: T, saisiPar: T,
|
|
24
|
+
}, [{ fields: { vehicule: 'asc', le: 'asc' } }]);
|
|
25
|
+
export const AffectationSchema = s('Affectation', 'flotte_affectations', {
|
|
26
|
+
vehicule: TR, conducteur: TR, du: TR, au: T, parAgent: T, closePar: T,
|
|
27
|
+
}, [{ fields: { vehicule: 'asc', au: 'asc' } }, { fields: { conducteur: 'asc', au: 'asc' } }]);
|
|
28
|
+
|
|
29
|
+
export const SCHEMAS = Object.freeze([VehiculeSchema, EcheanceSchema, PlanEntretienSchema, InterventionSchema, ReleveCompteurSchema, PleinSchema, AffectationSchema]);
|
|
30
|
+
|
|
31
|
+
export function register(registry) {
|
|
32
|
+
registry.register({
|
|
33
|
+
manifest: { name: 'flotte', package: '@mostajs/flotte', version: '0.1.0', type: 'business', priority: 45, displayName: 'Flotte (carnet du parc)',
|
|
34
|
+
description: 'Registre des véhicules, cycle, échéances, entretien, relevés, pleins, affectations, coût au km, peutPartir.', register: './src/schemas.js' },
|
|
35
|
+
schemas: SCHEMAS.map((x) => ({ name: x.name, schema: x })),
|
|
36
|
+
});
|
|
37
|
+
}
|