@mostajs/mailroom 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/CHANGELOG.md +8 -0
- package/README.md +11 -0
- package/docs/00-PROPOSITION-PLAN-MAILROOM-18062026.md +9 -0
- package/examples/registre/run.mjs +11 -0
- package/llms.txt +14 -0
- package/package.json +25 -0
- package/src/index.js +3 -0
- package/src/mailroom.js +36 -0
- package/src/memory-repo.js +7 -0
- package/src/schemas.js +14 -0
- package/test-scripts/unit/mailroom.test.mjs +11 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## [0.2.0] — 2026-06-20
|
|
2
|
+
### Added
|
|
3
|
+
- **Registre des visiteurs / RDV** (E9 Hadhinat) : `visitors.log/list/checkIn/checkOut` (repository `visitors` optionnel, rétro-compat) + `VisitorSchema` + `stats().visitors`. 6 tests.
|
|
4
|
+
|
|
5
|
+
# Changelog — @mostajs/mailroom
|
|
6
|
+
## [0.1.0] — 2026-06-18
|
|
7
|
+
### Added
|
|
8
|
+
- Registre courrier entrant/sortant (ref/statuts/affectation/pièce jointe) + journal d'appels + stats. Compose numbering/storage/notifications. 5 tests + exemple §12.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @mostajs/mailroom
|
|
2
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later · **Statut** : 0.1.0 (5 tests verts)
|
|
3
|
+
> Courrier entrant/sortant + journal d'appels (accueil/secrétariat). Autonome ; compose numbering/storage/notifications.
|
|
4
|
+
```js
|
|
5
|
+
import { createMailroom, createMemoryRepositories } from '@mostajs/mailroom';
|
|
6
|
+
const m = createMailroom({ repositories: createMemoryRepositories(), numbering, storage });
|
|
7
|
+
const c = await m.mail.register({ direction:'in', subject:'Demande', from:'Wilaya' });
|
|
8
|
+
await m.mail.assign(c.id, 'secretaire');
|
|
9
|
+
await m.calls.log({ caller:'0550...', subject:'RDV', handledBy:'accueil' });
|
|
10
|
+
```
|
|
11
|
+
Lancer : `node test-scripts/unit/mailroom.test.mjs && node examples/registre/run.mjs`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @mostajs/mailroom — 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 Hadhinat F3 + P3 secrétariat)
|
|
3
|
+
## §4 (règle d'or)
|
|
4
|
+
Aucun module ne couvre le **registre courrier entrant/sortant** + **journal d'appels** (accueil/secrétariat). queue=file d'attente, secu=accès, booking=RDV — complémentaires. → cas C autonome.
|
|
5
|
+
## §3 (périmètre/API)
|
|
6
|
+
**Entités** : Mail{ref,direction(in/out),subject,from,to,date,status,assignedTo,attachmentRef}, CallLog{at,caller,subject,handledBy,followUp}.
|
|
7
|
+
**API** : `createMailroom({repositories,numbering?,storage?,notifications?})` → `mail.{register,get,list,setStatus,assign}`, `calls.{log,list}`, `stats()`.
|
|
8
|
+
**Compose (§10)** : numbering (n° courrier ARR/DEP), storage (pièce jointe), notifications (affectation). **Jalons** : 0.1 registre+appels+stats · 0.2 RDV/convocations(booking) · 1.0 14 livrables.
|
|
9
|
+
## §4 (tests) : register in/out+n°+validation, pièce jointe(storage), affectation+notif, journal d'appels, stats. (5 verts)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createMailroom, createMemoryRepositories } from '../../src/index.js';
|
|
3
|
+
const m=createMailroom({ repositories:createMemoryRepositories(), numbering:{ next:(s)=> (s==='mail-in'?'ARR':'DEP')+'-2026-'+Math.floor(Math.random()*999) }, notifications:{ notify:(e)=>console.log(' ↪ affecté à',e.to) } });
|
|
4
|
+
// P1 Hadhinat / P3 ASSO-SEL : secrétariat
|
|
5
|
+
const arr=await m.mail.register({ direction:'in', subject:'Demande de subvention', from:'DAS Mostaganem' });
|
|
6
|
+
await m.mail.assign(arr.id,'secretaire-1');
|
|
7
|
+
await m.mail.register({ direction:'out', subject:'Accusé de réception', to:'DAS Mostaganem' });
|
|
8
|
+
await m.calls.log({ caller:'0561 23 45', subject:'Suivi dossier', handledBy:'accueil', followUp:true });
|
|
9
|
+
const s=await m.stats();
|
|
10
|
+
assert.deepEqual(s,{in:1,out:1,calls:1});
|
|
11
|
+
console.log('✅ mailroom — registre: %d entrant · %d sortant · %d appel(s) · %s', s.in, s.out, s.calls, arr.ref);
|
package/llms.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @mostajs/mailroom — fiche LLM
|
|
2
|
+
RÔLE
|
|
3
|
+
Courrier & accueil : registre courrier entrant/sortant (ref ARR/DEP, statuts, affectation, pièce jointe) + journal d'appels.
|
|
4
|
+
Autonome/DB-agnostique. Compose (injectés) : numbering (n°), storage (pièce jointe), notifications (affectation).
|
|
5
|
+
EXPORTS
|
|
6
|
+
createMailroom({ repositories, numbering?, storage?, notifications?, now? }) -> { mail, calls, stats } ; createMemoryRepositories(); MailSchema, CallSchema
|
|
7
|
+
API
|
|
8
|
+
mail.register({direction:'in'|'out',subject,from,to,bytes?|attachmentRef})/get/list({direction,status})/setStatus(id,status)/assign(id,to)
|
|
9
|
+
calls.log({caller,subject,handledBy,followUp})/list(filter) ; stats() -> {in,out,calls}
|
|
10
|
+
PIÈGES
|
|
11
|
+
- direction in/out + subject requis. ref généré ARR-/DEP- via numbering. assign notifie (compose notifications). pièce jointe via storage.
|
|
12
|
+
|
|
13
|
+
VISITEURS/RDV (0.2.0)
|
|
14
|
+
visitors.log({name*,org,purpose,host,appointmentAt}) | list(f) | checkIn(id) | checkOut(id) ; repositories.visitors optionnel ; stats().visitors
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/mailroom",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Courrier & accueil : registre courrier entrant/sortant + journal d'appels + rendez-vous/convocations. DB-agnostique ; compose numbering/storage/notifications.",
|
|
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
|
+
"mailroom",
|
|
16
|
+
"courrier",
|
|
17
|
+
"registre",
|
|
18
|
+
"accueil",
|
|
19
|
+
"calls"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node test-scripts/unit/mailroom.test.mjs",
|
|
23
|
+
"example": "node examples/registre/run.mjs"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.js
ADDED
package/src/mailroom.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @mostajs/mailroom — courrier & accueil. Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
// Autonome/DB-agnostique. Compose (injectés) : numbering (n° courrier), storage (pièce jointe), notifications (affectation).
|
|
3
|
+
export function createMailroom({ repositories, numbering, storage, notifications, now=()=>new Date() }={}){
|
|
4
|
+
if(!repositories?.mails) throw new Error('createMailroom: repositories.mails requis');
|
|
5
|
+
const { mails, calls, visitors } = repositories;
|
|
6
|
+
const ref=(dir)=> (numbering?.next ? numbering.next('mail-'+dir) : `${dir==='in'?'ARR':'DEP'}-${now().getFullYear()}-${Math.floor(now().getTime()%1000000)}`);
|
|
7
|
+
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.ref); };
|
|
8
|
+
const api={
|
|
9
|
+
mail:{
|
|
10
|
+
async register({direction,subject,from,to,bytes,mime,attachmentRef}={}){
|
|
11
|
+
if(!['in','out'].includes(direction)) throw new Error("direction 'in'|'out' requise");
|
|
12
|
+
if(!subject) throw new Error('subject requis');
|
|
13
|
+
let att=attachmentRef; if(bytes&&storage) att=await putFile(bytes,mime);
|
|
14
|
+
return mails.create({ ref:ref(direction), direction, subject, from, to, date:now(), status: direction==='in'?'registered':'registered', attachmentRef:att });
|
|
15
|
+
},
|
|
16
|
+
get:(id)=>mails.findById(id),
|
|
17
|
+
list:(f={})=>mails.find(m=>Object.entries(f).every(([k,v])=>m[k]===v)),
|
|
18
|
+
setStatus:(id,status)=>mails.update(id,{status}),
|
|
19
|
+
async assign(id,to){ const m=await mails.update(id,{assignedTo:to,status:'assigned'}); notifications?.notify?.({type:'mail.assigned',mailId:id,to}); return m; },
|
|
20
|
+
},
|
|
21
|
+
calls:{
|
|
22
|
+
log:(dto)=>calls.create({at:now(),...dto}),
|
|
23
|
+
list:(f={})=>calls.find(c=>Object.entries(f).every(([k,v])=>c[k]===v)),
|
|
24
|
+
},
|
|
25
|
+
// Visiteurs & RDV (registre d'accueil) — repository optionnel (rétro-compat : silencieux si non fourni).
|
|
26
|
+
visitors:{
|
|
27
|
+
log:(dto)=>{ if(!visitors) throw new Error('repositories.visitors requis pour le registre des visiteurs'); return visitors.create({at:now(),status:'expected',...dto}); },
|
|
28
|
+
list:(f={})=> visitors? visitors.find(v=>Object.entries(f).every(([k,v2])=>v[k]===v2)) : Promise.resolve([]),
|
|
29
|
+
checkIn:(id)=> visitors.update(id,{status:'arrived',arrivedAt:now()}),
|
|
30
|
+
checkOut:(id)=> visitors.update(id,{status:'left',leftAt:now()}),
|
|
31
|
+
},
|
|
32
|
+
async stats(){ return { in: await mails.count(m=>m.direction==='in'), out: await mails.count(m=>m.direction==='out'), calls: await calls.count(), visitors: visitors? await visitors.count() : 0 }; },
|
|
33
|
+
repositories,
|
|
34
|
+
};
|
|
35
|
+
return api;
|
|
36
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
function coll(){ const m=new Map(); return {
|
|
2
|
+
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}; },
|
|
3
|
+
async findById(id){ const r=m.get(id); return r?{...r}:null; },
|
|
4
|
+
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}; },
|
|
5
|
+
async find(f=()=>true){ return [...m.values()].filter(f).map(r=>({...r})); },
|
|
6
|
+
async count(f=()=>true){ return [...m.values()].filter(f).length; } }; }
|
|
7
|
+
export function createMemoryRepositories(){ return { mails:coll(), calls:coll(), visitors:coll() }; }
|
package/src/schemas.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @mostajs/mailroom — schémas. Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
export const MailSchema = { name:'Mail', collection:'mails', timestamps:true, fields:{
|
|
3
|
+
ref:{type:'string'}, direction:{type:'string',enum:['in','out'],required:true}, subject:{type:'string',required:true},
|
|
4
|
+
from:{type:'string'}, to:{type:'string'}, date:{type:'date',default:'now'},
|
|
5
|
+
status:{type:'string',enum:['received','registered','assigned','processed','sent','archived'],default:'registered'},
|
|
6
|
+
assignedTo:{type:'string',default:null}, attachmentRef:{type:'string',default:null} },
|
|
7
|
+
indexes:[{fields:{direction:'asc'}},{fields:{status:'asc'}}] };
|
|
8
|
+
export const CallSchema = { name:'CallLog', collection:'call_logs', timestamps:true, fields:{
|
|
9
|
+
at:{type:'date',default:'now'}, caller:{type:'string'}, subject:{type:'string'}, handledBy:{type:'string'}, followUp:{type:'boolean',default:false} } };
|
|
10
|
+
// Visiteur / RDV (registre d'accueil) — ajout 0.2.0 (E9 Hadhinat).
|
|
11
|
+
export const VisitorSchema = { name:'Visitor', collection:'visitors', timestamps:true, fields:{
|
|
12
|
+
at:{type:'date',default:'now'}, name:{type:'string',required:true}, org:{type:'string'}, purpose:{type:'string'},
|
|
13
|
+
host:{type:'string'}, appointmentAt:{type:'date',default:null},
|
|
14
|
+
status:{type:'string',enum:['expected','arrived','left'],default:'expected'}, arrivedAt:{type:'date',default:null}, leftAt:{type:'date',default:null} } };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createMailroom, createMemoryRepositories } from '../../src/index.js';
|
|
3
|
+
let pass=0; const test=async(n,f)=>{await f();pass++;console.log(' ✓',n);};
|
|
4
|
+
const mk=(o={})=>createMailroom({ repositories:createMemoryRepositories(), ...o });
|
|
5
|
+
await test('register courrier entrant + n° + validation', async()=>{ const m=mk(); await assert.rejects(()=>m.mail.register({direction:'x',subject:'s'}),/direction/); await assert.rejects(()=>m.mail.register({direction:'in'}),/subject/); const c=await m.mail.register({direction:'in',subject:'Demande',from:'Wilaya'}); assert.ok(c.ref.startsWith('ARR')); assert.equal(c.status,'registered'); });
|
|
6
|
+
await test('courrier sortant + pièce jointe (storage)', async()=>{ const m=mk({ storage:{ async put(b){return 'att-1';} } }); const c=await m.mail.register({direction:'out',subject:'Réponse',to:'Banque',bytes:'pdf'}); assert.ok(c.ref.startsWith('DEP')); assert.equal(c.attachmentRef,'att-1'); });
|
|
7
|
+
await test('affectation + notification', async()=>{ const ev=[]; const m=mk({ notifications:{ notify:(e)=>ev.push(e) } }); const c=await m.mail.register({direction:'in',subject:'X'}); await m.mail.assign(c.id,'secretaire'); assert.equal((await m.mail.get(c.id)).status,'assigned'); assert.equal(ev[0].type,'mail.assigned'); });
|
|
8
|
+
await test('journal d\'appels', async()=>{ const m=mk(); await m.calls.log({caller:'0550...',subject:'RDV',handledBy:'accueil'}); assert.equal((await m.calls.list()).length,1); });
|
|
9
|
+
await test('liste filtrée + stats', async()=>{ const m=mk(); await m.mail.register({direction:'in',subject:'a'}); await m.mail.register({direction:'out',subject:'b'}); await m.calls.log({caller:'x'}); assert.equal((await m.mail.list({direction:'in'})).length,1); const s=await m.stats(); assert.deepEqual(s,{in:1,out:1,calls:1,visitors:0}); });
|
|
10
|
+
await test('registre des visiteurs / RDV (0.2.0) : log → checkIn → checkOut + stats', async()=>{ const m=mk(); const v=await m.visitors.log({name:'M. Bensaid',org:'ANDI',purpose:'Audit',host:'Direction'}); assert.equal(v.status,'expected'); assert.equal((await m.visitors.checkIn(v.id)).status,'arrived'); assert.equal((await m.visitors.checkOut(v.id)).status,'left'); assert.equal((await m.visitors.list()).length,1); assert.equal((await m.stats()).visitors,1); });
|
|
11
|
+
console.log(`\n✅ @mostajs/mailroom — ${pass} tests OK`);
|