@mostajs/staff 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 +5 -0
- package/README.md +11 -0
- package/docs/00-PROPOSITION-PLAN-STAFF-18062026.md +11 -0
- package/examples/intervenants/run.mjs +22 -0
- package/llms.txt +15 -0
- package/package.json +26 -0
- package/src/index.js +4 -0
- package/src/memory-repo.js +7 -0
- package/src/schemas.js +18 -0
- package/src/staff.js +45 -0
- package/test-scripts/unit/staff.test.mjs +48 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Changelog — @mostajs/staff
|
|
2
|
+
## [0.1.0] — 2026-06-18
|
|
3
|
+
### Added
|
|
4
|
+
- Profil intervenant (lié users) + diplômes (storage) + contrats + heures + vacations (heures×tarif, payout via payment). Disponibilités.
|
|
5
|
+
- Généralisation de faculty. 7 tests + exemple §12 (enseignant ATC + expert incubateur).
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @mostajs/staff
|
|
2
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later · **Statut** : 0.1.0 (7 tests verts)
|
|
3
|
+
> Intervenants (enseignants/experts/enquêteurs) : profil lié @mostajs/users, CV/diplômes, contrats, disponibilités, heures, vacations.
|
|
4
|
+
```js
|
|
5
|
+
import { createStaff, createMemoryRepositories } from '@mostajs/staff';
|
|
6
|
+
const s = createStaff({ repositories: createMemoryRepositories(), users, payment });
|
|
7
|
+
const t = await s.staff.create({ userId, type:'teacher', hourlyRate:1500 });
|
|
8
|
+
await s.work.log(t.id, { hours: 4, sessionRef:'ENG-A1' });
|
|
9
|
+
const due = await s.pay.vacationDue(t.id); // { hours, rate, amount }
|
|
10
|
+
```
|
|
11
|
+
Lancer : `node test-scripts/unit/staff.test.mjs && node examples/intervenants/run.mjs`
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @mostajs/staff — DEVRULES de bout en bout (§4 + #3 condensés)
|
|
2
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Date** : 2026-06-18 · **Statut** : cas C (P1+P2+P3)
|
|
3
|
+
**Principe** : GÉNÉRALISATION de `faculty` (enseignants) → intervenants (enseignant/expert/enquêteur). La PERSONNE = @mostajs/users.
|
|
4
|
+
## §4 / §3
|
|
5
|
+
Profil intervenant (lié `userId`) + **CV/diplômes** + **contrats** + **disponibilités** + **heures réalisées** + **vacations**.
|
|
6
|
+
Autonome/DB-agnostique. **Compose** (optionnels) : users (personne), storage (diplômes), booking (dispo), payment (payout vacations).
|
|
7
|
+
**Frontière** : la personne/identité = users ; la séance = training ; staff = la dimension « intervenant rémunéré ».
|
|
8
|
+
**API** : `createStaff({repositories,users?,storage?,payment?})` → `staff.{create,get,byUser,list,setAvailability,getAvailability,withPerson}`,
|
|
9
|
+
`diplomas.{add,list}`, `contracts.{add,list,activeAt}`, `work.{log,list,hours}`, `pay.{vacationDue,settle}`.
|
|
10
|
+
**Réutilisabilité 3 P** : enseignants (P2 ATC), experts/consultants (P1 Hadhinat), enquêteurs sociaux (P3 ASSO-SEL).
|
|
11
|
+
**Jalons** : 0.1 profil+diplômes+contrats+heures+vacations · 0.2 booking(dispo) · 1.0 14 livrables.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Exemple §12 — intervenants : enseignant (ATC) + expert (incubateur). node examples/intervenants/run.mjs
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createStaff, createMemoryRepositories } from '../../src/index.js';
|
|
4
|
+
import { createUsers, createMemoryRepositories as userRepos } from '../../../mosta-users-stack/mosta-users/src/index.js';
|
|
5
|
+
const users=createUsers({ repositories:userRepos() });
|
|
6
|
+
const staff=createStaff({ repositories:createMemoryRepositories(), users, payment:{ async payout(p){ console.log(' ↪ vacation payée:',p.amount,'DA',`(${p.hours}h)`); } } });
|
|
7
|
+
// P2 ATC : enseignant d'anglais
|
|
8
|
+
const prof=await users.create({ firstName:'Mme', lastName:'Benali' });
|
|
9
|
+
const teacher=await staff.staff.create({ userId:prof.id, type:'teacher', specialties:['anglais'], hourlyRate:1500 });
|
|
10
|
+
await staff.diplomas.add(teacher.id,{ title:'Master Anglais', year:2018 });
|
|
11
|
+
await staff.contracts.add(teacher.id,{ kind:'vacation', start:'2026-01-01' });
|
|
12
|
+
await staff.work.log(teacher.id,{ date:'2026-06-07', hours:4, sessionRef:'ENG-A1-mardi' });
|
|
13
|
+
await staff.work.log(teacher.id,{ date:'2026-06-14', hours:4, sessionRef:'ENG-A1-mardi' });
|
|
14
|
+
const settle=await staff.pay.settle(teacher.id);
|
|
15
|
+
assert.equal(settle.amount, 8*1500);
|
|
16
|
+
const card=await staff.staff.withPerson(teacher.id);
|
|
17
|
+
assert.equal(card.person.name,'Mme Benali');
|
|
18
|
+
// P1 incubateur : expert accompagnement
|
|
19
|
+
const exp=await users.create({ firstName:'Dr', lastName:'Karim' });
|
|
20
|
+
await staff.staff.create({ userId:exp.id, type:'expert', specialties:['business-plan'], hourlyRate:3000 });
|
|
21
|
+
assert.equal((await staff.staff.list({type:'expert'})).length,1);
|
|
22
|
+
console.log('✅ staff — enseignant %s : %dh → %d DA · + expert incubateur', card.person.name, settle.hours, settle.amount);
|
package/llms.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @mostajs/staff — fiche LLM
|
|
2
|
+
RÔLE
|
|
3
|
+
Intervenants (teacher/expert/trainer/investigator) : profil lié @mostajs/users (userId), CV/diplômes, contrats,
|
|
4
|
+
disponibilités, heures réalisées, vacations. Généralisation de faculty. Autonome/DB-agnostique.
|
|
5
|
+
Compose (optionnels) : users (personne/withPerson), storage (diplômes), booking (dispo), payment (payout).
|
|
6
|
+
EXPORTS
|
|
7
|
+
createStaff({ repositories, users?, storage?, payment?, now? }) -> { staff, diplomas, contracts, work, pay }
|
|
8
|
+
createMemoryRepositories(); StaffSchema, DiplomaSchema, ContractSchema, WorkLogSchema
|
|
9
|
+
API
|
|
10
|
+
staff.create({userId,type,specialties,hourlyRate})/get/byUser/list({type,status})/setAvailability/getAvailability/withPerson
|
|
11
|
+
diplomas.add(staffId,{title,year,bytes|fileRef,mime})/list ; contracts.add/list/activeAt(date)
|
|
12
|
+
work.log(staffId,{date,hours,sessionRef})/list/hours({from,to}) ; pay.vacationDue/settle(staffId,{from,to}) -> {hours,rate,amount}
|
|
13
|
+
PIÈGES
|
|
14
|
+
- La personne reste @mostajs/users (userId opaque). vacation = heures × hourlyRate. diplômes via storage (sinon fileRef).
|
|
15
|
+
- settle compose @mostajs/payment (payout) si injecté. Disponibilités : composer @mostajs/booking en prod.
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/staff",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Intervenants (enseignants/experts/enquêteurs) : profil lié à @mostajs/users, CV/diplômes, contrats, disponibilités, heures réalisées, vacations. Généralisation de faculty. 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
|
+
"staff",
|
|
16
|
+
"faculty",
|
|
17
|
+
"teachers",
|
|
18
|
+
"experts",
|
|
19
|
+
"contracts",
|
|
20
|
+
"vacations"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node test-scripts/unit/staff.test.mjs",
|
|
24
|
+
"example": "node examples/intervenants/run.mjs"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// @mostajs/staff — point d'entrée. Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
export { createStaff } from './staff.js';
|
|
3
|
+
export { createMemoryRepositories } from './memory-repo.js';
|
|
4
|
+
export { StaffSchema, DiplomaSchema, ContractSchema, WorkLogSchema } from './schemas.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @mostajs/staff — 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 { staff: coll(), diplomas: coll(), contracts: coll(), worklogs: coll() }; }
|
package/src/schemas.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @mostajs/staff — schémas (EntitySchema @mostajs/orm). Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
// GÉNÉRALISATION de faculty (SecuAccessPro/MostaGare). La PERSONNE = @mostajs/users (userId) ; staff = profil intervenant.
|
|
3
|
+
export const StaffSchema = { name: 'Staff', collection: 'staff', timestamps: true, fields: {
|
|
4
|
+
userId: { type: 'string', required: true }, // → @mostajs/users
|
|
5
|
+
type: { type: 'string', enum: ['teacher', 'expert', 'trainer', 'investigator', 'other'], default: 'teacher' },
|
|
6
|
+
specialties: { type: 'array', arrayOf: 'string', default: () => [] },
|
|
7
|
+
hourlyRate: { type: 'number', default: 0 }, // tarif vacation
|
|
8
|
+
status: { type: 'string', enum: ['active', 'inactive'], default: 'active' } },
|
|
9
|
+
indexes: [{ fields: { userId: 'asc' } }, { fields: { type: 'asc' } }] };
|
|
10
|
+
export const DiplomaSchema = { name: 'Diploma', collection: 'diplomas', timestamps: true, fields: {
|
|
11
|
+
staffId: { type: 'string', required: true }, title: { type: 'string', required: true }, year: { type: 'number' },
|
|
12
|
+
fileRef: { type: 'string', default: null } }, indexes: [{ fields: { staffId: 'asc' } }] }; // CV/diplôme via @mostajs/storage
|
|
13
|
+
export const ContractSchema = { name: 'Contract', collection: 'contracts', timestamps: true, fields: {
|
|
14
|
+
staffId: { type: 'string', required: true }, kind: { type: 'string', enum: ['permanent', 'vacation', 'freelance'], default: 'vacation' },
|
|
15
|
+
start: { type: 'date' }, end: { type: 'date', default: null }, terms: { type: 'string' } }, indexes: [{ fields: { staffId: 'asc' } }] };
|
|
16
|
+
export const WorkLogSchema = { name: 'WorkLog', collection: 'work_logs', timestamps: true, fields: {
|
|
17
|
+
staffId: { type: 'string', required: true }, date: { type: 'date', default: 'now' }, hours: { type: 'number', required: true },
|
|
18
|
+
sessionRef: { type: 'string', default: null }, note: { type: 'string' } }, indexes: [{ fields: { staffId: 'asc' } }] };
|
package/src/staff.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @mostajs/staff — intervenants. Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
// Autonome (DB-agnostique). Compose (injectés, optionnels) : users (personne), storage (CV/diplômes),
|
|
3
|
+
// booking (disponibilités), payment (vacations). La personne reste @mostajs/users (userId).
|
|
4
|
+
|
|
5
|
+
export function createStaff({ repositories, users, storage, payment, now = () => new Date() } = {}) {
|
|
6
|
+
if (!repositories?.staff) throw new Error('createStaff: repositories.staff requis');
|
|
7
|
+
const { staff, diplomas, contracts, worklogs } = repositories;
|
|
8
|
+
const putFile = async (bytes, mime) => { const fn = storage?.put || storage?.createFile; if (!fn) return null; const r = await fn.call(storage, bytes, { mime, mimeType: mime }); return typeof r === 'string' ? r : (r.id ?? r.key ?? r.ref); };
|
|
9
|
+
const inWindow = ({ from, to } = {}) => (r) => (!from || new Date(r.date) >= new Date(from)) && (!to || new Date(r.date) <= new Date(to));
|
|
10
|
+
|
|
11
|
+
const api = {
|
|
12
|
+
staff: {
|
|
13
|
+
create: (dto) => staff.create({ type: 'teacher', specialties: [], hourlyRate: 0, status: 'active', availability: [], ...dto }),
|
|
14
|
+
get: (id) => staff.findById(id),
|
|
15
|
+
byUser: async (userId) => (await staff.find((s) => s.userId === userId))[0] || null,
|
|
16
|
+
list: (f = {}) => staff.find((s) => Object.entries(f).every(([k, v]) => s[k] === v)),
|
|
17
|
+
setAvailability: (id, slots) => staff.update(id, { availability: slots }), // (compose @mostajs/booking en prod)
|
|
18
|
+
getAvailability: async (id) => (await staff.findById(id))?.availability || [],
|
|
19
|
+
/** Fusionne le profil personne (@mostajs/users) si injecté. */
|
|
20
|
+
async withPerson(id) { const s = await staff.findById(id); if (!s) return null; const u = users?.get ? await users.get(s.userId) : null; return { ...s, person: u && { name: users.fullName ? users.fullName(u) : `${u.firstName || ''} ${u.lastName || ''}`.trim(), email: u.email, photo: u.photo } }; },
|
|
21
|
+
},
|
|
22
|
+
diplomas: {
|
|
23
|
+
async add(staffId, { title, year, bytes, mime, fileRef } = {}) { let ref = fileRef; if (bytes && storage) ref = await putFile(bytes, mime); return diplomas.create({ staffId, title, year, fileRef: ref }); },
|
|
24
|
+
list: (staffId) => diplomas.find((d) => d.staffId === staffId),
|
|
25
|
+
},
|
|
26
|
+
contracts: {
|
|
27
|
+
add: (staffId, dto) => contracts.create({ staffId, kind: 'vacation', ...dto }),
|
|
28
|
+
list: (staffId) => contracts.find((c) => c.staffId === staffId),
|
|
29
|
+
async activeAt(staffId, date = now()) { const t = new Date(date).getTime(); return (await contracts.find((c) => c.staffId === staffId)).filter((c) => (!c.start || new Date(c.start).getTime() <= t) && (!c.end || new Date(c.end).getTime() >= t)); },
|
|
30
|
+
},
|
|
31
|
+
work: {
|
|
32
|
+
log: (staffId, { date, hours, sessionRef, note } = {}) => worklogs.create({ staffId, date: date || now(), hours, sessionRef, note }),
|
|
33
|
+
list: (staffId, window) => worklogs.find((w) => w.staffId === staffId).then((a) => a.filter(inWindow(window))),
|
|
34
|
+
async hours(staffId, window) { return (await worklogs.find((w) => w.staffId === staffId)).filter(inWindow(window)).reduce((s, w) => s + (Number(w.hours) || 0), 0); },
|
|
35
|
+
},
|
|
36
|
+
pay: {
|
|
37
|
+
/** Vacation due = heures réalisées × tarif horaire. */
|
|
38
|
+
async vacationDue(staffId, window) { const s = await staff.findById(staffId); const h = await api.work.hours(staffId, window); return { hours: h, rate: s?.hourlyRate || 0, amount: h * (s?.hourlyRate || 0) }; },
|
|
39
|
+
/** Règlement (compose @mostajs/payment si injecté). */
|
|
40
|
+
async settle(staffId, window) { const due = await api.pay.vacationDue(staffId, window); if (payment?.payout) await payment.payout({ staffId, ...due }); return due; },
|
|
41
|
+
},
|
|
42
|
+
repositories,
|
|
43
|
+
};
|
|
44
|
+
return api;
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @mostajs/staff — tests (DEVRULES §5). node test-scripts/unit/staff.test.mjs
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createStaff, createMemoryRepositories } from '../../src/index.js';
|
|
4
|
+
import { createUsers, createMemoryRepositories as userRepos } from '../../../mosta-users-stack/mosta-users/src/index.js';
|
|
5
|
+
let pass=0; const test=async(n,f)=>{await f();pass++;console.log(' ✓',n);};
|
|
6
|
+
const mk=(o={})=>createStaff({ repositories:createMemoryRepositories(), ...o });
|
|
7
|
+
|
|
8
|
+
await test('create + byUser + list par type', async()=>{
|
|
9
|
+
const s=mk(); await s.staff.create({userId:'u1',type:'teacher',hourlyRate:1000});
|
|
10
|
+
await s.staff.create({userId:'u2',type:'expert'});
|
|
11
|
+
assert.equal((await s.staff.byUser('u1')).type,'teacher');
|
|
12
|
+
assert.equal((await s.staff.list({type:'teacher'})).length,1);
|
|
13
|
+
});
|
|
14
|
+
await test('diplômes (storage composé)', async()=>{
|
|
15
|
+
const store=[]; const s=mk({ storage:{ async put(b){const id='f'+store.length;store.push(b);return {id};} } });
|
|
16
|
+
const st=await s.staff.create({userId:'u1'});
|
|
17
|
+
const d=await s.diplomas.add(st.id,{title:'Master',year:2020,bytes:'pdf-bytes',mime:'application/pdf'});
|
|
18
|
+
assert.equal(d.fileRef,'f0'); assert.equal((await s.diplomas.list(st.id)).length,1);
|
|
19
|
+
});
|
|
20
|
+
await test('contrats + actif à une date', async()=>{
|
|
21
|
+
const s=mk(); const st=await s.staff.create({userId:'u1'});
|
|
22
|
+
await s.contracts.add(st.id,{kind:'vacation',start:'2026-01-01',end:'2026-12-31'});
|
|
23
|
+
assert.equal((await s.contracts.activeAt(st.id,'2026-06-01')).length,1);
|
|
24
|
+
assert.equal((await s.contracts.activeAt(st.id,'2025-01-01')).length,0);
|
|
25
|
+
});
|
|
26
|
+
await test('heures réalisées + vacation due (heures × tarif)', async()=>{
|
|
27
|
+
const s=mk(); const st=await s.staff.create({userId:'u1',hourlyRate:1200});
|
|
28
|
+
await s.work.log(st.id,{date:'2026-06-01',hours:3}); await s.work.log(st.id,{date:'2026-06-02',hours:2});
|
|
29
|
+
assert.equal(await s.work.hours(st.id),5);
|
|
30
|
+
const due=await s.pay.vacationDue(st.id); assert.equal(due.amount,6000);
|
|
31
|
+
assert.equal(await s.work.hours(st.id,{from:'2026-06-02'}),2,'fenêtre temporelle');
|
|
32
|
+
});
|
|
33
|
+
await test('settle compose payment (payout)', async()=>{
|
|
34
|
+
const paid=[]; const s=mk({ payment:{ async payout(p){paid.push(p);} } });
|
|
35
|
+
const st=await s.staff.create({userId:'u1',hourlyRate:1000}); await s.work.log(st.id,{hours:2});
|
|
36
|
+
const r=await s.pay.settle(st.id); assert.equal(r.amount,2000); assert.equal(paid[0].amount,2000);
|
|
37
|
+
});
|
|
38
|
+
await test('withPerson fusionne le profil @mostajs/users', async()=>{
|
|
39
|
+
const users=createUsers({ repositories:userRepos() }); const u=await users.create({firstName:'Mme',lastName:'X'});
|
|
40
|
+
const s=mk({ users }); const st=await s.staff.create({userId:u.id,type:'teacher'});
|
|
41
|
+
const full=await s.staff.withPerson(st.id); assert.equal(full.person.name,'Mme X');
|
|
42
|
+
});
|
|
43
|
+
await test('disponibilités (set/get)', async()=>{
|
|
44
|
+
const s=mk(); const st=await s.staff.create({userId:'u1'});
|
|
45
|
+
await s.staff.setAvailability(st.id,[{day:'samedi',from:'14:00',to:'17:00'}]);
|
|
46
|
+
assert.equal((await s.staff.getAvailability(st.id))[0].day,'samedi');
|
|
47
|
+
});
|
|
48
|
+
console.log(`\n✅ @mostajs/staff — ${pass} tests OK`);
|