@mostajs/users-ui 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 +15 -0
- package/README.md +31 -0
- package/docs/.gitkeep +0 -0
- package/docs/03-PLAN-DEV-USERS-UI.md +76 -0
- package/docs/04-PLAN-TESTS-USERS-UI.md +111 -0
- package/docs/PROPOSITION-MODULE-USERS-UI.md +91 -0
- package/examples/users-ui-headless/run.mjs +32 -0
- package/examples/web-demo/README.md +23 -0
- package/examples/web-demo/app.mjs +50 -0
- package/examples/web-demo/index.html +66 -0
- package/examples/web-demo/serve.sh +13 -0
- package/examples/web-react/README.md +28 -0
- package/examples/web-react/app.js +53 -0
- package/examples/web-react/index.html +57 -0
- package/examples/web-react/serve.mjs +37 -0
- package/llms.txt +25 -0
- package/package.json +33 -0
- package/src/headless.js +70 -0
- package/src/index.js +8 -0
- package/src/react/MemberCardGenerator.jsx +32 -0
- package/src/react/PhotoCapture.jsx +39 -0
- package/src/react/UserForm.jsx +36 -0
- package/src/react/UsersList.jsx +42 -0
- package/src/react/index.js +6 -0
- package/test-scripts/unit/headless.test.mjs +52 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog — @mostajs/users-ui
|
|
2
|
+
|
|
3
|
+
Format : Keep a Changelog · SemVer.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] — 2026-06-18
|
|
6
|
+
### Added
|
|
7
|
+
- Logique **headless** : `validateUserForm`, `buildUsersListViewModel`, `buildMemberCard`, `createUsersUiController` (filtres/pagination/submit/toggleStatus/remove).
|
|
8
|
+
- Composants **React** (`@mostajs/users-ui/react`) : `<UsersList>`, `<UserForm>`, `<PhotoCapture>`/`<FaceEnroll>`, `<MemberCardGenerator>`.
|
|
9
|
+
- Exemple §12 headless + tests `test-scripts/unit` (8 verts).
|
|
10
|
+
- Démos web : `examples/web-demo` (vanilla, admin liste) et `examples/web-react` (composants React + **carte/QR rendu par `@mostajs/qrpanel`** via `useQrDataUrl`) avec scripts de démarrage.
|
|
11
|
+
- `<MemberCardGenerator>` compose `@mostajs/qrpanel/qr-image` (QR), ne réembarque pas `qrcode`.
|
|
12
|
+
|
|
13
|
+
### Notes
|
|
14
|
+
- **Peer-dependency stricte `@mostajs/users`** (le contrôleur throw sans le service). `password` → `@mostajs/auth` ; rôles → `@mostajs/rbac`.
|
|
15
|
+
- Extraction depuis `SolutionCh/MostaGare` (components/admin/User*) + `SolutionCh/SecuAccessPro` (FaceDetector/ClientCardGenerator). Composants Roles → contribués à `@mostajs/rbac`.
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @mostajs/users-ui
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
|
|
4
|
+
**Stack** : membre de `@mostajs/users-stack` (`../`). **Statut** : 0.1.0 (headless exécutable — 8 tests verts).
|
|
5
|
+
|
|
6
|
+
> **UI d'administration des utilisateurs** — couche de présentation de `@mostajs/users`.
|
|
7
|
+
> **Logique headless** (testable sans navigateur) + **composants React** (`@mostajs/users-ui/react`).
|
|
8
|
+
|
|
9
|
+
## ⚠ Dépendance stricte
|
|
10
|
+
**`@mostajs/users-ui` → `@mostajs/users`** (peer). Le contrôleur **throw** sans le service `users`.
|
|
11
|
+
`password` → `@mostajs/auth` · rôles → `@mostajs/rbac` · composants Roles → contribués à `rbac` (pas ici).
|
|
12
|
+
|
|
13
|
+
## Headless (exécutable)
|
|
14
|
+
```js
|
|
15
|
+
import { createUsersUiController, validateUserForm } from '@mostajs/users-ui';
|
|
16
|
+
const ui = createUsersUiController({ service: usersService }); // usersService = createUsers()
|
|
17
|
+
const r = await ui.submit({ firstName:'Inès', email:'ines@ex.dz' }); // valide + crée via users
|
|
18
|
+
const vm = await ui.refresh(); // {rows, stats, ...}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## React
|
|
22
|
+
```jsx
|
|
23
|
+
import { UsersList, UserForm, PhotoCapture, MemberCardGenerator } from '@mostajs/users-ui/react';
|
|
24
|
+
<UsersList service={usersService} t={t} onEdit={...} />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Lancer
|
|
28
|
+
```bash
|
|
29
|
+
node examples/users-ui-headless/run.mjs
|
|
30
|
+
node test-scripts/unit/headless.test.mjs
|
|
31
|
+
```
|
package/docs/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 03 — Plan de développement (EXTRACTION) — `@mostajs/users-ui`
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
**Date** : 2026-06-18
|
|
5
|
+
**Statut** : proposition à discuter (livrable §9 #3). **Précède le scaffold code.**
|
|
6
|
+
**Principe** : **EXTRAIRE, NE PAS REDÉVELOPPER.** Chaque composant = *lift-and-adapt* d'un composant existant, ou *composition* d'un module. Aucune primitive réécrite.
|
|
7
|
+
**Amont** : `PROPOSITION-MODULE-USERS-UI.md` (§4, carte U1–U7) · `../mosta-users/docs/03-PLAN-DEV-USERS.md`
|
|
8
|
+
**Sources** : `SolutionCh/MostaGare` (`components/admin/User*`, ~2041 l.) · `SolutionCh/SecuAccessPro` (`components/clients/{FaceDetector,ClientForm,ClientCardGenerator}`)
|
|
9
|
+
**Dépendance** : **peer stricte `@mostajs/users`** + `react>=18`. Livré **après `@mostajs/users@0.1`**.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Périmètre & responsabilité unique
|
|
14
|
+
Composants/pages React d'**administration des utilisateurs** — **couche de présentation** de `@mostajs/users`.
|
|
15
|
+
**Hors périmètre (délégué)** : données/actions (`users`), rôles UI (`rbac`), primitives (`ui`), visage (`face`), photo bytes (`storage`), QR (`qrpanel`), abonnement (`subscriptions-plan`), i18n (`i18n`), lecture badge (`scan`/`pwa-scan`). **Composants Roles → contribués à `rbac` (cas B), pas ici.**
|
|
16
|
+
|
|
17
|
+
## 2. Architecture d'extraction — paramétrage des endpoints (adaptation transverse)
|
|
18
|
+
Les composants MostaGare appellent des **URLs en dur** (`/api/admin/users…`). À l'extraction, on les **paramètre** :
|
|
19
|
+
- `UsersApiProvider` (contexte React) **ou** prop `endpoints` → branché sur `makeUsersHandlers()` de `@mostajs/users`.
|
|
20
|
+
- défauts conventionnels (`/api/admin/users`, `/api/user-preferences`) surchargés par config.
|
|
21
|
+
- Aucun composant ne code d'URL en dur ni de logique métier (déléguée à `users`).
|
|
22
|
+
|
|
23
|
+
## 3. Carte d'extraction composant-par-composant (source → cible)
|
|
24
|
+
|
|
25
|
+
| # | Source | Cible `mosta-users-ui/src/` | Action |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| U1 | MostaGare `UsersList.tsx` (472 l. : search/role/status, pagination, stats, toggle-status, delete) | `UsersList.tsx` | **LIFT** + endpoints paramétrés + `ui`/`i18n` |
|
|
28
|
+
| U2 | MostaGare `UserForm.tsx` (344 l. : password/confirm, roles via `/api/admin/roles`) | `UserForm.tsx` | **LIFT** + **ADAPT** : password→`auth`, roles→`rbac`, endpoints |
|
|
29
|
+
| U3 | MostaGare `UserEditForm.tsx` (76 l. : fetch `/api/admin/users/[id]`) | `UserEditForm.tsx` | **LIFT** + endpoint paramétré |
|
|
30
|
+
| U4 | SecuAccessPro `FaceDetector.tsx` (`onCapture({photo,faceDescriptor})`, webcam, loadModels) | `PhotoCapture.tsx` + `FaceEnroll.tsx` | **LIFT** + **DROP→COMPOSE `@mostajs/face`** (retire `faceApi` local) |
|
|
31
|
+
| U5 | SecuAccessPro `ClientCardGenerator.tsx` (`qrcode`+`html2canvas`, download/print) | `MemberCardGenerator.tsx` | **LIFT** + **COMPOSE `qrpanel`** (QR) + `subscriptions-plan` ; garder rasterisation (`html2canvas`) |
|
|
32
|
+
| U6 | SecuAccessPro `ClientForm.tsx` (`handleFaceCapture`→form) | câblage `<PhotoCapture>` dans `<UserForm>` | **PATTERN** (intégration) |
|
|
33
|
+
| U7 | MostaGare `user-preferences` UI + `messages/{fr,ar,en}.json` | `UserPreferencesPanel.tsx` + `i18n/` | **EXTRACT** + compose `i18n` |
|
|
34
|
+
| U8 | (dérivé U1/U2/U3) | `pages/` (UsersAdminPage prête à brancher) | **ASSEMBLE** (façon rbac UI) |
|
|
35
|
+
| — | MostaGare `Role*` (~1149 l.) | **→ `@mostajs/rbac` (cas B)** | **OUT** |
|
|
36
|
+
|
|
37
|
+
> Actions ∈ {LIFT, ADAPT, EXTRACT, COMPOSE, DROP→COMPOSE, ASSEMBLE, PATTERN}. **Aucune ligne « réécrire de zéro ».**
|
|
38
|
+
|
|
39
|
+
## 4. Composition (§10) — déclarée
|
|
40
|
+
**`@mostajs/users` (peer, requis)** · `rbac` (rôles) · `ui` (primitives) · `i18n` (FR/AR/EN + RTL) · `face` (visage) · `storage` (photo) · `qrpanel` (QR) · `subscriptions-plan` (carte) · `scan`/`pwa-scan` (lecture badge).
|
|
41
|
+
|
|
42
|
+
## 5. Versions (jalons — ordre d'extraction)
|
|
43
|
+
| Version | Contenu | Extraction | Prérequis |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| **0.1.0** | `<UsersList>` + `<UserForm>` + `<UserEditForm>` + `UsersApiProvider` + `llms.txt`/`CHANGELOG` | U1, U2, U3, §2 | **`@mostajs/users@0.1`** |
|
|
46
|
+
| **0.2.0** | `<PhotoCapture>` + `<FaceEnroll>` (compose `face`/`storage`) + câblage dans `<UserForm>` | U4, U6 | `users@0.3` (enrollFace) |
|
|
47
|
+
| **0.3.0** | `<MemberCardGenerator>` (compose `qrpanel`+`subscriptions-plan`) | U5 | — |
|
|
48
|
+
| **0.4.0** | `<UserPreferencesPanel>` + `<UserRoleAssign>` + i18n FR/AR/EN | U7 | `users@0.2` (preferences) |
|
|
49
|
+
| **0.5.0** | `pages/` admin prêtes (`UsersAdminPage`) + a11y (WCAG) + RTL | U8 | — |
|
|
50
|
+
| **1.0.0** | 14 livrables §9 + portes §11 + exemple §12 + article #17 + **migration MostaGare & SecuAccessPro** | — | — |
|
|
51
|
+
|
|
52
|
+
## 6. Migrations consommateurs (preuve d'extraction)
|
|
53
|
+
- **MostaGare** : remplacer `components/admin/User*` + pages admin par `@mostajs/users-ui` (endpoints → `makeUsersHandlers`). `Role*` → contribués à `rbac`.
|
|
54
|
+
- **SecuAccessPro** : remplacer `dashboard/users` + `FaceDetector`/`ClientCardGenerator` par les composants `users-ui` (Client = entité **dérivée** de `users`).
|
|
55
|
+
|
|
56
|
+
## 7. Risques & portes
|
|
57
|
+
| Risque | Mitigation |
|
|
58
|
+
|---|---|
|
|
59
|
+
| URLs en dur non paramétrables | `UsersApiProvider`/prop `endpoints` dès 0.1 (§2) |
|
|
60
|
+
| Re-tomber dans le doublon face/QR | U4/U5 = **DROP→COMPOSE** impératif (`face`/`qrpanel`) |
|
|
61
|
+
| Couplage password dans l'UI | champ délégué à `@mostajs/auth` (U2) ; `users` n'a pas de password |
|
|
62
|
+
| Désync avec `users` | peer-dependency versionnée ; versions alignées (tableau §5) |
|
|
63
|
+
|
|
64
|
+
**Portes §11** : **§11.1 qualité (bloquante)** build/typecheck/lint + tests composants ; **§11.5 i18n/a11y (UI)** FR/AR/EN + RTL + WCAG 2.1 AA + responsive ; **§11.2/§11.3 informatives** (photo/biométrie affichée → DPIA #16 informatif : consentement, minimisation — *informe l'exploitant, ne bloque pas le dev*) ; §11.8 SemVer + alignement peer `users`.
|
|
65
|
+
|
|
66
|
+
## 8. Plan de test (aperçu → #4)
|
|
67
|
+
- **Rendu/interaction** (RTL/Testing-Library) : `<UsersList>` (recherche/filtre/pagination/toggle/delete), `<UserForm>` (validation, password→auth, role→rbac), `<PhotoCapture>` (onCapture mock `face`), `<MemberCardGenerator>` (QR via `qrpanel` mock).
|
|
68
|
+
- **i18n/RTL/a11y** : clés FR/AR/EN, sens RTL, axe-core.
|
|
69
|
+
- Tests dans `test-scripts/` (jamais `/tmp`, §5). **Exemple §12** : page admin headless composant `users`+`users-ui` (assertions) AVANT article #17.
|
|
70
|
+
|
|
71
|
+
## 9. Jalons §9
|
|
72
|
+
`#1` état de l'art (UI admin/IAM — mutualisable avec `users`) · `#2` audit (= doc audit stack) · **`#3` ce plan** · `#4` plan de test · `#6` `llms.txt` dès 0.1 · `#7` CHANGELOG · §12 exemple AVANT #17.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
**Fin du plan de dev (extraction) `@mostajs/users-ui`.**
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# 04 — Plan de test — `@mostajs/users-ui`
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
**Date** : 2026-06-18
|
|
5
|
+
**Statut** : proposition à discuter (livrable §9 #4). Rend la porte **§11.1** vérifiable.
|
|
6
|
+
**Amont** : `03-PLAN-DEV-USERS-UI.md` (carte U1–U8) · `PROPOSITION-MODULE-USERS-UI.md`
|
|
7
|
+
**Emplacement des tests** : `test-scripts/` (JAMAIS `/tmp` — §5), rejouables & committés ; sorties dans `.out*` (gitignorées).
|
|
8
|
+
**Dépendance** : teste l'UI **présentation de `@mostajs/users`** → `users` (et `face`/`rbac`/`qrpanel`/`storage`) sont **mockés** (DI/endpoints).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Stratégie
|
|
13
|
+
- **Composants** (Testing-Library + jsdom) : rendu + interaction, **sans vrai back** — `UsersApiProvider` pointé sur un **mock d'endpoints** (ou `users` mémoire via `makeUsersHandlers` en MSW).
|
|
14
|
+
- **Accessibilité** : `axe-core` (WCAG 2.1 AA) sur chaque écran.
|
|
15
|
+
- **i18n / RTL** : rendu FR/AR/EN + sens **RTL** (AR).
|
|
16
|
+
- **Exemple §12** (`examples/`) : page admin headless composant `users`+`users-ui` avec assertions (DoD, **avant** article #17).
|
|
17
|
+
- **Couverture cible** : composants ≥ 80 % lignes/branches (§11.1).
|
|
18
|
+
- **Isolation des dépendances** : `@mostajs/face` (caméra/modèles), `qrpanel`, `storage`, `rbac`, `auth` → **mockés** (pas d'accès matériel/réseau en CI).
|
|
19
|
+
|
|
20
|
+
## 2. Matrice de couverture (T-cases)
|
|
21
|
+
|
|
22
|
+
### 2.1 `<UsersList>` (← U1)
|
|
23
|
+
| ID | Cas | Attendu |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| TC-LIST-01 | rendu liste depuis endpoint mock | lignes = données ; colonnes nom/email/rôle/statut |
|
|
26
|
+
| TC-LIST-02 | recherche (`search`) | requête endpoint avec param + debounce |
|
|
27
|
+
| TC-LIST-03 | filtre rôle + statut | params `role`/`status` transmis |
|
|
28
|
+
| TC-LIST-04 | pagination | param `page` ; nav page suivante/précédente |
|
|
29
|
+
| TC-LIST-05 | stats (total/actifs/…) | affichage des compteurs renvoyés |
|
|
30
|
+
| TC-LIST-06 | toggle-status (lock/unlock) | PATCH endpoint + maj optimiste/refetch |
|
|
31
|
+
| TC-LIST-07 | delete (confirm) | dialog confirm → DELETE endpoint |
|
|
32
|
+
| TC-LIST-08 | état vide / erreur réseau | message vide / message d'erreur (pas de crash) |
|
|
33
|
+
|
|
34
|
+
### 2.2 `<UserForm>` / `<UserEditForm>` (← U2/U3)
|
|
35
|
+
| ID | Cas | Attendu |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| TC-FORM-01 | création : champs requis | validation (email format, nom requis) |
|
|
38
|
+
| TC-FORM-02 | **password → `auth`** | le password n'est PAS écrit dans `users` ; routé via handler auth (mock) |
|
|
39
|
+
| TC-FORM-03 | password ≠ confirm (création) | erreur, submit bloqué |
|
|
40
|
+
| TC-FORM-04 | **rôles via `rbac`** | liste des rôles chargée depuis endpoint rbac (mock), admin filtré selon droits |
|
|
41
|
+
| TC-FORM-05 | édition : préremplissage | `UserEditForm` fetch `[id]` → champs remplis |
|
|
42
|
+
| TC-FORM-06 | submit OK / erreur serveur | toast succès / message erreur |
|
|
43
|
+
| TC-FORM-07 | endpoints paramétrés (Provider) | aucune URL en dur ; utilise `UsersApiProvider` |
|
|
44
|
+
|
|
45
|
+
### 2.3 `<PhotoCapture>` / `<FaceEnroll>` (← U4 ; compose `face`/`storage`)
|
|
46
|
+
| ID | Cas | Attendu |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| TC-FACE-01 | `onCapture({photo, faceDescriptor})` émis | callback reçoit photo (dataURL) + descripteur (mock `face`) |
|
|
49
|
+
| TC-FACE-02 | **compose `@mostajs/face`** (pas de `faceApi` local) | `loadModels/detectFace` appelés sur le module mocké |
|
|
50
|
+
| TC-FACE-03 | aucun visage détecté | état « pas de visage » ; `faceDescriptor=null` |
|
|
51
|
+
| TC-FACE-04 | photo → `storage` | upload délégué au mock `storage` (URL signée) |
|
|
52
|
+
| TC-FACE-05 | permission caméra refusée | message d'erreur gracieux (pas de crash) |
|
|
53
|
+
|
|
54
|
+
### 2.4 `<MemberCardGenerator>` (← U5 ; compose `qrpanel`/`subscriptions-plan`)
|
|
55
|
+
| ID | Cas | Attendu |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| TC-CARD-01 | QR généré via **`qrpanel`** (mock) | data QR = `qrCode`/id ; pas de `qrcode` réembarqué |
|
|
58
|
+
| TC-CARD-02 | carte = photo + QR + **abonnement** | infos plan via `subscriptions-plan` (mock) |
|
|
59
|
+
| TC-CARD-03 | download / print | rasterisation (html2canvas) → image ; handlers appelés |
|
|
60
|
+
|
|
61
|
+
### 2.5 `<UserPreferencesPanel>` (← U7)
|
|
62
|
+
| ID | Cas | Attendu |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| TC-PREF-01 | thème/langue/notifications | GET preferences (mock) → valeurs ; PUT au changement |
|
|
65
|
+
| TC-PREF-02 | i18n des libellés | clés FR/AR/EN résolues (compose `i18n`) |
|
|
66
|
+
|
|
67
|
+
### 2.6 `<UserRoleAssign>` (← délégué rbac)
|
|
68
|
+
| ID | Cas | Attendu |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| TC-ROLE-01 | affecter / révoquer rôle | appelle handlers rbac (mock) ; pas de logique rôle locale |
|
|
71
|
+
|
|
72
|
+
### 2.7 Pages (← U8) & transverses
|
|
73
|
+
| ID | Cas | Attendu |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| TC-PAGE-01 | `UsersAdminPage` assemble liste+form | navigation liste ↔ création/édition |
|
|
76
|
+
| TC-A11Y-01 | axe-core sur chaque écran | 0 violation critique WCAG 2.1 AA |
|
|
77
|
+
| TC-RTL-01 | rendu AR | direction RTL, libellés AR |
|
|
78
|
+
| TC-DEP-01 | **peer `@mostajs/users` absent** | erreur explicite/typed (le module ne fonctionne pas sans `users`) |
|
|
79
|
+
|
|
80
|
+
## 3. Doublures de test (mocks/DI)
|
|
81
|
+
| Dépendance | Doublure |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `@mostajs/users` (handlers) | MSW sur `makeUsersHandlers()` **ou** mock `endpoints` du `UsersApiProvider` |
|
|
84
|
+
| `@mostajs/face` | mock `loadModels/detectFace/extractDescriptor` (descripteur factice) |
|
|
85
|
+
| `@mostajs/qrpanel` | mock renvoyant un dataURL QR déterministe |
|
|
86
|
+
| `@mostajs/storage` | mock upload → URL signée factice |
|
|
87
|
+
| `@mostajs/rbac` | mock liste rôles |
|
|
88
|
+
| `@mostajs/auth` | mock handler password |
|
|
89
|
+
| caméra (`getUserMedia`) | stub jsdom |
|
|
90
|
+
|
|
91
|
+
## 4. Emplacement & exécution
|
|
92
|
+
```
|
|
93
|
+
mosta-users-ui/
|
|
94
|
+
├── test-scripts/
|
|
95
|
+
│ ├── components/ # TC-LIST/FORM/FACE/CARD/PREF/ROLE (Testing-Library + jsdom)
|
|
96
|
+
│ ├── a11y/ # axe-core (TC-A11Y), RTL (TC-RTL)
|
|
97
|
+
│ └── .out*/ # sorties (gitignorées)
|
|
98
|
+
└── examples/
|
|
99
|
+
└── users-admin/ # page admin headless composant users+users-ui + assertions (§12)
|
|
100
|
+
```
|
|
101
|
+
- Rejouables & **committés** ; CI : `build + typecheck + lint + tests` (porte §11.1) — bloquant avant tag/publish.
|
|
102
|
+
|
|
103
|
+
## 5. Critère de Definition of Done (test)
|
|
104
|
+
- Toutes les T-cases **vertes** ; couverture composants **≥ 80 %**.
|
|
105
|
+
- **0 violation a11y critique** (axe-core) ; rendu **RTL** OK.
|
|
106
|
+
- **Aucune dépendance réelle** appelée en CI (face/qr/storage mockés).
|
|
107
|
+
- **Exemple §12 exécuté** (assertions vertes) → **prérequis de l'article #17**.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
**Fin du plan de test `@mostajs/users-ui`.**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Proposition de module (cas C — DEVRULES §4) — `@mostajs/users-ui`
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
**Date** : 2026-06-18
|
|
5
|
+
**Statut** : **proposition à discuter** (cas C — précède le scaffold code).
|
|
6
|
+
**Stack** : `@mostajs/users-stack` (`../../`) · **livré APRÈS `@mostajs/users`** (dépendance stricte).
|
|
7
|
+
**Principe** : **EXTRACTION de l'existant — NE PAS redévelopper.** *Lift-and-adapt* des composants React éprouvés de `MostaGare` (UI admin riche) + `SecuAccessPro` (photo/visage/carte).
|
|
8
|
+
**Amont** : `../../docs/00-AUDIT-EXTRACTION-USERS-ET-USERS-UI-18062026.md` · `../mosta-users/docs/PROPOSITION-MODULE-USERS.md`
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 4.1 Le besoin
|
|
13
|
+
|
|
14
|
+
L'**UI d'administration des utilisateurs** (liste, recherche, création/édition de profil avec **photo + visage**, préférences, affectation de rôle, **carte/badge** photo+QR+abonnement) est aujourd'hui **réimplémentée** dans `MostaGare` (~2041 lignes de composants admin) et `SecuAccessPro` (UI clients avec photo/face/carte). Besoin **transverse** (tout back-office) → à **extraire** en composants React réutilisables, **couche de présentation** de `@mostajs/users`.
|
|
15
|
+
|
|
16
|
+
**Consommateurs** : MostaGare, SecuAccessPro + P1/P2/P3 + iquesta (≥ 2 immédiats — §4.4 satisfaite).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 4.2 Application de la règle d'or (§0)
|
|
21
|
+
|
|
22
|
+
| Proche | Couvre | Ne couvre PAS |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `@mostajs/rbac` (UI) | composants **rôles/permissions** (matrice, managers) | l'UI de **gestion des utilisateurs** (liste/profil/photo/visage/carte/préférences) |
|
|
25
|
+
| `@mostajs/ui` | primitives (button/table/dialog/input…) | les **écrans métier** utilisateurs (assemblage) |
|
|
26
|
+
| `@mostajs/face`, `qrpanel` | primitives visage / QR | les **composants utilisateur** qui les **assemblent** (`<FaceEnroll>`, `<MemberCardGenerator>`) |
|
|
27
|
+
|
|
28
|
+
→ **cas A/B écartés** ; **cas C** confirmé. (Les composants **Roles** de MostaGare relèvent de **l'UI de `rbac`** → contribués à `rbac` en **cas B**, pas ici.)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 4.3 L'argumentation
|
|
33
|
+
|
|
34
|
+
- **Module séparé du back (`users`)** : même logique que `rbac` (schémas) vs ses composants — un back réutilisable, une UI **optionnelle**. Permet d'utiliser `users` côté serveur sans imposer React.
|
|
35
|
+
- **Dépendance stricte `users-ui` → `users`** : `users-ui` est la **présentation** de `users` ; il consomme ses types/actions/handlers. **Peer-dependency** `peerDependencies: { "@mostajs/users": "^x", "react": ">=18" }`. Sens unique. Build/publish : **`users` d'abord**.
|
|
36
|
+
- **Nom** : `@mostajs/users-ui`, dans `mosta-users-stack/mosta-users-ui/`.
|
|
37
|
+
- **Extraction-first** : composants en prod (MostaGare/SecuAccessPro) → on **modularise**, on ne réécrit pas.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 4.4 La discussion du besoin
|
|
42
|
+
|
|
43
|
+
- **Bon moment ?** Oui — UI dupliquée sur 2 projets, `users` en cours d'extraction.
|
|
44
|
+
- **Réutilisable ?** Oui — tout back-office.
|
|
45
|
+
- **Duplication/chevauchement ?** Maîtrisé par **composition** : visage→`face`, QR→`qrpanel`, abonnement→`subscriptions-plan`, rôles→`rbac`, primitives→`ui`, i18n→`i18n`, lecture badge→`scan`/`pwa-scan`. `users-ui` **assemble**, n'implémente aucune primitive.
|
|
46
|
+
- **Alternatives rejetées** : (1) laisser l'UI dans chaque app → divergence (cas actuel) ; (2) tout mettre dans `rbac` UI → mélange rôles/utilisateurs ; (3) réembarquer `face-api`/`qrcode` → réimplémentation interdite (§0) → composer les modules.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 4.5 Documentation technique — **composants EXTRAITS**
|
|
51
|
+
|
|
52
|
+
### Composants (cible) — façon `rbac` UI, endpoints **paramétrables**
|
|
53
|
+
| Composant | Rôle | Compose |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `<UsersList>` | tableau + recherche + filtre rôle/statut + pagination + stats + toggle-status/delete | `users` (handlers), `ui` |
|
|
56
|
+
| `<UserForm>` / `<UserEditForm>` | création / édition profil + sélection rôle | `users`, `rbac` (roles), **`auth`** (password) |
|
|
57
|
+
| `<PhotoCapture>` / `<FaceEnroll>` | webcam → photo + descripteur ; `onCapture({photo, faceDescriptor})` | **`face`** (détection/descripteur), `storage` (photo) |
|
|
58
|
+
| `<MemberCardGenerator>` | carte/badge : photo + **QR** + **abonnement**, download/print | **`qrpanel`**, `subscriptions-plan` |
|
|
59
|
+
| `<UserPreferencesPanel>` | thème / langue / notifications | `users` (preferences), `i18n` |
|
|
60
|
+
| `<UserRoleAssign>` | affecter/révoquer rôles | délégué `rbac` |
|
|
61
|
+
|
|
62
|
+
### Carte d'extraction (source → cible — *lift/adapt/compose*)
|
|
63
|
+
| # | Source | Cible | Action |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| U1 | MostaGare `components/admin/UsersList.tsx` (472 l. ; fetch `/api/admin/users` search/role/status/pagination/stats, toggle-status, delete) | `src/UsersList.tsx` | **LIFT** + **paramétrer les endpoints** (props/context au lieu d'URL en dur) |
|
|
66
|
+
| U2 | MostaGare `components/admin/UserForm.tsx` (344 l. ; roles via `/api/admin/roles`, password/confirm) | `src/UserForm.tsx` | **LIFT** + **ADAPT** : password → `@mostajs/auth` ; roles → `@mostajs/rbac` |
|
|
67
|
+
| U3 | MostaGare `components/admin/UserEditForm.tsx` (76 l. ; fetch `/api/admin/users/[id]`) | `src/UserEditForm.tsx` | **LIFT** + paramétrer endpoint |
|
|
68
|
+
| U4 | SecuAccessPro `components/clients/FaceDetector.tsx` (`onCapture({photo,faceDescriptor})`, webcam) | `src/PhotoCapture.tsx` + `src/FaceEnroll.tsx` | **LIFT** + **DROP→COMPOSE `@mostajs/face`** (remplace `faceApi`) |
|
|
69
|
+
| U5 | SecuAccessPro `components/clients/ClientCardGenerator.tsx` (`qrcode` + `html2canvas`, download/print) | `src/MemberCardGenerator.tsx` | **LIFT** + **COMPOSE `@mostajs/qrpanel`** (QR) + `subscriptions-plan` (abonnement) ; garder rasterisation carte |
|
|
70
|
+
| U6 | SecuAccessPro `ClientForm.tsx` (câblage `handleFaceCapture` → form) | `<UserForm>` intègre `<PhotoCapture>` | **PATTERN** (montre le câblage photo/visage) |
|
|
71
|
+
| U7 | MostaGare `user-preferences` UI + `messages/{fr,ar,en}.json` | `src/UserPreferencesPanel.tsx` + i18n | **EXTRACT** + compose `i18n` |
|
|
72
|
+
| — | MostaGare `RolesList`/`RoleForm`/`RolePermissionsManager` | **→ `@mostajs/rbac` (cas B)** | **OUT** (hors `users-ui`) |
|
|
73
|
+
|
|
74
|
+
> **Adaptation transversale** : les composants MostaGare appellent des **URLs en dur** (`/api/admin/users…`). À l'extraction, **paramétrer** (prop `endpoints`/contexte `UsersApiProvider`) pour brancher les `makeUsersHandlers()` de `@mostajs/users`. Aucune logique métier réécrite.
|
|
75
|
+
|
|
76
|
+
### Composition (§10)
|
|
77
|
+
**`@mostajs/users` (peer — requis)** · `rbac` (rôles) · `ui` (primitives) · `i18n` (FR/AR/EN) · `face` (visage) · `storage` (photo) · `qrpanel` (QR) · `subscriptions-plan` (carte) · `scan`/`pwa-scan` (lecture badge).
|
|
78
|
+
|
|
79
|
+
### Squelettes
|
|
80
|
+
`README.md` happy-path (install `@mostajs/users-ui @mostajs/users` → `<UsersList endpoints=…/>`). `llms.txt` : RÔLE (UI admin utilisateurs, présentation de `users`), EXPORTS (composants), PIÈGES (peer `users` requis ; endpoints à paramétrer ; ne pas réembarquer face-api/qrcode → composer ; password→auth ; roles→rbac).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Suite (après validation §4)
|
|
85
|
+
Plan de dev #3 (versions : 0.1 `<UsersList>`/`<UserForm>`/`<UserEditForm>` ; 0.2 `<PhotoCapture>`/`<FaceEnroll>`/`<MemberCardGenerator>` ; 0.3 `<UserPreferencesPanel>`/`<UserRoleAssign>` + pages admin) → #4 tests → scaffold code **après `@mostajs/users@0.1`**. Portes §11.1 (bloquante), §11.5 i18n/a11y (UI), §11.2/§11.3 **informatives**.
|
|
86
|
+
|
|
87
|
+
**Validation demandée** : périmètre (utilisateurs uniquement, Roles → rbac), dépendance stricte `users`, paramétrage des endpoints, noms des composants.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
**Fin de la proposition `@mostajs/users-ui`.**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Exemple §12 — @mostajs/users-ui (headless) composant @mostajs/users. node examples/users-ui-headless/run.mjs
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createUsers, createMemoryRepositories } from '../../../mosta-users/src/index.js';
|
|
4
|
+
import { createUsersUiController, validateUserForm, buildMemberCard } from '../../src/index.js';
|
|
5
|
+
|
|
6
|
+
const service = createUsers({ repositories: createMemoryRepositories(), qr: { generate: (id) => `QR:${id}` } });
|
|
7
|
+
const ui = createUsersUiController({ service, t: (k) => k });
|
|
8
|
+
|
|
9
|
+
// validation
|
|
10
|
+
assert.equal(validateUserForm({ email: 'bad' }).valid, false);
|
|
11
|
+
assert.equal(validateUserForm({ firstName: 'A', email: 'a@b.dz' }).valid, true);
|
|
12
|
+
|
|
13
|
+
// submit via le contrôleur (password délégué à auth, jamais persisté dans users)
|
|
14
|
+
const r = await ui.submit({ firstName: 'Leïla', lastName: 'Hadj', email: 'leila@ex.dz', password: 'x', confirmPassword: 'x' });
|
|
15
|
+
assert.ok(r.ok && r.user.id);
|
|
16
|
+
assert.equal(r._passwordDelegatedToAuth, true);
|
|
17
|
+
assert.ok(!('password' in r.user), 'users ne stocke pas le password');
|
|
18
|
+
|
|
19
|
+
// liste view-model + stats
|
|
20
|
+
await ui.submit({ firstName: 'Omar' });
|
|
21
|
+
const vm = await ui.refresh();
|
|
22
|
+
assert.equal(vm.total, 2);
|
|
23
|
+
assert.ok(vm.rows.find((x) => x.displayName === 'Leïla Hadj'));
|
|
24
|
+
|
|
25
|
+
// carte membre
|
|
26
|
+
const card = buildMemberCard(r.user);
|
|
27
|
+
assert.ok(card.qr.startsWith('QR:'));
|
|
28
|
+
|
|
29
|
+
// dépendance stricte : sans service → erreur
|
|
30
|
+
assert.throws(() => createUsersUiController({}), /service @mostajs\/users requis/);
|
|
31
|
+
|
|
32
|
+
console.log('✅ @mostajs/users-ui (headless) — OK (dépend de @mostajs/users)');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Démo web — interface `@mostajs/users-ui` (autonome)
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
|
|
5
|
+
Interface **dédiée** d'administration des utilisateurs (application autonome, **non fusionnée** avec allocations).
|
|
6
|
+
Zéro-build (DEVRULES §12) : importmap → modules ESM réels, état **in-memory** navigateur.
|
|
7
|
+
|
|
8
|
+
## Démarrer
|
|
9
|
+
```bash
|
|
10
|
+
./serve.sh # (ou ./serve.sh 8080)
|
|
11
|
+
```
|
|
12
|
+
Le script sert depuis la **racine commune** (`mosta-users-stack/`, car l'importmap référence le module voisin
|
|
13
|
+
`@mostajs/users`) et affiche l'URL exacte à ouvrir, p. ex. :
|
|
14
|
+
`http://localhost:8000/mosta-users-ui/examples/web-demo/index.html`
|
|
15
|
+
|
|
16
|
+
> Lancement manuel équivalent : depuis `mosta-users-stack/`, `python3 -m http.server` puis ouvrir l'URL ci-dessus.
|
|
17
|
+
> Pour exécuter une commande dans cette session, tape-la préfixée de `!`.
|
|
18
|
+
|
|
19
|
+
## Ce que ça montre
|
|
20
|
+
- Création/validation d'utilisateur, liste + recherche + stats, verrouiller/déverrouiller/supprimer — pilotés par
|
|
21
|
+
la **logique headless** de `@mostajs/users-ui` (`createUsersUiController`, `validateUserForm`) au-dessus de `@mostajs/users`.
|
|
22
|
+
- **QR / carte membre** : le rendu de l'image QR **compose `@mostajs/qrpanel`** dans le composant React
|
|
23
|
+
`<MemberCardGenerator>` (`useQrDataUrl`). Cette démo vanilla affiche l'admin liste ; pour la carte/QR live, utiliser le composant React `@mostajs/users-ui/react`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Interface @mostajs/users-ui (application autonome). Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
import { createUsers, createMemoryRepositories } from '@mostajs/users';
|
|
3
|
+
import { createUsersUiController, validateUserForm } from '@mostajs/users-ui';
|
|
4
|
+
|
|
5
|
+
const $ = (id) => document.getElementById(id);
|
|
6
|
+
const t = (k) => ({ 'status.active': 'actif', 'status.locked': 'verrouillé', 'status.disabled': 'désactivé' }[k] || k);
|
|
7
|
+
|
|
8
|
+
// users-ui pilote @mostajs/users (dépendance)
|
|
9
|
+
// Le payload de carte (qrCode) est généré par défaut ; le RENDU de l'image QR compose @mostajs/qrpanel
|
|
10
|
+
// (composant <MemberCardGenerator> / useQrDataUrl). Ici, démo vanilla → on n'affiche que l'admin liste.
|
|
11
|
+
const service = createUsers({ repositories: createMemoryRepositories() });
|
|
12
|
+
const ui = createUsersUiController({ service, t });
|
|
13
|
+
|
|
14
|
+
$('u-add').onclick = async () => {
|
|
15
|
+
const form = { firstName: $('u-first').value.trim(), lastName: $('u-last').value.trim(), email: $('u-email').value.trim() };
|
|
16
|
+
const v = validateUserForm(form);
|
|
17
|
+
$('u-err').textContent = v.valid ? '' : Object.values(v.errors).join(' · ');
|
|
18
|
+
if (!v.valid) return;
|
|
19
|
+
const r = await ui.submit(form);
|
|
20
|
+
if (!r.ok) { $('u-err').textContent = Object.values(r.errors).join(' · '); return; }
|
|
21
|
+
$('u-first').value = $('u-last').value = $('u-email').value = '';
|
|
22
|
+
render();
|
|
23
|
+
};
|
|
24
|
+
$('u-search').oninput = (e) => { ui.setFilters({ search: e.target.value }); render(); };
|
|
25
|
+
|
|
26
|
+
async function render() {
|
|
27
|
+
const vm = await ui.refresh();
|
|
28
|
+
$('u-stats').textContent = `total ${vm.stats.total} · ${t('status.active')} ${vm.stats.active} · ${t('status.locked')} ${vm.stats.locked} · ${t('status.disabled')} ${vm.stats.disabled}`;
|
|
29
|
+
$('u-rows').innerHTML = vm.rows.map((r) => `
|
|
30
|
+
<tr>
|
|
31
|
+
<td>${r.displayName}</td><td>${r.email || ''}</td>
|
|
32
|
+
<td><span class="pill s-${r.status}">${r.statusLabel}</span></td>
|
|
33
|
+
<td>
|
|
34
|
+
<button class="sm ghost" data-do="toggle" data-id="${r.id}" data-st="${r.status}">${r.status === 'locked' ? 'déverrouiller' : 'verrouiller'}</button>
|
|
35
|
+
<button class="sm ghost" data-do="del" data-id="${r.id}">suppr.</button>
|
|
36
|
+
</td>
|
|
37
|
+
</tr>`).join('');
|
|
38
|
+
$('u-rows').querySelectorAll('[data-do]').forEach((b) => {
|
|
39
|
+
b.onclick = async () => {
|
|
40
|
+
if (b.dataset.do === 'toggle') await ui.toggleStatus(b.dataset.id, b.dataset.st);
|
|
41
|
+
else await ui.remove(b.dataset.id);
|
|
42
|
+
render();
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// seed démo
|
|
48
|
+
for (const n of [['Amina', 'Belkacem', 'amina@ex.dz'], ['Karim', 'Saidi', 'karim@ex.dz']])
|
|
49
|
+
await ui.submit({ firstName: n[0], lastName: n[1], email: n[2] });
|
|
50
|
+
render();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="fr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Interface — @mostajs/users-ui</title>
|
|
7
|
+
<!--
|
|
8
|
+
Auteur : Dr Hamid MADANI <drmdh@msn.com>
|
|
9
|
+
Démo navigateur ZÉRO-BUILD (DEVRULES §12) — interface DÉDIÉE @mostajs/users-ui (application autonome).
|
|
10
|
+
Lancer : ./serve.sh (ou python3 -m http.server) → http://localhost:8000
|
|
11
|
+
-->
|
|
12
|
+
<script type="importmap">
|
|
13
|
+
{
|
|
14
|
+
"imports": {
|
|
15
|
+
"@mostajs/users": "../../../mosta-users/src/index.js",
|
|
16
|
+
"@mostajs/users-ui": "../../src/index.js"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
<style>
|
|
21
|
+
:root { --card:#1e293b; --mut:#94a3b8; --ok:#22c55e; --warn:#f59e0b; --bad:#ef4444; --acc:#a78bfa; }
|
|
22
|
+
*{box-sizing:border-box} body{margin:0;font:14px/1.5 system-ui,sans-serif;background:#0b1220;color:#e2e8f0}
|
|
23
|
+
header{padding:16px 24px;background:#0f172a;border-bottom:1px solid #334155}
|
|
24
|
+
header h1{margin:0;font-size:18px;color:var(--acc)} header small{color:var(--mut)}
|
|
25
|
+
.wrap{max-width:880px;margin:0 auto;padding:16px 24px}
|
|
26
|
+
.card{background:var(--card);border:1px solid #334155;border-radius:12px;padding:16px;margin-bottom:16px}
|
|
27
|
+
.card h2{margin:0 0 12px;font-size:15px}
|
|
28
|
+
label{display:block;font-size:12px;color:var(--mut);margin:8px 0 2px}
|
|
29
|
+
input,select{width:100%;padding:8px;border-radius:8px;border:1px solid #475569;background:#0f172a;color:#e2e8f0}
|
|
30
|
+
button{padding:7px 12px;border-radius:8px;border:0;background:var(--acc);color:#1a1033;font-weight:700;cursor:pointer}
|
|
31
|
+
button.ghost{background:#334155;color:#e2e8f0} button.sm{padding:4px 8px;font-size:12px}
|
|
32
|
+
.row{display:flex;gap:8px;align-items:end}.row>*{flex:1}
|
|
33
|
+
.stats{color:var(--mut);font-size:12px;margin:8px 0}
|
|
34
|
+
table{width:100%;border-collapse:collapse;margin-top:10px}th,td{text-align:left;padding:6px 8px;border-bottom:1px solid #334155;font-size:13px}
|
|
35
|
+
.pill{padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700}
|
|
36
|
+
.s-active{background:rgba(34,197,94,.15);color:var(--ok)} .s-locked{background:rgba(245,158,11,.15);color:var(--warn)} .s-disabled{background:rgba(239,68,68,.15);color:var(--bad)}
|
|
37
|
+
.err{color:var(--bad);font-size:12px;min-height:16px}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<header>
|
|
42
|
+
<h1>👤 @mostajs/users-ui</h1>
|
|
43
|
+
<small>Application autonome — administration des utilisateurs (logique headless du module). État in-memory.</small>
|
|
44
|
+
</header>
|
|
45
|
+
<div class="wrap">
|
|
46
|
+
<section class="card">
|
|
47
|
+
<h2>Créer un utilisateur</h2>
|
|
48
|
+
<div class="row">
|
|
49
|
+
<div><label>Prénom</label><input id="u-first" /></div>
|
|
50
|
+
<div><label>Nom</label><input id="u-last" /></div>
|
|
51
|
+
</div>
|
|
52
|
+
<label>Email</label><input id="u-email" placeholder="nom@ex.dz" />
|
|
53
|
+
<div id="u-err" class="err"></div>
|
|
54
|
+
<button id="u-add">+ Créer</button>
|
|
55
|
+
</section>
|
|
56
|
+
|
|
57
|
+
<section class="card">
|
|
58
|
+
<h2>Utilisateurs</h2>
|
|
59
|
+
<label>Recherche</label><input id="u-search" placeholder="nom, email…" />
|
|
60
|
+
<div class="stats" id="u-stats"></div>
|
|
61
|
+
<table><thead><tr><th>Nom</th><th>Email</th><th>Statut</th><th></th></tr></thead><tbody id="u-rows"></tbody></table>
|
|
62
|
+
</section>
|
|
63
|
+
</div>
|
|
64
|
+
<script type="module" src="./app.mjs"></script>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Démarre la démo web @mostajs/users-ui (zéro-build). Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
# L'importmap référence des modules voisins (../../../) → on sert depuis la RACINE commune.
|
|
4
|
+
# Usage : ./serve.sh [port]
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
ROOT="$(cd "$DIR/../../.." && pwd)" # mosta-users-stack (contient mosta-users + mosta-users-ui)
|
|
8
|
+
REL="${DIR#"$ROOT"/}/index.html"
|
|
9
|
+
PORT="${1:-8000}"
|
|
10
|
+
echo "📂 racine servie : $ROOT"
|
|
11
|
+
echo "🌐 ouvrir : http://localhost:$PORT/$REL"
|
|
12
|
+
cd "$ROOT"
|
|
13
|
+
exec python3 -m http.server "$PORT"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Démo React — `@mostajs/users-ui` (carte + QR qrpanel en direct)
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
|
|
5
|
+
Variante **React** zéro-build qui charge les **vrais composants** `@mostajs/users-ui/react`
|
|
6
|
+
(`<UserForm>`, `<UsersList>`, `<MemberCardGenerator>`). La **carte membre** affiche un **QR rendu en direct
|
|
7
|
+
par `@mostajs/qrpanel`** (`useQrDataUrl` de `@mostajs/qrpanel/qr-image`).
|
|
8
|
+
|
|
9
|
+
## Démarrer
|
|
10
|
+
```bash
|
|
11
|
+
./serve.mjs # (ou node serve.mjs 8080)
|
|
12
|
+
```
|
|
13
|
+
Le serveur Node sert les `.jsx`/`.mjs` en `text/javascript` (ce que `python http.server` ne fait pas) et
|
|
14
|
+
sert depuis la racine `mostajs/` (pour atteindre `@mostajs/qrpanel`). Il affiche l'URL à ouvrir, p. ex. :
|
|
15
|
+
`http://localhost:8000/mosta-users-stack/mosta-users-ui/examples/web-react/index.html`
|
|
16
|
+
|
|
17
|
+
> **Internet requis au runtime** : `react`, `react-dom`, `qrcode`, `html2canvas` sont chargés via `esm.sh`
|
|
18
|
+
> (importmap). Nos modules (`@mostajs/*`) sont servis en local. Pour exécuter ici, tape `!…/serve.mjs`.
|
|
19
|
+
|
|
20
|
+
## Ce que ça montre
|
|
21
|
+
- Création d'un utilisateur via le composant **`<UserForm>`** (validation headless, password délégué à `auth`).
|
|
22
|
+
- Liste **`<UsersList>`** (recherche, statut, verrouiller/déverrouiller).
|
|
23
|
+
- **`<MemberCardGenerator>`** : carte (photo avatar + abonnement) + **QR généré par qrpanel** ; bouton de
|
|
24
|
+
téléchargement PNG (html2canvas).
|
|
25
|
+
|
|
26
|
+
## Composition (DEVRULES §10)
|
|
27
|
+
`@mostajs/users` (service, peer) · `@mostajs/users-ui/react` (composants) · **`@mostajs/qrpanel`** (QR) ·
|
|
28
|
+
`react`/`html2canvas` (CDN). Aucune primitive QR réembarquée.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Démo React @mostajs/users-ui (carte + QR qrpanel live). Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
// React.createElement (pas de JSX → pas de transpilation). Charge les VRAIS composants du module.
|
|
3
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
4
|
+
import { createRoot } from 'react-dom/client';
|
|
5
|
+
import { createUsers, createMemoryRepositories } from '@mostajs/users';
|
|
6
|
+
import { UsersList, UserForm, MemberCardGenerator } from '@mostajs/users-ui/react';
|
|
7
|
+
|
|
8
|
+
const h = React.createElement;
|
|
9
|
+
const t = (k) => ({
|
|
10
|
+
'status.active': 'actif', 'status.locked': 'verrouillé', 'status.disabled': 'désactivé',
|
|
11
|
+
firstName: 'Prénom', lastName: 'Nom', email: 'Email', role: 'Rôle', password: 'Mot de passe',
|
|
12
|
+
save: 'Enregistrer', edit: 'éditer', lock: 'verrouiller', unlock: 'déverrouiller',
|
|
13
|
+
search: 'Recherche…', loading: '…', download: 'Télécharger la carte', capture: 'Capturer',
|
|
14
|
+
}[k] || k);
|
|
15
|
+
|
|
16
|
+
// avatar SVG (data URL) pour que la carte ait une photo
|
|
17
|
+
const avatar = (a, b) => {
|
|
18
|
+
const ini = `${(a || '?')[0]}${(b || '')[0] || ''}`.toUpperCase();
|
|
19
|
+
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='96' height='96'><rect width='100%' height='100%' rx='12' fill='#a78bfa'/><text x='50%' y='55%' font-size='38' text-anchor='middle' dominant-baseline='middle' font-family='sans-serif' fill='#1a1033'>${ini}</text></svg>`;
|
|
20
|
+
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function App() {
|
|
24
|
+
const service = useMemo(() => createUsers({ repositories: createMemoryRepositories() }), []);
|
|
25
|
+
const [selected, setSelected] = useState(null);
|
|
26
|
+
const [tick, setTick] = useState(0);
|
|
27
|
+
const refresh = useCallback(() => setTick((n) => n + 1), []);
|
|
28
|
+
|
|
29
|
+
const onSaved = useCallback(async (u) => {
|
|
30
|
+
// attache un avatar de démo + sélectionne pour afficher la carte
|
|
31
|
+
const withPhoto = await service.update(u.id, { photo: avatar(u.firstName, u.lastName) });
|
|
32
|
+
setSelected(withPhoto); refresh();
|
|
33
|
+
}, [service, refresh]);
|
|
34
|
+
|
|
35
|
+
const onEdit = useCallback(async (id) => setSelected(await service.get(id)), [service]);
|
|
36
|
+
|
|
37
|
+
return [
|
|
38
|
+
h('section', { className: 'card', key: 'left' },
|
|
39
|
+
h('h2', null, 'Créer / éditer'),
|
|
40
|
+
h(UserForm, { service, t, onSaved }),
|
|
41
|
+
h('h2', { style: { marginTop: 18 } }, 'Utilisateurs'),
|
|
42
|
+
h(UsersList, { service, t, onEdit, key: tick }), // key=tick → re-render après création
|
|
43
|
+
),
|
|
44
|
+
h('section', { className: 'card', key: 'right' },
|
|
45
|
+
h('h2', null, '🪪 Carte membre (QR via @mostajs/qrpanel)'),
|
|
46
|
+
selected
|
|
47
|
+
? h(MemberCardGenerator, { user: selected, t, subscription: { planName: 'Abonnement Gold' } })
|
|
48
|
+
: h('div', { style: { color: '#94a3b8' } }, 'Crée un utilisateur ou clique « éditer » pour voir sa carte + QR.'),
|
|
49
|
+
),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
createRoot(document.getElementById('root')).render(h(App));
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="fr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Démo React — @mostajs/users-ui (carte + QR qrpanel live)</title>
|
|
7
|
+
<!--
|
|
8
|
+
Auteur : Dr Hamid MADANI <drmdh@msn.com>
|
|
9
|
+
Variante REACT zéro-build : charge les VRAIS composants @mostajs/users-ui/react + <MemberCardGenerator>,
|
|
10
|
+
dont le QR est rendu EN DIRECT par @mostajs/qrpanel (useQrDataUrl). React/qrcode via esm.sh (CDN, internet requis).
|
|
11
|
+
Lancer : ./serve.mjs (serveur Node qui sert les .jsx en text/javascript) → URL affichée.
|
|
12
|
+
-->
|
|
13
|
+
<script type="importmap">
|
|
14
|
+
{
|
|
15
|
+
"imports": {
|
|
16
|
+
"react": "https://esm.sh/react@18.3.1",
|
|
17
|
+
"react/jsx-runtime": "https://esm.sh/react@18.3.1/jsx-runtime",
|
|
18
|
+
"react-dom/client": "https://esm.sh/react-dom@18.3.1/client",
|
|
19
|
+
"qrcode": "https://esm.sh/qrcode@1.5.4",
|
|
20
|
+
"html2canvas": "https://esm.sh/html2canvas@1.4.1",
|
|
21
|
+
"@mostajs/users": "../../../mosta-users/src/index.js",
|
|
22
|
+
"@mostajs/users-ui": "../../src/index.js",
|
|
23
|
+
"@mostajs/users-ui/react": "../../src/react/index.js",
|
|
24
|
+
"@mostajs/qrpanel/qr-image": "../../../../mosta-qrpanel/dist/qr-image.js"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
<style>
|
|
29
|
+
:root{--card:#1e293b;--mut:#94a3b8;--ok:#22c55e;--warn:#f59e0b;--acc:#a78bfa}
|
|
30
|
+
*{box-sizing:border-box} body{margin:0;font:14px/1.5 system-ui,sans-serif;background:#0b1220;color:#e2e8f0}
|
|
31
|
+
header{padding:16px 24px;background:#0f172a;border-bottom:1px solid #334155}
|
|
32
|
+
header h1{margin:0;font-size:18px;color:var(--acc)} header small{color:var(--mut)}
|
|
33
|
+
.wrap{display:grid;grid-template-columns:1.2fr .8fr;gap:16px;padding:16px 24px;align-items:start}
|
|
34
|
+
@media(max-width:880px){.wrap{grid-template-columns:1fr}}
|
|
35
|
+
.card{background:var(--card);border:1px solid #334155;border-radius:12px;padding:16px}
|
|
36
|
+
.card h2{margin:0 0 12px;font-size:15px;color:var(--acc)}
|
|
37
|
+
input,select{width:100%;padding:8px;border-radius:8px;border:1px solid #475569;background:#0f172a;color:#e2e8f0;margin:4px 0}
|
|
38
|
+
button{padding:7px 12px;border-radius:8px;border:0;background:var(--acc);color:#1a1033;font-weight:700;cursor:pointer}
|
|
39
|
+
button.ghost{background:#334155;color:#e2e8f0} .mosta-users-list table{width:100%;border-collapse:collapse}
|
|
40
|
+
.mosta-users-list td,.mosta-users-list th{padding:6px;border-bottom:1px solid #334155;font-size:13px;text-align:left}
|
|
41
|
+
.mosta-users-list input{margin-bottom:8px} .mosta-user-form{display:flex;flex-direction:column;gap:6px}
|
|
42
|
+
/* carte membre */
|
|
43
|
+
.mosta-member-card{width:260px;background:linear-gradient(135deg,#312e81,#1e293b);border:1px solid #475569;border-radius:14px;padding:16px;display:flex;flex-direction:column;align-items:center;gap:8px;margin-bottom:10px}
|
|
44
|
+
.mosta-member-card .photo{width:84px;height:84px;border-radius:12px;object-fit:cover}
|
|
45
|
+
.mosta-member-card .qr{width:120px;height:120px;background:#fff;border-radius:8px;padding:4px}
|
|
46
|
+
.mosta-member-card strong{font-size:16px}
|
|
47
|
+
</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<header>
|
|
51
|
+
<h1>👤 @mostajs/users-ui — variante React</h1>
|
|
52
|
+
<small>Vrais composants React du module · carte membre avec <b>QR rendu en direct par @mostajs/qrpanel</b></small>
|
|
53
|
+
</header>
|
|
54
|
+
<div id="root" class="wrap"></div>
|
|
55
|
+
<script type="module" src="./app.js"></script>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Serveur statique zéro-dépendance pour la démo React. Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// Sert les .jsx/.mjs en text/javascript (que python http.server ne fait pas) ; racine = mostajs/.
|
|
4
|
+
// Usage : ./serve.mjs [port] (ou: node serve.mjs 8080)
|
|
5
|
+
import { createServer } from 'node:http';
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { dirname, join, normalize, extname } from 'node:path';
|
|
9
|
+
|
|
10
|
+
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const ROOT = normalize(join(DIR, '..', '..', '..', '..')); // mostajs/ (contient mosta-users-stack + mosta-qrpanel)
|
|
12
|
+
const REL = DIR.slice(ROOT.length + 1).split(/[\\/]/).join('/') + '/index.html';
|
|
13
|
+
const PORT = Number(process.argv[2]) || 8000;
|
|
14
|
+
|
|
15
|
+
const MIME = {
|
|
16
|
+
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
|
|
17
|
+
'.mjs': 'text/javascript; charset=utf-8', '.jsx': 'text/javascript; charset=utf-8',
|
|
18
|
+
'.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8',
|
|
19
|
+
'.svg': 'image/svg+xml', '.map': 'application/json',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
createServer(async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const url = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
25
|
+
const path = normalize(join(ROOT, url === '/' ? '/index.html' : url));
|
|
26
|
+
if (!path.startsWith(ROOT)) { res.writeHead(403).end('forbidden'); return; }
|
|
27
|
+
const body = await readFile(path);
|
|
28
|
+
res.writeHead(200, { 'content-type': MIME[extname(path)] || 'application/octet-stream' });
|
|
29
|
+
res.end(body);
|
|
30
|
+
} catch {
|
|
31
|
+
res.writeHead(404, { 'content-type': 'text/plain' }).end('404');
|
|
32
|
+
}
|
|
33
|
+
}).listen(PORT, () => {
|
|
34
|
+
console.log(`📂 racine servie : ${ROOT}`);
|
|
35
|
+
console.log(`🌐 ouvrir : http://localhost:${PORT}/${REL}`);
|
|
36
|
+
console.log(' (React + qrcode via esm.sh — connexion internet requise au runtime)');
|
|
37
|
+
});
|
package/llms.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @mostajs/users-ui — fiche LLM
|
|
2
|
+
|
|
3
|
+
RÔLE
|
|
4
|
+
UI d'administration des utilisateurs — couche de PRÉSENTATION de @mostajs/users.
|
|
5
|
+
Logique HEADLESS (framework-agnostique, testable sans navigateur) + composants React (src/react).
|
|
6
|
+
DÉPEND de @mostajs/users (peer stricte) ; compose rbac (rôles), ui, i18n, face, qrpanel, subscriptions-plan.
|
|
7
|
+
|
|
8
|
+
EXPORTS (headless — "@mostajs/users-ui")
|
|
9
|
+
validateUserForm(data,{isEditing}) -> {valid,errors}
|
|
10
|
+
buildUsersListViewModel(listResult,{t}) -> {rows,total,page,pageSize,stats}
|
|
11
|
+
buildMemberCard(user,{subscription}) -> {name,photo,qr,subscription}
|
|
12
|
+
createUsersUiController({service,t}) -> { query, setFilters, refresh, submit, toggleStatus, remove }
|
|
13
|
+
EXPORTS (react — "@mostajs/users-ui/react")
|
|
14
|
+
<UsersList> <UserForm> <PhotoCapture>/<FaceEnroll> <MemberCardGenerator>
|
|
15
|
+
|
|
16
|
+
DÉPENDANCE
|
|
17
|
+
createUsersUiController EXIGE `service` = createUsers() de @mostajs/users (sinon throw). password → @mostajs/auth ; rôles → @mostajs/rbac.
|
|
18
|
+
|
|
19
|
+
EXTRACTION
|
|
20
|
+
MostaGare components/admin/User* (UsersList/UserForm/UserEditForm) + SecuAccessPro FaceDetector/ClientCardGenerator.
|
|
21
|
+
|
|
22
|
+
PIÈGES
|
|
23
|
+
- Ne fonctionne pas sans @mostajs/users. Les composants Roles → @mostajs/rbac (pas ici).
|
|
24
|
+
- PhotoCapture/MemberCardGenerator composent face/qrpanel (ne réembarquent pas face-api/qrcode).
|
|
25
|
+
- Endpoints/serveur : le contrôleur appelle le service users (in-process) ou des handlers — paramétrable.
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/users-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "UI d'administration des utilisateurs — couche de présentation de @mostajs/users (liste, profil, photo/visage, carte, préférences). Logique headless + composants React.",
|
|
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
|
+
"./react": "./src/react/index.js"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"@mostajs/users": "^0.1.0",
|
|
15
|
+
"react": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"peerDependenciesMeta": {
|
|
18
|
+
"react": {
|
|
19
|
+
"optional": true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mostajs",
|
|
24
|
+
"users",
|
|
25
|
+
"admin",
|
|
26
|
+
"ui",
|
|
27
|
+
"react"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node test-scripts/unit/headless.test.mjs",
|
|
31
|
+
"example": "node examples/users-ui-headless/run.mjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/headless.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @mostajs/users-ui — logique HEADLESS (framework-agnostique), vérifiable sans navigateur.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// Dépend de @mostajs/users (service injecté). Les composants React (src/react) enveloppent cette logique.
|
|
4
|
+
|
|
5
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6
|
+
|
|
7
|
+
/** Validation de formulaire utilisateur (password collecté ici mais délégué à @mostajs/auth). */
|
|
8
|
+
export function validateUserForm(data = {}, { isEditing = false } = {}) {
|
|
9
|
+
const errors = {};
|
|
10
|
+
if (!data.firstName && !data.lastName && !data.username) errors.name = 'nom ou identifiant requis';
|
|
11
|
+
if (data.email && !EMAIL_RE.test(data.email)) errors.email = 'email invalide';
|
|
12
|
+
if (!isEditing) {
|
|
13
|
+
if (data.password != null && data.confirmPassword != null && data.password !== data.confirmPassword)
|
|
14
|
+
errors.confirmPassword = 'les mots de passe diffèrent';
|
|
15
|
+
}
|
|
16
|
+
return { valid: Object.keys(errors).length === 0, errors };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Construit le view-model d'une liste à partir du résultat de users.list(). */
|
|
20
|
+
export function buildUsersListViewModel(listResult, { t = (k) => k } = {}) {
|
|
21
|
+
const STATUS = { active: t('status.active'), locked: t('status.locked'), disabled: t('status.disabled') };
|
|
22
|
+
return {
|
|
23
|
+
rows: (listResult.items || []).map((u) => ({
|
|
24
|
+
id: u.id,
|
|
25
|
+
displayName: `${u.firstName || ''} ${u.lastName || ''}`.trim() || u.username || u.email || u.id,
|
|
26
|
+
email: u.email,
|
|
27
|
+
status: u.status,
|
|
28
|
+
statusLabel: STATUS[u.status] || u.status,
|
|
29
|
+
hasPhoto: !!u.photo,
|
|
30
|
+
hasFace: Array.isArray(u.faceDescriptor) && u.faceDescriptor.length > 0,
|
|
31
|
+
})),
|
|
32
|
+
total: listResult.total,
|
|
33
|
+
page: listResult.page,
|
|
34
|
+
pageSize: listResult.pageSize,
|
|
35
|
+
stats: listResult.stats,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Modèle de carte/badge (photo + QR + abonnement) — compose qrpanel/subscriptions-plan côté composant. */
|
|
40
|
+
export function buildMemberCard(user, { subscription } = {}) {
|
|
41
|
+
return {
|
|
42
|
+
name: `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.username,
|
|
43
|
+
photo: user.photo || null,
|
|
44
|
+
qr: user.qrCode || null,
|
|
45
|
+
subscription: subscription || null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Contrôleur d'admin utilisateurs (state filtres/pagination) au-dessus du service @mostajs/users.
|
|
51
|
+
* @param {object} opts.service l'API renvoyée par createUsers() (PEER @mostajs/users — REQUIS)
|
|
52
|
+
*/
|
|
53
|
+
export function createUsersUiController({ service, t = (k) => k } = {}) {
|
|
54
|
+
if (!service?.list) throw new Error('users-ui: service @mostajs/users requis (peer dependency)');
|
|
55
|
+
let q = { search: '', role: '', status: '', page: 1, pageSize: 20 };
|
|
56
|
+
return {
|
|
57
|
+
get query() { return { ...q }; },
|
|
58
|
+
setFilters(patch) { q = { ...q, ...patch, page: patch.page ?? 1 }; return this; },
|
|
59
|
+
async refresh() { return buildUsersListViewModel(await service.list(q), { t }); },
|
|
60
|
+
async submit(form, { isEditing = false, id } = {}) {
|
|
61
|
+
const v = validateUserForm(form, { isEditing });
|
|
62
|
+
if (!v.valid) return { ok: false, errors: v.errors };
|
|
63
|
+
const { password, confirmPassword, ...profile } = form; // password → @mostajs/auth (hors users)
|
|
64
|
+
const user = isEditing ? await service.update(id, profile) : await service.create(profile);
|
|
65
|
+
return { ok: true, user, _passwordDelegatedToAuth: password != null };
|
|
66
|
+
},
|
|
67
|
+
toggleStatus(id, status) { return status === 'locked' ? service.unlock(id) : service.lock(id); },
|
|
68
|
+
remove(id) { return service.repositories.users.remove(id); },
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// @mostajs/users-ui — point d'entrée (logique headless ; composants React via "@mostajs/users-ui/react")
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
export {
|
|
4
|
+
validateUserForm,
|
|
5
|
+
buildUsersListViewModel,
|
|
6
|
+
buildMemberCard,
|
|
7
|
+
createUsersUiController,
|
|
8
|
+
} from './headless.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @mostajs/users-ui/react — <MemberCardGenerator> — EXTRAIT de SecuAccessPro ClientCardGenerator.tsx.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// COMPOSE @mostajs/qrpanel (QR — useQrDataUrl) + @mostajs/subscriptions-plan (abonnement).
|
|
4
|
+
// Carte = photo + QR (qrpanel) + abonnement. Ne réembarque PAS `qrcode` (délégué à qrpanel).
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { useQrDataUrl } from '@mostajs/qrpanel/qr-image';
|
|
7
|
+
import { buildMemberCard } from '../headless.js';
|
|
8
|
+
|
|
9
|
+
/** @param {{ user:object, subscription?:object }} props */
|
|
10
|
+
export function MemberCardGenerator({ user, subscription, t = (k) => k }) {
|
|
11
|
+
const cardRef = React.useRef(null);
|
|
12
|
+
const model = buildMemberCard(user, { subscription });
|
|
13
|
+
const { src: qrSrc } = useQrDataUrl(model.qr || user.id); // ← QR via @mostajs/qrpanel
|
|
14
|
+
|
|
15
|
+
const download = async () => {
|
|
16
|
+
const { default: html2canvas } = await import('html2canvas');
|
|
17
|
+
const canvas = await html2canvas(cardRef.current);
|
|
18
|
+
const a = document.createElement('a');
|
|
19
|
+
a.href = canvas.toDataURL('image/png'); a.download = `card-${user.id}.png`; a.click();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return React.createElement('div', null,
|
|
23
|
+
React.createElement('div', { ref: cardRef, className: 'mosta-member-card' },
|
|
24
|
+
model.photo && React.createElement('img', { src: model.photo, alt: model.name, className: 'photo' }),
|
|
25
|
+
React.createElement('strong', null, model.name),
|
|
26
|
+
model.subscription && React.createElement('span', null, model.subscription.planName),
|
|
27
|
+
qrSrc && React.createElement('img', { src: qrSrc, alt: 'QR', className: 'qr' }),
|
|
28
|
+
),
|
|
29
|
+
React.createElement('button', { type: 'button', onClick: download }, t('download')),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
export default MemberCardGenerator;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @mostajs/users-ui/react — <PhotoCapture> / <FaceEnroll> (wrapper) — EXTRAIT de SecuAccessPro FaceDetector.tsx.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// COMPOSE @mostajs/face (détection/descripteur) — ne réembarque PAS face-api.
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
/** @param {{ face:object, onCapture:(d:{photo:string,faceDescriptor:number[]|null})=>void }} props (face = @mostajs/face) */
|
|
7
|
+
export function PhotoCapture({ face, onCapture, t = (k) => k }) {
|
|
8
|
+
const videoRef = React.useRef(null);
|
|
9
|
+
const [ready, setReady] = React.useState(false);
|
|
10
|
+
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
let stream;
|
|
13
|
+
(async () => {
|
|
14
|
+
try {
|
|
15
|
+
await face.loadModels?.();
|
|
16
|
+
stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
17
|
+
if (videoRef.current) { videoRef.current.srcObject = stream; setReady(true); }
|
|
18
|
+
} catch (e) { /* permission caméra refusée — dégradation gracieuse */ }
|
|
19
|
+
})();
|
|
20
|
+
return () => stream?.getTracks().forEach((tk) => tk.stop());
|
|
21
|
+
}, [face]);
|
|
22
|
+
|
|
23
|
+
const capture = async () => {
|
|
24
|
+
const video = videoRef.current;
|
|
25
|
+
const canvas = document.createElement('canvas');
|
|
26
|
+
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
|
|
27
|
+
canvas.getContext('2d').drawImage(video, 0, 0);
|
|
28
|
+
const photo = canvas.toDataURL('image/jpeg');
|
|
29
|
+
const faceDescriptor = (await face.extractDescriptor?.(video)) || null;
|
|
30
|
+
onCapture({ photo, faceDescriptor: faceDescriptor ? Array.from(faceDescriptor) : null });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return React.createElement('div', { className: 'mosta-photo-capture' },
|
|
34
|
+
React.createElement('video', { ref: videoRef, autoPlay: true, muted: true }),
|
|
35
|
+
React.createElement('button', { type: 'button', disabled: !ready, onClick: capture }, t('capture')),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
export const FaceEnroll = PhotoCapture;
|
|
39
|
+
export default PhotoCapture;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @mostajs/users-ui/react — <UserForm> / <UserEditForm> (wrapper headless)
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// EXTRAIT de MostaGare UserForm.tsx. password → @mostajs/auth ; rôles → @mostajs/rbac.
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { createUsersUiController, validateUserForm } from '../headless.js';
|
|
6
|
+
|
|
7
|
+
export function UserForm({ service, t = (k) => k, isEditing = false, userId, initialData = {}, roles = [], onSaved }) {
|
|
8
|
+
const controller = React.useMemo(() => createUsersUiController({ service, t }), [service]);
|
|
9
|
+
const [form, setForm] = React.useState({ firstName: '', lastName: '', email: '', role: '', ...initialData });
|
|
10
|
+
const [errors, setErrors] = React.useState({});
|
|
11
|
+
|
|
12
|
+
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
|
|
13
|
+
const submit = async (e) => {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
const v = validateUserForm(form, { isEditing });
|
|
16
|
+
if (!v.valid) return setErrors(v.errors);
|
|
17
|
+
const res = await controller.submit(form, { isEditing, id: userId });
|
|
18
|
+
if (!res.ok) return setErrors(res.errors);
|
|
19
|
+
onSaved?.(res.user);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return React.createElement('form', { onSubmit: submit, className: 'mosta-user-form' },
|
|
23
|
+
React.createElement('input', { value: form.firstName, onChange: set('firstName'), placeholder: t('firstName') }),
|
|
24
|
+
React.createElement('input', { value: form.lastName, onChange: set('lastName'), placeholder: t('lastName') }),
|
|
25
|
+
React.createElement('input', { value: form.email, onChange: set('email'), placeholder: t('email') }),
|
|
26
|
+
errors.email && React.createElement('span', { className: 'err' }, errors.email),
|
|
27
|
+
// rôle (← @mostajs/rbac)
|
|
28
|
+
React.createElement('select', { value: form.role, onChange: set('role') },
|
|
29
|
+
React.createElement('option', { value: '' }, t('role')),
|
|
30
|
+
roles.map((r) => React.createElement('option', { key: r.id, value: r.id }, r.name))),
|
|
31
|
+
// password (← @mostajs/auth) seulement à la création
|
|
32
|
+
!isEditing && React.createElement('input', { type: 'password', onChange: set('password'), placeholder: t('password') }),
|
|
33
|
+
React.createElement('button', { type: 'submit' }, t('save')),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
export default UserForm;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @mostajs/users-ui/react — <UsersList> (thin wrapper sur la logique headless)
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
// Compose @mostajs/users (service), @mostajs/ui (primitives), @mostajs/i18n. EXTRAIT de MostaGare UsersList.tsx.
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { createUsersUiController } from '../headless.js';
|
|
6
|
+
|
|
7
|
+
/** @param {{ service:object, t?:Function, onEdit?:Function }} props (service = @mostajs/users, PEER requis) */
|
|
8
|
+
export function UsersList({ service, t = (k) => k, onEdit }) {
|
|
9
|
+
const controller = React.useMemo(() => createUsersUiController({ service, t }), [service]);
|
|
10
|
+
const [vm, setVm] = React.useState(null);
|
|
11
|
+
const [search, setSearch] = React.useState('');
|
|
12
|
+
|
|
13
|
+
const refresh = React.useCallback(async () => {
|
|
14
|
+
controller.setFilters({ search });
|
|
15
|
+
setVm(await controller.refresh());
|
|
16
|
+
}, [controller, search]);
|
|
17
|
+
|
|
18
|
+
React.useEffect(() => { refresh(); }, [refresh]);
|
|
19
|
+
if (!vm) return React.createElement('div', null, t('loading'));
|
|
20
|
+
|
|
21
|
+
return React.createElement('div', { className: 'mosta-users-list' },
|
|
22
|
+
React.createElement('input', {
|
|
23
|
+
value: search, placeholder: t('search'),
|
|
24
|
+
onChange: (e) => setSearch(e.target.value),
|
|
25
|
+
}),
|
|
26
|
+
React.createElement('div', { className: 'stats' },
|
|
27
|
+
`${t('status.active')}: ${vm.stats.active} · ${t('status.locked')}: ${vm.stats.locked} · ${t('status.disabled')}: ${vm.stats.disabled}`),
|
|
28
|
+
React.createElement('table', null,
|
|
29
|
+
React.createElement('tbody', null, vm.rows.map((r) =>
|
|
30
|
+
React.createElement('tr', { key: r.id },
|
|
31
|
+
React.createElement('td', null, r.displayName),
|
|
32
|
+
React.createElement('td', null, r.email),
|
|
33
|
+
React.createElement('td', null, r.statusLabel),
|
|
34
|
+
React.createElement('td', null,
|
|
35
|
+
React.createElement('button', { onClick: () => onEdit?.(r.id) }, t('edit')),
|
|
36
|
+
React.createElement('button', { onClick: () => controller.toggleStatus(r.id, r.status).then(refresh) },
|
|
37
|
+
r.status === 'locked' ? t('unlock') : t('lock')),
|
|
38
|
+
),
|
|
39
|
+
)))),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
export default UsersList;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @mostajs/users-ui/react — composants React (peer: react, @mostajs/users)
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
export { UsersList } from './UsersList.jsx';
|
|
4
|
+
export { UserForm } from './UserForm.jsx';
|
|
5
|
+
export { PhotoCapture, FaceEnroll } from './PhotoCapture.jsx';
|
|
6
|
+
export { MemberCardGenerator } from './MemberCardGenerator.jsx';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @mostajs/users-ui — tests unitaires (headless) (DEVRULES §5 : test-scripts/, rejouables, committés).
|
|
2
|
+
// node test-scripts/unit/headless.test.mjs
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { createUsers, createMemoryRepositories } from '../../../mosta-users/src/index.js';
|
|
5
|
+
import { validateUserForm, buildUsersListViewModel, buildMemberCard, createUsersUiController } from '../../src/index.js';
|
|
6
|
+
|
|
7
|
+
let pass = 0; const test = async (n, fn) => { await fn(); pass++; console.log(' ✓', n); };
|
|
8
|
+
const svc = () => createUsers({ repositories: createMemoryRepositories(), qr: { generate: (id) => `QR:${id}` } });
|
|
9
|
+
|
|
10
|
+
await test('validateUserForm: email invalide / nom requis', async () => {
|
|
11
|
+
assert.equal(validateUserForm({ email: 'bad' }).valid, false);
|
|
12
|
+
assert.equal(validateUserForm({}).valid, false);
|
|
13
|
+
assert.equal(validateUserForm({ firstName: 'A', email: 'a@b.dz' }).valid, true);
|
|
14
|
+
});
|
|
15
|
+
await test('validateUserForm: confirmation mot de passe (création)', async () => {
|
|
16
|
+
assert.equal(validateUserForm({ firstName: 'A', password: 'x', confirmPassword: 'y' }).valid, false);
|
|
17
|
+
assert.equal(validateUserForm({ firstName: 'A', password: 'x', confirmPassword: 'x' }).valid, true);
|
|
18
|
+
});
|
|
19
|
+
await test('controller: dépendance stricte à @mostajs/users', async () => {
|
|
20
|
+
assert.throws(() => createUsersUiController({}), /service @mostajs\/users requis/);
|
|
21
|
+
});
|
|
22
|
+
await test('controller.submit: crée via users, password délégué à auth', async () => {
|
|
23
|
+
const ui = createUsersUiController({ service: svc() });
|
|
24
|
+
const r = await ui.submit({ firstName: 'A', email: 'a@b.dz', password: 'p', confirmPassword: 'p' });
|
|
25
|
+
assert.ok(r.ok && r.user.id);
|
|
26
|
+
assert.equal(r._passwordDelegatedToAuth, true);
|
|
27
|
+
assert.ok(!('password' in r.user));
|
|
28
|
+
});
|
|
29
|
+
await test('controller.refresh: view-model + stats', async () => {
|
|
30
|
+
const ui = createUsersUiController({ service: svc() });
|
|
31
|
+
await ui.submit({ firstName: 'Amina' }); await ui.submit({ firstName: 'Karim' });
|
|
32
|
+
const vm = await ui.refresh();
|
|
33
|
+
assert.equal(vm.total, 2); assert.equal(vm.stats.active, 2);
|
|
34
|
+
assert.ok(vm.rows[0].displayName);
|
|
35
|
+
});
|
|
36
|
+
await test('controller: filtre recherche', async () => {
|
|
37
|
+
const ui = createUsersUiController({ service: svc() });
|
|
38
|
+
await ui.submit({ firstName: 'Amina' }); await ui.submit({ firstName: 'Karim' });
|
|
39
|
+
ui.setFilters({ search: 'kar' });
|
|
40
|
+
const vm = await ui.refresh(); assert.equal(vm.rows.length, 1);
|
|
41
|
+
});
|
|
42
|
+
await test('buildUsersListViewModel: labels statut + flags photo/face', async () => {
|
|
43
|
+
const vm = buildUsersListViewModel({ items: [{ id: '1', firstName: 'A', status: 'locked', faceDescriptor: [1] }], total: 1, stats: {} },
|
|
44
|
+
{ t: (k) => k });
|
|
45
|
+
assert.equal(vm.rows[0].status, 'locked'); assert.equal(vm.rows[0].hasFace, true);
|
|
46
|
+
});
|
|
47
|
+
await test('buildMemberCard: photo + QR', async () => {
|
|
48
|
+
const card = buildMemberCard({ firstName: 'A', lastName: 'B', qrCode: 'QR:1', photo: 'data:...' });
|
|
49
|
+
assert.equal(card.name, 'A B'); assert.equal(card.qr, 'QR:1'); assert.ok(card.photo);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(`\n✅ @mostajs/users-ui — ${pass} tests OK`);
|