@mostajs/queue-bridge 0.2.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/README.md +25 -0
- package/llms.txt +39 -0
- package/package.json +31 -0
- package/src/index.js +100 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @mostajs/queue-bridge
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
Pont **générique** de **file d'attente / RDV** — accueil → file → appel, avec **identification**
|
|
6
|
+
par **badge QR** ou **reconnaissance faciale**. Orchestration pure (DEVRULES §10) : injecte un
|
|
7
|
+
**moteur de file** (`ticketing`, ex. `@mostajs/queue` de TicketFlow) et un **port d'identité**
|
|
8
|
+
(`identity`). Entité propre : **Visit**. DB-agnostique.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import { createQueueBridge, VisitSchema } from '@mostajs/queue-bridge';
|
|
12
|
+
const q = createQueueBridge({
|
|
13
|
+
repositories: { visits }, // ORM/data-plug
|
|
14
|
+
identity: { resolve: (tok) => verifyBadge(tok) }, // badge QR / face → { personId }
|
|
15
|
+
// ticketing: { issue: (svc) => queue.issue(svc) }, // pont TicketFlow (optionnel)
|
|
16
|
+
});
|
|
17
|
+
const who = await q.identify(badgeToken);
|
|
18
|
+
const v = await q.checkIn({ service: 'medecin', personId: who.personId }); // ticket + file
|
|
19
|
+
await q.position(v.id); // rang dans la file
|
|
20
|
+
const next = await q.callNext('medecin', { counter: 'box-1' });
|
|
21
|
+
await q.complete(next.id);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Tests & exemple (§11.1 / §12) : `npm install && npm test`. Conception : `DESIGN.md`. Proposition &
|
|
25
|
+
livrables : `docs/`. Fiche LLM : `llms.txt`.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @mostajs/queue-bridge — fiche LLM
|
|
2
|
+
> Pont générique de file d'attente / RDV : accueil → file → appel, identification badge QR / reconnaissance faciale.
|
|
3
|
+
|
|
4
|
+
- Version: 0.1.0 (MVP) · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
- Conception: DESIGN.md
|
|
6
|
+
|
|
7
|
+
## RÔLE
|
|
8
|
+
Orchestration PURE (DEVRULES §10) d'une file d'attente avec RDV. Entité propre : **Visit**
|
|
9
|
+
(personne ↔ ticket ↔ RDV). Il INJECTE (ne réimplémente pas) : le **moteur de file** (`ticketing`,
|
|
10
|
+
ex. @mostajs/queue de TicketFlow) et l'**identité** (`identity` : badge QR via qrpanel/auth-magic-qr,
|
|
11
|
+
ou reconnaissance faciale via @mostajs/face). DB-agnostique (repositories injectés).
|
|
12
|
+
|
|
13
|
+
## EXPORTS
|
|
14
|
+
- createQueueBridge({ repositories, ticketing?, identity?, now? }) -> API
|
|
15
|
+
- VisitSchema (EntitySchema @mostajs/orm)
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
- checkIn({ personId?, service, appointmentId?, label?, by? }) -> Visit (accueil → file)
|
|
19
|
+
- identify(token) -> { personId, … } | null (badge QR ou face → personne, via port identity)
|
|
20
|
+
- listWaiting(service?) -> Visit[] (FIFO par heure d'arrivée)
|
|
21
|
+
- callNext(service, { counter?, by? }) -> Visit | null
|
|
22
|
+
- start(visitId) / complete(visitId) / cancel(visitId) -> Visit
|
|
23
|
+
- status(visitId) -> Visit ; position(visitId) -> number (1-based, 0 si non en attente)
|
|
24
|
+
|
|
25
|
+
## PORTS injectés
|
|
26
|
+
- repositories.visits : { create, findById, update, find(predicate) } [requis]
|
|
27
|
+
- ticketing: { issue(service) -> ticket } [optionnel ; def: compteur interne par service]
|
|
28
|
+
- identity: { resolve(token) -> { personId } } [optionnel ; badge QR / reconnaissance faciale]
|
|
29
|
+
|
|
30
|
+
## STATUTS Visit
|
|
31
|
+
waiting → called → in_progress → done (ou cancelled)
|
|
32
|
+
|
|
33
|
+
## COMPOSITION (cible)
|
|
34
|
+
- Salsabil : services = manager/médecin/gestionnaire/agent social/comptable ; identity = badge QR
|
|
35
|
+
(@mostajs/qrpanel scan) + face (@mostajs/face) ; pont TicketFlow via adaptateur `ticketing`.
|
|
36
|
+
|
|
37
|
+
## ROADMAP (cf. DESIGN.md)
|
|
38
|
+
RDV (@mostajs/booking), reconnaissance faciale (@mostajs/face 128-D), pont TicketFlow temps réel
|
|
39
|
+
(Socket.io + waiting-room), multi-guichets.
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/queue-bridge",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pont générique de file d'attente / RDV — orchestration Visit (accueil → file → appel), composant un moteur de file (ticketing) et l'identification (badge QR / reconnaissance faciale). DB-agnostique.",
|
|
5
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"llms.txt"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"queue",
|
|
19
|
+
"file-attente",
|
|
20
|
+
"ticketing",
|
|
21
|
+
"rdv",
|
|
22
|
+
"appointment",
|
|
23
|
+
"mostajs"
|
|
24
|
+
],
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@mostajs/mjs-unit": "^0.3.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node test-scripts/unit/queue-bridge.test.mjs && node examples/run.mjs"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @mostajs/queue-bridge — pont générique de file d'attente / RDV (couche logique). MVP.
|
|
2
|
+
// Orchestration PURE (DEVRULES §10) : ne réimplémente pas le moteur de file — il l'injecte
|
|
3
|
+
// (`ticketing`, ex. @mostajs/queue de TicketFlow) — ni l'identité (`identity`, badge QR / face).
|
|
4
|
+
// Entité propre : Visit (personne ↔ ticket ↔ RDV). DB-agnostique (repositories injectés).
|
|
5
|
+
// @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
/** Visite = passage d'une personne à l'accueil pour un service (EntitySchema @mostajs/orm). */
|
|
8
|
+
export const VisitSchema = {
|
|
9
|
+
name: 'Visit', collection: 'queue_visits', timestamps: true,
|
|
10
|
+
fields: {
|
|
11
|
+
personId: { type: 'string', default: null }, service: { type: 'string', required: true },
|
|
12
|
+
ticket: { type: 'number', default: 0 }, status: { type: 'string', default: 'waiting' }, // waiting|called|in_progress|done|cancelled|no_show
|
|
13
|
+
appointmentId: { type: 'string', default: null }, label: { type: 'string', default: null },
|
|
14
|
+
reason: { type: 'string', default: null }, priority: { type: 'number', default: 0 }, // 0 normal, >0 prioritaire
|
|
15
|
+
recalls: { type: 'number', default: 0 }, fromVisitId: { type: 'string', default: null }, // parcours (re-orientation)
|
|
16
|
+
counter: { type: 'string', default: null }, by: { type: 'string', default: null },
|
|
17
|
+
checkedInAt: { type: 'date' }, calledAt: { type: 'date', default: null }, doneAt: { type: 'date', default: null },
|
|
18
|
+
},
|
|
19
|
+
indexes: [{ fields: { service: 'asc' } }, { fields: { status: 'asc' } }],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {object} opts.repositories { visits } — { create, findById, update, find(predicate) }
|
|
25
|
+
* @param {object} [opts.ticketing] port moteur de file : { issue(service) -> ticket } (def: compteur interne)
|
|
26
|
+
* @param {object} [opts.identity] port d'identification : { resolve(token) -> { personId, ... } } (badge QR / face)
|
|
27
|
+
* @param {() => Date} [opts.now]
|
|
28
|
+
*/
|
|
29
|
+
export function createQueueBridge({ repositories = {}, ticketing = null, identity = null, now = () => new Date() } = {}) {
|
|
30
|
+
const visits = repositories.visits;
|
|
31
|
+
if (!visits) throw new Error('createQueueBridge: repositories.visits requis');
|
|
32
|
+
|
|
33
|
+
async function nextTicket(service) {
|
|
34
|
+
if (ticketing?.issue) return ticketing.issue(service); // pont externe (TicketFlow / @mostajs/queue)
|
|
35
|
+
const same = await visits.find((v) => v.service === service); // repli : compteur interne par service
|
|
36
|
+
return same.length + 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Enregistre une visite (accueil) → mise en file d'attente. */
|
|
40
|
+
async function checkIn({ personId = null, service, appointmentId = null, label = null, reason = null, priority = 0, fromVisitId = null, by = null } = {}) {
|
|
41
|
+
if (!service) throw new Error('queue-bridge: service requis');
|
|
42
|
+
const ticket = await nextTicket(service);
|
|
43
|
+
return visits.create({ personId, service, ticket, status: 'waiting', appointmentId, label, reason, priority: Number(priority) || 0, recalls: 0, fromVisitId, counter: null, by, checkedInAt: now(), calledAt: null, doneAt: null });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Identifie une personne par badge QR ou reconnaissance faciale (port `identity`). */
|
|
47
|
+
async function identify(token) {
|
|
48
|
+
if (!identity?.resolve) return null;
|
|
49
|
+
return (await identity.resolve(token)) || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** File d'attente d'un service (ou globale) : priorité décroissante puis heure d'arrivée (FIFO). */
|
|
53
|
+
async function listWaiting(service) {
|
|
54
|
+
const v = await visits.find((x) => x.status === 'waiting' && (!service || x.service === service));
|
|
55
|
+
return v.sort((a, b) => (Number(b.priority) || 0) - (Number(a.priority) || 0)
|
|
56
|
+
|| new Date(a.checkedInAt).getTime() - new Date(b.checkedInAt).getTime());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Appelle la prochaine visite en attente d'un service. */
|
|
60
|
+
async function callNext(service, { counter = null, by = null } = {}) {
|
|
61
|
+
const [next] = await listWaiting(service);
|
|
62
|
+
if (!next) return null;
|
|
63
|
+
return visits.update(next.id, { status: 'called', calledAt: now(), counter, by });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const start = (visitId) => visits.update(visitId, { status: 'in_progress' });
|
|
67
|
+
const complete = (visitId) => visits.update(visitId, { status: 'done', doneAt: now() });
|
|
68
|
+
const cancel = (visitId) => visits.update(visitId, { status: 'cancelled' });
|
|
69
|
+
const status = (visitId) => visits.findById(visitId);
|
|
70
|
+
|
|
71
|
+
/** Rappelle une visite déjà appelée (ré-annonce). */
|
|
72
|
+
async function recall(visitId) {
|
|
73
|
+
const v = await visits.findById(visitId);
|
|
74
|
+
return visits.update(visitId, { status: 'called', calledAt: now(), recalls: (Number(v?.recalls) || 0) + 1 });
|
|
75
|
+
}
|
|
76
|
+
/** Marque une visite « absent » (appelée mais non présentée). */
|
|
77
|
+
const noShow = (visitId) => visits.update(visitId, { status: 'no_show' });
|
|
78
|
+
|
|
79
|
+
/** Ré-oriente vers un autre service (parcours multi-guichets) : clôt la visite courante
|
|
80
|
+
* et en crée une nouvelle (nouveau ticket) dans la file cible, en conservant la personne/le RDV. */
|
|
81
|
+
async function transfer(visitId, { service, reason = null, priority, by = null } = {}) {
|
|
82
|
+
if (!service) throw new Error('queue-bridge.transfer: service cible requis');
|
|
83
|
+
const v = await visits.findById(visitId);
|
|
84
|
+
if (!v) throw new Error('queue-bridge.transfer: visite introuvable');
|
|
85
|
+
await visits.update(visitId, { status: 'done', doneAt: now() });
|
|
86
|
+
return checkIn({ personId: v.personId, service, appointmentId: v.appointmentId, label: v.label, reason, priority: priority != null ? priority : v.priority, fromVisitId: visitId, by });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Position (1-based) d'une visite dans sa file ; 0 si non en attente. */
|
|
90
|
+
async function position(visitId) {
|
|
91
|
+
const v = await visits.findById(visitId);
|
|
92
|
+
if (!v || v.status !== 'waiting') return 0;
|
|
93
|
+
const w = await listWaiting(v.service);
|
|
94
|
+
return w.findIndex((x) => x.id === visitId) + 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { now, checkIn, identify, listWaiting, callNext, start, complete, cancel, recall, noShow, transfer, status, position };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default createQueueBridge;
|