@mostajs/attendance 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 ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog — @mostajs/attendance
2
+ ## [0.1.0] — 2026-06-18
3
+ ### Added
4
+ - Pointage manuel/QR/visage, absences, alertes (notifications), stats (taux de présence). Compose @mostajs/users + @mostajs/face.
5
+ - 5 tests + exemple §12 (classe : visage+QR+manuel, alerte parent).
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # @mostajs/attendance
2
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later · **Statut** : 0.1.0 (5 tests verts)
3
+ > Présences : manuel/QR/visage, absences, alertes parents, stats. Détenteur = @mostajs/users ; compose @mostajs/face + notifications.
4
+ ```js
5
+ import { createAttendance, createMemoryRepositories } from '@mostajs/attendance';
6
+ const at = createAttendance({ repositories: createMemoryRepositories(), users, notifications });
7
+ await at.markByQr(sessionId, badgeCode); // ou markByFace(sessionId, descriptor) / mark(...)
8
+ await at.alertAbsence(learnerId, { contact: 'parent@x' });
9
+ ```
10
+ Lancer : `node test-scripts/unit/attendance.test.mjs && node examples/classe/run.mjs`
@@ -0,0 +1,12 @@
1
+ # @mostajs/attendance — DEVRULES de bout en bout (§4 + #3 + #4 condensés)
2
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Date** : 2026-06-18 · **Statut** : cas C (P1, ATC F4 + accès)
3
+ **Principe** : EXTRACTION du patron présence (ATC + contrôle d'accès SecuAccessPro). Détenteur = @mostajs/users.
4
+ ## §4 / §3
5
+ Pointage **manuel/QR/carte/visage** + **absences** + **alertes parents** + **stats**. DB-agnostique.
6
+ **Compose** : @mostajs/users (identifier par qrCode/matchFace→@mostajs/face), notifications (alertes), scan/pwa-scan (capture).
7
+ **Frontière** : la séance/cours = @mostajs/training (sessionId opaque) ; attendance ne gère que la présence.
8
+ **API** : `createAttendance({repositories,users?,notifications?})` → `mark(sessionId,learnerId,{status,method,by})`,
9
+ `markByQr(sessionId,code)`, `markByFace(sessionId,descriptor)`, `listBySession/listByLearner/absences`, `stats(sessionId)`, `alertAbsence(learnerId,{contact})`.
10
+ **Jalons** : 0.1 mark+stats · 0.2 markByQr/markByFace(users/face)+absences/alertes · 0.3 scan/pwa-scan UI · 1.0 14 livrables.
11
+ ## §4 (tests)
12
+ mark idempotent, markByQr (users), markByFace (face réel), stats(taux), absences+alerte notifications. (5 verts)
@@ -0,0 +1,20 @@
1
+ // Exemple §12 — présences d'une classe (manuel + QR + visage) + alerte parent. node examples/classe/run.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { createAttendance, createMemoryRepositories } from '../../src/index.js';
4
+ import { createUsers, createMemoryRepositories as userRepos } from '../../../mosta-users-stack/mosta-users/src/index.js';
5
+ import { findMatch } from '../../../mosta-face/dist/lib/face-matcher.js';
6
+ const desc=(s)=>Array.from({length:128},(_,i)=>Math.sin(s*(i+1))*0.5);
7
+ const users=createUsers({ repositories:userRepos(), face:{ findMatch }, qr:{ generate:(id)=>'BADGE:'+id } });
8
+ const e1=await users.create({firstName:'Inès'}); await users.enrollFace(e1.id, desc(1));
9
+ const e2=await users.create({firstName:'Karim'});
10
+ const e3=await users.create({firstName:'Sami'});
11
+ const at=createAttendance({ repositories:createMemoryRepositories(), users, notifications:{ notify:(e)=>console.log(' ↪ alerte parent:',e.type,e.contact) } });
12
+ const SES='ENG-A1-mardi';
13
+ await at.markByFace(SES, desc(1).map(x=>x+0.002)); // Inès par visage
14
+ await at.markByQr(SES, e2.qrCode); // Karim par badge
15
+ await at.mark(SES, e3.id, { status:'absent' }); // Sami absent (pointage manuel)
16
+ const st=await at.stats(SES);
17
+ assert.equal(st.present,2); assert.equal(st.absent,1);
18
+ await at.alertAbsence(e3.id, { contact:'parent.sami@ex.dz' });
19
+ console.log('✅ attendance — classe %s : présents %d/%d (taux %s) · visage+QR+manuel',
20
+ SES, st.present, st.total, st.presenceRate.toFixed(2));
package/llms.txt ADDED
@@ -0,0 +1,13 @@
1
+ # @mostajs/attendance — fiche LLM
2
+ RÔLE
3
+ Présences génériques : pointage manuel/QR/carte/visage, absences, alertes parents, stats. DB-agnostique.
4
+ Détenteur = @mostajs/users (résolution qrCode/matchFace→@mostajs/face). Compose notifications/scan. Délègue la séance à @mostajs/training.
5
+ EXPORTS
6
+ createAttendance({ repositories, users?, notifications?, now? }) -> api ; createMemoryRepositories(); AttendanceRecordSchema
7
+ API
8
+ mark(sessionId,learnerId,{status:'present'|'absent'|'late'|'excused',method:'manual'|'qr'|'card'|'face',by}) (1 enreg./séance+apprenant)
9
+ markByQr(sessionId,code) markByFace(sessionId,descriptor) listBySession/listByLearner/absences
10
+ stats(sessionId) -> {total,present,late,absent,excused,presenceRate} alertAbsence(learnerId,{contact})
11
+ PIÈGES
12
+ - markByQr exige users (qrCode) ; markByFace exige users.matchFace (face injecté). sessionId opaque (→ training).
13
+ - mark = upsert par (séance, apprenant). Alerte parents = compose notifications.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@mostajs/attendance",
3
+ "version": "0.1.0",
4
+ "description": "Présences génériques : pointage manuel/QR/carte/visage, absences, alertes. Détenteur = @mostajs/users ; compose face/scan/notifications. DB-agnostique.",
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
+ "./schemas": "./src/schemas.js"
12
+ },
13
+ "keywords": [
14
+ "mostajs",
15
+ "attendance",
16
+ "presence",
17
+ "qr",
18
+ "face",
19
+ "absences"
20
+ ],
21
+ "scripts": {
22
+ "test": "node test-scripts/unit/attendance.test.mjs",
23
+ "example": "node examples/classe/run.mjs"
24
+ }
25
+ }
@@ -0,0 +1,50 @@
1
+ // @mostajs/attendance — présences. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ // Détenteur = @mostajs/users (identification par QR/visage). Compose users(matchFace)/notifications. Délègue séance à training.
3
+
4
+ export function createAttendance({ repositories, users, notifications, now = () => new Date() } = {}) {
5
+ if (!repositories?.records) throw new Error('createAttendance: repositories.records requis');
6
+ const { records } = repositories;
7
+ const findOne = async (sessionId, learnerId) => (await records.find((r) => r.sessionId === sessionId && r.learnerId === learnerId))[0];
8
+
9
+ async function mark(sessionId, learnerId, { status = 'present', method = 'manual', by } = {}) {
10
+ const ex = await findOne(sessionId, learnerId);
11
+ return ex ? records.update(ex.id, { status, method, at: now(), markedBy: by }) // un enregistrement par (séance, apprenant)
12
+ : records.create({ sessionId, learnerId, status, method, at: now(), markedBy: by });
13
+ }
14
+ const listByLearner = (learnerId) => records.find((r) => r.learnerId === learnerId);
15
+ const absences = (learnerId) => records.find((r) => r.learnerId === learnerId && r.status === 'absent');
16
+
17
+ const api = {
18
+ mark,
19
+ /** Pointage par badge QR (résout l'apprenant via @mostajs/users qrCode). */
20
+ async markByQr(sessionId, code, opts = {}) {
21
+ if (!users?.repositories?.users) throw new Error('users requis (markByQr)');
22
+ const u = (await users.repositories.users.find((x) => x.qrCode === code))[0];
23
+ if (!u) throw new Error('badge inconnu');
24
+ return mark(sessionId, u.id, { method: 'qr', status: 'present', ...opts });
25
+ },
26
+ /** Pointage par visage (résout via @mostajs/users matchFace → @mostajs/face). */
27
+ async markByFace(sessionId, descriptor, opts = {}) {
28
+ if (!users?.matchFace) throw new Error('users.matchFace requis (markByFace)');
29
+ const m = await users.matchFace(descriptor);
30
+ if (!m) throw new Error('visage inconnu');
31
+ return mark(sessionId, m.match.id, { method: 'face', status: 'present', ...opts });
32
+ },
33
+ listBySession: (sessionId) => records.find((r) => r.sessionId === sessionId),
34
+ listByLearner, absences,
35
+ async stats(sessionId) {
36
+ const rs = await records.find((r) => r.sessionId === sessionId);
37
+ const by = (s) => rs.filter((r) => r.status === s).length;
38
+ const present = by('present') + by('late');
39
+ return { total: rs.length, present: by('present'), late: by('late'), absent: by('absent'), excused: by('excused'), presenceRate: rs.length ? present / rs.length : 0 };
40
+ },
41
+ /** Alerte d'absence (compose @mostajs/notifications → parents). */
42
+ async alertAbsence(learnerId, { contact } = {}) {
43
+ const abs = await absences(learnerId);
44
+ if (abs.length) notifications?.notify?.({ type: 'attendance.absence', learnerId, contact, count: abs.length });
45
+ return abs.length;
46
+ },
47
+ repositories,
48
+ };
49
+ return api;
50
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // @mostajs/attendance — point d'entrée. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ export { createAttendance } from './attendance.js';
3
+ export { createMemoryRepositories } from './memory-repo.js';
4
+ export { AttendanceRecordSchema } from './schemas.js';
@@ -0,0 +1,7 @@
1
+ // @mostajs/attendance — repos mémoire. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ function coll() { const m = new Map(); return {
3
+ async create(d) { const id = d.id || globalThis.crypto.randomUUID(); const now = new Date(); const r = { id, createdAt: now, updatedAt: now, ...d }; m.set(id, r); return { ...r }; },
4
+ async findById(id) { const r = m.get(id); return r ? { ...r } : null; },
5
+ async update(id, p) { const r = m.get(id); if (!r) return null; const x = { ...r, ...p, updatedAt: new Date() }; m.set(id, x); return { ...x }; },
6
+ async find(f = () => true) { return [...m.values()].filter(f).map((r) => ({ ...r })); } }; }
7
+ export function createMemoryRepositories() { return { records: coll() }; }
package/src/schemas.js ADDED
@@ -0,0 +1,13 @@
1
+ // @mostajs/attendance — schéma (EntitySchema @mostajs/orm). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ export const AttendanceRecordSchema = {
3
+ name: 'AttendanceRecord', collection: 'attendance_records', timestamps: true,
4
+ fields: {
5
+ sessionId: { type: 'string', required: true }, // séance/groupe (ex. @mostajs/training Session)
6
+ learnerId: { type: 'string', required: true }, // détenteur (@mostajs/users)
7
+ status: { type: 'string', enum: ['present', 'absent', 'late', 'excused'], default: 'present' },
8
+ method: { type: 'string', enum: ['manual', 'qr', 'card', 'face'], default: 'manual' },
9
+ at: { type: 'date', default: 'now' },
10
+ markedBy: { type: 'string', default: null },
11
+ },
12
+ indexes: [{ fields: { sessionId: 'asc' } }, { fields: { learnerId: 'asc' } }],
13
+ };
@@ -0,0 +1,41 @@
1
+ // @mostajs/attendance — tests (DEVRULES §5). node test-scripts/unit/attendance.test.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { createAttendance, createMemoryRepositories } from '../../src/index.js';
4
+ import { createUsers, createMemoryRepositories as userRepos } from '../../../mosta-users-stack/mosta-users/src/index.js';
5
+ import { findMatch } from '../../../mosta-face/dist/lib/face-matcher.js';
6
+ let pass=0; const test=async(n,f)=>{await f();pass++;console.log(' ✓',n);};
7
+ const desc=(s)=>Array.from({length:128},(_,i)=>Math.sin(s*(i+1))*0.5);
8
+
9
+ await test('mark idempotent par (séance, apprenant)', async()=>{
10
+ const at=createAttendance({ repositories:createMemoryRepositories() });
11
+ await at.mark('s1','l1',{status:'absent'}); await at.mark('s1','l1',{status:'present',method:'manual'});
12
+ const rs=await at.listBySession('s1'); assert.equal(rs.length,1); assert.equal(rs[0].status,'present');
13
+ });
14
+ await test('markByQr résout via users (qrCode)', async()=>{
15
+ const users=createUsers({ repositories:userRepos(), qr:{ generate:(id)=>'BADGE:'+id } });
16
+ const u=await users.create({firstName:'A'});
17
+ const at=createAttendance({ repositories:createMemoryRepositories(), users });
18
+ const r=await at.markByQr('s1', u.qrCode); assert.equal(r.learnerId,u.id); assert.equal(r.method,'qr');
19
+ await assert.rejects(()=>at.markByQr('s1','BADGE:x'), /badge inconnu/);
20
+ });
21
+ await test('markByFace résout via users.matchFace (@mostajs/face)', async()=>{
22
+ const users=createUsers({ repositories:userRepos(), face:{ findMatch } });
23
+ const u=await users.create({firstName:'A'}); await users.enrollFace(u.id, desc(1));
24
+ const at=createAttendance({ repositories:createMemoryRepositories(), users });
25
+ const r=await at.markByFace('s1', desc(1).map(x=>x+0.001)); assert.equal(r.learnerId,u.id); assert.equal(r.method,'face');
26
+ await assert.rejects(()=>at.markByFace('s1', desc(99)), /visage inconnu/);
27
+ });
28
+ await test('stats : taux de présence', async()=>{
29
+ const at=createAttendance({ repositories:createMemoryRepositories() });
30
+ await at.mark('s1','a',{status:'present'}); await at.mark('s1','b',{status:'late'}); await at.mark('s1','c',{status:'absent'});
31
+ const st=await at.stats('s1'); assert.equal(st.total,3); assert.equal(st.absent,1);
32
+ assert.equal(st.presenceRate.toFixed(3),(2/3).toFixed(3));
33
+ });
34
+ await test('absences + alerte parents (notifications)', async()=>{
35
+ const ev=[]; const at=createAttendance({ repositories:createMemoryRepositories(), notifications:{ notify:(e)=>ev.push(e) } });
36
+ await at.mark('s1','l1',{status:'absent'}); await at.mark('s2','l1',{status:'absent'});
37
+ assert.equal((await at.absences('l1')).length,2);
38
+ assert.equal(await at.alertAbsence('l1',{contact:'parent@x'}),2);
39
+ assert.equal(ev[0].type,'attendance.absence'); assert.equal(ev[0].count,2);
40
+ });
41
+ console.log(`\n✅ @mostajs/attendance — ${pass} tests OK`);