Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae2beeac8 | ||
|
|
71b1897678 | ||
|
|
f745079f0a | ||
|
|
3218daa12e | ||
|
|
1d6261b312 | ||
|
|
5b83102a59 | ||
|
|
9557cf9947 | ||
|
|
939e7777ac | ||
|
|
b474842e19 | ||
|
|
a312d9c0fa | ||
|
|
b5b32062b3 | ||
|
|
eb5e4aa915 | ||
|
|
d6d8b299dd | ||
|
|
ea0e97d930 | ||
|
|
84e46162fd | ||
|
|
14580c34e0 | ||
|
|
ae610733cc | ||
|
|
846afed86c | ||
|
|
99a6c17c23 | ||
|
|
04f49cb62f | ||
|
|
3c7f4f6e16 | ||
|
|
dcd407a3da | ||
|
|
fde63f8e72 | ||
|
|
1f8f1b9507 |
+53
-70
@@ -1,94 +1,77 @@
|
|||||||
# 📚 Index de la Documentation - PtitsPas App
|
# Index de la documentation — P'titsPas
|
||||||
|
|
||||||
Bienvenue dans la documentation complète de l'application PtitsPas.
|
Index de navigation du dépôt. Dernière révision : **septembre 2026** (clôture doc 0.1.0).
|
||||||
|
|
||||||
Ce fichier sert d'index pour naviguer dans toute la documentation du projet.
|
## Produit & versions
|
||||||
|
|
||||||
## 📖 Table des matières
|
| Doc | Contenu |
|
||||||
|
|-----|---------|
|
||||||
|
| [01 — Cahier des charges](./01_CAHIER-DES-CHARGES.md) | CDC actuel (V1.3) — amendement via **#117** |
|
||||||
|
| [EVOLUTIONS_CDC.md](./EVOLUTIONS_CDC.md) | Écarts CDC → app (intrant amendement) |
|
||||||
|
| [05 — Versions & milestones](./05_VERSIONS-ET-MILESTONES.md) | Semver Gitea + bilans |
|
||||||
|
| [29 — Bilan version 0.1.0](./29_BILAN-VERSION-0.1.0.md) | Tickets livrés 0.1.0 + thèmes |
|
||||||
|
| [04 — Roadmap générale](./04_ROADMAP-GENERALE.md) | Vision phases long terme |
|
||||||
|
| [28 — Évolution famille / responsables](./28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md) | Modèle dossier / foyer |
|
||||||
|
|
||||||
### 📋 Cahier des Charges
|
## Architecture & infra
|
||||||
- [**01 - Cahier des Charges**](./01_CAHIER-DES-CHARGES.md) - Cahier des charges complet du projet P'titsPas (V1.3 - 24/11/2025)
|
|
||||||
|
|
||||||
### Architecture & Infrastructure
|
| Doc | Contenu |
|
||||||
- [**02 - Architecture**](./02_ARCHITECTURE.md) - Vue d'ensemble de l'architecture mono-repo et multi-conteneurs
|
|-----|---------|
|
||||||
- [**03 - Déploiement**](./03_DEPLOYMENT.md) - Guide complet de déploiement et configuration CI/CD
|
| [02 — Architecture](./02_ARCHITECTURE.md) | Mono-repo, conteneurs |
|
||||||
|
| [03 — Déploiement](./03_DEPLOYMENT.md) | Deploy / CI-CD |
|
||||||
|
| [10 — Database](./10_DATABASE.md) | Schéma BDD |
|
||||||
|
| [11 — API](./11_API.md) | Endpoints REST |
|
||||||
|
| [21 — Configuration système](./21_CONFIGURATION-SYSTEME.md) | Config on-premise |
|
||||||
|
| [99 — Règles de codage](./99_REGLES-CODAGE.md) | Conventions |
|
||||||
|
|
||||||
### Planification
|
## Workflows & métier
|
||||||
- [**04 - Roadmap Générale**](./04_ROADMAP-GENERALE.md) - Roadmap complète du projet (Phases 1 à 5+)
|
|
||||||
|
|
||||||
### Développement
|
| Doc | Contenu |
|
||||||
- [**10 - Database Schema**](./10_DATABASE.md) - Schéma de la base de données et modèles
|
|-----|---------|
|
||||||
- [**11 - API Documentation**](./11_API.md) - Documentation complète des endpoints REST
|
| [20 — Workflow création de compte](./20_WORKFLOW-CREATION-COMPTE.md) | Inscription / validation |
|
||||||
- [**14 - Note backend config setup**](./14_NOTE-BACKEND-CONFIG-SETUP.md) - Setup configuration
|
| [juridique/](./juridique/README.md) | CGU / CGC / privacy + [22 technique](./juridique/22_DOCUMENTS-LEGAUX.md) |
|
||||||
- [**92 - Note backend gestionnaires**](./92_NOTE-BACKEND-GESTIONNAIRES.md) - Gestionnaires
|
| [CHARTE_GRAPHIQUE.md](./CHARTE_GRAPHIQUE.md) | Charte UI |
|
||||||
- [**99 - Règles de codage**](./99_REGLES-CODAGE.md) - Conventions de code
|
|
||||||
|
|
||||||
### Workflows Fonctionnels
|
## Projet & outillage
|
||||||
- [**20 - Workflow Création de Compte**](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow complet de création et validation des comptes utilisateurs
|
|
||||||
- [**21 - Configuration Système**](./21_CONFIGURATION-SYSTEME.md) - Configuration on-premise dynamique
|
|
||||||
- [**22 - Documents Légaux**](./juridique/22_DOCUMENTS-LEGAUX.md) - Gestion CGU/Privacy avec versioning
|
|
||||||
|
|
||||||
### Juridique (sources & technique)
|
| Doc | Contenu |
|
||||||
- [**Dossier juridique**](./juridique/README.md) - Index : CGU/CGC en Markdown,
|
|-----|---------|
|
||||||
export PDF, lien vers la doc technique n°22
|
| [23 — Suivi tickets](./23_SUIVI-TICKETS.md) | Pointeur Gitea (plus de liste figée) |
|
||||||
|
| [24 — Décisions projet](./24_DECISIONS-PROJET.md) | ADR / décisions |
|
||||||
|
| [26 — API Gitea](./26_GITEA-API.md) | Issues, PR, milestones |
|
||||||
|
| [27 — Briefing frontend](./27_BRIEFING-FRONTEND.md) | Accès Git, priorités |
|
||||||
|
|
||||||
### Projet & suivi (Gitea / tickets)
|
## Audit
|
||||||
- [**23 - Liste des Tickets**](./23_LISTE-TICKETS.md) - 61 tickets Phase 1 détaillés
|
|
||||||
- [**24 - Décisions Projet**](./24_DECISIONS-PROJET.md) - Décisions architecturales et fonctionnelles
|
|
||||||
- [**25 - Backlog Phase 2**](./25_PHASE-2-BACKLOG.md) - Fonctionnalités techniques reportées
|
|
||||||
- [**28 - Évolution famille et responsables**](./28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md) - Modèle dossier/famille, recompositions, v1.0.0 vs post-1.0.0
|
|
||||||
- [**26 - API Gitea**](./26_GITEA-API.md) - Procédure d'utilisation de l'API Gitea (issues, PR, branches, labels)
|
|
||||||
- [**27 - Briefing frontend**](./27_BRIEFING-FRONTEND.md) - Accès Git, priorités, scripts Gitea (token)
|
|
||||||
|
|
||||||
### Archive & convention de nommage
|
| Doc | Contenu |
|
||||||
- [**Dossier archive**](./archive/README.md) - Fichiers **sans** `NN_` déplacés
|
|-----|---------|
|
||||||
(temporaires, obsolètes) ; règles de rangement et suppression
|
| [90 — Audit YNOV](./90_AUDIT.md) | Analyse code étudiant |
|
||||||
- Pointeur : [PROCEDURE-API-GITEA.md](./PROCEDURE-API-GITEA.md) → voir **26**
|
|
||||||
|
|
||||||
### Exceptions de nommage (racine `docs/`)
|
## Archive
|
||||||
Fichiers **sans préfixe numérique** encore à la racine par **héritage** ou
|
|
||||||
références outils (`.cursorrules`, etc.) — **à renommer** en `NN_` quand
|
|
||||||
possible :
|
|
||||||
- `CHARTE_GRAPHIQUE.md`
|
|
||||||
- [`EVOLUTIONS_CDC.md`](./EVOLUTIONS_CDC.md) — écarts CDC / app ; voir aussi [**28 - Évolution famille**](./28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md)
|
|
||||||
- `SuperNounou_Cahier_Des_Charges_Complet_V1.1.md`
|
|
||||||
- `SuperNounou_SSS-001.md`
|
|
||||||
|
|
||||||
### Administration (À créer)
|
| Emplacement | Usage |
|
||||||
- [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs
|
|-------------|--------|
|
||||||
- [**31 - Troubleshooting**](./31_TROUBLESHOOTING.md) - Résolution des problèmes courants
|
| [archive/](./archive/README.md) | Obsolete / temporaires |
|
||||||
|
| [archive/obsolete/](./archive/obsolete/) | CDC SuperNounou, ancienne liste tickets, backlog Phase 2 figé, notes ponctuelles |
|
||||||
|
|
||||||
### Frontend (À créer)
|
## Données de test
|
||||||
- [**40 - Frontend Flutter**](./40_FRONTEND.md) - Structure de l'application mobile/web
|
|
||||||
|
|
||||||
### Audit & Analyse
|
| Doc | Contenu |
|
||||||
- [**90 - Audit du projet YNOV**](./90_AUDIT.md) - Analyse complète du code étudiant et fonctionnalités
|
|-----|---------|
|
||||||
|
| [test-data/](./test-data/README.md) | Jeux utilisateurs test |
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Cloner le projet
|
git clone … ptitspas-app
|
||||||
git clone ssh://gitea-jmartin/jmartin/app.git ptitspas-app
|
|
||||||
|
|
||||||
# Lancer l'environnement de développement
|
|
||||||
cd ptitspas-app
|
cd ptitspas-app
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
# Front https://app.ptits-pas.fr — API /api — PgAdmin /pgadmin
|
||||||
# Accéder aux services
|
|
||||||
Frontend: https://app.ptits-pas.fr
|
|
||||||
API: https://app.ptits-pas.fr/api
|
|
||||||
PgAdmin: https://app.ptits-pas.fr/pgadmin
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔗 Liens utiles
|
## Liens
|
||||||
|
|
||||||
- **Gitea** : https://git.ptits-pas.fr
|
- Gitea : https://git.ptits-pas.fr/jmartin/petitspas
|
||||||
- **Production** : https://app.ptits-pas.fr
|
- Prod : https://app.ptits-pas.fr
|
||||||
- **Mail** : https://mail.ptits-pas.fr
|
|
||||||
|
|
||||||
## 📝 Maintenance
|
|
||||||
|
|
||||||
Cette documentation est maintenue par Julien Martin (julien.martin@ptits-pas.fr).
|
|
||||||
|
|
||||||
Dernière mise à jour : Juin 2026
|
|
||||||
|
|
||||||
|
Mainteneur : Julien Martin (julien.martin@ptits-pas.fr).
|
||||||
|
|||||||
+15
-15
@@ -44,22 +44,21 @@ Les **Phases 2, 3, 4+** sont des **ébauches indicatives** qui seront affinées
|
|||||||
- ✅ Logging & Monitoring
|
- ✅ Logging & Monitoring
|
||||||
- ✅ Tests & Documentation
|
- ✅ Tests & Documentation
|
||||||
|
|
||||||
### Versions incrémentales
|
### Versions incrémentales (semver / Gitea)
|
||||||
|
|
||||||
| Version | Objectif | Tickets | Estimation |
|
La table historique « ~21 tickets / 0.1.0 » est **obsolète**.
|
||||||
|---------|----------|---------|------------|
|
État réel des milestones, bilans et tag : **[05_VERSIONS-ET-MILESTONES.md](./05_VERSIONS-ET-MILESTONES.md)**.
|
||||||
| **0.1.0** | MVP Fonctionnel | ~21 | ~45h |
|
|
||||||
| **0.2.0** | Sécurité & RGPD | ~10 | ~35h |
|
|
||||||
| **0.3.0** | Interfaces Complètes | ~17 | ~52h |
|
|
||||||
| **0.4.0** | Tests & Documentation | ~6 | ~24h |
|
|
||||||
| **0.5.0** | Monitoring & Optimisations | ~7 | ~17h |
|
|
||||||
| **1.0.0** | 🎉 **Release Phase 1** | **61** | **~173h** |
|
|
||||||
|
|
||||||
### Livrable
|
| Version | Statut (sept. 2026) |
|
||||||
|
|---------|---------------------|
|
||||||
|
| **0.1.0** | **Terminée** — [bilan](./29_BILAN-VERSION-0.1.0.md) (48 tickets fermés) |
|
||||||
|
| **0.2.0+** | Ouvertes — voir Gitea + doc 05 |
|
||||||
|
|
||||||
Application installable avec création et validation de comptes utilisateurs.
|
### Livrable Phase 1 (visée)
|
||||||
|
|
||||||
**Référence** : [23_LISTE-TICKETS.md](./23_LISTE-TICKETS.md)
|
Application installable avec création et validation de comptes utilisateurs, puis enrichissements dashboard / dossiers (0.1.0 livré).
|
||||||
|
|
||||||
|
**Tickets** : Gitea — pointeur [23_SUIVI-TICKETS.md](./23_SUIVI-TICKETS.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -216,7 +215,7 @@ Suivi quotidien des enfants + Fonctionnalités complémentaires.
|
|||||||
|
|
||||||
Application mature, optimisée et riche en fonctionnalités.
|
Application mature, optimisée et riche en fonctionnalités.
|
||||||
|
|
||||||
**Référence** : [25_PHASE-2-BACKLOG.md](./25_PHASE-2-BACKLOG.md) (anciennes fonctionnalités techniques)
|
**Référence** : [archive/obsolete/25_PHASE-2-BACKLOG.md](./archive/obsolete/25_PHASE-2-BACKLOG.md) (figé) + [05_VERSIONS-ET-MILESTONES.md](./05_VERSIONS-ET-MILESTONES.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -317,9 +316,10 @@ Exemples :
|
|||||||
- [00_INDEX.md](./00_INDEX.md) - Index général de la documentation
|
- [00_INDEX.md](./00_INDEX.md) - Index général de la documentation
|
||||||
- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md) - Cahier des charges v1.3
|
- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md) - Cahier des charges v1.3
|
||||||
- [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow création de comptes
|
- [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow création de comptes
|
||||||
- [23_LISTE-TICKETS.md](./23_LISTE-TICKETS.md) - Liste des 61 tickets Phase 1
|
- [23_SUIVI-TICKETS.md](./23_SUIVI-TICKETS.md) / [29_BILAN-VERSION-0.1.0.md](./29_BILAN-VERSION-0.1.0.md) — suivi Gitea + bilan 0.1.0
|
||||||
- [24_DECISIONS-PROJET.md](./24_DECISIONS-PROJET.md) - Décisions architecturales
|
- [24_DECISIONS-PROJET.md](./24_DECISIONS-PROJET.md) - Décisions architecturales
|
||||||
- [25_PHASE-2-BACKLOG.md](./25_PHASE-2-BACKLOG.md) - Anciennes fonctionnalités techniques
|
- [05_VERSIONS-ET-MILESTONES.md](./05_VERSIONS-ET-MILESTONES.md) — milestones Gitea
|
||||||
|
- [archive/obsolete/25_PHASE-2-BACKLOG.md](./archive/obsolete/25_PHASE-2-BACKLOG.md) — backlog Phase 2 figé
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Versions & milestones — P'titsPas
|
||||||
|
|
||||||
|
**Source de vérité tickets** : Gitea [`jmartin/petitspas`](https://git.ptits-pas.fr/jmartin/petitspas)
|
||||||
|
**Bilans de version** : documents `29_BILAN-…` (et suivants)
|
||||||
|
|
||||||
|
Ce fichier remplace, pour le **semver / milestones**, les anciennes tables figées de la roadmap Phase 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## État des milestones
|
||||||
|
|
||||||
|
| Milestone | Rôle | Statut |
|
||||||
|
|-----------|------|--------|
|
||||||
|
| **0.1.0** | MVP opérable (auth, inscription, dashboard dossiers/fiches, suppressions, cleanups) | **Terminée** — [bilan](./29_BILAN-VERSION-0.1.0.md) |
|
||||||
|
| **0.2.0** | Suite produit (ex. recherche / échanges — sans contrat) | Ouverte |
|
||||||
|
| **0.3.0** | Contrat + planning | Ouverte |
|
||||||
|
| **0.4.0** | Carnet de liaison | Ouverte |
|
||||||
|
| **0.9.0** | Hors périmètre cleanup 0.1.0 (doublons, upload, tech auth/photos, UX erreurs…) | Ouverte |
|
||||||
|
| **1.0.0** | Release majeure Phase 1 (critères PO) | Réserve |
|
||||||
|
| **Backlog transverse** | Doc étendue, CI/tests, RGPD avancé, monitoring — hors semver dédié | Ouverte |
|
||||||
|
|
||||||
|
Liens Gitea : [milestones](https://git.ptits-pas.fr/jmartin/petitspas/milestones).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bilans
|
||||||
|
|
||||||
|
| Version | Document |
|
||||||
|
|---------|----------|
|
||||||
|
| 0.1.0 | [29_BILAN-VERSION-0.1.0.md](./29_BILAN-VERSION-0.1.0.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relation avec la roadmap phases
|
||||||
|
|
||||||
|
La vision long terme (Phases 2–5 : mise en relation, contrats, carnet…) reste dans [04_ROADMAP-GENERALE.md](./04_ROADMAP-GENERALE.md).
|
||||||
|
Les **jalons livrables** se gèrent ici + dans Gitea.
|
||||||
|
|
||||||
|
Ancien backlog « Phase 2 » technique ([archive](./archive/obsolete/25_PHASE-2-BACKLOG.md)) : à croiser avec les milestones ci-dessus ; ne plus maintenir en double.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suivi des tickets
|
||||||
|
|
||||||
|
- **Création / état** : Gitea uniquement.
|
||||||
|
- **Mémoire d’une version livrée** : bilan `29_…` (pas de re-copie exhaustive dans un fichier tickets).
|
||||||
|
- Ancienne liste figée Phase 1 : [archive/obsolete/23_LISTE-TICKETS.md](./archive/obsolete/23_LISTE-TICKETS.md).
|
||||||
|
- Pointeur court : [23_SUIVI-TICKETS.md](./23_SUIVI-TICKETS.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tag Git
|
||||||
|
|
||||||
|
| Tag | Condition |
|
||||||
|
|-----|-----------|
|
||||||
|
| `v0.1.0` | Milestone 0.1.0 fermée + bilan mergé sur `master` |
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Fichier déplacé
|
|
||||||
|
|
||||||
La documentation **Documents légaux** a été déplacée vers :
|
|
||||||
|
|
||||||
**[juridique/22_DOCUMENTS-LEGAUX.md](./juridique/22_DOCUMENTS-LEGAUX.md)**
|
|
||||||
|
|
||||||
Voir aussi le dossier **[juridique/](./juridique/)** pour les sources **CGU**
|
|
||||||
et **CGC** en Markdown.
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Suivi des tickets — P'titsPas
|
||||||
|
|
||||||
|
**Source de vérité** : [Gitea — issues](https://git.ptits-pas.fr/jmartin/petitspas/issues)
|
||||||
|
**Milestones / versions** : [05_VERSIONS-ET-MILESTONES.md](./05_VERSIONS-ET-MILESTONES.md)
|
||||||
|
**API Gitea** : [26_GITEA-API.md](./26_GITEA-API.md)
|
||||||
|
|
||||||
|
Ne plus maintenir de catalogue exhaustif des tickets dans le dépôt : l’état (ouvert / fermé / milestone) change dans Gitea.
|
||||||
|
|
||||||
|
Pour une **version livrée**, lire le bilan correspondant (ex. [29_BILAN-VERSION-0.1.0.md](./29_BILAN-VERSION-0.1.0.md)).
|
||||||
|
|
||||||
|
Archive historique (liste Phase 1 figée, avril 2026) :
|
||||||
|
[archive/obsolete/23_LISTE-TICKETS.md](./archive/obsolete/23_LISTE-TICKETS.md).
|
||||||
+13
-14
@@ -423,31 +423,30 @@ ptitspas-app/
|
|||||||
- Maintenance (tout au même endroit)
|
- Maintenance (tout au même endroit)
|
||||||
- Versioning (Git)
|
- Versioning (Git)
|
||||||
|
|
||||||
**Structure** :
|
**Structure** (sept. 2026) :
|
||||||
```
|
```
|
||||||
docs/
|
docs/
|
||||||
├── 00_INDEX.md
|
├── 00_INDEX.md
|
||||||
├── 01_CAHIER-DES-CHARGES.md
|
├── 01_CAHIER-DES-CHARGES.md
|
||||||
├── 02_ARCHITECTURE.md
|
├── 02_ARCHITECTURE.md
|
||||||
├── 03_DEPLOYMENT.md
|
├── 03_DEPLOYMENT.md
|
||||||
|
├── 04_ROADMAP-GENERALE.md
|
||||||
|
├── 05_VERSIONS-ET-MILESTONES.md
|
||||||
├── 10_DATABASE.md
|
├── 10_DATABASE.md
|
||||||
├── 11_API.md
|
├── 11_API.md
|
||||||
├── 20_WORKFLOW-CREATION-COMPTE.md
|
├── 20_WORKFLOW-CREATION-COMPTE.md
|
||||||
├── 21_CONFIGURATION-SYSTEME.md
|
├── 21_CONFIGURATION-SYSTEME.md
|
||||||
├── 22_DOCUMENTS-LEGAUX.md # pointeur → juridique/
|
├── 23_SUIVI-TICKETS.md
|
||||||
├── 27_BRIEFING-FRONTEND.md
|
|
||||||
├── PROCEDURE-API-GITEA.md # pointeur → 26_GITEA-API.md
|
|
||||||
├── juridique/
|
|
||||||
│ ├── README.md
|
|
||||||
│ ├── cgu.md
|
|
||||||
│ ├── cgc.md
|
|
||||||
│ └── 22_DOCUMENTS-LEGAUX.md
|
|
||||||
├── archive/
|
|
||||||
│ ├── README.md
|
|
||||||
│ ├── temporaires/
|
|
||||||
│ └── obsolete/
|
|
||||||
├── 23_LISTE-TICKETS.md
|
|
||||||
├── 24_DECISIONS-PROJET.md (ce document)
|
├── 24_DECISIONS-PROJET.md (ce document)
|
||||||
|
├── 26_GITEA-API.md
|
||||||
|
├── 27_BRIEFING-FRONTEND.md
|
||||||
|
├── 28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md
|
||||||
|
├── 29_BILAN-VERSION-0.1.0.md
|
||||||
|
├── 99_REGLES-CODAGE.md
|
||||||
|
├── EVOLUTIONS_CDC.md
|
||||||
|
├── CHARTE_GRAPHIQUE.md
|
||||||
|
├── juridique/ # CGU + 22_DOCUMENTS-LEGAUX.md
|
||||||
|
├── archive/ # obsolete / temporaires
|
||||||
├── 90_AUDIT.md
|
├── 90_AUDIT.md
|
||||||
└── test-data/
|
└── test-data/
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
**Version** : 1.1
|
**Version** : 1.1
|
||||||
**Date** : 16 juin 2026
|
**Date** : 16 juin 2026
|
||||||
**Statut** : Réflexions produit / architecture — complément au [CDC](./01_CAHIER-DES-CHARGES.md)
|
**Statut** : Réflexions produit / architecture — complément au [CDC](./01_CAHIER-DES-CHARGES.md)
|
||||||
**Documents liés** : [EVOLUTIONS_CDC.md](./EVOLUTIONS_CDC.md), [24_DECISIONS-PROJET.md](./24_DECISIONS-PROJET.md), [23_LISTE-TICKETS.md](./23_LISTE-TICKETS.md)
|
**Documents liés** : [EVOLUTIONS_CDC.md](./EVOLUTIONS_CDC.md), [24_DECISIONS-PROJET.md](./24_DECISIONS-PROJET.md), [23_SUIVI-TICKETS.md](./23_SUIVI-TICKETS.md), [29_BILAN-VERSION-0.1.0.md](./29_BILAN-VERSION-0.1.0.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# Bilan — Version 0.1.0
|
||||||
|
|
||||||
|
**Statut** : terminée
|
||||||
|
**Milestone Gitea** : [0.1.0](https://git.ptits-pas.fr/jmartin/petitspas/milestone/10)
|
||||||
|
**Dépôt** : `jmartin/petitspas`
|
||||||
|
**Tag prévu** : `v0.1.0` (sur `master` après merge de cette doc)
|
||||||
|
|
||||||
|
Ce document est la **mémoire produit** de la version 0.1.0 : ce qui a été livré, ticket par ticket, et ce qui a été reporté.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Périmètre produit livré
|
||||||
|
|
||||||
|
La 0.1.0 couvre le **MVP opérable** pour une collectivité :
|
||||||
|
|
||||||
|
- Authentification, création / oubli de mot de passe, e-mails associés
|
||||||
|
- Inscription parent & AM, validation / refus gestionnaire, reprise après refus
|
||||||
|
- Dashboard staff (admin + gestionnaire) : listes, fiches, rattachements
|
||||||
|
- Onglet **Dossiers** + wizards création / édition (famille & AM)
|
||||||
|
- Suppressions métier (droits, confirms, cascades API)
|
||||||
|
- Cleanups structurels (préfixe `Admin*`, panels dashboard, modale staff, retrait `est_multiple`)
|
||||||
|
|
||||||
|
**Hors 0.1.0** (reporté) : doublons avancés, famille N responsables, statut enfant gardé/sans garde, combobox RPE AM, chantier CDC (#117), tickets tech/observabilité — voir §4 et [05_VERSIONS-ET-MILESTONES.md](./05_VERSIONS-ET-MILESTONES.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Thèmes livrés
|
||||||
|
|
||||||
|
### 2.1 Auth, mot de passe, e-mails
|
||||||
|
|
||||||
|
| # | Titre | Livré |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 24 | API Création mot de passe | Endpoints token → création MDP post-validation |
|
||||||
|
| 28 | Templates Email — Validation | Mails validation avec lien MDP |
|
||||||
|
| 30 | Connexion — Vérification statut | Blocage comptes pending / suspendus à la connexion |
|
||||||
|
| 43 | Écran Création Mot de Passe | UI lien e-mail création MDP |
|
||||||
|
| 47 | Écran Changement MDP Obligatoire | Première connexion staff |
|
||||||
|
| 50 | Affichage dynamique CGU | CGU/Privacy versionnées à l’inscription |
|
||||||
|
| 118 | Page création mot de passe (Front + API) | Alignement front/API du flux lien e-mail |
|
||||||
|
| 123 | Durcissement token création MDP | TTL, usage unique, contrôles API |
|
||||||
|
| 127 | Mot de passe oublié | Demande → e-mail → réinitialisation (flux distinct de #24/#43) |
|
||||||
|
|
||||||
|
### 2.2 Inscription, numéros de dossier, reprise
|
||||||
|
|
||||||
|
| # | Titre | Livré |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 104 | Numéro de dossier — frontend | Affichage listes / mails / modales ; format AAAA-… |
|
||||||
|
| 112 | Reprise après refus — frontend | Lien e-mail `/reprise` + reprise par n° dossier |
|
||||||
|
| 120 | Inscription AM — photo & UX | Chaîne photo / API / UX alignée parents |
|
||||||
|
| 144 | Consentement photo enfant | Persistance du consentement à l’inscription |
|
||||||
|
|
||||||
|
### 2.3 Dashboard — fiches, listes, rattachements
|
||||||
|
|
||||||
|
| # | Titre | Livré |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 115 | Rattachement enfants — backend | Attach/detach parent↔enfant et AM↔enfant |
|
||||||
|
| 116 | Rattachement enfants — frontend | UI fiches parent / AM |
|
||||||
|
| 130 | UserService — APIs métier | Branchement parents / AM / enfants côté front |
|
||||||
|
| 131 | Édition fiche parent + AM | Modales édition dashboard |
|
||||||
|
| 132 | Création enfant (onglet Enfants) | Création + rattachement foyer |
|
||||||
|
| 136 | API enfants — droits & liste | Droits gestionnaire + enrichissement liste |
|
||||||
|
| 137 | Onglet Enfants — liste globale | Panneau liste dashboard |
|
||||||
|
| 138 | Fiche enfant + liste dans parent | Fiche enfant ; enfants dans fiche parent |
|
||||||
|
| 140 | Epic fiche parent / affiliation | Livraison regroupée dashboard admin/gestionnaire |
|
||||||
|
| 142 | Clic carte → modale | Ouverture fiche depuis les listes |
|
||||||
|
| 145 | Lien co-parent cliquable | Navigation fiche parent → co-parent |
|
||||||
|
| 146 | Modale sélection enfant | UX rattacher enfant (AM + parent) |
|
||||||
|
| 147 | Modale sélection AM | UX rattacher AM depuis fiche enfant |
|
||||||
|
| 148 | Capacité max AM | Désactivation rattachement si capacité atteinte |
|
||||||
|
| 149 | Case libre AM → rattacher | Clic emplacement vide pour rattacher |
|
||||||
|
| 151 | GET /relais pour gestionnaire | Combo relais dans modale staff |
|
||||||
|
| 157 | Enfant sans responsable | Détachement dernier parent + alerte liste |
|
||||||
|
| 158 | Affiliation foyer (pivot + co-parent) | Attach/detach cohérents sur le foyer |
|
||||||
|
|
||||||
|
### 2.4 Dossiers staff (création, édition, liste)
|
||||||
|
|
||||||
|
| # | Titre | Livré |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 129 | Création dossier parent | Wizard + API staff famille |
|
||||||
|
| 135 | Édition dossier + 2ᵉ parent | Mode edit wizards + `POST …/co-parent` |
|
||||||
|
| 153 | Onglet Dossiers | Liste unifiée + à valider (sans création dans l’onglet) |
|
||||||
|
| 156 | Création dossier AM | Wizard + API staff AM |
|
||||||
|
|
||||||
|
### 2.5 Suppressions & droits staff
|
||||||
|
|
||||||
|
| # | Titre | Livré |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 133 | Suppression parent + AM (UI) | Première vague UI (complétée par #160) |
|
||||||
|
| 134 | Droits « Ajouter gestionnaire » | Visibilité / API selon rôle |
|
||||||
|
| 143 | Bug supprimer sa propre fiche | Masquage / interdiction auto-suppression |
|
||||||
|
| 154 | Epic suppressions | Cadrage règles métier suppressions |
|
||||||
|
| 159 | Suppressions métier — backend | Cascades, garde-fous, droits API |
|
||||||
|
| 160 | Suppressions dashboard — frontend | Poubelles + dialogues de confirmation |
|
||||||
|
| 161 | Admin création staff 403 | Admin peut créer gestionnaire et administrateur |
|
||||||
|
|
||||||
|
### 2.6 Cleanups structure & UX
|
||||||
|
|
||||||
|
| # | Titre | Livré |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 25 | API Liste comptes en attente | Historique ; couvert par flux dossiers / validation |
|
||||||
|
| 26 | API Validation / Refus | Historique ; couvert par flux dossiers / validation |
|
||||||
|
| 152 | Retrait `est_multiple` | Suppression full stack (BDD, API, front, docs) |
|
||||||
|
| 155 | Rename préfixe `Admin*` | Widgets partagés sans préfixe Admin (option C) |
|
||||||
|
| 162 | Panels → `widgets/dashboard/` | Suite #155 — panels staff sous dashboard |
|
||||||
|
| 164 | Modale staff uniformisée | `StaffUserFormModal` (shell 930, champs contrôlés) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Inventaire exhaustif (48 tickets fermés, milestone 0.1.0)
|
||||||
|
|
||||||
|
| # | Titre |
|
||||||
|
|---|-------|
|
||||||
|
| 24 | [Backend] API Création mot de passe |
|
||||||
|
| 25 | [Backend] API Liste comptes en attente |
|
||||||
|
| 26 | [Backend] API Validation/Refus comptes |
|
||||||
|
| 28 | [Backend] Templates Email - Validation |
|
||||||
|
| 30 | [Backend] Connexion - Vérification statut |
|
||||||
|
| 43 | [Frontend] Écran Création Mot de Passe |
|
||||||
|
| 47 | [Frontend] Écran Changement MDP Obligatoire |
|
||||||
|
| 50 | [Frontend] Affichage dynamique CGU lors inscription |
|
||||||
|
| 104 | Numéro de dossier – frontend |
|
||||||
|
| 112 | Reprise après refus – frontend |
|
||||||
|
| 115 | [Backend] Rattachement enfants — parent et AM |
|
||||||
|
| 116 | [Frontend] Rattachement enfants — parent et AM |
|
||||||
|
| 118 | Page création mot de passe (lien email) – Front + API |
|
||||||
|
| 120 | [Full-stack] Inscription AM — photo, API et UX alignés sur les parents |
|
||||||
|
| 123 | [Tech] Durcissement token création MDP |
|
||||||
|
| 127 | [Full-stack] Mot de passe oublié — flux complet |
|
||||||
|
| 129 | Création dossier parent (wizard + API staff) |
|
||||||
|
| 130 | [Frontend] UserService — brancher APIs parents, AM et enfants |
|
||||||
|
| 131 | [Frontend] Édition fiche parent + AM |
|
||||||
|
| 132 | Création enfant depuis l’onglet Enfants |
|
||||||
|
| 133 | [Frontend] Suppression compte parent + AM |
|
||||||
|
| 134 | Droits bouton « Ajouter gestionnaire » |
|
||||||
|
| 135 | Mode édition dossier (+ ajout 2ᵉ parent) |
|
||||||
|
| 136 | [Backend] API enfants — droits gestionnaire + enrichissement liste |
|
||||||
|
| 137 | [Frontend] Onglet Enfants — liste globale |
|
||||||
|
| 138 | [Frontend] Fiche enfant + liste enfants dans fiche parent |
|
||||||
|
| 140 | Dashboard admin — fiche parent, enfants et affiliation |
|
||||||
|
| 142 | Clic sur carte → ouvrir la modale |
|
||||||
|
| 143 | Bug — Gestionnaire Supprimer sur sa propre fiche |
|
||||||
|
| 144 | Bug — Consentement photo enfant non sauvegardé |
|
||||||
|
| 145 | Lien co-parent cliquable |
|
||||||
|
| 146 | UX — modale sélection d'enfant |
|
||||||
|
| 147 | UX — modale sélection d'AM |
|
||||||
|
| 148 | Bug — capacité max AM |
|
||||||
|
| 149 | Fiche AM — clic case libre pour rattacher |
|
||||||
|
| 151 | Bug — GET /relais gestionnaire |
|
||||||
|
| 152 | Cleanup — supprimer `est_multiple` |
|
||||||
|
| 153 | Onglet permanent « Dossiers » |
|
||||||
|
| 154 | Epic — suppressions utilisateurs / dossiers / enfants / AM |
|
||||||
|
| 155 | Cleanup — renommer préfixe Admin* |
|
||||||
|
| 156 | Création dossier AM (wizard + API staff) |
|
||||||
|
| 157 | Enfant sans responsable |
|
||||||
|
| 158 | Affiliation enfant au foyer (pivot + co-parent) |
|
||||||
|
| 159 | Backend suppressions métier (#154) |
|
||||||
|
| 160 | Frontend suppressions dashboard (#154) |
|
||||||
|
| 161 | Bug — Admin création gestionnaire / administrateur |
|
||||||
|
| 162 | Cleanup — panels vers `widgets/dashboard/` |
|
||||||
|
| 164 | Uniformisation modale staff + champs contrôlés |
|
||||||
|
|
||||||
|
Issues : https://git.ptits-pas.fr/jmartin/petitspas/issues?q=&type=all&state=closed&labels=&milestone=10&assignee=0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Reporté hors 0.1.0
|
||||||
|
|
||||||
|
| # | Titre | Destination typique |
|
||||||
|
|---|-------|---------------------|
|
||||||
|
| 113 / 114 | Doublons inscription / alerte gestionnaire | 0.9.0 |
|
||||||
|
| 117 | Évolution du cahier des charges | Doc (amendement CDC post-0.1.0) |
|
||||||
|
| 121–122, 124–125 | Tech auth / photos / DB | 0.9.0 |
|
||||||
|
| 126 | Upload documents légaux 500 | 0.9.0 |
|
||||||
|
| 128 | Audit / traçabilité modifications | 0.9.0 |
|
||||||
|
| 139 | Famille complexe N responsables | Post-0.1.0 / epic |
|
||||||
|
| 141 | Statut enfant gardé / sans garde | Post-0.1.0 |
|
||||||
|
| 150 | Combobox rattachement RPE (AM) | 0.2.0 |
|
||||||
|
|
||||||
|
Voir aussi [05_VERSIONS-ET-MILESTONES.md](./05_VERSIONS-ET-MILESTONES.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Suite documentaire
|
||||||
|
|
||||||
|
1. **Amendement CDC** — ticket **#117** : intégrer les écarts réellement livrés (dossiers staff, onglet Enfants, suppressions, retrait naissance multiple, etc.) à partir de ce bilan, [EVOLUTIONS_CDC.md](./EVOLUTIONS_CDC.md) et [28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md](./28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md).
|
||||||
|
2. **Tag** `v0.1.0` sur `master` lorsque milestone fermée + ce bilan mergé.
|
||||||
|
3. Enchaîner les milestones **0.2.0+** selon [05_VERSIONS-ET-MILESTONES.md](./05_VERSIONS-ET-MILESTONES.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Références code (points d’entrée)
|
||||||
|
|
||||||
|
- Modale staff : `frontend/lib/widgets/dashboard/staff_user_form_modal.dart`
|
||||||
|
- Fiches : `parent_edit_modal.dart`, `am_edit_modal.dart`, `child_detail_modal.dart`
|
||||||
|
- Wizards dossiers : `parent_dossier_wizard.dart`, `am_dossier_wizard.dart`
|
||||||
|
- Règles suppressions : tickets #154 / #159 / #160
|
||||||
|
- Cleanup `est_multiple` : #152
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
Ce document liste les modifications à apporter au cahier des charges original pour le rendre conforme à l'application développée.
|
Ce document liste les modifications à apporter au cahier des charges original pour le rendre conforme à l'application développée.
|
||||||
|
|
||||||
> **Document complémentaire (juin 2026)** — réflexions sur le **modèle famille / numéro de dossier**, familles recomposées, tuteurs et responsables légaux : voir **[28 - Évolution famille et responsables](./28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md)**.
|
> **Intrant pour #117** (amendement CDC post-0.1.0). Compléter avec le [bilan 0.1.0](./29_BILAN-VERSION-0.1.0.md) et **[28 - Évolution famille et responsables](./28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md)**.
|
||||||
|
|
||||||
|
> **Obsolète depuis #152** : ne plus proposer de champ « naissance multiple / `est_multiple` » — retiré de l’app (BDD, API, front).
|
||||||
|
|
||||||
## 1. Gestion des Enfants
|
## 1. Gestion des Enfants
|
||||||
|
|
||||||
@@ -11,7 +13,6 @@ Ce document liste les modifications à apporter au cahier des charges original p
|
|||||||
#### Situation actuelle dans le CDC :
|
#### Situation actuelle dans le CDC :
|
||||||
- Mentionne uniquement la collecte d'informations sur l'enfant
|
- Mentionne uniquement la collecte d'informations sur l'enfant
|
||||||
- Ne précise pas la possibilité d'ajouter plusieurs enfants
|
- Ne précise pas la possibilité d'ajouter plusieurs enfants
|
||||||
- Ne mentionne pas la gestion des naissances multiples
|
|
||||||
- Ne mentionne pas la gestion des enfants à naître
|
- Ne mentionne pas la gestion des enfants à naître
|
||||||
|
|
||||||
#### Modifications proposées :
|
#### Modifications proposées :
|
||||||
@@ -22,17 +23,18 @@ Ajouter le paragraphe suivant après la description de la collecte d'information
|
|||||||
Les parents peuvent ajouter autant d'enfants que nécessaire. Pour chaque enfant, les informations suivantes sont collectées :
|
Les parents peuvent ajouter autant d'enfants que nécessaire. Pour chaque enfant, les informations suivantes sont collectées :
|
||||||
- Prénom
|
- Prénom
|
||||||
- Date de naissance (ou date prévue pour les enfants à naître)
|
- Date de naissance (ou date prévue pour les enfants à naître)
|
||||||
|
- Genre
|
||||||
- Photo (optionnelle)
|
- Photo (optionnelle)
|
||||||
- Consentement pour l'utilisation de la photo
|
- Consentement pour l'utilisation de la photo
|
||||||
- Indication si l'enfant fait partie d'une naissance multiple (jumeaux, triplés, etc.)
|
|
||||||
|
|
||||||
Les parents peuvent :
|
Les parents peuvent :
|
||||||
- Ajouter un nouvel enfant à tout moment
|
- Ajouter un nouvel enfant à tout moment
|
||||||
- Supprimer un enfant ajouté
|
- Supprimer un enfant ajouté
|
||||||
- Modifier les informations d'un enfant existant
|
- Modifier les informations d'un enfant existant
|
||||||
- Indiquer si l'enfant est à naître
|
- Indiquer si l'enfant est à naître
|
||||||
- Indiquer si l'enfant fait partie d'une naissance multiple
|
|
||||||
- Donner ou retirer leur consentement pour l'utilisation de la photo de l'enfant
|
- Donner ou retirer leur consentement pour l'utilisation de la photo de l'enfant
|
||||||
|
|
||||||
|
Note : le concept de « naissance multiple » / jumeaux n'est pas géré par un champ dédié (retiré en 0.1.0, #152).
|
||||||
```
|
```
|
||||||
|
|
||||||
### Modifications à apporter dans la section "Workflow de création de compte"
|
### Modifications à apporter dans la section "Workflow de création de compte"
|
||||||
@@ -50,9 +52,9 @@ Remplacer l'étape 3 par :
|
|||||||
- Pour chaque enfant :
|
- Pour chaque enfant :
|
||||||
* Saisie du prénom
|
* Saisie du prénom
|
||||||
* Saisie de la date de naissance (ou date prévue)
|
* Saisie de la date de naissance (ou date prévue)
|
||||||
|
* Genre
|
||||||
* Option d'ajout d'une photo
|
* Option d'ajout d'une photo
|
||||||
* Option de consentement photo
|
* Option de consentement photo
|
||||||
* Indication si naissance multiple
|
|
||||||
* Indication si enfant à naître
|
* Indication si enfant à naître
|
||||||
- Possibilité de modifier ou supprimer un enfant
|
- Possibilité de modifier ou supprimer un enfant
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# Fichier déplacé / fusionné
|
|
||||||
|
|
||||||
La procédure **API Gitea** est désormais documentée sous :
|
|
||||||
|
|
||||||
**[26_GITEA-API.md](./26_GITEA-API.md)**
|
|
||||||
|
|
||||||
L’ancienne copie `PROCEDURE-API-GITEA.md` est archivée dans
|
|
||||||
`docs/archive/obsolete/` (doublon).
|
|
||||||
+8
-19
@@ -1,30 +1,19 @@
|
|||||||
# Archive documentation · P'titsPas
|
# Archive documentation · P'titsPas
|
||||||
|
|
||||||
Ce dossier regroupe les fichiers **sans préfixe numérique** à la racine de
|
Fichiers **hors références actives** : brouillons livrés, CDC historiques, listes figées.
|
||||||
`docs/` qui ne sont plus des **références actives**, ou qui sont des
|
|
||||||
**brouillons / temporaires**.
|
|
||||||
|
|
||||||
## Règle de nommage (racine `docs/`)
|
## Règle de nommage (racine `docs/`)
|
||||||
|
|
||||||
- Les documents **normatifs** à la racine portent un préfixe **`NN_`**
|
- Documents **normatifs** : préfixe **`NN_`**.
|
||||||
(deux chiffres), ex. `23_LISTE-TICKETS.md`.
|
- Exceptions héritage listées dans [00_INDEX.md](../00_INDEX.md) (`CHARTE_GRAPHIQUE.md`, `EVOLUTIONS_CDC.md`).
|
||||||
- **Exceptions** (héritage ou outillage) listées dans
|
|
||||||
[**00_INDEX.md**](../00_INDEX.md#exceptions-de-nommage) : charte, CDC
|
|
||||||
historique, évolutions — **cible** : les renommer progressivement en `NN_`
|
|
||||||
et mettre à jour `.cursorrules` / liens.
|
|
||||||
|
|
||||||
## Sous-dossiers ici
|
## Sous-dossiers
|
||||||
|
|
||||||
| Dossier | Usage |
|
| Dossier | Usage |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| [**temporaires/**](./temporaires/) | Notes jetables, exports de travail.
|
| [**temporaires/**](./temporaires/) | Brouillons jetables. **Vider** dès livraison. |
|
||||||
**Supprimables** quand la tâche associée est close. |
|
| [**obsolete/**](./obsolete/) | Doc remplacée (CDC SuperNounou, ancienne liste tickets, notes ponctuelles, backlog Phase 2 figé). |
|
||||||
| [**obsolete/**](./obsolete/) | Ancienne doc **remplacée** ou **doublon**
|
|
||||||
(conservée un temps pour historique). **Supprimer** après bascule confirmée
|
|
||||||
si plus aucune référence. |
|
|
||||||
|
|
||||||
## Hors `docs/` racine
|
## Politique `tmp/`
|
||||||
|
|
||||||
Les dossiers thématiques (**`juridique/`**, **`test-data/`**, etc.) peuvent
|
Le dossier `docs/tmp/` **n’est plus utilisé**. Les mini-specs de tickets livrés sont purgés ; la mémoire produit = bilans de version (`29_…`) + tickets Gitea.
|
||||||
contenir des fichiers sans `NN_` : la règle `NN_` s’applique surtout aux
|
|
||||||
fichiers **directement** sous `docs/`.
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ Ancienne documentation **déplacée** depuis `docs/` :
|
|||||||
|
|
||||||
| Fichier | Motif |
|
| Fichier | Motif |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| `PROCEDURE-API-GITEA.md` | Doublon fonctionnel de
|
| `PROCEDURE-API-GITEA.md` | Doublon de [26_GITEA-API.md](../../26_GITEA-API.md) |
|
||||||
[**26_GITEA-API.md**](../../26_GITEA-API.md). |
|
| `ARCHITECTURE_TECHNIQUE.md` | Remplacé par [02_ARCHITECTURE.md](../../02_ARCHITECTURE.md) |
|
||||||
| `ARCHITECTURE_TECHNIQUE.md` | Non référencé ; la vue d’ensemble est dans
|
| `STATUS-APPLICATION.md` | Instantané daté |
|
||||||
[**02_ARCHITECTURE.md**](../../02_ARCHITECTURE.md). |
|
| `23_LISTE-TICKETS.md` | Liste Phase 1 figée (avr. 2026) — suivi = Gitea + bilans |
|
||||||
| `STATUS-APPLICATION.md` | Instantané daté ; non tenu comme doc vivante. |
|
| `25_PHASE-2-BACKLOG.md` | Backlog technique figé — voir [05_VERSIONS…](../../05_VERSIONS-ET-MILESTONES.md) |
|
||||||
|
| `SuperNounou_*` | CDC / SSS historiques |
|
||||||
|
| `14_NOTE-BACKEND-CONFIG-SETUP.md` | Note ticket ponctuelle |
|
||||||
|
| `92_NOTE-BACKEND-GESTIONNAIRES.md` | Note ticket ponctuelle |
|
||||||
|
|
||||||
Après vérification qu’aucun lien externe ne pointe encore vers ces chemins, on
|
Mémoire produit des versions livrées : [29_BILAN-VERSION-0.1.0.md](../../29_BILAN-VERSION-0.1.0.md).
|
||||||
peut **supprimer** ce sous-dossier ou ne garder que des pointeurs minimalistes.
|
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
# #131 — En-tête fiche parent : co-parent (note front → back)
|
|
||||||
|
|
||||||
**Ticket :** #131 (fiche parent dashboard, doc `28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md` §6.1)
|
|
||||||
**Date :** 2026-06-01
|
|
||||||
**Statut front :** livré (en-tête dynamique)
|
|
||||||
**Modif backend demandée :** **aucune fonctionnelle** — ce document fixe le contrat attendu ; le back valide `co_parent` et masque les champs sensibles.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Comportement UI (front)
|
|
||||||
|
|
||||||
Dans la modale **fiche parent** (`AdminParentEditModal`) :
|
|
||||||
|
|
||||||
| Zone | Contenu |
|
|
||||||
|------|---------|
|
|
||||||
| **Titre** | `prenom` + `nom` du parent affiché (plus le libellé fixe « Fiche parent ») |
|
|
||||||
| **Sous-titre** | `Co-parent : {prenom} {nom}` — affiché **uniquement** si un co-parent est connu |
|
|
||||||
|
|
||||||
Le titre se met à jour en direct pendant l’édition des champs nom/prénom.
|
|
||||||
Le sous-titre provient du co-parent **chargé depuis l’API** (pas saisi à la main dans la modale).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Endpoints consommés
|
|
||||||
|
|
||||||
| Méthode | Route | Usage front |
|
|
||||||
|---------|-------|-------------|
|
|
||||||
| `GET` | `/api/v1/parents` | Liste parents (onglet Parents) |
|
|
||||||
| `GET` | `/api/v1/parents/:userId` | Rechargement fiche après rattachement/détachement enfant |
|
|
||||||
| `PATCH` | `/api/v1/parents/:userId/fiche` | Sauvegarde identité + statut (inchangé) |
|
|
||||||
|
|
||||||
Rôles : `super_admin`, `gestionnaire`, `administrateur` (selon route).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Contrat JSON attendu pour `co_parent`
|
|
||||||
|
|
||||||
Le front parse `ParentModel.fromJson` avec la clé **`co_parent`** (snake_case), objet utilisateur imbriqué.
|
|
||||||
|
|
||||||
### Champs minimum utilisés pour le sous-titre
|
|
||||||
|
|
||||||
| Clé JSON | Usage |
|
|
||||||
|----------|--------|
|
|
||||||
| `co_parent` | Objet ou absent/`null` |
|
|
||||||
| `co_parent.id` | Identifiant (futur lien cliquable éventuel) |
|
|
||||||
| `co_parent.prenom` | Affichage |
|
|
||||||
| `co_parent.nom` | Affichage |
|
|
||||||
|
|
||||||
Affichage front : `'{prenom} {nom}'.trim()` → libellé `Co-parent : …`.
|
|
||||||
|
|
||||||
### Exemple de fragment de réponse (`GET /parents/:id`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_id": "33333333-3333-3333-3333-333333333333",
|
|
||||||
"numero_dossier": "2026-000042",
|
|
||||||
"user": {
|
|
||||||
"id": "33333333-3333-3333-3333-333333333333",
|
|
||||||
"email": "parent1@example.com",
|
|
||||||
"prenom": "Paul",
|
|
||||||
"nom": "PARENT",
|
|
||||||
"statut": "actif",
|
|
||||||
"telephone": "0601020304"
|
|
||||||
},
|
|
||||||
"co_parent": {
|
|
||||||
"id": "44444444-4444-4444-4444-444444444444",
|
|
||||||
"email": "coparent1@example.com",
|
|
||||||
"prenom": "Clara",
|
|
||||||
"nom": "COPARENT",
|
|
||||||
"role": "parent",
|
|
||||||
"statut": "actif"
|
|
||||||
},
|
|
||||||
"parentChildren": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note :** le front lit `user` (pas `utilisateur`). La doc `11_API.md` § Parents mentionne encore `utilisateur` / `id_co_parent` seul — le contrat **effectif** côté Nest/TypeORM est l’entité `Parents` sérialisée (`user`, `co_parent`, `parentChildren`, …).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. État backend
|
|
||||||
|
|
||||||
### Relations (déjà en place)
|
|
||||||
|
|
||||||
- `findAll()` et `findOne(user_id)` chargent **`co_parent`** ;
|
|
||||||
- FK : `parents.id_co_parent` → `utilisateurs.id` ;
|
|
||||||
- inscription couple : les deux sens renseignés en principe (`auth.service.ts`).
|
|
||||||
|
|
||||||
### Livraison back (#131)
|
|
||||||
|
|
||||||
- `mapParentForApi` / `sanitizeUserForApi` : réponses `GET/PATCH/POST/DELETE` parents **sans** `password`, `token_creation_mdp`, `password_reset_*` sur `user` et `co_parent`.
|
|
||||||
|
|
||||||
**Checklist validation :**
|
|
||||||
|
|
||||||
- [x] `GET /parents/:id` renvoie `co_parent` peuplé quand `id_co_parent` est non null
|
|
||||||
- [x] `GET /parents` (liste) inclut `co_parent`
|
|
||||||
- [x] `prenom` / `nom` du co-parent présents
|
|
||||||
- [x] Pas de fuite `password` / tokens sur `user` ni `co_parent`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Points d’attention (hors périmètre immédiat)
|
|
||||||
|
|
||||||
| Sujet | Détail |
|
|
||||||
|-------|--------|
|
|
||||||
| **Lien inverse** | Si B est co-parent de A (`A.id_co_parent = B`) mais `B.id_co_parent` est `null`, le sous-titre **ne s’affichera pas** sur la fiche de B. Pas de résolution inverse côté front. |
|
|
||||||
| **Familles > 2 adultes** | Sous-titre = co-parent direct (`id_co_parent`) uniquement. |
|
|
||||||
| **Trou AM ↔ enfants en garde** | Pas de lien AM–enfant aujourd’hui (à documenter / traiter plus tard). |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Fichiers back concernés
|
|
||||||
|
|
||||||
| Fichier | Rôle |
|
|
||||||
|---------|------|
|
|
||||||
| `backend/src/routes/parents/parents.service.ts` | `findOne`, `findAll` + relations |
|
|
||||||
| `backend/src/routes/parents/parents.controller.ts` | `mapParentForApi` sur les réponses |
|
|
||||||
| `backend/src/routes/parents/parents.mapper.ts` | Sérialisation API |
|
|
||||||
| `backend/src/common/utils/sanitize-user-for-api.ts` | Masquage secrets |
|
|
||||||
| `backend/src/entities/parents.entity.ts` | relation `co_parent` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Références
|
|
||||||
|
|
||||||
- `docs/28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md` §6.1
|
|
||||||
- Ticket Gitea **#131**
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Archivé docs/archive/temporaires/ — export jetable, supprimer si inutile.
|
|
||||||
Point tickets frontend (API Gitea) - 27/01/2026
|
|
||||||
================================================
|
|
||||||
|
|
||||||
Issues avec label "frontend" : 20 (ouvertes: 12, fermees: 8)
|
|
||||||
|
|
||||||
Num | Etat | Titre
|
|
||||||
----+--------+--------------------------------------------------------
|
|
||||||
35 | open | [Frontend] Écran Création Gestionnaire
|
|
||||||
36 | closed | [Frontend] Inscription Parent - Étape 1 (Parent 1)
|
|
||||||
37 | closed | [Frontend] Inscription Parent - Étape 2 (Parent 2)
|
|
||||||
38 | closed | [Frontend] Inscription Parent - Étape 3 (Enfants)
|
|
||||||
39 | closed | [Frontend] Inscription Parent - Étapes 4-6 (Finalisatio
|
|
||||||
40 | closed | [Frontend] Inscription AM - Panneau 1 (Identité)
|
|
||||||
41 | closed | [Frontend] Inscription AM - Panneau 2 (Infos pro)
|
|
||||||
42 | closed | [Frontend] Inscription AM - Finalisation
|
|
||||||
43 | open | [Frontend] Écran Création Mot de Passe
|
|
||||||
44 | closed | [Frontend] Dashboard Gestionnaire - Structure
|
|
||||||
45 | open | [Frontend] Dashboard Gestionnaire - Liste Parents
|
|
||||||
46 | open | [Frontend] Dashboard Gestionnaire - Liste AM
|
|
||||||
47 | open | [Frontend] Écran Changement MDP Obligatoire
|
|
||||||
48 | open | [Frontend] Gestion Erreurs & Messages
|
|
||||||
49 | open | [Frontend] Écran Gestion Documents Légaux (Admin)
|
|
||||||
50 | open | [Frontend] Affichage dynamique CGU lors inscription
|
|
||||||
51 | open | [Frontend] Écran Logs Admin (optionnel v1.1)
|
|
||||||
54 | open | [Tests] Tests E2E Frontend
|
|
||||||
82 | closed | [Frontend] Adapter �cran Login pour mobile
|
|
||||||
83 | closed | [Frontend] Adapter �cran Choix Inscription pour mobile
|
|
||||||
|
|
||||||
Suivi doc 23_LISTE-TICKETS (Gitea #73,78,79,81,82,83):
|
|
||||||
#73 closed labels=[]
|
|
||||||
#78 closed labels=[]
|
|
||||||
#79 closed labels=[]
|
|
||||||
#81 closed labels=[]
|
|
||||||
#82 closed (écran Login mobile)
|
|
||||||
#83 closed labels=['frontend', 'p3', 'phase-1', 'ux']
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
# Temporaires
|
# Temporaires
|
||||||
|
|
||||||
Fichiers **non numérotés** de travail (brouillons, listes de tickets exportées,
|
Dossier **vide** après clôture 0.1.0 (purge sept. 2026).
|
||||||
alignements UI en cours, etc.).
|
|
||||||
|
|
||||||
- Préfixe conseillé pour les nouveaux fichiers jetables : **`TEMP_`** ou
|
Si un brouillon de travail est nécessaire un temps :
|
||||||
**`WIP_`** dans ce dossier.
|
|
||||||
- **Suppression** : dès que la fonctionnalité est livrée ou le sujet clos,
|
- le placer ici avec préfixe `TEMP_` / `WIP_` ;
|
||||||
supprimer le fichier (ou le déplacer vers `obsolete/` si une trace utile
|
- le **supprimer** dès livraison (ne pas laisser pourrir) ;
|
||||||
reste nécessaire).
|
- pour une trace utile durable → bilan de version ou archive `obsolete/`.
|
||||||
|
|
||||||
|
Ne plus utiliser `docs/tmp/`.
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
# #112 — Alignement front après évolution back (reprise dossier complet)
|
|
||||||
|
|
||||||
**Branche déployée :** `feature/112-reprise-apres-refus-front`
|
|
||||||
**Commit back :** `d70577b1` — `feat(#112): reprise après refus — dossier complet GET/PATCH`
|
|
||||||
**Date :** 2026-06-16
|
|
||||||
|
|
||||||
Ce document décrit le **contrat API réel** après extension du back, et ce que le front doit encore brancher pour exploiter le dossier complet (au-delà de l’identité seule).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Endpoints (inchangés côté URL)
|
|
||||||
|
|
||||||
| Méthode | Route | Auth |
|
|
||||||
|---------|-------|------|
|
|
||||||
| `GET` | `/api/v1/auth/reprise-dossier?token={uuid}` | Public |
|
|
||||||
| `PATCH` | `/api/v1/auth/reprise-resoumettre` | Public |
|
|
||||||
| `POST` | `/api/v1/auth/reprise-identify` | Public (inchangé) |
|
|
||||||
|
|
||||||
> **Note :** le ticket #111 parlait de `PUT` ; l’implémentation reste en **`PATCH`** (comme avant).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. `GET /auth/reprise-dossier` — réponse enrichie
|
|
||||||
|
|
||||||
### Champs communs (toujours présents)
|
|
||||||
|
|
||||||
Identiques à avant : `id`, `email`, `prenom`, `nom`, `telephone`, `adresse`, `ville`, `code_postal`, `numero_dossier`, `role`, `photo_url`, `genre`, `situation_familiale`.
|
|
||||||
|
|
||||||
### Rôle `parent` (+ champs #119)
|
|
||||||
|
|
||||||
Alignés sur `DossierFamilleCompletDto` :
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"parents": [
|
|
||||||
{
|
|
||||||
"user_id": "uuid",
|
|
||||||
"email": "…",
|
|
||||||
"prenom": "…",
|
|
||||||
"nom": "…",
|
|
||||||
"telephone": "…",
|
|
||||||
"adresse": "…",
|
|
||||||
"ville": "…",
|
|
||||||
"code_postal": "…",
|
|
||||||
"statut": "refuse",
|
|
||||||
"co_parent_id": "uuid-parent-entity"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"enfants": [
|
|
||||||
{
|
|
||||||
"id": "uuid-enfant",
|
|
||||||
"first_name": "Emma",
|
|
||||||
"last_name": "MARTIN",
|
|
||||||
"genre": "F",
|
|
||||||
"status": "actif",
|
|
||||||
"birth_date": "2023-02-15T00:00:00.000Z",
|
|
||||||
"due_date": null,
|
|
||||||
"photo_url": "/uploads/photos/…",
|
|
||||||
"consent_photo": true,
|
|
||||||
"est_multiple": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"texte_motivation": "Nous recherchons…"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mapping front suggéré :**
|
|
||||||
|
|
||||||
| JSON back | Modèle / wizard parent |
|
|
||||||
|-----------|-------------------------|
|
|
||||||
| `parents[]` | `UserRegistrationData.parent1` + `parent2` (matcher par `email` ou ordre : titulaire = `id` du GET racine) |
|
|
||||||
| `enfants[].first_name` / `last_name` | `ChildData.firstName` / `lastName` |
|
|
||||||
| `enfants[].birth_date` | `ChildData.birthDate` (ISO → `DateTime`) |
|
|
||||||
| `enfants[].due_date` | `ChildData.dueDate` (enfant `a_naitre`) |
|
|
||||||
| `enfants[].status` | `actif` = né, `a_naitre` = à naître |
|
|
||||||
| `enfants[].photo_url` | `ApiConfig.absoluteMediaUrl()` + conserver pour reprise sans re-upload |
|
|
||||||
| `enfants[].id` | **Obligatoire** pour le PATCH (update par id) |
|
|
||||||
| `enfants[].est_multiple` | `grossesse_multiple` si utilisé |
|
|
||||||
| `texte_motivation` | étape présentation / motivation |
|
|
||||||
|
|
||||||
Si `numero_dossier` absent : pas de `parents[]` / `enfants[]` / `texte_motivation` (identité seule).
|
|
||||||
|
|
||||||
### Rôle `assistante_maternelle`
|
|
||||||
|
|
||||||
Champs racine + fiche pro (structure **aplatie**, pas de sous-objet `user`) :
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"consentement_photo": true,
|
|
||||||
"date_naissance": "1985-03-12T00:00:00.000Z",
|
|
||||||
"lieu_naissance_ville": "Paris",
|
|
||||||
"lieu_naissance_pays": "France",
|
|
||||||
"numero_agrement": "AGR-2024-12345",
|
|
||||||
"nir": "123456789012345",
|
|
||||||
"date_agrement": "2024-06-01T00:00:00.000Z",
|
|
||||||
"nb_max_enfants": 4,
|
|
||||||
"place_disponible": 2,
|
|
||||||
"biographie": "…"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mapping `AmRegistrationData` :**
|
|
||||||
|
|
||||||
| JSON back | Champ front |
|
|
||||||
|-----------|-------------|
|
|
||||||
| `nb_max_enfants` | `capaciteAccueil` |
|
|
||||||
| `place_disponible` | `placesDisponibles` |
|
|
||||||
| `numero_agrement` | `numeroAgrement` |
|
|
||||||
| `biographie` | `biographie` / présentation |
|
|
||||||
| `photo_url` | déjà géré via `RepriseSession.photoUrl` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. `PATCH /auth/reprise-resoumettre` — body étendu
|
|
||||||
|
|
||||||
### Commun
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "token": "uuid-reprise" }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parent — champs à envoyer depuis le wizard
|
|
||||||
|
|
||||||
| Champ PATCH | Source wizard | Notes |
|
|
||||||
|-------------|---------------|-------|
|
|
||||||
| `prenom`, `nom`, `telephone`, `adresse`, `ville`, `code_postal` | Parent 1 (titulaire token) | Champs racine |
|
|
||||||
| `co_parent_prenom`, `co_parent_nom`, `co_parent_telephone` | Parent 2 | |
|
|
||||||
| `co_parent_meme_adresse`, `co_parent_adresse`, `co_parent_code_postal`, `co_parent_ville` | Parent 2 adresse | |
|
|
||||||
| `texte_motivation` **ou** `presentation_dossier` | Étape motivation | Les deux alias acceptés |
|
|
||||||
| `enfants[]` | Liste enfants | Voir ci-dessous |
|
|
||||||
|
|
||||||
**Structure `enfants[]` (miroir inscription + `id` obligatoire) :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-enfant-existant",
|
|
||||||
"prenom": "Emma",
|
|
||||||
"nom": "MARTIN",
|
|
||||||
"date_naissance": "2023-02-15",
|
|
||||||
"date_previsionnelle_naissance": null,
|
|
||||||
"genre": "F",
|
|
||||||
"photo_base64": "data:image/jpeg;base64,…",
|
|
||||||
"photo_filename": "emma.jpg",
|
|
||||||
"grossesse_multiple": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **v1 back :** update par `id` uniquement — pas de création/suppression d’enfant.
|
|
||||||
- Si `id` inconnu pour ce dossier → **400** `Enfant inconnu pour ce dossier : {id}`.
|
|
||||||
- Sans nouvelle photo : ne pas envoyer `photo_base64` (l’existant est conservé).
|
|
||||||
|
|
||||||
### AM — champs à envoyer
|
|
||||||
|
|
||||||
| Champ PATCH | Source |
|
|
||||||
|-------------|--------|
|
|
||||||
| Identité + `photo_url` ou `photo_base64` + `photo_filename` | Étapes 1–2 |
|
|
||||||
| `consentement_photo`, `date_naissance`, `lieu_naissance_ville`, `lieu_naissance_pays` | Identité |
|
|
||||||
| `numero_agrement`, `nir`, `date_agrement` | Pro |
|
|
||||||
| `capacite_accueil`, `places_disponibles` | Pro |
|
|
||||||
| `biographie` | Présentation |
|
|
||||||
|
|
||||||
Validation NIR identique à l’inscription si `nir` fourni.
|
|
||||||
|
|
||||||
### Réponse succès (nouveau format)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Dossier resoumis avec succès. Il est de nouveau en attente de validation.",
|
|
||||||
"statut": "en_attente",
|
|
||||||
"user_id": "uuid",
|
|
||||||
"numero_dossier": "2026-000021"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Code HTTP : **200** (pas de corps `Users` brut comme l’ancien back).
|
|
||||||
|
|
||||||
### Effet métier
|
|
||||||
|
|
||||||
- **Parent :** tous les users `role=parent` avec le même `numero_dossier` passent en `en_attente` ; `token_reprise` invalidé sur **tous** (symétrique refus #110).
|
|
||||||
- **AM :** un seul user.
|
|
||||||
|
|
||||||
### E-mail accusé resoumission (parent)
|
|
||||||
|
|
||||||
Après `PATCH` réussi, un e-mail est envoyé à **chaque parent** du dossier (`sendResoumissionPendingEmail`) :
|
|
||||||
- confirmation de resoumission ;
|
|
||||||
- rappel du **numéro de dossier** ;
|
|
||||||
- mention « en attente de validation ».
|
|
||||||
|
|
||||||
Échec SMTP : logué, **ne bloque pas** la resoumission (même règle que l'inscription initiale).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Fichiers front à modifier (checklist)
|
|
||||||
|
|
||||||
### Modèles
|
|
||||||
|
|
||||||
- [ ] `lib/models/reprise_dossier.dart` — parser `parents[]`, `enfants[]`, `texte_motivation`, champs AM
|
|
||||||
- [ ] Réutiliser ou mapper vers `DossierFamilleEnfant` / structures existantes (#119 admin) si possible
|
|
||||||
|
|
||||||
### Session / préremplissage
|
|
||||||
|
|
||||||
- [ ] `lib/services/reprise_session.dart`
|
|
||||||
- `applyToParent` : remplir parent1/parent2 depuis `parents[]`, enfants, motivation
|
|
||||||
- `applyToAm` : remplir tous les champs AM
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
- [ ] `lib/services/auth_service.dart` — `resoumettreReprise()` : accepter body complet (parent + AM), pas seulement identité
|
|
||||||
- [ ] Étendre `UserRegistrationData` / `AmRegistrationData` helpers `toReprisePatchBody()` si utile
|
|
||||||
|
|
||||||
### Écrans fin de parcours
|
|
||||||
|
|
||||||
- [ ] `parent_register_step5_screen.dart` — PATCH avec co-parent, enfants, motivation
|
|
||||||
- [ ] `am_register_step4_screen.dart` — PATCH avec fiche AM complète
|
|
||||||
|
|
||||||
### Hors scope back (inchangé)
|
|
||||||
|
|
||||||
RIB / IBAN / attestation CAF (étape 5 wizard parent) : **non persistés** — rien à envoyer en reprise.
|
|
||||||
|
|
||||||
### Non implémenté front (ticket #112 initial)
|
|
||||||
|
|
||||||
- [ ] Modale login « J’ai un numéro de dossier » → `POST /auth/reprise-identify` (back prêt, front absent)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Tests manuels suggérés
|
|
||||||
|
|
||||||
1. Refuser un dossier parent complet (≥1 enfant + co-parent + motivation).
|
|
||||||
2. Ouvrir le lien mail `/reprise?token=…`.
|
|
||||||
3. Vérifier dans DevTools que le GET contient `enfants[]` et `texte_motivation`.
|
|
||||||
4. Après branchement front : wizard prérempli sur toutes les étapes.
|
|
||||||
5. Resoumettre → statut `en_attente` pour les deux parents ; dossier visible file validation admin (#119).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Références code back
|
|
||||||
|
|
||||||
```
|
|
||||||
backend/src/routes/auth/dto/reprise-dossier.dto.ts
|
|
||||||
backend/src/routes/auth/dto/resoumettre-reprise.dto.ts
|
|
||||||
backend/src/routes/auth/dto/enfant-reprise.dto.ts
|
|
||||||
backend/src/routes/auth/auth.service.ts → getRepriseDossier, resoumettreReprise
|
|
||||||
backend/src/routes/parents/dto/dossier-famille-complet.dto.ts
|
|
||||||
```
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# #131 — Fiche AM éditable + affiliation enfants (note front → back)
|
|
||||||
|
|
||||||
**Ticket :** #131 (partie AM, doc `28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md` §6.1)
|
|
||||||
**Date :** 2026-06-01
|
|
||||||
**Statut front :** modale livrée (2 onglets) — **API affiliation AM↔enfant à implémenter**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Comportement UI (front)
|
|
||||||
|
|
||||||
Modale `AdminAmEditModal` — même shell que la fiche parent (~930 px) :
|
|
||||||
|
|
||||||
| Onglet | Contenu |
|
|
||||||
|--------|---------|
|
|
||||||
| **Identité & professionnel** | `IdentityBlock` éditable + grille pro (agrément, ville résidence, capacité, places, NIR/agrément date en lecture seule, biographie, switch disponible) + gélule statut |
|
|
||||||
| **Enfants accueillis** | Liste cartes enfants (réutilise `AdminChildrenAffiliationPanel` / `AdminEnfantUserCard`) + rattacher / détacher |
|
|
||||||
|
|
||||||
En-tête : prénom nom · sous-titre `Zone · Agrément · Dossier`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Endpoints consommés
|
|
||||||
|
|
||||||
### Déjà existants (partiels)
|
|
||||||
|
|
||||||
| Méthode | Route | Usage |
|
|
||||||
|---------|-------|-------|
|
|
||||||
| `GET` | `/api/v1/assistantes-maternelles` | Liste AM |
|
|
||||||
| `GET` | `/api/v1/assistantes-maternelles/:userId` | Détail (403 possible pour `administrateur` → fallback liste) |
|
|
||||||
| `PATCH` | `/api/v1/users/:userId` | Identité + statut (admin / super_admin uniquement) |
|
|
||||||
| `PATCH` | `/api/v1/assistantes-maternelles/:userId` | Champs pro (gestionnaire / super_admin) |
|
|
||||||
|
|
||||||
### À créer (recommandé — miroir parent #131 / #115)
|
|
||||||
|
|
||||||
| Méthode | Route | Rôle |
|
|
||||||
|---------|-------|------|
|
|
||||||
| `PATCH` | `/api/v1/assistantes-maternelles/:userId/fiche` | Mise à jour unifiée identité + pro + statut (`super_admin`, `gestionnaire`, `administrateur`) |
|
|
||||||
| `POST` | `/api/v1/assistantes-maternelles/:userId/enfants/:enfantId` | Rattacher un enfant |
|
|
||||||
| `DELETE` | `/api/v1/assistantes-maternelles/:userId/enfants/:enfantId` | Détacher un enfant |
|
|
||||||
| `GET` | `/api/v1/assistantes-maternelles/:userId` | Inclure `amChildren[]` (relation enfant) |
|
|
||||||
|
|
||||||
Le front appelle déjà ces routes ; en l’absence de `PATCH …/fiche`, il tente un fallback `PATCH users` + `PATCH assistantes-maternelles` (échoue selon le rôle connecté).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Modèle de données affiliation AM ↔ enfant
|
|
||||||
|
|
||||||
**À définir côté BDD** (pas de table dédiée aujourd’hui, contrairement à `enfants_parents`) :
|
|
||||||
|
|
||||||
Proposition alignée parent :
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Piste : enfants_assistantes_maternelles
|
|
||||||
CREATE TABLE enfants_assistantes_maternelles (
|
|
||||||
id_am UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
|
||||||
id_enfant UUID NOT NULL REFERENCES enfants(id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (id_am, id_enfant)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Réponse API attendue sur `GET /assistantes-maternelles/:id` :
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_id": "uuid-am",
|
|
||||||
"user": { "id": "…", "prenom": "Claire", "nom": "MARTIN", "statut": "actif" },
|
|
||||||
"approval_number": "AGR-2024-12345",
|
|
||||||
"residence_city": "Bezons",
|
|
||||||
"max_children": 4,
|
|
||||||
"places_available": 2,
|
|
||||||
"available": true,
|
|
||||||
"amChildren": [
|
|
||||||
{
|
|
||||||
"child": {
|
|
||||||
"id": "uuid-enfant",
|
|
||||||
"first_name": "Emma",
|
|
||||||
"last_name": "MARTIN",
|
|
||||||
"status": "actif",
|
|
||||||
"birth_date": "2023-02-15"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Le front parse `amChildren` / `am_children` / `assistanteChildren` (même logique que `parentChildren`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Body `PATCH …/fiche` suggéré
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"nom": "MARTIN",
|
|
||||||
"prenom": "Claire",
|
|
||||||
"email": "claire@example.com",
|
|
||||||
"telephone": "0612345678",
|
|
||||||
"adresse": "5 place Bellecour",
|
|
||||||
"ville": "Lyon",
|
|
||||||
"code_postal": "69002",
|
|
||||||
"statut": "actif",
|
|
||||||
"approval_number": "AGR-2024-12345",
|
|
||||||
"residence_city": "Lyon",
|
|
||||||
"max_children": 4,
|
|
||||||
"places_available": 2,
|
|
||||||
"biography": "…",
|
|
||||||
"available": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
NIR et date d’agrément : lecture seule dans la modale (modification hors périmètre admin v1).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Fichiers front concernés
|
|
||||||
|
|
||||||
| Fichier | Rôle |
|
|
||||||
|---------|------|
|
|
||||||
| `frontend/lib/widgets/admin/common/admin_am_edit_modal.dart` | Modale 2 onglets |
|
|
||||||
| `frontend/lib/widgets/admin/common/admin_children_affiliation_panel.dart` | Liste enfants partagée parent/AM |
|
|
||||||
| `frontend/lib/widgets/admin/common/admin_status_capsule.dart` | Gélule statut partagée |
|
|
||||||
| `frontend/lib/models/assistante_maternelle_model.dart` | Parse champs pro + `amChildren` |
|
|
||||||
| `frontend/lib/services/user_service.dart` | `getAssistanteMaternelle`, `updateAmFiche`, `attachEnfantToAm`, `detachEnfantFromAm` |
|
|
||||||
| `frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart` | Ouverture modale au clic Modifier |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Références
|
|
||||||
|
|
||||||
- Fiche parent : `PATCH /parents/:id/fiche`, `POST|DELETE /parents/:id/enfants/:enfantId`
|
|
||||||
- Ticket Gitea **#131**, **#115**
|
|
||||||
- `docs/archive/temporaires/TEMP_131-back-fiche-parent-co-parent.md`
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
# #131 — En-tête fiche parent : co-parent (note front → back)
|
|
||||||
|
|
||||||
**Ticket :** #131 (fiche parent dashboard, doc `28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md` §6.1)
|
|
||||||
**Date :** 2026-06-01
|
|
||||||
**Statut front :** livré (en-tête dynamique)
|
|
||||||
**Modif backend demandée :** **aucune** — ce document fixe le contrat attendu et invite à valider que l’existant le couvre.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Comportement UI (front)
|
|
||||||
|
|
||||||
Dans la modale **fiche parent** (`AdminParentEditModal`) :
|
|
||||||
|
|
||||||
| Zone | Contenu |
|
|
||||||
|------|---------|
|
|
||||||
| **Titre** | `prenom` + `nom` du parent affiché (plus le libellé fixe « Fiche parent ») |
|
|
||||||
| **Sous-titre** | `Co-parent : {prenom} {nom}` — affiché **uniquement** si un co-parent est connu |
|
|
||||||
|
|
||||||
Le titre se met à jour en direct pendant l’édition des champs nom/prénom.
|
|
||||||
Le sous-titre provient du co-parent **chargé depuis l’API** (pas saisi à la main dans la modale).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Endpoints consommés
|
|
||||||
|
|
||||||
| Méthode | Route | Usage front |
|
|
||||||
|---------|-------|-------------|
|
|
||||||
| `GET` | `/api/v1/parents` | Liste parents (onglet Parents) |
|
|
||||||
| `GET` | `/api/v1/parents/:userId` | Rechargement fiche après rattachement/détachement enfant |
|
|
||||||
| `PATCH` | `/api/v1/parents/:userId/fiche` | Sauvegarde identité + statut (inchangé) |
|
|
||||||
|
|
||||||
Rôles : `super_admin`, `gestionnaire`, `administrateur` (selon route).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Contrat JSON attendu pour `co_parent`
|
|
||||||
|
|
||||||
Le front parse `ParentModel.fromJson` avec la clé **`co_parent`** (snake_case), objet utilisateur imbriqué.
|
|
||||||
|
|
||||||
### Champs minimum utilisés pour le sous-titre
|
|
||||||
|
|
||||||
| Clé JSON | Usage |
|
|
||||||
|----------|--------|
|
|
||||||
| `co_parent` | Objet ou absent/`null` |
|
|
||||||
| `co_parent.id` | Identifiant (futur lien cliquable éventuel) |
|
|
||||||
| `co_parent.prenom` | Affichage |
|
|
||||||
| `co_parent.nom` | Affichage |
|
|
||||||
|
|
||||||
Affichage front : `'{prenom} {nom}'.trim()` → libellé `Co-parent : …`.
|
|
||||||
|
|
||||||
### Exemple de fragment de réponse (`GET /parents/:id`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_id": "33333333-3333-3333-3333-333333333333",
|
|
||||||
"numero_dossier": "2026-000042",
|
|
||||||
"user": {
|
|
||||||
"id": "33333333-3333-3333-3333-333333333333",
|
|
||||||
"email": "parent1@example.com",
|
|
||||||
"prenom": "Paul",
|
|
||||||
"nom": "PARENT",
|
|
||||||
"statut": "actif",
|
|
||||||
"telephone": "0601020304"
|
|
||||||
},
|
|
||||||
"co_parent": {
|
|
||||||
"id": "44444444-4444-4444-4444-444444444444",
|
|
||||||
"email": "coparent1@example.com",
|
|
||||||
"prenom": "Clara",
|
|
||||||
"nom": "COPARENT",
|
|
||||||
"role": "parent",
|
|
||||||
"statut": "actif"
|
|
||||||
},
|
|
||||||
"parentChildren": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note :** le front lit `user` (pas `utilisateur`). La doc `11_API.md` § Parents mentionne encore `utilisateur` / `id_co_parent` seul — le contrat **effectif** côté Nest/TypeORM est l’entité `Parents` sérialisée (`user`, `co_parent`, `parentChildren`, …).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. État backend (à valider, pas à refaire)
|
|
||||||
|
|
||||||
D’après le code actuel (`parents.service.ts`) :
|
|
||||||
|
|
||||||
- `findAll()` et `findOne(user_id)` chargent déjà la relation **`co_parent`** ;
|
|
||||||
- la FK métier est `parents.id_co_parent` → `utilisateurs.id` ;
|
|
||||||
- à l’inscription couple, les deux sens sont en principe renseignés (`auth.service.ts`).
|
|
||||||
|
|
||||||
**Checklist validation back :**
|
|
||||||
|
|
||||||
- [ ] `GET /parents/:id` renvoie bien `co_parent` peuplé quand `id_co_parent` est non null
|
|
||||||
- [ ] `GET /parents` (liste) inclut aussi `co_parent` (sous-titre disponible dès l’ouverture sans re-fetch)
|
|
||||||
- [ ] Les champs `prenom` / `nom` du co-parent sont présents dans la réponse JSON
|
|
||||||
|
|
||||||
Si ces trois points passent en recette, **aucun changement backend n’est nécessaire** pour cette fonctionnalité.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Points d’attention (hors périmètre immédiat)
|
|
||||||
|
|
||||||
| Sujet | Détail |
|
|
||||||
|-------|--------|
|
|
||||||
| **Lien inverse** | Si le parent B est le co-parent de A (`A.id_co_parent = B`) mais que `B.id_co_parent` est `null`, le sous-titre **ne s’affichera pas** sur la fiche de B. Le front ne fait pas de résolution inverse. À traiter côté back **seulement si** des données legacy ont un lien à sens unique. |
|
|
||||||
| **Familles > 2 adultes** | Le sous-titre n’affiche que le co-parent direct (`id_co_parent`). Les autres responsables liés uniquement via `enfants_parents` ne sont pas listés ici (cf. doc 28 §6). |
|
|
||||||
| **Données sensibles** | Vérifier que la sérialisation de `co_parent` n’expose pas `password` / tokens (même remarque que pour `user`). |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Fichiers front concernés
|
|
||||||
|
|
||||||
| Fichier | Rôle |
|
|
||||||
|---------|------|
|
|
||||||
| `frontend/lib/models/parent_model.dart` | Parse `co_parent` → `AppUser? coParent` |
|
|
||||||
| `frontend/lib/widgets/admin/common/admin_parent_edit_modal.dart` | Titre + sous-titre |
|
|
||||||
| `frontend/lib/services/user_service.dart` | `getParents()` / `getParent()` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Références
|
|
||||||
|
|
||||||
- `docs/28_EVOLUTION-FAMILLE-ET-RESPONSABLES.md` §6.1
|
|
||||||
- `backend/src/routes/parents/parents.service.ts` — `findOne`, `findAll`
|
|
||||||
- `backend/src/entities/parents.entity.ts` — relation `co_parent`
|
|
||||||
- Ticket Gitea **#131**
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# TEMP — Alignement front / API (inscription AM & validation gestionnaire)
|
|
||||||
|
|
||||||
> **Archivé** (`docs/archive/temporaires/`) — **fichier temporaire** ; à
|
|
||||||
> **supprimer** une fois le front livré ou le sujet clos (voir
|
|
||||||
> `docs/archive/temporaires/README.md`).
|
|
||||||
|
|
||||||
Ce document décrit les changements **côté API** et ce que **Flutter** doit faire pour rester aligné. Aucune modification front n’a été faite dans le chantier backend associé.
|
|
||||||
|
|
||||||
## 1. `POST /auth/register/am` — lieu de naissance obligatoire
|
|
||||||
|
|
||||||
- **`lieu_naissance_ville`** et **`lieu_naissance_pays`** sont **obligatoires** (non vides après trim, min. **2 caractères** chacun, max 100).
|
|
||||||
- Réponses **400** si manquants ou invalides (messages class-validator).
|
|
||||||
- **Action front** : champs obligatoires dans le parcours AM (étapes identité / naissance), validation UI avant envoi ; afficher les erreurs renvoyées par l’API.
|
|
||||||
|
|
||||||
## 2. Réponse `GET /dossiers/:numeroDossier` (type `am`)
|
|
||||||
|
|
||||||
Sous `dossier.user`, l’API peut inclure :
|
|
||||||
|
|
||||||
| Clé JSON | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `date_naissance` | Date (si renseignée à l’inscription) |
|
|
||||||
| `lieu_naissance_ville` | Ville de naissance |
|
|
||||||
| `lieu_naissance_pays` | Pays de naissance |
|
|
||||||
| `consentement_photo` | Booléen (exposé dans `dossier.user`) |
|
|
||||||
|
|
||||||
À la **racine** de `dossier` (objet AM), champs déjà renvoyés par le backend : `disponible`, `annees_experience`, `specialite`, `nb_max_enfants`, `place_disponible`, etc.
|
|
||||||
|
|
||||||
**Action front** :
|
|
||||||
|
|
||||||
- Étendre **`AppUser.fromJson` / `toJson`** (`lib/models/user.dart`) pour mapper `date_naissance`, `lieu_naissance_ville`, `lieu_naissance_pays`, `consentement_photo`.
|
|
||||||
- Étendre **`DossierAM.fromJson`** (`lib/models/dossier_unifie.dart`) pour parser `disponible`, `annees_experience`, `specialite` à la racine du dossier (noms snake_case comme dans la réponse JSON Nest).
|
|
||||||
|
|
||||||
## 3. `ValidationAmWizard` (admin)
|
|
||||||
|
|
||||||
Afficher pour cohérence avec le formulaire d’inscription :
|
|
||||||
|
|
||||||
- **Informations personnelles** : date de naissance, ville / pays de naissance, consentement photo (Oui/Non).
|
|
||||||
- **Informations professionnelles** : disponibilité, années d’expérience, spécialité (afficher « – » si `null`).
|
|
||||||
|
|
||||||
## 4. `place_disponible` à l’inscription
|
|
||||||
|
|
||||||
- Le backend initialise **`place_disponible`** sur la fiche AM à la **même valeur** que **`capacite_accueil`** à la création. Le wizard peut donc afficher une valeur cohérente avec la capacité sans champ séparé côté public.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Dernière mise à jour : alignement backend branche `feature/120-inscription-am-photo-backend`.*
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
# #112 — Alignement front après évolution back (reprise dossier complet)
|
|
||||||
|
|
||||||
**Branche déployée :** `feature/112-reprise-apres-refus-front`
|
|
||||||
**Commit back :** `d70577b1` — `feat(#112): reprise après refus — dossier complet GET/PATCH`
|
|
||||||
**Date :** 2026-06-16
|
|
||||||
|
|
||||||
Ce document décrit le **contrat API réel** après extension du back, et ce que le front doit encore brancher pour exploiter le dossier complet (au-delà de l’identité seule).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Endpoints (inchangés côté URL)
|
|
||||||
|
|
||||||
| Méthode | Route | Auth |
|
|
||||||
|---------|-------|------|
|
|
||||||
| `GET` | `/api/v1/auth/reprise-dossier?token={uuid}` | Public |
|
|
||||||
| `PATCH` | `/api/v1/auth/reprise-resoumettre` | Public |
|
|
||||||
| `POST` | `/api/v1/auth/reprise-identify` | Public (inchangé) |
|
|
||||||
|
|
||||||
> **Note :** le ticket #111 parlait de `PUT` ; l’implémentation reste en **`PATCH`** (comme avant).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. `GET /auth/reprise-dossier` — réponse enrichie
|
|
||||||
|
|
||||||
### Champs communs (toujours présents)
|
|
||||||
|
|
||||||
Identiques à avant : `id`, `email`, `prenom`, `nom`, `telephone`, `adresse`, `ville`, `code_postal`, `numero_dossier`, `role`, `photo_url`, `genre`, `situation_familiale`.
|
|
||||||
|
|
||||||
### Rôle `parent` (+ champs #119)
|
|
||||||
|
|
||||||
Alignés sur `DossierFamilleCompletDto` :
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"parents": [
|
|
||||||
{
|
|
||||||
"user_id": "uuid",
|
|
||||||
"email": "…",
|
|
||||||
"prenom": "…",
|
|
||||||
"nom": "…",
|
|
||||||
"telephone": "…",
|
|
||||||
"adresse": "…",
|
|
||||||
"ville": "…",
|
|
||||||
"code_postal": "…",
|
|
||||||
"statut": "refuse",
|
|
||||||
"co_parent_id": "uuid-parent-entity"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"enfants": [
|
|
||||||
{
|
|
||||||
"id": "uuid-enfant",
|
|
||||||
"first_name": "Emma",
|
|
||||||
"last_name": "MARTIN",
|
|
||||||
"genre": "F",
|
|
||||||
"status": "actif",
|
|
||||||
"birth_date": "2023-02-15T00:00:00.000Z",
|
|
||||||
"due_date": null,
|
|
||||||
"photo_url": "/uploads/photos/…",
|
|
||||||
"consent_photo": true,
|
|
||||||
"est_multiple": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"texte_motivation": "Nous recherchons…"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mapping front suggéré :**
|
|
||||||
|
|
||||||
| JSON back | Modèle / wizard parent |
|
|
||||||
|-----------|-------------------------|
|
|
||||||
| `parents[]` | `UserRegistrationData.parent1` + `parent2` (matcher par `email` ou ordre : titulaire = `id` du GET racine) |
|
|
||||||
| `enfants[].first_name` / `last_name` | `ChildData.firstName` / `lastName` |
|
|
||||||
| `enfants[].birth_date` | `ChildData.birthDate` (ISO → `DateTime`) |
|
|
||||||
| `enfants[].due_date` | `ChildData.dueDate` (enfant `a_naitre`) |
|
|
||||||
| `enfants[].status` | `actif` = né, `a_naitre` = à naître |
|
|
||||||
| `enfants[].photo_url` | `ApiConfig.absoluteMediaUrl()` + conserver pour reprise sans re-upload |
|
|
||||||
| `enfants[].id` | **Obligatoire** pour le PATCH (update par id) |
|
|
||||||
| `enfants[].est_multiple` | `grossesse_multiple` si utilisé |
|
|
||||||
| `texte_motivation` | étape présentation / motivation |
|
|
||||||
|
|
||||||
Si `numero_dossier` absent : pas de `parents[]` / `enfants[]` / `texte_motivation` (identité seule).
|
|
||||||
|
|
||||||
### Rôle `assistante_maternelle`
|
|
||||||
|
|
||||||
Champs racine + fiche pro (structure **aplatie**, pas de sous-objet `user`) :
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"consentement_photo": true,
|
|
||||||
"date_naissance": "1985-03-12T00:00:00.000Z",
|
|
||||||
"lieu_naissance_ville": "Paris",
|
|
||||||
"lieu_naissance_pays": "France",
|
|
||||||
"numero_agrement": "AGR-2024-12345",
|
|
||||||
"nir": "123456789012345",
|
|
||||||
"date_agrement": "2024-06-01T00:00:00.000Z",
|
|
||||||
"nb_max_enfants": 4,
|
|
||||||
"place_disponible": 2,
|
|
||||||
"biographie": "…"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mapping `AmRegistrationData` :**
|
|
||||||
|
|
||||||
| JSON back | Champ front |
|
|
||||||
|-----------|-------------|
|
|
||||||
| `nb_max_enfants` | `capaciteAccueil` |
|
|
||||||
| `place_disponible` | `placesDisponibles` |
|
|
||||||
| `numero_agrement` | `numeroAgrement` |
|
|
||||||
| `biographie` | `biographie` / présentation |
|
|
||||||
| `photo_url` | déjà géré via `RepriseSession.photoUrl` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. `PATCH /auth/reprise-resoumettre` — body étendu
|
|
||||||
|
|
||||||
### Commun
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "token": "uuid-reprise" }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parent — champs à envoyer depuis le wizard
|
|
||||||
|
|
||||||
| Champ PATCH | Source wizard | Notes |
|
|
||||||
|-------------|---------------|-------|
|
|
||||||
| `prenom`, `nom`, `telephone`, `adresse`, `ville`, `code_postal` | Parent 1 (titulaire token) | Champs racine |
|
|
||||||
| `co_parent_prenom`, `co_parent_nom`, `co_parent_telephone` | Parent 2 | |
|
|
||||||
| `co_parent_meme_adresse`, `co_parent_adresse`, `co_parent_code_postal`, `co_parent_ville` | Parent 2 adresse | |
|
|
||||||
| `texte_motivation` **ou** `presentation_dossier` | Étape motivation | Les deux alias acceptés |
|
|
||||||
| `enfants[]` | Liste enfants | Voir ci-dessous |
|
|
||||||
|
|
||||||
**Structure `enfants[]` (miroir inscription + `id` obligatoire) :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid-enfant-existant",
|
|
||||||
"prenom": "Emma",
|
|
||||||
"nom": "MARTIN",
|
|
||||||
"date_naissance": "2023-02-15",
|
|
||||||
"date_previsionnelle_naissance": null,
|
|
||||||
"genre": "F",
|
|
||||||
"photo_base64": "data:image/jpeg;base64,…",
|
|
||||||
"photo_filename": "emma.jpg",
|
|
||||||
"grossesse_multiple": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **v1 back :** update par `id` uniquement — pas de création/suppression d’enfant.
|
|
||||||
- Si `id` inconnu pour ce dossier → **400** `Enfant inconnu pour ce dossier : {id}`.
|
|
||||||
- Sans nouvelle photo : ne pas envoyer `photo_base64` (l’existant est conservé).
|
|
||||||
|
|
||||||
### AM — champs à envoyer
|
|
||||||
|
|
||||||
| Champ PATCH | Source |
|
|
||||||
|-------------|--------|
|
|
||||||
| Identité + `photo_url` ou `photo_base64` + `photo_filename` | Étapes 1–2 |
|
|
||||||
| `consentement_photo`, `date_naissance`, `lieu_naissance_ville`, `lieu_naissance_pays` | Identité |
|
|
||||||
| `numero_agrement`, `nir`, `date_agrement` | Pro |
|
|
||||||
| `capacite_accueil`, `places_disponibles` | Pro |
|
|
||||||
| `biographie` | Présentation |
|
|
||||||
|
|
||||||
Validation NIR identique à l’inscription si `nir` fourni.
|
|
||||||
|
|
||||||
### Réponse succès (nouveau format)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Dossier resoumis avec succès. Il est de nouveau en attente de validation.",
|
|
||||||
"statut": "en_attente",
|
|
||||||
"user_id": "uuid",
|
|
||||||
"numero_dossier": "2026-000021"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Code HTTP : **200** (pas de corps `Users` brut comme l’ancien back).
|
|
||||||
|
|
||||||
### Effet métier
|
|
||||||
|
|
||||||
- **Parent :** tous les users `role=parent` avec le même `numero_dossier` passent en `en_attente` ; `token_reprise` invalidé sur **tous** (symétrique refus #110).
|
|
||||||
- **AM :** un seul user.
|
|
||||||
|
|
||||||
### E-mail accusé resoumission (parent)
|
|
||||||
|
|
||||||
Après `PATCH` réussi, un e-mail est envoyé à **chaque parent** du dossier (`sendResoumissionPendingEmail`) :
|
|
||||||
- confirmation de resoumission ;
|
|
||||||
- rappel du **numéro de dossier** ;
|
|
||||||
- mention « en attente de validation ».
|
|
||||||
|
|
||||||
Échec SMTP : logué, **ne bloque pas** la resoumission (même règle que l'inscription initiale).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Fichiers front à modifier (checklist)
|
|
||||||
|
|
||||||
### Modèles
|
|
||||||
|
|
||||||
- [ ] `lib/models/reprise_dossier.dart` — parser `parents[]`, `enfants[]`, `texte_motivation`, champs AM
|
|
||||||
- [ ] Réutiliser ou mapper vers `DossierFamilleEnfant` / structures existantes (#119 admin) si possible
|
|
||||||
|
|
||||||
### Session / préremplissage
|
|
||||||
|
|
||||||
- [ ] `lib/services/reprise_session.dart`
|
|
||||||
- `applyToParent` : remplir parent1/parent2 depuis `parents[]`, enfants, motivation
|
|
||||||
- `applyToAm` : remplir tous les champs AM
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
- [ ] `lib/services/auth_service.dart` — `resoumettreReprise()` : accepter body complet (parent + AM), pas seulement identité
|
|
||||||
- [ ] Étendre `UserRegistrationData` / `AmRegistrationData` helpers `toReprisePatchBody()` si utile
|
|
||||||
|
|
||||||
### Écrans fin de parcours
|
|
||||||
|
|
||||||
- [ ] `parent_register_step5_screen.dart` — PATCH avec co-parent, enfants, motivation
|
|
||||||
- [ ] `am_register_step4_screen.dart` — PATCH avec fiche AM complète
|
|
||||||
|
|
||||||
### Hors scope back (inchangé)
|
|
||||||
|
|
||||||
RIB / IBAN / attestation CAF (étape 5 wizard parent) : **non persistés** — rien à envoyer en reprise.
|
|
||||||
|
|
||||||
### Non implémenté front (ticket #112 initial)
|
|
||||||
|
|
||||||
- [ ] Modale login « J’ai un numéro de dossier » → `POST /auth/reprise-identify` (back prêt, front absent)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Tests manuels suggérés
|
|
||||||
|
|
||||||
1. Refuser un dossier parent complet (≥1 enfant + co-parent + motivation).
|
|
||||||
2. Ouvrir le lien mail `/reprise?token=…`.
|
|
||||||
3. Vérifier dans DevTools que le GET contient `enfants[]` et `texte_motivation`.
|
|
||||||
4. Après branchement front : wizard prérempli sur toutes les étapes.
|
|
||||||
5. Resoumettre → statut `en_attente` pour les deux parents ; dossier visible file validation admin (#119).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Références code back
|
|
||||||
|
|
||||||
```
|
|
||||||
backend/src/routes/auth/dto/reprise-dossier.dto.ts
|
|
||||||
backend/src/routes/auth/dto/resoumettre-reprise.dto.ts
|
|
||||||
backend/src/routes/auth/dto/enfant-reprise.dto.ts
|
|
||||||
backend/src/routes/auth/auth.service.ts → getRepriseDossier, resoumettreReprise
|
|
||||||
backend/src/routes/parents/dto/dossier-famille-complet.dto.ts
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Mini-spec — Rename préfixe `Admin*` dashboard partagé (#155)
|
||||||
|
|
||||||
|
**Ticket** : **#155**
|
||||||
|
**Branche** : `feature/155-rename-admin-prefix-dashboard` (depuis `develop`)
|
||||||
|
**Décision naming** : **option C** — dossier neutre `widgets/dashboard/` + noms **sans** préfixe `Admin`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Principe
|
||||||
|
|
||||||
|
Les widgets partagés **admin + gestionnaire** ne doivent plus s’appeler `Admin*`.
|
||||||
|
On garde `Admin*` seulement là où c’est vraiment le rôle administrateur.
|
||||||
|
|
||||||
|
## Gardé `Admin*` (hors rename)
|
||||||
|
|
||||||
|
| Élément | Raison |
|
||||||
|
|---------|--------|
|
||||||
|
| `AdminManagementWidget` | Onglet **Administrateurs** |
|
||||||
|
| `screens/administrateurs/*` | `AdminDashboardScreen`, `AdminCreateDialog`, `AdminUserFormDialog` |
|
||||||
|
| `EnfantAdminModel` | Modèle API (pas un widget) — hors scope ticket |
|
||||||
|
|
||||||
|
## Renames faits
|
||||||
|
|
||||||
|
| Avant | Après |
|
||||||
|
|-------|--------|
|
||||||
|
| `widgets/admin/common/admin_child_detail_modal.dart` → `AdminChildDetailModal` | `widgets/dashboard/child_detail_modal.dart` → `ChildDetailModal` |
|
||||||
|
| `admin_am_edit_modal` → `AdminAmEditModal` | `am_edit_modal` → `AmEditModal` |
|
||||||
|
| `admin_parent_edit_modal` → `AdminParentEditModal` | `parent_edit_modal` → `ParentEditModal` |
|
||||||
|
| `admin_user_card` → `AdminUserCard` | `user_card` → `UserCard` |
|
||||||
|
| `admin_enfant_user_card` → `AdminEnfantUserCard` | `enfant_user_card` → `EnfantUserCard` |
|
||||||
|
| `admin_am_photo_frame` → `AdminAmPhotoFrame` | `am_photo_frame` → `AmPhotoFrame` |
|
||||||
|
| `admin_am_children_capacity_grid` | `am_children_capacity_grid` → `AmChildrenCapacityGrid` |
|
||||||
|
| `admin_children_affiliation_panel` | `children_affiliation_panel` → `ChildrenAffiliationPanel` |
|
||||||
|
| `admin_select_*` / `AdminSelect*` / `AdminFamilleFoyer` | `select_*` / `Select*` / `FamilleFoyer` |
|
||||||
|
| `admin_status_capsule` | `status_capsule` → `StatusCapsule` |
|
||||||
|
| `admin_list_state` → `AdminListState` | `user_list_state` → `UserListState` |
|
||||||
|
| `admin_detail_modal` → `AdminDetailModal` / `AdminDetailField` | `detail_modal` → `DetailModal` / `DetailField` |
|
||||||
|
| `dashboard_admin.dart` | `user_management_sub_bar.dart` (`DashboardUserManagementSubBar` inchangé) |
|
||||||
|
|
||||||
|
Dossier `widgets/admin/` conserve encore les panels métier (`user_management_panel`, wizards, etc.) + `AdminManagementWidget`.
|
||||||
|
|
||||||
|
**Phase 2** (même ticket #155) : déplacer ces panels → `widgets/dashboard/` — voir [155-suite-move-admin-panels-to-dashboard.md](./155-suite-move-admin-panels-to-dashboard.md).
|
||||||
|
|
||||||
|
## Hors scope
|
||||||
|
|
||||||
|
- Refonte UX modales (ticket dédié)
|
||||||
|
- Rename API / back
|
||||||
|
- Déplacer tout `widgets/admin/` → `widgets/dashboard/` (panels) — possible follow-up
|
||||||
|
|
||||||
|
## Critères
|
||||||
|
|
||||||
|
- [x] Plus de préfixe `Admin` sur les composants **partagés** listés
|
||||||
|
- [ ] Build Flutter / recette dashboard admin + gestionnaire OK
|
||||||
|
- [x] Pas de changement comportemental (rename mécanique)
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# Mini-spec — Déplacer les panels `widgets/admin/` → `widgets/dashboard/`
|
||||||
|
|
||||||
|
**Ticket** : **#155** (phase 2 — même ticket que le rename `Admin*`)
|
||||||
|
**Phase 1** : widgets `Admin*` → `widgets/dashboard/` (déjà sur `feature/155-rename-admin-prefix-dashboard`)
|
||||||
|
**Branche** : poursuivre / rebaser `feature/155-rename-admin-prefix-dashboard` (ou nouvelle branche depuis `develop` après merge phase 1)
|
||||||
|
**Nature** : rename / move mécanique — **zéro** changement UX / métier
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Après la phase 1 (#155), la situation est **hybride** :
|
||||||
|
|
||||||
|
| Emplacement | Contenu |
|
||||||
|
|-------------|---------|
|
||||||
|
| `widgets/dashboard/` | Composants partagés sans préfixe `Admin*` (modales, cartes, selects, sub-bar…) |
|
||||||
|
| `widgets/admin/` | **Panels** du dashboard staff (listes, wizards, validation, shell `UserManagementPanel`…) + `AdminManagementWidget` |
|
||||||
|
|
||||||
|
Le dossier `admin/` laisse encore croire « réservé administrateur », alors que **gestionnaire** consomme les mêmes panels (`GestionnaireDashboardScreen` → `UserManagementPanel`).
|
||||||
|
|
||||||
|
Ce ticket **termine l’option C** au niveau dossier : tout le dashboard staff vit sous `widgets/dashboard/`, sauf ce qui est **vraiment** rôle admin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/lib/widgets/admin/<panels & common partagés>
|
||||||
|
↓ git mv + update imports
|
||||||
|
frontend/lib/widgets/dashboard/…
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère : un nouveau dev ne doit plus ouvrir `widgets/admin/` pour du code partagé admin+gestionnaire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cible d’arborescence (proposée)
|
||||||
|
|
||||||
|
```
|
||||||
|
widgets/dashboard/
|
||||||
|
├── (déjà là #155) child_detail_modal.dart, am_edit_modal.dart, user_card.dart, …
|
||||||
|
├── user_management_panel.dart ← shell onglets
|
||||||
|
├── user_management_sub_bar.dart ← déjà déplacé #155
|
||||||
|
├── dossiers_management_widget.dart
|
||||||
|
├── dossier_list_card.dart
|
||||||
|
├── parent_management_widget.dart ← corriger le typo managmant au passage ?
|
||||||
|
├── enfant_management_widget.dart
|
||||||
|
├── assistante_maternelle_management_widget.dart
|
||||||
|
├── gestionnaire_management_widget.dart
|
||||||
|
├── pending_validation_widget.dart
|
||||||
|
├── parent_dossier_create_modal.dart
|
||||||
|
├── parent_dossier_wizard.dart
|
||||||
|
├── am_dossier_create_modal.dart
|
||||||
|
├── am_dossier_wizard.dart
|
||||||
|
├── validation_*.dart ← family/am wizards, refus, theme, confirm
|
||||||
|
├── parametres_panel.dart ← utilisé par écran admin (OK dans dashboard)
|
||||||
|
├── relais_management_panel.dart
|
||||||
|
├── common/ ← sous-dossier optionnel
|
||||||
|
│ ├── suppression_confirm_dialog.dart
|
||||||
|
│ ├── user_list.dart
|
||||||
|
│ └── validation_detail_section.dart
|
||||||
|
└── …
|
||||||
|
|
||||||
|
widgets/admin/ ← mince, rôle admin seulement
|
||||||
|
└── admin_management_widget.dart ← onglet Administrateurs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variante B (plus stricte)
|
||||||
|
|
||||||
|
`AdminManagementWidget` + éventuels helpers purement admin →
|
||||||
|
`screens/administrateurs/widgets/`
|
||||||
|
et **suppression** du dossier `widgets/admin/`.
|
||||||
|
|
||||||
|
**Reco** : **variante A** (garder `widgets/admin/` minimal avec seulement `AdminManagementWidget`) — moins de churn screens, clair.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inventaire à déplacer (état actuel)
|
||||||
|
|
||||||
|
### Racine `widgets/admin/` → `widgets/dashboard/`
|
||||||
|
|
||||||
|
| Fichier actuel | Notes |
|
||||||
|
|----------------|--------|
|
||||||
|
| `user_management_panel.dart` | Shell partagé admin + gestionnaire |
|
||||||
|
| `dossiers_management_widget.dart` | |
|
||||||
|
| `dossier_list_card.dart` | |
|
||||||
|
| `parent_managmant_widget.dart` | Typo historique `managmant` — **option** : renommer → `parent_management_widget.dart` dans le même ticket ou ticket typo séparé |
|
||||||
|
| `enfant_management_widget.dart` | |
|
||||||
|
| `assistante_maternelle_management_widget.dart` | |
|
||||||
|
| `gestionnaire_management_widget.dart` | |
|
||||||
|
| `pending_validation_widget.dart` | |
|
||||||
|
| `parent_dossier_create_modal.dart` | |
|
||||||
|
| `parent_dossier_wizard.dart` | |
|
||||||
|
| `am_dossier_create_modal.dart` | |
|
||||||
|
| `am_dossier_wizard.dart` | |
|
||||||
|
| `validation_am_wizard.dart` | |
|
||||||
|
| `validation_family_wizard.dart` | |
|
||||||
|
| `validation_dossier_modal.dart` | |
|
||||||
|
| `validation_modal_theme.dart` | |
|
||||||
|
| `validation_refus_form.dart` | |
|
||||||
|
| `validation_valider_confirm_dialog.dart` | |
|
||||||
|
| `parametres_panel.dart` | Écran admin seulement, mais pas préfixé Admin — OK dashboard |
|
||||||
|
| `relais_management_panel.dart` | |
|
||||||
|
|
||||||
|
### `widgets/admin/common/` → `widgets/dashboard/common/` (ou plat)
|
||||||
|
|
||||||
|
| Fichier | Notes |
|
||||||
|
|---------|--------|
|
||||||
|
| `suppression_confirm_dialog.dart` | Partagé (y compris `screens/administrateurs/creation/*`) |
|
||||||
|
| `user_list.dart` | |
|
||||||
|
| `validation_detail_section.dart` | |
|
||||||
|
|
||||||
|
### **Ne pas** déplacer
|
||||||
|
|
||||||
|
| Fichier | Destination |
|
||||||
|
|---------|-------------|
|
||||||
|
| `admin_management_widget.dart` | Reste `widgets/admin/` (ou variante B → screens) |
|
||||||
|
|
||||||
|
### Déjà fait (#155) — ne pas retraiter
|
||||||
|
|
||||||
|
Tout ce qui est déjà sous `widgets/dashboard/` (`child_detail_modal`, `am_edit_modal`, `user_card`, `select_*`, `user_management_sub_bar`, …).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consommateurs d’imports (à mettre à jour)
|
||||||
|
|
||||||
|
### Screens
|
||||||
|
- `screens/administrateurs/admin_dashboardScreen.dart` — `UserManagementPanel`, `ParametresPanel`
|
||||||
|
- `screens/gestionnaire/gestionnaire_dashboard_screen.dart` — `UserManagementPanel`
|
||||||
|
- `screens/administrateurs/creation/admin_create.dart` — `suppression_confirm_dialog`
|
||||||
|
- `screens/administrateurs/creation/gestionnaires_create.dart` — idem
|
||||||
|
|
||||||
|
### Widgets déjà en `dashboard/`
|
||||||
|
- `am_edit_modal`, `child_detail_modal`, `parent_edit_modal`, `select_*` — imports vers `widgets/admin/common/*` ou panels
|
||||||
|
|
||||||
|
### Divers
|
||||||
|
- `widgets/common/identity_block.dart` (si import admin)
|
||||||
|
- Tous les fichiers **déplacés** entre eux (imports relatifs / package)
|
||||||
|
|
||||||
|
### Hors scope rename classes
|
||||||
|
Sauf décision explicite sur le typo `parent_managmant_widget` → pas de rename de **classes** métier dans ce ticket (seulement chemins de fichiers + imports).
|
||||||
|
`AdminManagementWidget` **conserve** son nom.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plan d’exécution
|
||||||
|
|
||||||
|
1. Partir de `feature/155-rename-admin-prefix-dashboard` (phase 1) **ou** `develop` si phase 1 déjà mergée
|
||||||
|
2. `git mv` fichiers selon inventaire
|
||||||
|
3. Remplacer globalement
|
||||||
|
`package:p_tits_pas/widgets/admin/` → `package:p_tits_pas/widgets/dashboard/`
|
||||||
|
**sauf** `…/widgets/admin/admin_management_widget.dart`
|
||||||
|
4. Corriger imports relatifs cassés
|
||||||
|
5. Grep de contrôle (ci-dessous)
|
||||||
|
6. Build Flutter web (Docker) + smoke dashboard admin **et** gestionnaire
|
||||||
|
7. Merge → squash master si flux habituel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vérifs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Plus de panels partagés sous admin (seul AdminManagement attendu)
|
||||||
|
find frontend/lib/widgets/admin -name '*.dart'
|
||||||
|
|
||||||
|
# Plus d’imports panels vers l’ancien chemin (sauf AdminManagement)
|
||||||
|
rg -n "widgets/admin/(user_management|dossiers_|parent_|enfant_|assistante|gestionnaire|pending|validation_|parametres|relais|am_dossier|parent_dossier|dossier_list|common/)" frontend/lib
|
||||||
|
|
||||||
|
# Screens OK
|
||||||
|
rg -n "widgets/admin/" frontend/lib/screens
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu screens : **0** hit vers panels ; éventuellement plus aucun hit `widgets/admin/` sauf si import explicite `AdminManagementWidget` depuis `user_management_panel` (chemin `widgets/admin/admin_management_widget.dart`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hors scope
|
||||||
|
|
||||||
|
- Refonte UX des modales / panels (ticket dédié annoncé)
|
||||||
|
- Rename `EnfantAdminModel`
|
||||||
|
- Rename `screens/administrateurs/`
|
||||||
|
- Rename `AdminUserFormDialog` / `AdminCreateDialog`
|
||||||
|
- Changement API / back
|
||||||
|
- #152 (`est_multiple`) — autre branche
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critères d’acceptation
|
||||||
|
|
||||||
|
- [ ] Inventaire déplacé selon tableau
|
||||||
|
- [ ] `widgets/admin/` ne contient plus que `admin_management_widget.dart` (variante A)
|
||||||
|
- [ ] Imports screens + widgets à jour
|
||||||
|
- [ ] Build Flutter OK
|
||||||
|
- [ ] Recette : dashboard **administrateur** et **gestionnaire** (listes, ouverture fiches, validation, création dossier) sans régression
|
||||||
|
- [ ] Aucun changement comportemental volontaire
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risques / notes
|
||||||
|
|
||||||
|
- **Conflits de merge** si d’autres features touchent les panels → faire ce ticket quand la surface dashboard est calme (fin 0.1.0 OK)
|
||||||
|
- Typo `parent_managmant_widget` : soit inclus (bonus), soit ticket cleanup 1-ligne séparé
|
||||||
|
- Docs d’archive citant `widgets/admin/…` : pas obligatoire de mettre à jour ; `docs/27_BRIEFING-FRONTEND.md` oui si encore listé
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Mini-spec — Uniformisation modale staff (Gestionnaire / Administrateur)
|
||||||
|
|
||||||
|
**Ticket** : **#164** — https://git.ptits-pas.fr/jmartin/petitspas/issues/164
|
||||||
|
**Branche** : `feature/164-staff-modal-uniformisation` (depuis `develop`)
|
||||||
|
**Périmètre** : **front only** — pas d’API / BDD
|
||||||
|
**Milestone** : **0.1.0**
|
||||||
|
|
||||||
|
Voir le corps du ticket #164 pour la spec complète.
|
||||||
|
|
||||||
|
## Livré
|
||||||
|
|
||||||
|
- `frontend/lib/widgets/dashboard/staff_user_form_modal.dart` → `StaffUserFormModal`
|
||||||
|
- Ancien `AdminUserFormDialog` / `gestionnaires_create.dart` retiré
|
||||||
|
- Imports : `user_management_panel`, `gestionnaire_management_widget`, `admin_management_widget`
|
||||||
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:p_tits_pas/services/configuration_service.dart';
|
import 'package:p_tits_pas/services/configuration_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_management_sub_bar.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parametres_panel.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parametres_panel.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/user_management_panel.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_management_panel.dart';
|
||||||
import 'package:p_tits_pas/widgets/app_footer.dart';
|
import 'package:p_tits_pas/widgets/app_footer.dart';
|
||||||
import 'package:p_tits_pas/widgets/dashboard/dashboard_bandeau.dart';
|
import 'package:p_tits_pas/widgets/dashboard/dashboard_bandeau.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:p_tits_pas/utils/phone_utils.dart';
|
|||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/email_text_field.dart';
|
import 'package:p_tits_pas/widgets/email_text_field.dart';
|
||||||
import 'package:p_tits_pas/widgets/french_phone_field.dart';
|
import 'package:p_tits_pas/widgets/french_phone_field.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,675 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:p_tits_pas/models/relais_model.dart';
|
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
|
||||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/email_text_field.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/french_phone_field.dart';
|
|
||||||
import 'package:p_tits_pas/models/user.dart';
|
|
||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
|
||||||
import 'package:p_tits_pas/services/relais_service.dart';
|
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
|
||||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
|
||||||
|
|
||||||
class AdminUserFormDialog extends StatefulWidget {
|
|
||||||
final AppUser? initialUser;
|
|
||||||
final bool withRelais;
|
|
||||||
final bool adminMode;
|
|
||||||
final bool readOnly;
|
|
||||||
|
|
||||||
const AdminUserFormDialog({
|
|
||||||
super.key,
|
|
||||||
this.initialUser,
|
|
||||||
this.withRelais = true,
|
|
||||||
this.adminMode = false,
|
|
||||||
this.readOnly = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AdminUserFormDialog> createState() => _AdminUserFormDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
final _nomController = TextEditingController();
|
|
||||||
final _prenomController = TextEditingController();
|
|
||||||
final _emailController = TextEditingController();
|
|
||||||
final _passwordController = TextEditingController();
|
|
||||||
final _telephoneController = TextEditingController();
|
|
||||||
final _passwordToggleFocusNode =
|
|
||||||
FocusNode(skipTraversal: true, canRequestFocus: false);
|
|
||||||
|
|
||||||
bool _isSubmitting = false;
|
|
||||||
bool _obscurePassword = true;
|
|
||||||
bool _isLoadingRelais = true;
|
|
||||||
List<RelaisModel> _relais = [];
|
|
||||||
String? _selectedRelaisId;
|
|
||||||
String? _currentUserId;
|
|
||||||
String? _currentUserRole;
|
|
||||||
bool get _isEditMode => widget.initialUser != null;
|
|
||||||
bool get _isSuperAdminTarget =>
|
|
||||||
(widget.initialUser?.role ?? '').toLowerCase() == 'super_admin';
|
|
||||||
bool get _isSelfTarget =>
|
|
||||||
_isEditMode &&
|
|
||||||
_currentUserId != null &&
|
|
||||||
widget.initialUser!.id == _currentUserId;
|
|
||||||
/// Gestionnaire : pas de delete staff. Admin+ seulement (#154 / #160).
|
|
||||||
bool get _canDeleteTarget {
|
|
||||||
if (!_isEditMode || widget.readOnly) return false;
|
|
||||||
if (_isSelfTarget || _isSuperAdminTarget) return false;
|
|
||||||
return canDeleteGestionnaire(_currentUserRole);
|
|
||||||
}
|
|
||||||
bool get _isLockedAdminIdentity =>
|
|
||||||
_isEditMode && widget.adminMode && _isSuperAdminTarget;
|
|
||||||
String get _targetRoleKey {
|
|
||||||
if (widget.initialUser != null) {
|
|
||||||
return (widget.initialUser!.role).toLowerCase();
|
|
||||||
}
|
|
||||||
return widget.adminMode ? 'administrateur' : 'gestionnaire';
|
|
||||||
}
|
|
||||||
|
|
||||||
String get _targetRoleLabel {
|
|
||||||
switch (_targetRoleKey) {
|
|
||||||
case 'super_admin':
|
|
||||||
return 'Super administrateur';
|
|
||||||
case 'administrateur':
|
|
||||||
return 'Administrateur';
|
|
||||||
case 'gestionnaire':
|
|
||||||
return 'Gestionnaire';
|
|
||||||
case 'assistante_maternelle':
|
|
||||||
return 'Assistante maternelle';
|
|
||||||
case 'parent':
|
|
||||||
return 'Parent';
|
|
||||||
default:
|
|
||||||
return 'Utilisateur';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData get _targetRoleIcon {
|
|
||||||
switch (_targetRoleKey) {
|
|
||||||
case 'super_admin':
|
|
||||||
return Icons.verified_user_outlined;
|
|
||||||
case 'administrateur':
|
|
||||||
return Icons.admin_panel_settings_outlined;
|
|
||||||
case 'gestionnaire':
|
|
||||||
return Icons.assignment_ind_outlined;
|
|
||||||
case 'assistante_maternelle':
|
|
||||||
return Icons.child_care_outlined;
|
|
||||||
case 'parent':
|
|
||||||
return Icons.supervisor_account_outlined;
|
|
||||||
default:
|
|
||||||
return Icons.person_outline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final user = widget.initialUser;
|
|
||||||
if (user != null) {
|
|
||||||
_nomController.text = user.nom ?? '';
|
|
||||||
_prenomController.text = user.prenom ?? '';
|
|
||||||
_emailController.text = user.email;
|
|
||||||
_telephoneController.text = formatPhoneForDisplay(user.telephone ?? '');
|
|
||||||
// En édition, on ne préremplit jamais le mot de passe.
|
|
||||||
_passwordController.clear();
|
|
||||||
final initialRelaisId = user.relaisId?.trim();
|
|
||||||
_selectedRelaisId =
|
|
||||||
(initialRelaisId == null || initialRelaisId.isEmpty)
|
|
||||||
? null
|
|
||||||
: initialRelaisId;
|
|
||||||
}
|
|
||||||
if (widget.withRelais) {
|
|
||||||
_loadRelais();
|
|
||||||
} else {
|
|
||||||
_isLoadingRelais = false;
|
|
||||||
}
|
|
||||||
_loadCurrentUserId();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadCurrentUserId() async {
|
|
||||||
final cached = await AuthService.getCurrentUser();
|
|
||||||
if (!mounted) return;
|
|
||||||
if (cached != null) {
|
|
||||||
setState(() {
|
|
||||||
_currentUserId = cached.id;
|
|
||||||
_currentUserRole = cached.role;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final refreshed = await AuthService.refreshCurrentUser();
|
|
||||||
if (!mounted || refreshed == null) return;
|
|
||||||
setState(() {
|
|
||||||
_currentUserId = refreshed.id;
|
|
||||||
_currentUserRole = refreshed.role;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nomController.dispose();
|
|
||||||
_prenomController.dispose();
|
|
||||||
_emailController.dispose();
|
|
||||||
_passwordController.dispose();
|
|
||||||
_telephoneController.dispose();
|
|
||||||
_passwordToggleFocusNode.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fallback si GET /relais échoue : conserve le relais déjà connu sur l'utilisateur.
|
|
||||||
List<RelaisModel> _fallbackRelaisFromUser() {
|
|
||||||
final id = _selectedRelaisId?.trim();
|
|
||||||
if (id == null || id.isEmpty) return const [];
|
|
||||||
final nom = (widget.initialUser?.relaisNom ?? '').trim();
|
|
||||||
return [
|
|
||||||
RelaisModel(
|
|
||||||
id: id,
|
|
||||||
nom: nom.isNotEmpty ? nom : 'Relais actuel',
|
|
||||||
adresse: '',
|
|
||||||
actif: true,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadRelais() async {
|
|
||||||
try {
|
|
||||||
final list = await RelaisService.getRelais();
|
|
||||||
if (!mounted) return;
|
|
||||||
final uniqueById = <String, RelaisModel>{};
|
|
||||||
for (final relais in list) {
|
|
||||||
uniqueById[relais.id] = relais;
|
|
||||||
}
|
|
||||||
|
|
||||||
final filtered = uniqueById.values.where((r) => r.actif).toList();
|
|
||||||
if (_selectedRelaisId != null &&
|
|
||||||
!filtered.any((r) => r.id == _selectedRelaisId)) {
|
|
||||||
final selected = uniqueById[_selectedRelaisId!];
|
|
||||||
if (selected != null) {
|
|
||||||
filtered.add(selected);
|
|
||||||
} else {
|
|
||||||
// Garder l'id sélectionné et afficher un item de secours (nom carte).
|
|
||||||
filtered.addAll(_fallbackRelaisFromUser());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_relais = filtered;
|
|
||||||
_isLoadingRelais = false;
|
|
||||||
});
|
|
||||||
} catch (_) {
|
|
||||||
if (!mounted) return;
|
|
||||||
// Ne pas nullifier _selectedRelaisId (#151) — la carte a déjà le bon libellé.
|
|
||||||
setState(() {
|
|
||||||
_relais = _fallbackRelaisFromUser();
|
|
||||||
_isLoadingRelais = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _required(String? value, String field) {
|
|
||||||
if (value == null || value.trim().isEmpty) {
|
|
||||||
return '$field est requis';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _validateEmail(String? value) {
|
|
||||||
final base = _required(value, 'Email');
|
|
||||||
if (base != null) {
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
return validateEmail(value, allowEmpty: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _validatePassword(String? value) {
|
|
||||||
if (_isEditMode && (value == null || value.trim().isEmpty)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final base = _required(value, 'Mot de passe');
|
|
||||||
if (base != null) return base;
|
|
||||||
if (value!.trim().length < 6) return 'Minimum 6 caractères';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _validatePhone(String? value) {
|
|
||||||
if (_isEditMode && (value == null || value.trim().isEmpty)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final base = _required(value, 'Téléphone');
|
|
||||||
if (base != null) {
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
return validateFrenchNationalPhone(value, allowEmpty: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _toTitleCase(String raw) {
|
|
||||||
final trimmed = raw.trim();
|
|
||||||
if (trimmed.isEmpty) return trimmed;
|
|
||||||
final words = trimmed.split(RegExp(r'\s+'));
|
|
||||||
final normalizedWords = words.map(_capitalizeComposedWord).toList();
|
|
||||||
return normalizedWords.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
String _capitalizeComposedWord(String word) {
|
|
||||||
if (word.isEmpty) return word;
|
|
||||||
final lower = word.toLowerCase();
|
|
||||||
final separators = <String>{"-", "'", "’"};
|
|
||||||
final buffer = StringBuffer();
|
|
||||||
var capitalizeNext = true;
|
|
||||||
|
|
||||||
for (var i = 0; i < lower.length; i++) {
|
|
||||||
final char = lower[i];
|
|
||||||
if (capitalizeNext && RegExp(r'[a-zà-öø-ÿ]').hasMatch(char)) {
|
|
||||||
buffer.write(char.toUpperCase());
|
|
||||||
capitalizeNext = false;
|
|
||||||
} else {
|
|
||||||
buffer.write(char);
|
|
||||||
capitalizeNext = separators.contains(char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _submit() async {
|
|
||||||
if (widget.readOnly) return;
|
|
||||||
if (_isSubmitting) return;
|
|
||||||
if (!_formKey.currentState!.validate()) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isSubmitting = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
final normalizedNom = _toTitleCase(_nomController.text);
|
|
||||||
final normalizedPrenom = _toTitleCase(_prenomController.text);
|
|
||||||
final normalizedPhone = normalizePhone(_telephoneController.text);
|
|
||||||
final passwordProvided = _passwordController.text.trim().isNotEmpty;
|
|
||||||
|
|
||||||
if (_isEditMode) {
|
|
||||||
if (widget.adminMode) {
|
|
||||||
final lockedNom = _toTitleCase(widget.initialUser!.nom ?? '');
|
|
||||||
final lockedPrenom = _toTitleCase(widget.initialUser!.prenom ?? '');
|
|
||||||
await UserService.updateAdministrateur(
|
|
||||||
adminId: widget.initialUser!.id,
|
|
||||||
nom: _isLockedAdminIdentity ? lockedNom : normalizedNom,
|
|
||||||
prenom: _isLockedAdminIdentity ? lockedPrenom : normalizedPrenom,
|
|
||||||
email: _emailController.text.trim(),
|
|
||||||
telephone: normalizedPhone.isEmpty
|
|
||||||
? normalizePhone(widget.initialUser!.telephone ?? '')
|
|
||||||
: normalizedPhone,
|
|
||||||
password: passwordProvided ? _passwordController.text : null,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final currentUser = widget.initialUser!;
|
|
||||||
final initialNom = _toTitleCase(currentUser.nom ?? '');
|
|
||||||
final initialPrenom = _toTitleCase(currentUser.prenom ?? '');
|
|
||||||
final initialEmail = currentUser.email.trim();
|
|
||||||
final initialPhone = normalizePhone(currentUser.telephone ?? '');
|
|
||||||
|
|
||||||
final onlyRelaisChanged =
|
|
||||||
normalizedNom == initialNom &&
|
|
||||||
normalizedPrenom == initialPrenom &&
|
|
||||||
_emailController.text.trim() == initialEmail &&
|
|
||||||
normalizedPhone == initialPhone &&
|
|
||||||
!passwordProvided;
|
|
||||||
|
|
||||||
if (onlyRelaisChanged) {
|
|
||||||
await UserService.updateGestionnaireRelais(
|
|
||||||
gestionnaireId: currentUser.id,
|
|
||||||
relaisId: _selectedRelaisId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await UserService.updateGestionnaire(
|
|
||||||
gestionnaireId: currentUser.id,
|
|
||||||
nom: normalizedNom,
|
|
||||||
prenom: normalizedPrenom,
|
|
||||||
email: _emailController.text.trim(),
|
|
||||||
telephone: normalizedPhone.isEmpty ? initialPhone : normalizedPhone,
|
|
||||||
relaisId: _selectedRelaisId,
|
|
||||||
password: passwordProvided ? _passwordController.text : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (widget.adminMode) {
|
|
||||||
await UserService.createAdministrateur(
|
|
||||||
nom: normalizedNom,
|
|
||||||
prenom: normalizedPrenom,
|
|
||||||
email: _emailController.text.trim(),
|
|
||||||
password: _passwordController.text,
|
|
||||||
telephone: normalizePhone(_telephoneController.text),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await UserService.createGestionnaire(
|
|
||||||
nom: normalizedNom,
|
|
||||||
prenom: normalizedPrenom,
|
|
||||||
email: _emailController.text.trim(),
|
|
||||||
password: _passwordController.text,
|
|
||||||
telephone: normalizePhone(_telephoneController.text),
|
|
||||||
relaisId: _selectedRelaisId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
_isEditMode
|
|
||||||
? (widget.adminMode
|
|
||||||
? 'Administrateur modifié avec succès.'
|
|
||||||
: 'Gestionnaire modifié avec succès.')
|
|
||||||
: (widget.adminMode
|
|
||||||
? 'Administrateur créé avec succès.'
|
|
||||||
: 'Gestionnaire créé avec succès.'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop(true);
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
e.toString().replaceFirst('Exception: ', ''),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.red.shade700,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_isSubmitting = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _delete() async {
|
|
||||||
if (widget.readOnly) return;
|
|
||||||
if (!_canDeleteTarget) return;
|
|
||||||
if (!_isEditMode || _isSubmitting) return;
|
|
||||||
|
|
||||||
final name = widget.initialUser!.fullName.isEmpty
|
|
||||||
? widget.initialUser!.email
|
|
||||||
: widget.initialUser!.fullName;
|
|
||||||
final confirmed = await showSuppressionConfirmDialog(
|
|
||||||
context,
|
|
||||||
title: widget.adminMode
|
|
||||||
? 'Supprimer l\'administrateur'
|
|
||||||
: 'Supprimer le gestionnaire',
|
|
||||||
people: [
|
|
||||||
widget.adminMode
|
|
||||||
? SuppressionPersonLine.administrateur(name)
|
|
||||||
: SuppressionPersonLine.gestionnaire(name),
|
|
||||||
],
|
|
||||||
footnotes: const [
|
|
||||||
'Le compte sera définitivement supprimé.',
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isSubmitting = true;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await UserService.deleteUser(widget.initialUser!.id);
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Gestionnaire supprimé.')),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop(true);
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(e.toString().replaceFirst('Exception: ', '')),
|
|
||||||
backgroundColor: Colors.red.shade700,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
_isSubmitting = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 16,
|
|
||||||
backgroundColor: const Color(0xFFEDE5FA),
|
|
||||||
child: Icon(
|
|
||||||
_targetRoleIcon,
|
|
||||||
size: 20,
|
|
||||||
color: const Color(0xFF6B3FA0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
_isEditMode
|
|
||||||
? (widget.readOnly
|
|
||||||
? 'Consulter un "$_targetRoleLabel"'
|
|
||||||
: 'Modifier un "$_targetRoleLabel"')
|
|
||||||
: 'Créer un "$_targetRoleLabel"',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_isEditMode && !widget.readOnly)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
tooltip: 'Fermer',
|
|
||||||
onPressed: _isSubmitting
|
|
||||||
? null
|
|
||||||
: () => Navigator.of(context).pop(false),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: SizedBox(
|
|
||||||
width: 620,
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildPrenomField()),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(child: _buildNomField()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildEmailField(),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildPasswordField()),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(child: _buildTelephoneField()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (widget.withRelais) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildRelaisField(),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
if (widget.readOnly) ...[
|
|
||||||
FilledButton(
|
|
||||||
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false),
|
|
||||||
child: const Text('Fermer'),
|
|
||||||
),
|
|
||||||
] else if (_isEditMode) ...[
|
|
||||||
if (_canDeleteTarget)
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed: _isSubmitting ? null : _delete,
|
|
||||||
style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700),
|
|
||||||
child: const Text('Supprimer'),
|
|
||||||
),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: _isSubmitting ? null : _submit,
|
|
||||||
icon: _isSubmitting
|
|
||||||
? const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.edit),
|
|
||||||
label: Text(_isSubmitting ? 'Modification...' : 'Modifier'),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed:
|
|
||||||
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
|
||||||
child: const Text('Annuler'),
|
|
||||||
),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: _isSubmitting ? null : _submit,
|
|
||||||
icon: _isSubmitting
|
|
||||||
? const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.person_add_alt_1),
|
|
||||||
label: Text(_isSubmitting ? 'Création...' : 'Créer'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildNomField() {
|
|
||||||
return TextFormField(
|
|
||||||
controller: _nomController,
|
|
||||||
readOnly: widget.readOnly || _isLockedAdminIdentity,
|
|
||||||
textCapitalization: TextCapitalization.words,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Nom',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
validator: (widget.readOnly || _isLockedAdminIdentity)
|
|
||||||
? null
|
|
||||||
: (v) => _required(v, 'Nom'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPrenomField() {
|
|
||||||
return TextFormField(
|
|
||||||
controller: _prenomController,
|
|
||||||
readOnly: widget.readOnly || _isLockedAdminIdentity,
|
|
||||||
textCapitalization: TextCapitalization.words,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Prénom',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
validator: (widget.readOnly || _isLockedAdminIdentity)
|
|
||||||
? null
|
|
||||||
: (v) => _required(v, 'Prénom'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmailField() {
|
|
||||||
return EmailTextFormField(
|
|
||||||
controller: _emailController,
|
|
||||||
readOnly: widget.readOnly,
|
|
||||||
label: 'Email',
|
|
||||||
validator: widget.readOnly ? null : _validateEmail,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPasswordField() {
|
|
||||||
return TextFormField(
|
|
||||||
controller: _passwordController,
|
|
||||||
readOnly: widget.readOnly,
|
|
||||||
obscureText: _obscurePassword,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autocorrect: false,
|
|
||||||
autofillHints: _isEditMode
|
|
||||||
? const <String>[]
|
|
||||||
: const [AutofillHints.newPassword],
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: _isEditMode
|
|
||||||
? 'Nouveau mot de passe'
|
|
||||||
: 'Mot de passe',
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
suffixIcon: widget.readOnly
|
|
||||||
? null
|
|
||||||
: ExcludeFocus(
|
|
||||||
child: IconButton(
|
|
||||||
focusNode: _passwordToggleFocusNode,
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_obscurePassword = !_obscurePassword;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
validator: widget.readOnly ? null : _validatePassword,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTelephoneField() {
|
|
||||||
return FrenchPhoneTextFormField(
|
|
||||||
controller: _telephoneController,
|
|
||||||
readOnly: widget.readOnly,
|
|
||||||
label: 'Téléphone (ex: 06 12 34 56 78)',
|
|
||||||
validator: widget.readOnly ? null : _validatePhone,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRelaisField() {
|
|
||||||
final selectedValue = _selectedRelaisId != null &&
|
|
||||||
_relais.any((relais) => relais.id == _selectedRelaisId)
|
|
||||||
? _selectedRelaisId
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
DropdownButtonFormField<String?>(
|
|
||||||
isExpanded: true,
|
|
||||||
value: selectedValue,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Relais principal',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
const DropdownMenuItem<String?>(
|
|
||||||
value: null,
|
|
||||||
child: Text('Aucun relais'),
|
|
||||||
),
|
|
||||||
..._relais.map(
|
|
||||||
(relais) => DropdownMenuItem<String?>(
|
|
||||||
value: relais.id,
|
|
||||||
child: Text(relais.nom),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: (_isLoadingRelais || widget.readOnly)
|
|
||||||
? null
|
|
||||||
: (value) {
|
|
||||||
setState(() {
|
|
||||||
_selectedRelaisId = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_isLoadingRelais) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
const LinearProgressIndicator(minHeight: 2),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/user_management_panel.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_management_panel.dart';
|
||||||
import 'package:p_tits_pas/widgets/app_footer.dart';
|
import 'package:p_tits_pas/widgets/app_footer.dart';
|
||||||
import 'package:p_tits_pas/widgets/dashboard/dashboard_bandeau.dart';
|
import 'package:p_tits_pas/widgets/dashboard/dashboard_bandeau.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart';
|
import 'package:p_tits_pas/widgets/dashboard/staff_user_form_modal.dart';
|
||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||||
|
|
||||||
class AdminManagementWidget extends StatefulWidget {
|
class AdminManagementWidget extends StatefulWidget {
|
||||||
final String searchQuery;
|
final String searchQuery;
|
||||||
@@ -100,7 +100,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
return AdminUserFormDialog(
|
return StaffUserFormModal(
|
||||||
initialUser: user,
|
initialUser: user,
|
||||||
adminMode: true,
|
adminMode: true,
|
||||||
withRelais: false,
|
withRelais: false,
|
||||||
@@ -183,7 +183,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
|||||||
final isSuperAdmin = _isSuperAdmin(user);
|
final isSuperAdmin = _isSuperAdmin(user);
|
||||||
final canEdit = _canEditAdmin(user);
|
final canEdit = _canEditAdmin(user);
|
||||||
final canDelete = _canDeleteAdmin(user);
|
final canDelete = _canDeleteAdmin(user);
|
||||||
return AdminUserCard(
|
return UserCard(
|
||||||
title: user.fullName,
|
title: user.fullName,
|
||||||
fallbackIcon: isSuperAdmin
|
fallbackIcon: isSuperAdmin
|
||||||
? Icons.verified_user_outlined
|
? Icons.verified_user_outlined
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:p_tits_pas/models/dossier_unifie.dart';
|
|||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/validation_detail_section.dart';
|
||||||
|
|
||||||
/// Valeurs affichées dans un [IdentityBlock] en lecture seule.
|
/// Valeurs affichées dans un [IdentityBlock] en lecture seule.
|
||||||
class IdentityValues {
|
class IdentityValues {
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ import 'package:p_tits_pas/utils/date_display_utils.dart';
|
|||||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
|
|
||||||
/// Grille 2×2 des places d'accueil AM (max 4, limitée à [capacity]).
|
/// Grille 2×2 des places d'accueil AM (max 4, limitée à [capacity]).
|
||||||
class AdminAmChildrenCapacityGrid extends StatelessWidget {
|
class AmChildrenCapacityGrid extends StatelessWidget {
|
||||||
static const int _gridSlots = 4;
|
static const int _gridSlots = 4;
|
||||||
static const double _slotHeight = 44;
|
static const double _slotHeight = 44;
|
||||||
static const double _gridPadding = 10;
|
static const double _gridPadding = 10;
|
||||||
@@ -23,7 +23,7 @@ class AdminAmChildrenCapacityGrid extends StatelessWidget {
|
|||||||
/// Clic sur une case libre → même flux que « Rattacher un enfant » (#149).
|
/// Clic sur une case libre → même flux que « Rattacher un enfant » (#149).
|
||||||
final VoidCallback? onAttachEmpty;
|
final VoidCallback? onAttachEmpty;
|
||||||
|
|
||||||
const AdminAmChildrenCapacityGrid({
|
const AmChildrenCapacityGrid({
|
||||||
super.key,
|
super.key,
|
||||||
required this.children,
|
required this.children,
|
||||||
required this.capacity,
|
required this.capacity,
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/am_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_dossier_wizard.dart';
|
||||||
|
|
||||||
/// Modale de création dossier AM (#156) — même shell que [ValidationDossierModal].
|
/// Modale de création dossier AM (#156) — même shell que [ValidationDossierModal].
|
||||||
class AmDossierCreateModal extends StatefulWidget {
|
class AmDossierCreateModal extends StatefulWidget {
|
||||||
+18
-18
@@ -14,12 +14,12 @@ import 'package:p_tits_pas/utils/name_format_utils.dart';
|
|||||||
import 'package:p_tits_pas/utils/nir_utils.dart';
|
import 'package:p_tits_pas/utils/nir_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/postal_utils.dart';
|
import 'package:p_tits_pas/utils/postal_utils.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_photo_frame.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/detail_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/validation_detail_section.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_refus_form.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_refus_form.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_valider_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_valider_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
||||||
|
|
||||||
@@ -221,32 +221,32 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
|||||||
|
|
||||||
void _emitStep() => widget.onStepChanged?.call(_step, _stepCount);
|
void _emitStep() => widget.onStepChanged?.call(_step, _stepCount);
|
||||||
|
|
||||||
List<AdminDetailField> _photoProFields(DossierAM d) {
|
List<DetailField> _photoProFields(DossierAM d) {
|
||||||
final u = d.user;
|
final u = d.user;
|
||||||
return [
|
return [
|
||||||
AdminDetailField(label: 'NIR', value: _formatNirForDisplay(d.nir)),
|
DetailField(label: 'NIR', value: _formatNirForDisplay(d.nir)),
|
||||||
AdminDetailField(
|
DetailField(
|
||||||
label: 'Date de naissance',
|
label: 'Date de naissance',
|
||||||
value: formatIsoDateFr(u.dateNaissance),
|
value: formatIsoDateFr(u.dateNaissance),
|
||||||
),
|
),
|
||||||
AdminDetailField(
|
DetailField(
|
||||||
label: 'Ville de naissance',
|
label: 'Ville de naissance',
|
||||||
value: _v(u.lieuNaissanceVille),
|
value: _v(u.lieuNaissanceVille),
|
||||||
),
|
),
|
||||||
AdminDetailField(
|
DetailField(
|
||||||
label: 'Pays de naissance',
|
label: 'Pays de naissance',
|
||||||
value: _v(u.lieuNaissancePays),
|
value: _v(u.lieuNaissancePays),
|
||||||
),
|
),
|
||||||
AdminDetailField(label: 'N° Agrément', value: _v(d.numeroAgrement)),
|
DetailField(label: 'N° Agrément', value: _v(d.numeroAgrement)),
|
||||||
AdminDetailField(
|
DetailField(
|
||||||
label: 'Date d’agrément',
|
label: 'Date d’agrément',
|
||||||
value: formatIsoDateFr(d.dateAgrement),
|
value: formatIsoDateFr(d.dateAgrement),
|
||||||
),
|
),
|
||||||
AdminDetailField(
|
DetailField(
|
||||||
label: 'Capacité max (enfants)',
|
label: 'Capacité max (enfants)',
|
||||||
value: d.nbMaxEnfants != null ? d.nbMaxEnfants.toString() : '–',
|
value: d.nbMaxEnfants != null ? d.nbMaxEnfants.toString() : '–',
|
||||||
),
|
),
|
||||||
AdminDetailField(
|
DetailField(
|
||||||
label: 'Places disponibles',
|
label: 'Places disponibles',
|
||||||
value: d.placesDisponibles != null
|
value: d.placesDisponibles != null
|
||||||
? d.placesDisponibles.toString()
|
? d.placesDisponibles.toString()
|
||||||
@@ -741,14 +741,14 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
|||||||
|
|
||||||
Widget _buildStep1() {
|
Widget _buildStep1() {
|
||||||
// Modale calée sur les TF ; photo étirée sur toute la hauteur utile
|
// Modale calée sur les TF ; photo étirée sur toute la hauteur utile
|
||||||
// (largeur = [AdminAmPhotoFrame.columnWidthForHeight], cadre inclus).
|
// (largeur = [AmPhotoFrame.columnWidthForHeight], cadre inclus).
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, c) {
|
builder: (context, c) {
|
||||||
final maxRowW = c.maxWidth;
|
final maxRowW = c.maxWidth;
|
||||||
final maxRowH = c.maxHeight.clamp(0.0, double.infinity);
|
final maxRowH = c.maxHeight.clamp(0.0, double.infinity);
|
||||||
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
|
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
|
||||||
.clamp(0.0, double.infinity);
|
.clamp(0.0, double.infinity);
|
||||||
var photoW = AdminAmPhotoFrame.columnWidthForHeight(maxRowH)
|
var photoW = AmPhotoFrame.columnWidthForHeight(maxRowH)
|
||||||
.clamp(_photoColumnMinWidth, 360.0);
|
.clamp(_photoColumnMinWidth, 360.0);
|
||||||
if (photoW > maxPhotoW) photoW = maxPhotoW;
|
if (photoW > maxPhotoW) photoW = maxPhotoW;
|
||||||
|
|
||||||
@@ -762,7 +762,7 @@ class _AmDossierWizardState extends State<AmDossierWizard> {
|
|||||||
|
|
||||||
final Widget photo;
|
final Widget photo;
|
||||||
if (_isCreate) {
|
if (_isCreate) {
|
||||||
photo = AdminAmPhotoFrame(
|
photo = AmPhotoFrame(
|
||||||
imageBytes: _photoBytes,
|
imageBytes: _photoBytes,
|
||||||
onTap: _pickPhoto,
|
onTap: _pickPhoto,
|
||||||
onClear: _photoBytes != null ? _clearPhoto : null,
|
onClear: _photoBytes != null ? _clearPhoto : null,
|
||||||
+17
-17
@@ -7,31 +7,31 @@ import 'package:p_tits_pas/utils/date_display_utils.dart';
|
|||||||
import 'package:p_tits_pas/utils/nir_utils.dart';
|
import 'package:p_tits_pas/utils/nir_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_children_capacity_grid.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_children_capacity_grid.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_photo_frame.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/child_detail_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_select_enfant_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/select_enfant_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_status_capsule.dart';
|
import 'package:p_tits_pas/widgets/dashboard/status_capsule.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/validation_detail_section.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart';
|
||||||
|
|
||||||
/// Fiche AM éditable (ticket #131) — identité | fiche pro (photo) | enfants.
|
/// Fiche AM éditable (ticket #131) — identité | fiche pro (photo) | enfants.
|
||||||
class AdminAmEditModal extends StatefulWidget {
|
class AmEditModal extends StatefulWidget {
|
||||||
final AssistanteMaternelleModel assistante;
|
final AssistanteMaternelleModel assistante;
|
||||||
final VoidCallback? onSaved;
|
final VoidCallback? onSaved;
|
||||||
|
|
||||||
const AdminAmEditModal({
|
const AmEditModal({
|
||||||
super.key,
|
super.key,
|
||||||
required this.assistante,
|
required this.assistante,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AdminAmEditModal> createState() => _AdminAmEditModalState();
|
State<AmEditModal> createState() => _AmEditModalState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdminAmEditModalState extends State<AdminAmEditModal>
|
class _AmEditModalState extends State<AmEditModal>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late final TabController _tabCtrl;
|
late final TabController _tabCtrl;
|
||||||
late final TextEditingController _nomCtrl;
|
late final TextEditingController _nomCtrl;
|
||||||
@@ -404,7 +404,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AdminChildDetailModal(
|
builder: (ctx) => ChildDetailModal(
|
||||||
enfant: enfant,
|
enfant: enfant,
|
||||||
onSaved: _refreshChildrenDetails,
|
onSaved: _refreshChildrenDetails,
|
||||||
),
|
),
|
||||||
@@ -450,7 +450,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
|
|
||||||
Future<void> _attachChild() async {
|
Future<void> _attachChild() async {
|
||||||
if (_capacityFull || !mounted) return;
|
if (_capacityFull || !mounted) return;
|
||||||
final selected = await AdminSelectEnfantModal.show(
|
final selected = await SelectEnfantModal.show(
|
||||||
context,
|
context,
|
||||||
excludeIds: _children.map((c) => c.id).toSet(),
|
excludeIds: _children.map((c) => c.id).toSet(),
|
||||||
title: 'Rattacher un enfant',
|
title: 'Rattacher un enfant',
|
||||||
@@ -589,7 +589,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
final maxRowH =
|
final maxRowH =
|
||||||
c.maxHeight.isFinite ? c.maxHeight : _proTabHeight;
|
c.maxHeight.isFinite ? c.maxHeight : _proTabHeight;
|
||||||
final bodyH = maxRowH;
|
final bodyH = maxRowH;
|
||||||
final idealPhotoW = bodyH * AdminAmPhotoFrame.idPhotoAspectRatio + 16;
|
final idealPhotoW = bodyH * AmPhotoFrame.idPhotoAspectRatio + 16;
|
||||||
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
|
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
|
||||||
.clamp(0.0, double.infinity);
|
.clamp(0.0, double.infinity);
|
||||||
var photoW = idealPhotoW.clamp(_photoColumnMinWidth, 220.0);
|
var photoW = idealPhotoW.clamp(_photoColumnMinWidth, 220.0);
|
||||||
@@ -600,7 +600,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: photoW,
|
width: photoW,
|
||||||
child: AdminAmPhotoFrame(
|
child: AmPhotoFrame(
|
||||||
photoUrl: widget.assistante.user.photoUrl,
|
photoUrl: widget.assistante.user.photoUrl,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -728,7 +728,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
AdminAmChildrenCapacityGrid(
|
AmChildrenCapacityGrid(
|
||||||
children: _children,
|
children: _children,
|
||||||
capacity: capacity,
|
capacity: capacity,
|
||||||
onOpen: _openChild,
|
onOpen: _openChild,
|
||||||
@@ -820,7 +820,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 160,
|
width: 160,
|
||||||
child: AdminStatusCapsule(
|
child: StatusCapsule(
|
||||||
statut: _statut,
|
statut: _statut,
|
||||||
onChanged: (v) => setState(() {
|
onChanged: (v) => setState(() {
|
||||||
_statut = v;
|
_statut = v;
|
||||||
+2
-2
@@ -5,7 +5,7 @@ import 'package:p_tits_pas/services/api/api_config.dart';
|
|||||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
|
|
||||||
/// Cadre photo identité AM / enfant (35×45 mm) — même logique que [ValidationAmWizard].
|
/// Cadre photo identité AM / enfant (35×45 mm) — même logique que [ValidationAmWizard].
|
||||||
class AdminAmPhotoFrame extends StatelessWidget {
|
class AmPhotoFrame extends StatelessWidget {
|
||||||
final String? photoUrl;
|
final String? photoUrl;
|
||||||
final Uint8List? imageBytes;
|
final Uint8List? imageBytes;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
@@ -14,7 +14,7 @@ class AdminAmPhotoFrame extends StatelessWidget {
|
|||||||
|
|
||||||
static const double idPhotoAspectRatio = 35 / 45;
|
static const double idPhotoAspectRatio = 35 / 45;
|
||||||
|
|
||||||
const AdminAmPhotoFrame({
|
const AmPhotoFrame({
|
||||||
super.key,
|
super.key,
|
||||||
this.photoUrl,
|
this.photoUrl,
|
||||||
this.imageBytes,
|
this.imageBytes,
|
||||||
+6
-6
@@ -5,10 +5,10 @@ import 'package:p_tits_pas/utils/am_vigilance.dart';
|
|||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_edit_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||||
|
|
||||||
class AssistanteMaternelleManagementWidget extends StatefulWidget {
|
class AssistanteMaternelleManagementWidget extends StatefulWidget {
|
||||||
final String searchQuery;
|
final String searchQuery;
|
||||||
@@ -130,7 +130,7 @@ class _AssistanteMaternelleManagementWidgetState
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final assistante = filteredAssistantes[index];
|
final assistante = filteredAssistantes[index];
|
||||||
final vigilance = amPlacesVigilanceMessage(assistante);
|
final vigilance = amPlacesVigilanceMessage(assistante);
|
||||||
return AdminUserCard(
|
return UserCard(
|
||||||
title: assistante.user.fullName,
|
title: assistante.user.fullName,
|
||||||
avatarUrl: assistante.user.photoUrl,
|
avatarUrl: assistante.user.photoUrl,
|
||||||
fallbackIcon: Icons.face,
|
fallbackIcon: Icons.face,
|
||||||
@@ -161,7 +161,7 @@ class _AssistanteMaternelleManagementWidgetState
|
|||||||
void _openAssistanteDetails(AssistanteMaternelleModel assistante) {
|
void _openAssistanteDetails(AssistanteMaternelleModel assistante) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AdminAmEditModal(
|
builder: (context) => AmEditModal(
|
||||||
assistante: assistante,
|
assistante: assistante,
|
||||||
onSaved: _loadAssistantes,
|
onSaved: _loadAssistantes,
|
||||||
),
|
),
|
||||||
+20
-20
@@ -11,24 +11,24 @@ import 'package:p_tits_pas/utils/date_display_utils.dart';
|
|||||||
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_edit_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_photo_frame.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_parent_edit_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_edit_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_select_am_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/select_am_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_select_famille_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/select_famille_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/validation_detail_section.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
|
|
||||||
/// Fiche enfant consultation / édition (#138) ou création (#132).
|
/// Fiche enfant consultation / édition (#138) ou création (#132).
|
||||||
class AdminChildDetailModal extends StatefulWidget {
|
class ChildDetailModal extends StatefulWidget {
|
||||||
final EnfantAdminModel? enfant;
|
final EnfantAdminModel? enfant;
|
||||||
final VoidCallback? onSaved;
|
final VoidCallback? onSaved;
|
||||||
final VoidCallback? onDeleted;
|
final VoidCallback? onDeleted;
|
||||||
final bool isCreating;
|
final bool isCreating;
|
||||||
|
|
||||||
const AdminChildDetailModal({
|
const ChildDetailModal({
|
||||||
super.key,
|
super.key,
|
||||||
required EnfantAdminModel this.enfant,
|
required EnfantAdminModel this.enfant,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
@@ -36,7 +36,7 @@ class AdminChildDetailModal extends StatefulWidget {
|
|||||||
}) : isCreating = false;
|
}) : isCreating = false;
|
||||||
|
|
||||||
/// Création depuis l'onglet Enfants (#132).
|
/// Création depuis l'onglet Enfants (#132).
|
||||||
const AdminChildDetailModal.create({
|
const ChildDetailModal.create({
|
||||||
super.key,
|
super.key,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
}) : enfant = null,
|
}) : enfant = null,
|
||||||
@@ -44,10 +44,10 @@ class AdminChildDetailModal extends StatefulWidget {
|
|||||||
isCreating = true;
|
isCreating = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AdminChildDetailModal> createState() => _AdminChildDetailModalState();
|
State<ChildDetailModal> createState() => _ChildDetailModalState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
class _ChildDetailModalState extends State<ChildDetailModal> {
|
||||||
late final TextEditingController _prenomCtrl;
|
late final TextEditingController _prenomCtrl;
|
||||||
late final TextEditingController _nomCtrl;
|
late final TextEditingController _nomCtrl;
|
||||||
late final TextEditingController _birthCtrl;
|
late final TextEditingController _birthCtrl;
|
||||||
@@ -65,7 +65,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
String? _baselineAmUserId;
|
String? _baselineAmUserId;
|
||||||
|
|
||||||
/// Famille choisie en mode création (#132).
|
/// Famille choisie en mode création (#132).
|
||||||
AdminFamilleFoyer? _selectedFamily;
|
FamilleFoyer? _selectedFamily;
|
||||||
|
|
||||||
/// Liens parents locaux (édition) — mis à jour après rattachement foyer (#157).
|
/// Liens parents locaux (édition) — mis à jour après rattachement foyer (#157).
|
||||||
List<EnfantParentLink>? _localParentLinks;
|
List<EnfantParentLink>? _localParentLinks;
|
||||||
@@ -283,7 +283,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AdminParentEditModal(
|
builder: (ctx) => ParentEditModal(
|
||||||
parent: parent,
|
parent: parent,
|
||||||
onSaved: () => widget.onSaved?.call(),
|
onSaved: () => widget.onSaved?.call(),
|
||||||
),
|
),
|
||||||
@@ -624,7 +624,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
if (am == null) return;
|
if (am == null) return;
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AdminAmEditModal(
|
builder: (ctx) => AmEditModal(
|
||||||
assistante: am,
|
assistante: am,
|
||||||
onSaved: () async {
|
onSaved: () async {
|
||||||
await _reloadPlacementFromServer();
|
await _reloadPlacementFromServer();
|
||||||
@@ -676,7 +676,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final selected = await AdminSelectAmModal.show(
|
final selected = await SelectAmModal.show(
|
||||||
context,
|
context,
|
||||||
excludeIds: {
|
excludeIds: {
|
||||||
if (_linkedAm != null) _linkedAm!.user.id,
|
if (_linkedAm != null) _linkedAm!.user.id,
|
||||||
@@ -986,7 +986,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
final maxRowW = c.maxWidth;
|
final maxRowW = c.maxWidth;
|
||||||
final maxRowH = c.maxHeight;
|
final maxRowH = c.maxHeight;
|
||||||
final idealPhotoW =
|
final idealPhotoW =
|
||||||
maxRowH * AdminAmPhotoFrame.idPhotoAspectRatio + 16;
|
maxRowH * AmPhotoFrame.idPhotoAspectRatio + 16;
|
||||||
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
|
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
|
||||||
.clamp(0.0, double.infinity);
|
.clamp(0.0, double.infinity);
|
||||||
var photoW = idealPhotoW.clamp(_photoColumnMinWidth, _photoColumnMaxWidth);
|
var photoW = idealPhotoW.clamp(_photoColumnMinWidth, _photoColumnMaxWidth);
|
||||||
@@ -1001,7 +1001,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AdminAmPhotoFrame(
|
child: AmPhotoFrame(
|
||||||
photoUrl: widget.isCreating
|
photoUrl: widget.isCreating
|
||||||
? null
|
? null
|
||||||
: widget.enfant?.photoUrl,
|
: widget.enfant?.photoUrl,
|
||||||
@@ -1329,7 +1329,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
|
|
||||||
Future<void> _pickFamily() async {
|
Future<void> _pickFamily() async {
|
||||||
if (_busy) return;
|
if (_busy) return;
|
||||||
final selected = await AdminSelectFamilleModal.show(
|
final selected = await SelectFamilleModal.show(
|
||||||
context,
|
context,
|
||||||
title: 'Choisir une famille',
|
title: 'Choisir une famille',
|
||||||
);
|
);
|
||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/parent_child_summary.dart';
|
import 'package:p_tits_pas/models/parent_child_summary.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/enfant_user_card.dart';
|
||||||
|
|
||||||
/// Liste scrollable d'enfants rattachés (fiche parent / fiche AM).
|
/// Liste scrollable d'enfants rattachés (fiche parent / fiche AM).
|
||||||
class AdminChildrenAffiliationPanel extends StatelessWidget {
|
class ChildrenAffiliationPanel extends StatelessWidget {
|
||||||
final List<ParentChildSummary> children;
|
final List<ParentChildSummary> children;
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
final void Function(ParentChildSummary child) onOpen;
|
final void Function(ParentChildSummary child) onOpen;
|
||||||
@@ -14,7 +14,7 @@ class AdminChildrenAffiliationPanel extends StatelessWidget {
|
|||||||
static const double _itemHeight = 58;
|
static const double _itemHeight = 58;
|
||||||
static const double defaultViewportHeight = _itemHeight * 2.5 + 8;
|
static const double defaultViewportHeight = _itemHeight * 2.5 + 8;
|
||||||
|
|
||||||
const AdminChildrenAffiliationPanel({
|
const ChildrenAffiliationPanel({
|
||||||
super.key,
|
super.key,
|
||||||
required this.children,
|
required this.children,
|
||||||
required this.scrollController,
|
required this.scrollController,
|
||||||
@@ -62,7 +62,7 @@ class AdminChildrenAffiliationPanel extends StatelessWidget {
|
|||||||
itemCount: children.length,
|
itemCount: children.length,
|
||||||
itemBuilder: (_, i) {
|
itemBuilder: (_, i) {
|
||||||
final c = children[i];
|
final c = children[i];
|
||||||
return AdminEnfantUserCard.fromSummary(
|
return EnfantUserCard.fromSummary(
|
||||||
c,
|
c,
|
||||||
onCardTap: () => onOpen(c),
|
onCardTap: () => onOpen(c),
|
||||||
actions: [
|
actions: [
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_list_state.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_list_state.dart';
|
||||||
|
|
||||||
class UserList extends StatelessWidget {
|
class UserList extends StatelessWidget {
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
@@ -28,7 +28,7 @@ class UserList extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
AdminListState(
|
UserListState(
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
error: error,
|
error: error,
|
||||||
isEmpty: isEmpty,
|
isEmpty: isEmpty,
|
||||||
+2
-2
@@ -4,7 +4,7 @@ import 'package:p_tits_pas/utils/email_utils.dart';
|
|||||||
import 'package:p_tits_pas/utils/nir_utils.dart';
|
import 'package:p_tits_pas/utils/nir_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/postal_utils.dart';
|
import 'package:p_tits_pas/utils/postal_utils.dart';
|
||||||
import 'admin_detail_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/detail_modal.dart';
|
||||||
|
|
||||||
/// Réglages des formulaires validation / wizard AM — **jouer sur ces 3 leviers**.
|
/// Réglages des formulaires validation / wizard AM — **jouer sur ces 3 leviers**.
|
||||||
class ValidationFormMetrics {
|
class ValidationFormMetrics {
|
||||||
@@ -60,7 +60,7 @@ class ValidationFormMetrics {
|
|||||||
class ValidationDetailSection extends StatelessWidget {
|
class ValidationDetailSection extends StatelessWidget {
|
||||||
/// Si null ou vide, pas de bandeau titre (gain de place vertical, ex. wizard AM).
|
/// Si null ou vide, pas de bandeau titre (gain de place vertical, ex. wizard AM).
|
||||||
final String? title;
|
final String? title;
|
||||||
final List<AdminDetailField> fields;
|
final List<DetailField> fields;
|
||||||
|
|
||||||
/// Nombre de champs par ligne (1 = plein largeur, 2 = deux côte à côte). Ex. [2, 2, 1, 2] pour identité.
|
/// Nombre de champs par ligne (1 = plein largeur, 2 = deux côte à côte). Ex. [2, 2, 1, 2] pour identité.
|
||||||
final List<int>? rowLayout;
|
final List<int>? rowLayout;
|
||||||
+5
-5
@@ -1,23 +1,23 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AdminDetailField {
|
class DetailField {
|
||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String value;
|
||||||
|
|
||||||
const AdminDetailField({
|
const DetailField({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.value,
|
required this.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class AdminDetailModal extends StatelessWidget {
|
class DetailModal extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
final List<AdminDetailField> fields;
|
final List<DetailField> fields;
|
||||||
final VoidCallback onEdit;
|
final VoidCallback onEdit;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
|
|
||||||
const AdminDetailModal({
|
const DetailModal({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
|
|
||||||
/// Carte dossier unifiée (#153) — fond neutre, accent couleur sur l’icône.
|
/// Carte dossier unifiée (#153) — fond neutre, accent couleur sur l’icône.
|
||||||
class DossierListCard extends StatelessWidget {
|
class DossierListCard extends StatelessWidget {
|
||||||
@@ -54,7 +54,7 @@ class DossierListCard extends StatelessWidget {
|
|||||||
if (!emptyKids && countLabel != null) countLabel,
|
if (!emptyKids && countLabel != null) countLabel,
|
||||||
];
|
];
|
||||||
|
|
||||||
return AdminUserCard(
|
return UserCard(
|
||||||
title: num,
|
title: num,
|
||||||
subtitleLines: subtitle,
|
subtitleLines: subtitle,
|
||||||
avatarUrl: !isFamille && avatar.isNotEmpty ? avatar : null,
|
avatarUrl: !isFamille && avatar.isNotEmpty ? avatar : null,
|
||||||
+4
-4
@@ -3,10 +3,10 @@ import 'package:p_tits_pas/models/dossier_list_item.dart';
|
|||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dossier_list_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/dossier_list_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/pending_validation_widget.dart';
|
import 'package:p_tits_pas/widgets/dashboard/pending_validation_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_dossier_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_dossier_modal.dart';
|
||||||
|
|
||||||
/// Onglet permanent « Dossiers » (#153) : pending en haut + liste unifiée en bas.
|
/// Onglet permanent « Dossiers » (#153) : pending en haut + liste unifiée en bas.
|
||||||
class DossiersManagementWidget extends StatefulWidget {
|
class DossiersManagementWidget extends StatefulWidget {
|
||||||
+6
-6
@@ -5,10 +5,10 @@ import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
|||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/child_detail_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/enfant_user_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||||
|
|
||||||
/// Onglet liste globale des enfants (doc 28 §6.2, ticket #137).
|
/// Onglet liste globale des enfants (doc 28 §6.2, ticket #137).
|
||||||
class EnfantManagementWidget extends StatefulWidget {
|
class EnfantManagementWidget extends StatefulWidget {
|
||||||
@@ -68,7 +68,7 @@ class _EnfantManagementWidgetState extends State<EnfantManagementWidget> {
|
|||||||
Future<void> _openEnfant(EnfantAdminModel enfant) async {
|
Future<void> _openEnfant(EnfantAdminModel enfant) async {
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AdminChildDetailModal(
|
builder: (ctx) => ChildDetailModal(
|
||||||
enfant: enfant,
|
enfant: enfant,
|
||||||
onSaved: _loadEnfants,
|
onSaved: _loadEnfants,
|
||||||
onDeleted: _loadEnfants,
|
onDeleted: _loadEnfants,
|
||||||
@@ -224,7 +224,7 @@ class _EnfantManagementWidgetState extends State<EnfantManagementWidget> {
|
|||||||
itemCount: filtered.length,
|
itemCount: filtered.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final enfant = filtered[index];
|
final enfant = filtered[index];
|
||||||
return AdminEnfantUserCard.fromEnfant(
|
return EnfantUserCard.fromEnfant(
|
||||||
enfant,
|
enfant,
|
||||||
onCardTap: () => _openEnfant(enfant),
|
onCardTap: () => _openEnfant(enfant),
|
||||||
actions: [
|
actions: [
|
||||||
+8
-8
@@ -4,7 +4,7 @@ import 'package:p_tits_pas/models/parent_child_summary.dart';
|
|||||||
import 'package:p_tits_pas/utils/date_display_utils.dart';
|
import 'package:p_tits_pas/utils/date_display_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/enfant_vigilance.dart';
|
import 'package:p_tits_pas/utils/enfant_vigilance.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_card.dart';
|
||||||
|
|
||||||
List<String> enfantAdminSubtitleLines({
|
List<String> enfantAdminSubtitleLines({
|
||||||
required String status,
|
required String status,
|
||||||
@@ -29,7 +29,7 @@ List<String> enfantAdminSubtitleLines({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Carte enfant admin (photo, nom, âge) — même rendu onglet Enfants / fiche parent.
|
/// Carte enfant admin (photo, nom, âge) — même rendu onglet Enfants / fiche parent.
|
||||||
class AdminEnfantUserCard extends StatelessWidget {
|
class EnfantUserCard extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String? photoUrl;
|
final String? photoUrl;
|
||||||
final List<String> subtitleLines;
|
final List<String> subtitleLines;
|
||||||
@@ -40,7 +40,7 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
final Color? borderColor;
|
final Color? borderColor;
|
||||||
final String? vigilanceTooltip;
|
final String? vigilanceTooltip;
|
||||||
|
|
||||||
const AdminEnfantUserCard({
|
const EnfantUserCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.photoUrl,
|
this.photoUrl,
|
||||||
@@ -53,7 +53,7 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
this.vigilanceTooltip,
|
this.vigilanceTooltip,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AdminEnfantUserCard.fromEnfant(
|
factory EnfantUserCard.fromEnfant(
|
||||||
EnfantAdminModel enfant, {
|
EnfantAdminModel enfant, {
|
||||||
List<String> extraSubtitleLines = const [],
|
List<String> extraSubtitleLines = const [],
|
||||||
List<Widget> actions = const [],
|
List<Widget> actions = const [],
|
||||||
@@ -67,7 +67,7 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
.map((l) => l.parentName ?? 'Parent')
|
.map((l) => l.parentName ?? 'Parent')
|
||||||
.join(', ');
|
.join(', ');
|
||||||
final orphan = enfantHasNoResponsable(enfant);
|
final orphan = enfantHasNoResponsable(enfant);
|
||||||
return AdminEnfantUserCard(
|
return EnfantUserCard(
|
||||||
title: enfant.fullName,
|
title: enfant.fullName,
|
||||||
photoUrl: enfant.photoUrl,
|
photoUrl: enfant.photoUrl,
|
||||||
subtitleLines: enfantAdminSubtitleLines(
|
subtitleLines: enfantAdminSubtitleLines(
|
||||||
@@ -92,7 +92,7 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AdminEnfantUserCard.fromSummary(
|
factory EnfantUserCard.fromSummary(
|
||||||
ParentChildSummary child, {
|
ParentChildSummary child, {
|
||||||
List<String> extraSubtitleLines = const [],
|
List<String> extraSubtitleLines = const [],
|
||||||
List<Widget> actions = const [],
|
List<Widget> actions = const [],
|
||||||
@@ -100,7 +100,7 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
EdgeInsetsGeometry? margin,
|
EdgeInsetsGeometry? margin,
|
||||||
EdgeInsetsGeometry? contentPadding,
|
EdgeInsetsGeometry? contentPadding,
|
||||||
}) {
|
}) {
|
||||||
return AdminEnfantUserCard(
|
return EnfantUserCard(
|
||||||
title: child.fullName,
|
title: child.fullName,
|
||||||
photoUrl: child.photoUrl,
|
photoUrl: child.photoUrl,
|
||||||
subtitleLines: enfantAdminSubtitleLines(
|
subtitleLines: enfantAdminSubtitleLines(
|
||||||
@@ -118,7 +118,7 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AdminUserCard(
|
return UserCard(
|
||||||
title: title,
|
title: title,
|
||||||
fallbackIcon: Icons.child_care_outlined,
|
fallbackIcon: Icons.child_care_outlined,
|
||||||
avatarUrl: photoUrl,
|
avatarUrl: photoUrl,
|
||||||
+6
-6
@@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart';
|
import 'package:p_tits_pas/widgets/dashboard/staff_user_form_modal.dart';
|
||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||||
|
|
||||||
class GestionnaireManagementWidget extends StatefulWidget {
|
class GestionnaireManagementWidget extends StatefulWidget {
|
||||||
final String searchQuery;
|
final String searchQuery;
|
||||||
@@ -74,7 +74,7 @@ class _GestionnaireManagementWidgetState
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
return AdminUserFormDialog(initialUser: user);
|
return StaffUserFormModal(initialUser: user);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (changed == true) {
|
if (changed == true) {
|
||||||
@@ -141,7 +141,7 @@ class _GestionnaireManagementWidgetState
|
|||||||
final user = filteredGestionnaires[index];
|
final user = filteredGestionnaires[index];
|
||||||
final isSelf =
|
final isSelf =
|
||||||
_currentUserId != null && _currentUserId == user.id;
|
_currentUserId != null && _currentUserId == user.id;
|
||||||
return AdminUserCard(
|
return UserCard(
|
||||||
title: user.fullName,
|
title: user.fullName,
|
||||||
fallbackIcon: Icons.assignment_ind_outlined,
|
fallbackIcon: Icons.assignment_ind_outlined,
|
||||||
avatarUrl: user.photoUrl,
|
avatarUrl: user.photoUrl,
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:p_tits_pas/services/configuration_service.dart';
|
import 'package:p_tits_pas/services/configuration_service.dart';
|
||||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/relais_management_panel.dart';
|
import 'package:p_tits_pas/widgets/dashboard/relais_management_panel.dart';
|
||||||
|
|
||||||
/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé.
|
/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé.
|
||||||
class ParametresPanel extends StatefulWidget {
|
class ParametresPanel extends StatefulWidget {
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_dossier_wizard.dart';
|
||||||
|
|
||||||
/// Modale de création dossier famille (#129) — même shell que [AmDossierCreateModal].
|
/// Modale de création dossier famille (#129) — même shell que [AmDossierCreateModal].
|
||||||
class ParentDossierCreateModal extends StatefulWidget {
|
class ParentDossierCreateModal extends StatefulWidget {
|
||||||
+6
-6
@@ -14,11 +14,11 @@ import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
|||||||
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/postal_utils.dart';
|
import 'package:p_tits_pas/utils/postal_utils.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_photo_frame.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/validation_detail_section.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_refus_form.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_refus_form.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_valider_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_valider_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
||||||
|
|
||||||
@@ -860,7 +860,7 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: pw,
|
width: pw,
|
||||||
height: ph,
|
height: ph,
|
||||||
child: AdminAmPhotoFrame(
|
child: AmPhotoFrame(
|
||||||
photoUrl: child.photoBytes == null
|
photoUrl: child.photoBytes == null
|
||||||
? child.existingPhotoUrl
|
? child.existingPhotoUrl
|
||||||
: null,
|
: null,
|
||||||
+14
-14
@@ -4,30 +4,30 @@ import 'package:p_tits_pas/models/parent_child_summary.dart';
|
|||||||
import 'package:p_tits_pas/models/parent_model.dart';
|
import 'package:p_tits_pas/models/parent_model.dart';
|
||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/child_detail_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_children_affiliation_panel.dart';
|
import 'package:p_tits_pas/widgets/dashboard/children_affiliation_panel.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_select_enfant_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/select_enfant_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_status_capsule.dart';
|
import 'package:p_tits_pas/widgets/dashboard/status_capsule.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart';
|
||||||
|
|
||||||
/// Fiche parent éditable (doc 28 §6.1, tickets #131 / #138).
|
/// Fiche parent éditable (doc 28 §6.1, tickets #131 / #138).
|
||||||
/// Shell et typo alignés sur [ValidationDossierModal] / wizards validation.
|
/// Shell et typo alignés sur [ValidationDossierModal] / wizards validation.
|
||||||
class AdminParentEditModal extends StatefulWidget {
|
class ParentEditModal extends StatefulWidget {
|
||||||
final ParentModel parent;
|
final ParentModel parent;
|
||||||
final VoidCallback? onSaved;
|
final VoidCallback? onSaved;
|
||||||
|
|
||||||
const AdminParentEditModal({
|
const ParentEditModal({
|
||||||
super.key,
|
super.key,
|
||||||
required this.parent,
|
required this.parent,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AdminParentEditModal> createState() => _AdminParentEditModalState();
|
State<ParentEditModal> createState() => _ParentEditModalState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
class _ParentEditModalState extends State<ParentEditModal> {
|
||||||
late final TextEditingController _nomCtrl;
|
late final TextEditingController _nomCtrl;
|
||||||
late final TextEditingController _prenomCtrl;
|
late final TextEditingController _prenomCtrl;
|
||||||
late final TextEditingController _emailCtrl;
|
late final TextEditingController _emailCtrl;
|
||||||
@@ -129,7 +129,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AdminParentEditModal(
|
builder: (ctx) => ParentEditModal(
|
||||||
parent: parent,
|
parent: parent,
|
||||||
onSaved: () async {
|
onSaved: () async {
|
||||||
try {
|
try {
|
||||||
@@ -244,7 +244,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AdminChildDetailModal(
|
builder: (ctx) => ChildDetailModal(
|
||||||
enfant: enfant,
|
enfant: enfant,
|
||||||
onSaved: _reloadChildren,
|
onSaved: _reloadChildren,
|
||||||
),
|
),
|
||||||
@@ -322,7 +322,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
|
|
||||||
Future<void> _attachChild() async {
|
Future<void> _attachChild() async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final selected = await AdminSelectEnfantModal.show(
|
final selected = await SelectEnfantModal.show(
|
||||||
context,
|
context,
|
||||||
excludeIds: _children.map((c) => c.id).toSet(),
|
excludeIds: _children.map((c) => c.id).toSet(),
|
||||||
title: 'Rattacher un enfant',
|
title: 'Rattacher un enfant',
|
||||||
@@ -350,7 +350,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _childrenPanel() {
|
Widget _childrenPanel() {
|
||||||
return AdminChildrenAffiliationPanel(
|
return ChildrenAffiliationPanel(
|
||||||
children: _children,
|
children: _children,
|
||||||
scrollController: _childrenScrollCtrl,
|
scrollController: _childrenScrollCtrl,
|
||||||
onOpen: _openChild,
|
onOpen: _openChild,
|
||||||
@@ -437,7 +437,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 160,
|
width: 160,
|
||||||
child: AdminStatusCapsule(
|
child: StatusCapsule(
|
||||||
statut: _statut,
|
statut: _statut,
|
||||||
onChanged: (v) => setState(() {
|
onChanged: (v) => setState(() {
|
||||||
_statut = v;
|
_statut = v;
|
||||||
+6
-6
@@ -4,10 +4,10 @@ import 'package:p_tits_pas/models/parent_model.dart';
|
|||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_parent_edit_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_edit_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||||
|
|
||||||
class ParentManagementWidget extends StatefulWidget {
|
class ParentManagementWidget extends StatefulWidget {
|
||||||
final String searchQuery;
|
final String searchQuery;
|
||||||
@@ -146,7 +146,7 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
|
|||||||
itemCount: filteredParents.length,
|
itemCount: filteredParents.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final parent = filteredParents[index];
|
final parent = filteredParents[index];
|
||||||
return AdminUserCard(
|
return UserCard(
|
||||||
title: parent.user.fullName,
|
title: parent.user.fullName,
|
||||||
fallbackIcon: Icons.supervisor_account_outlined,
|
fallbackIcon: Icons.supervisor_account_outlined,
|
||||||
avatarUrl: parent.user.photoUrl,
|
avatarUrl: parent.user.photoUrl,
|
||||||
@@ -189,7 +189,7 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
|
|||||||
void _openParentDetails(ParentModel parent) {
|
void _openParentDetails(ParentModel parent) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AdminParentEditModal(
|
builder: (context) => ParentEditModal(
|
||||||
parent: parent,
|
parent: parent,
|
||||||
onSaved: _loadParents,
|
onSaved: _loadParents,
|
||||||
),
|
),
|
||||||
+3
-3
@@ -3,9 +3,9 @@ import 'package:p_tits_pas/models/dossier_list_item.dart';
|
|||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/models/pending_family.dart';
|
import 'package:p_tits_pas/models/pending_family.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dossier_list_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/dossier_list_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_dossier_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_dossier_modal.dart';
|
||||||
|
|
||||||
/// Section « dossiers à valider » (liste unifiée AM + familles). Ticket #107 / #153.
|
/// Section « dossiers à valider » (liste unifiée AM + familles). Ticket #107 / #153.
|
||||||
class PendingValidationWidget extends StatefulWidget {
|
class PendingValidationWidget extends StatefulWidget {
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:p_tits_pas/models/relais_model.dart';
|
import 'package:p_tits_pas/models/relais_model.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/services/relais_service.dart';
|
import 'package:p_tits_pas/services/relais_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
|
|
||||||
class RelaisManagementPanel extends StatefulWidget {
|
class RelaisManagementPanel extends StatefulWidget {
|
||||||
const RelaisManagementPanel({super.key});
|
const RelaisManagementPanel({super.key});
|
||||||
+11
-11
@@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/am_vigilance.dart';
|
import 'package:p_tits_pas/utils/am_vigilance.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_edit_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_select_list_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/select_list_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart';
|
||||||
|
|
||||||
List<String> _amSelectSubtitleLines(AssistanteMaternelleModel am) {
|
List<String> _amSelectSubtitleLines(AssistanteMaternelleModel am) {
|
||||||
final lines = <String>[];
|
final lines = <String>[];
|
||||||
@@ -28,22 +28,22 @@ List<String> _amSelectSubtitleLines(AssistanteMaternelleModel am) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sélection d'une AM à rattacher (fiche enfant) — ticket #147.
|
/// Sélection d'une AM à rattacher (fiche enfant) — ticket #147.
|
||||||
/// S'appuie sur [AdminSelectListModal] (shell partagé avec #146).
|
/// S'appuie sur [SelectListModal] (shell partagé avec #146).
|
||||||
class AdminSelectAmModal {
|
class SelectAmModal {
|
||||||
AdminSelectAmModal._();
|
SelectAmModal._();
|
||||||
|
|
||||||
static Future<AssistanteMaternelleModel?> show(
|
static Future<AssistanteMaternelleModel?> show(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
Set<String> excludeIds = const {},
|
Set<String> excludeIds = const {},
|
||||||
String title = 'Choisir une assistante maternelle',
|
String title = 'Choisir une assistante maternelle',
|
||||||
}) {
|
}) {
|
||||||
return AdminSelectListModal.show<AssistanteMaternelleModel>(
|
return SelectListModal.show<AssistanteMaternelleModel>(
|
||||||
context,
|
context,
|
||||||
title: title,
|
title: title,
|
||||||
searchHint: 'Rechercher par nom, prénom ou zone…',
|
searchHint: 'Rechercher par nom, prénom ou zone…',
|
||||||
emptyMessage: 'Aucune assistante maternelle disponible',
|
emptyMessage: 'Aucune assistante maternelle disponible',
|
||||||
noResultsMessage: 'Aucune AM avec place libre pour cette recherche',
|
noResultsMessage: 'Aucune AM avec place libre pour cette recherche',
|
||||||
toggleFilter: const AdminSelectToggleFilter<AssistanteMaternelleModel>(
|
toggleFilter: const SelectToggleFilter<AssistanteMaternelleModel>(
|
||||||
label: 'Libre',
|
label: 'Libre',
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
whenEnabled: amHasFreePlace,
|
whenEnabled: amHasFreePlace,
|
||||||
@@ -76,7 +76,7 @@ class AdminSelectAmModal {
|
|||||||
_resolveAmSelection(ctx, am, reload),
|
_resolveAmSelection(ctx, am, reload),
|
||||||
itemBuilder: (context, am, onSelect) {
|
itemBuilder: (context, am, onSelect) {
|
||||||
final full = !amHasFreePlace(am);
|
final full = !amHasFreePlace(am);
|
||||||
return AdminUserCard(
|
return UserCard(
|
||||||
title: am.user.fullName,
|
title: am.user.fullName,
|
||||||
avatarUrl: am.user.photoUrl,
|
avatarUrl: am.user.photoUrl,
|
||||||
fallbackIcon: Icons.face,
|
fallbackIcon: Icons.face,
|
||||||
@@ -150,7 +150,7 @@ class _AmNoPlaceWarningDialogState extends State<_AmNoPlaceWarningDialog> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AdminAmEditModal(
|
builder: (ctx) => AmEditModal(
|
||||||
assistante: _am,
|
assistante: _am,
|
||||||
onSaved: () async {
|
onSaved: () async {
|
||||||
try {
|
try {
|
||||||
+8
-8
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/enfant_user_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_select_list_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/select_list_modal.dart';
|
||||||
|
|
||||||
/// Sélection d'un enfant à rattacher (fiche AM / fiche parent) — ticket #146.
|
/// Sélection d'un enfant à rattacher (fiche AM / fiche parent) — ticket #146.
|
||||||
/// S'appuie sur [AdminSelectListModal] (shell partagé avec #147).
|
/// S'appuie sur [SelectListModal] (shell partagé avec #147).
|
||||||
class AdminSelectEnfantModal {
|
class SelectEnfantModal {
|
||||||
AdminSelectEnfantModal._();
|
SelectEnfantModal._();
|
||||||
|
|
||||||
static Future<EnfantAdminModel?> show(
|
static Future<EnfantAdminModel?> show(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
@@ -17,7 +17,7 @@ class AdminSelectEnfantModal {
|
|||||||
/// Affiché uniquement depuis la fiche AM : filtre les enfants déjà en garde.
|
/// Affiché uniquement depuis la fiche AM : filtre les enfants déjà en garde.
|
||||||
bool showSansGardeFilter = false,
|
bool showSansGardeFilter = false,
|
||||||
}) {
|
}) {
|
||||||
return AdminSelectListModal.show<EnfantAdminModel>(
|
return SelectListModal.show<EnfantAdminModel>(
|
||||||
context,
|
context,
|
||||||
title: title,
|
title: title,
|
||||||
searchHint: 'Rechercher par nom ou prénom…',
|
searchHint: 'Rechercher par nom ou prénom…',
|
||||||
@@ -26,7 +26,7 @@ class AdminSelectEnfantModal {
|
|||||||
? 'Aucun enfant sans garde pour cette recherche'
|
? 'Aucun enfant sans garde pour cette recherche'
|
||||||
: 'Aucun résultat pour cette recherche',
|
: 'Aucun résultat pour cette recherche',
|
||||||
toggleFilter: showSansGardeFilter
|
toggleFilter: showSansGardeFilter
|
||||||
? AdminSelectToggleFilter<EnfantAdminModel>(
|
? SelectToggleFilter<EnfantAdminModel>(
|
||||||
label: 'Sans garde',
|
label: 'Sans garde',
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
whenEnabled: (e) =>
|
whenEnabled: (e) =>
|
||||||
@@ -50,7 +50,7 @@ class AdminSelectEnfantModal {
|
|||||||
return name.contains(q) || fn.contains(q) || ln.contains(q);
|
return name.contains(q) || fn.contains(q) || ln.contains(q);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, e, onSelect) {
|
itemBuilder: (context, e, onSelect) {
|
||||||
return AdminEnfantUserCard.fromEnfant(
|
return EnfantUserCard.fromEnfant(
|
||||||
e,
|
e,
|
||||||
onCardTap: onSelect,
|
onCardTap: onSelect,
|
||||||
margin: const EdgeInsets.only(bottom: 4),
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
+12
-12
@@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/parent_model.dart';
|
import 'package:p_tits_pas/models/parent_model.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_select_list_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/select_list_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_card.dart';
|
||||||
|
|
||||||
/// Foyer / famille sélectionnable pour rattacher un nouvel enfant (#132 / #157).
|
/// Foyer / famille sélectionnable pour rattacher un nouvel enfant (#132 / #157).
|
||||||
class AdminFamilleFoyer {
|
class FamilleFoyer {
|
||||||
/// Parent pivot pour `POST /enfants` (`parent_user_id`).
|
/// Parent pivot pour `POST /enfants` (`parent_user_id`).
|
||||||
final String pivotParentUserId;
|
final String pivotParentUserId;
|
||||||
/// Co-parent éventuel (rattachement foyer #157).
|
/// Co-parent éventuel (rattachement foyer #157).
|
||||||
@@ -14,7 +14,7 @@ class AdminFamilleFoyer {
|
|||||||
final String displayTitle;
|
final String displayTitle;
|
||||||
final List<String> parentNames;
|
final List<String> parentNames;
|
||||||
|
|
||||||
const AdminFamilleFoyer({
|
const FamilleFoyer({
|
||||||
required this.pivotParentUserId,
|
required this.pivotParentUserId,
|
||||||
required this.displayTitle,
|
required this.displayTitle,
|
||||||
required this.parentNames,
|
required this.parentNames,
|
||||||
@@ -42,10 +42,10 @@ class AdminFamilleFoyer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Construit la liste des foyers uniques à partir de `GET /parents`.
|
/// Construit la liste des foyers uniques à partir de `GET /parents`.
|
||||||
List<AdminFamilleFoyer> buildFamilleFoyers(List<ParentModel> parents) {
|
List<FamilleFoyer> buildFamilleFoyers(List<ParentModel> parents) {
|
||||||
final seenDossiers = <String>{};
|
final seenDossiers = <String>{};
|
||||||
final seenUserIds = <String>{};
|
final seenUserIds = <String>{};
|
||||||
final foyers = <AdminFamilleFoyer>[];
|
final foyers = <FamilleFoyer>[];
|
||||||
|
|
||||||
for (final p in parents) {
|
for (final p in parents) {
|
||||||
final dossier = (p.user.numeroDossier ?? '').trim();
|
final dossier = (p.user.numeroDossier ?? '').trim();
|
||||||
@@ -70,7 +70,7 @@ List<AdminFamilleFoyer> buildFamilleFoyers(List<ParentModel> parents) {
|
|||||||
: (names.isNotEmpty ? names.first : 'Famille');
|
: (names.isNotEmpty ? names.first : 'Famille');
|
||||||
|
|
||||||
foyers.add(
|
foyers.add(
|
||||||
AdminFamilleFoyer(
|
FamilleFoyer(
|
||||||
pivotParentUserId: p.user.id,
|
pivotParentUserId: p.user.id,
|
||||||
coParentUserId: co?.id,
|
coParentUserId: co?.id,
|
||||||
numeroDossier: dossier.isNotEmpty ? dossier : null,
|
numeroDossier: dossier.isNotEmpty ? dossier : null,
|
||||||
@@ -87,14 +87,14 @@ List<AdminFamilleFoyer> buildFamilleFoyers(List<ParentModel> parents) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sélection d'une famille / dossier pour créer un enfant — ticket #132.
|
/// Sélection d'une famille / dossier pour créer un enfant — ticket #132.
|
||||||
class AdminSelectFamilleModal {
|
class SelectFamilleModal {
|
||||||
AdminSelectFamilleModal._();
|
SelectFamilleModal._();
|
||||||
|
|
||||||
static Future<AdminFamilleFoyer?> show(
|
static Future<FamilleFoyer?> show(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
String title = 'Choisir une famille',
|
String title = 'Choisir une famille',
|
||||||
}) {
|
}) {
|
||||||
return AdminSelectListModal.show<AdminFamilleFoyer>(
|
return SelectListModal.show<FamilleFoyer>(
|
||||||
context,
|
context,
|
||||||
title: title,
|
title: title,
|
||||||
searchHint: 'Rechercher par dossier, nom…',
|
searchHint: 'Rechercher par dossier, nom…',
|
||||||
@@ -111,7 +111,7 @@ class AdminSelectFamilleModal {
|
|||||||
return dossier.contains(q) || title.contains(q) || names.contains(q);
|
return dossier.contains(q) || title.contains(q) || names.contains(q);
|
||||||
},
|
},
|
||||||
itemBuilder: (context, f, onSelect) {
|
itemBuilder: (context, f, onSelect) {
|
||||||
return AdminUserCard(
|
return UserCard(
|
||||||
title: f.displayTitle,
|
title: f.displayTitle,
|
||||||
fallbackIcon: Icons.family_restroom,
|
fallbackIcon: Icons.family_restroom,
|
||||||
subtitleLines: [
|
subtitleLines: [
|
||||||
+11
-11
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart';
|
||||||
|
|
||||||
/// Filtre optionnel (switch) sur la même ligne que la barre de recherche.
|
/// Filtre optionnel (switch) sur la même ligne que la barre de recherche.
|
||||||
class AdminSelectToggleFilter<T> {
|
class SelectToggleFilter<T> {
|
||||||
final String label;
|
final String label;
|
||||||
final bool initialValue;
|
final bool initialValue;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ class AdminSelectToggleFilter<T> {
|
|||||||
/// [whenEnabled] renvoie `true`.
|
/// [whenEnabled] renvoie `true`.
|
||||||
final bool Function(T item) whenEnabled;
|
final bool Function(T item) whenEnabled;
|
||||||
|
|
||||||
const AdminSelectToggleFilter({
|
const SelectToggleFilter({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.whenEnabled,
|
required this.whenEnabled,
|
||||||
this.initialValue = true,
|
this.initialValue = true,
|
||||||
@@ -19,7 +19,7 @@ class AdminSelectToggleFilter<T> {
|
|||||||
|
|
||||||
/// Shell générique « rechercher + liste + sélection » pour les modales admin.
|
/// Shell générique « rechercher + liste + sélection » pour les modales admin.
|
||||||
/// Utilisé par la sélection d'enfant (#146) et la sélection d'AM (#147).
|
/// Utilisé par la sélection d'enfant (#146) et la sélection d'AM (#147).
|
||||||
class AdminSelectListModal<T> extends StatefulWidget {
|
class SelectListModal<T> extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String searchHint;
|
final String searchHint;
|
||||||
final Future<List<T>> Function() loadItems;
|
final Future<List<T>> Function() loadItems;
|
||||||
@@ -40,7 +40,7 @@ class AdminSelectListModal<T> extends StatefulWidget {
|
|||||||
final int minVisibleCards;
|
final int minVisibleCards;
|
||||||
|
|
||||||
/// Switch optionnel à droite du champ de recherche (ex. « Sans garde », « Libre »).
|
/// Switch optionnel à droite du champ de recherche (ex. « Sans garde », « Libre »).
|
||||||
final AdminSelectToggleFilter<T>? toggleFilter;
|
final SelectToggleFilter<T>? toggleFilter;
|
||||||
|
|
||||||
/// Si fourni, appelé avant de valider la sélection.
|
/// Si fourni, appelé avant de valider la sélection.
|
||||||
/// Retourne l'élément à pop (éventuellement rafraîchi), ou `null` pour annuler.
|
/// Retourne l'élément à pop (éventuellement rafraîchi), ou `null` pour annuler.
|
||||||
@@ -50,7 +50,7 @@ class AdminSelectListModal<T> extends StatefulWidget {
|
|||||||
Future<void> Function() reload,
|
Future<void> Function() reload,
|
||||||
)? resolveSelect;
|
)? resolveSelect;
|
||||||
|
|
||||||
const AdminSelectListModal({
|
const SelectListModal({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.loadItems,
|
required this.loadItems,
|
||||||
@@ -82,7 +82,7 @@ class AdminSelectListModal<T> extends StatefulWidget {
|
|||||||
double modalWidth = 930,
|
double modalWidth = 930,
|
||||||
double cardExtent = 52,
|
double cardExtent = 52,
|
||||||
int minVisibleCards = 8,
|
int minVisibleCards = 8,
|
||||||
AdminSelectToggleFilter<T>? toggleFilter,
|
SelectToggleFilter<T>? toggleFilter,
|
||||||
Future<T?> Function(
|
Future<T?> Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
T item,
|
T item,
|
||||||
@@ -91,7 +91,7 @@ class AdminSelectListModal<T> extends StatefulWidget {
|
|||||||
}) {
|
}) {
|
||||||
return showDialog<T>(
|
return showDialog<T>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AdminSelectListModal<T>(
|
builder: (ctx) => SelectListModal<T>(
|
||||||
title: title,
|
title: title,
|
||||||
loadItems: loadItems,
|
loadItems: loadItems,
|
||||||
matchesQuery: matchesQuery,
|
matchesQuery: matchesQuery,
|
||||||
@@ -109,11 +109,11 @@ class AdminSelectListModal<T> extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AdminSelectListModal<T>> createState() =>
|
State<SelectListModal<T>> createState() =>
|
||||||
_AdminSelectListModalState<T>();
|
_SelectListModalState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdminSelectListModalState<T> extends State<AdminSelectListModal<T>> {
|
class _SelectListModalState<T> extends State<SelectListModal<T>> {
|
||||||
final _searchCtrl = TextEditingController();
|
final _searchCtrl = TextEditingController();
|
||||||
List<T> _all = [];
|
List<T> _all = [];
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
@@ -0,0 +1,794 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:p_tits_pas/models/relais_model.dart';
|
||||||
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
|
import 'package:p_tits_pas/services/relais_service.dart';
|
||||||
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
|
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||||
|
import 'package:p_tits_pas/utils/name_format_utils.dart';
|
||||||
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
|
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/dashboard/common/validation_detail_section.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/dashboard/validation_modal_theme.dart';
|
||||||
|
|
||||||
|
/// Modale création / édition / consultation staff (gestionnaire / admin) — #164.
|
||||||
|
class StaffUserFormModal extends StatefulWidget {
|
||||||
|
final AppUser? initialUser;
|
||||||
|
final bool withRelais;
|
||||||
|
final bool adminMode;
|
||||||
|
final bool readOnly;
|
||||||
|
|
||||||
|
const StaffUserFormModal({
|
||||||
|
super.key,
|
||||||
|
this.initialUser,
|
||||||
|
this.withRelais = true,
|
||||||
|
this.adminMode = false,
|
||||||
|
this.readOnly = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StaffUserFormModal> createState() => _StaffUserFormModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StaffUserFormModalState extends State<StaffUserFormModal> {
|
||||||
|
static const double _modalWidth = 930;
|
||||||
|
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _nomController = TextEditingController();
|
||||||
|
final _prenomController = TextEditingController();
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _telephoneController = TextEditingController();
|
||||||
|
final _passwordToggleFocusNode =
|
||||||
|
FocusNode(skipTraversal: true, canRequestFocus: false);
|
||||||
|
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
bool _obscurePassword = true;
|
||||||
|
bool _isLoadingRelais = true;
|
||||||
|
bool _dirty = false;
|
||||||
|
List<RelaisModel> _relais = [];
|
||||||
|
String? _selectedRelaisId;
|
||||||
|
String? _currentUserId;
|
||||||
|
String? _currentUserRole;
|
||||||
|
|
||||||
|
String _baselineNom = '';
|
||||||
|
String _baselinePrenom = '';
|
||||||
|
String _baselineEmail = '';
|
||||||
|
String _baselinePhone = '';
|
||||||
|
String? _baselineRelaisId;
|
||||||
|
|
||||||
|
bool get _isEditMode => widget.initialUser != null;
|
||||||
|
bool get _isSuperAdminTarget =>
|
||||||
|
(widget.initialUser?.role ?? '').toLowerCase() == 'super_admin';
|
||||||
|
bool get _isSelfTarget =>
|
||||||
|
_isEditMode &&
|
||||||
|
_currentUserId != null &&
|
||||||
|
widget.initialUser!.id == _currentUserId;
|
||||||
|
bool get _canDeleteTarget {
|
||||||
|
if (!_isEditMode || widget.readOnly) return false;
|
||||||
|
if (_isSelfTarget || _isSuperAdminTarget) return false;
|
||||||
|
return canDeleteGestionnaire(_currentUserRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isLockedAdminIdentity =>
|
||||||
|
_isEditMode && widget.adminMode && _isSuperAdminTarget;
|
||||||
|
|
||||||
|
bool get _fieldsEnabled => !widget.readOnly && !_isSubmitting;
|
||||||
|
|
||||||
|
String get _targetRoleKey {
|
||||||
|
if (widget.initialUser != null) {
|
||||||
|
return (widget.initialUser!.role).toLowerCase();
|
||||||
|
}
|
||||||
|
return widget.adminMode ? 'administrateur' : 'gestionnaire';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _targetRoleLabel {
|
||||||
|
switch (_targetRoleKey) {
|
||||||
|
case 'super_admin':
|
||||||
|
return 'Super administrateur';
|
||||||
|
case 'administrateur':
|
||||||
|
return 'Administrateur';
|
||||||
|
case 'gestionnaire':
|
||||||
|
return 'Gestionnaire';
|
||||||
|
default:
|
||||||
|
return 'Utilisateur';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData get _targetRoleIcon {
|
||||||
|
switch (_targetRoleKey) {
|
||||||
|
case 'super_admin':
|
||||||
|
return Icons.verified_user_outlined;
|
||||||
|
case 'administrateur':
|
||||||
|
return Icons.admin_panel_settings_outlined;
|
||||||
|
case 'gestionnaire':
|
||||||
|
return Icons.assignment_ind_outlined;
|
||||||
|
default:
|
||||||
|
return Icons.person_outline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final user = widget.initialUser;
|
||||||
|
if (user != null) {
|
||||||
|
_nomController.text = user.nom ?? '';
|
||||||
|
_prenomController.text = user.prenom ?? '';
|
||||||
|
_emailController.text = user.email;
|
||||||
|
_telephoneController.text = formatPhoneForDisplay(user.telephone ?? '');
|
||||||
|
_passwordController.clear();
|
||||||
|
final initialRelaisId = user.relaisId?.trim();
|
||||||
|
_selectedRelaisId =
|
||||||
|
(initialRelaisId == null || initialRelaisId.isEmpty)
|
||||||
|
? null
|
||||||
|
: initialRelaisId;
|
||||||
|
_captureBaseline();
|
||||||
|
}
|
||||||
|
for (final c in [
|
||||||
|
_nomController,
|
||||||
|
_prenomController,
|
||||||
|
_emailController,
|
||||||
|
_passwordController,
|
||||||
|
_telephoneController,
|
||||||
|
]) {
|
||||||
|
c.addListener(_onFieldChanged);
|
||||||
|
}
|
||||||
|
if (widget.withRelais) {
|
||||||
|
_loadRelais();
|
||||||
|
} else {
|
||||||
|
_isLoadingRelais = false;
|
||||||
|
}
|
||||||
|
_loadCurrentUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _captureBaseline() {
|
||||||
|
_baselineNom = formatPersonNameCase(_nomController.text);
|
||||||
|
_baselinePrenom = formatPersonNameCase(_prenomController.text);
|
||||||
|
_baselineEmail = normalizeEmailText(_emailController.text);
|
||||||
|
_baselinePhone = normalizePhone(_telephoneController.text);
|
||||||
|
_baselineRelaisId = _selectedRelaisId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFieldChanged() {
|
||||||
|
if (widget.readOnly || !_isEditMode) return;
|
||||||
|
final dirty = _computeDirty();
|
||||||
|
if (dirty != _dirty) setState(() => _dirty = dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _computeDirty() {
|
||||||
|
if (!_isEditMode) return true;
|
||||||
|
final nom = formatPersonNameCase(_nomController.text);
|
||||||
|
final prenom = formatPersonNameCase(_prenomController.text);
|
||||||
|
final email = normalizeEmailText(_emailController.text);
|
||||||
|
final phone = normalizePhone(_telephoneController.text);
|
||||||
|
final passwordProvided = _passwordController.text.trim().isNotEmpty;
|
||||||
|
return nom != _baselineNom ||
|
||||||
|
prenom != _baselinePrenom ||
|
||||||
|
email != _baselineEmail ||
|
||||||
|
phone != _baselinePhone ||
|
||||||
|
passwordProvided ||
|
||||||
|
_selectedRelaisId != _baselineRelaisId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCurrentUserId() async {
|
||||||
|
final cached = await AuthService.getCurrentUser();
|
||||||
|
if (!mounted) return;
|
||||||
|
if (cached != null) {
|
||||||
|
setState(() {
|
||||||
|
_currentUserId = cached.id;
|
||||||
|
_currentUserRole = cached.role;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final refreshed = await AuthService.refreshCurrentUser();
|
||||||
|
if (!mounted || refreshed == null) return;
|
||||||
|
setState(() {
|
||||||
|
_currentUserId = refreshed.id;
|
||||||
|
_currentUserRole = refreshed.role;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (final c in [
|
||||||
|
_nomController,
|
||||||
|
_prenomController,
|
||||||
|
_emailController,
|
||||||
|
_passwordController,
|
||||||
|
_telephoneController,
|
||||||
|
]) {
|
||||||
|
c.removeListener(_onFieldChanged);
|
||||||
|
c.dispose();
|
||||||
|
}
|
||||||
|
_passwordToggleFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RelaisModel> _fallbackRelaisFromUser() {
|
||||||
|
final id = _selectedRelaisId?.trim();
|
||||||
|
if (id == null || id.isEmpty) return const [];
|
||||||
|
final nom = (widget.initialUser?.relaisNom ?? '').trim();
|
||||||
|
return [
|
||||||
|
RelaisModel(
|
||||||
|
id: id,
|
||||||
|
nom: nom.isNotEmpty ? nom : 'Relais actuel',
|
||||||
|
adresse: '',
|
||||||
|
actif: true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadRelais() async {
|
||||||
|
try {
|
||||||
|
final list = await RelaisService.getRelais();
|
||||||
|
if (!mounted) return;
|
||||||
|
final uniqueById = <String, RelaisModel>{};
|
||||||
|
for (final relais in list) {
|
||||||
|
uniqueById[relais.id] = relais;
|
||||||
|
}
|
||||||
|
|
||||||
|
final filtered = uniqueById.values.where((r) => r.actif).toList();
|
||||||
|
if (_selectedRelaisId != null &&
|
||||||
|
!filtered.any((r) => r.id == _selectedRelaisId)) {
|
||||||
|
final selected = uniqueById[_selectedRelaisId!];
|
||||||
|
if (selected != null) {
|
||||||
|
filtered.add(selected);
|
||||||
|
} else {
|
||||||
|
filtered.addAll(_fallbackRelaisFromUser());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_relais = filtered;
|
||||||
|
_isLoadingRelais = false;
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_relais = _fallbackRelaisFromUser();
|
||||||
|
_isLoadingRelais = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _required(String? value, String field) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return '$field est requis';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validatePassword(String? value) {
|
||||||
|
if (_isEditMode && (value == null || value.trim().isEmpty)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final base = _required(value, 'Mot de passe');
|
||||||
|
if (base != null) return base;
|
||||||
|
if (value!.trim().length < 6) return 'Minimum 6 caractères';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
if (widget.readOnly) return;
|
||||||
|
if (_isSubmitting) return;
|
||||||
|
if (_isEditMode && !_dirty) return;
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final normalizedNom = formatPersonNameCase(_nomController.text);
|
||||||
|
final normalizedPrenom = formatPersonNameCase(_prenomController.text);
|
||||||
|
final normalizedPhone = normalizePhone(_telephoneController.text);
|
||||||
|
final passwordProvided = _passwordController.text.trim().isNotEmpty;
|
||||||
|
|
||||||
|
if (_isEditMode) {
|
||||||
|
if (widget.adminMode) {
|
||||||
|
final lockedNom = formatPersonNameCase(widget.initialUser!.nom ?? '');
|
||||||
|
final lockedPrenom =
|
||||||
|
formatPersonNameCase(widget.initialUser!.prenom ?? '');
|
||||||
|
await UserService.updateAdministrateur(
|
||||||
|
adminId: widget.initialUser!.id,
|
||||||
|
nom: _isLockedAdminIdentity ? lockedNom : normalizedNom,
|
||||||
|
prenom: _isLockedAdminIdentity ? lockedPrenom : normalizedPrenom,
|
||||||
|
email: normalizeEmailText(_emailController.text),
|
||||||
|
telephone: normalizedPhone.isEmpty
|
||||||
|
? normalizePhone(widget.initialUser!.telephone ?? '')
|
||||||
|
: normalizedPhone,
|
||||||
|
password: passwordProvided ? _passwordController.text : null,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final currentUser = widget.initialUser!;
|
||||||
|
final initialNom = formatPersonNameCase(currentUser.nom ?? '');
|
||||||
|
final initialPrenom = formatPersonNameCase(currentUser.prenom ?? '');
|
||||||
|
final initialEmail = normalizeEmailText(currentUser.email);
|
||||||
|
final initialPhone = normalizePhone(currentUser.telephone ?? '');
|
||||||
|
|
||||||
|
final onlyRelaisChanged = normalizedNom == initialNom &&
|
||||||
|
normalizedPrenom == initialPrenom &&
|
||||||
|
normalizeEmailText(_emailController.text) == initialEmail &&
|
||||||
|
normalizedPhone == initialPhone &&
|
||||||
|
!passwordProvided;
|
||||||
|
|
||||||
|
if (onlyRelaisChanged) {
|
||||||
|
await UserService.updateGestionnaireRelais(
|
||||||
|
gestionnaireId: currentUser.id,
|
||||||
|
relaisId: _selectedRelaisId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await UserService.updateGestionnaire(
|
||||||
|
gestionnaireId: currentUser.id,
|
||||||
|
nom: normalizedNom,
|
||||||
|
prenom: normalizedPrenom,
|
||||||
|
email: normalizeEmailText(_emailController.text),
|
||||||
|
telephone:
|
||||||
|
normalizedPhone.isEmpty ? initialPhone : normalizedPhone,
|
||||||
|
relaisId: _selectedRelaisId,
|
||||||
|
password: passwordProvided ? _passwordController.text : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (widget.adminMode) {
|
||||||
|
await UserService.createAdministrateur(
|
||||||
|
nom: normalizedNom,
|
||||||
|
prenom: normalizedPrenom,
|
||||||
|
email: normalizeEmailText(_emailController.text),
|
||||||
|
password: _passwordController.text,
|
||||||
|
telephone: normalizePhone(_telephoneController.text),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await UserService.createGestionnaire(
|
||||||
|
nom: normalizedNom,
|
||||||
|
prenom: normalizedPrenom,
|
||||||
|
email: normalizeEmailText(_emailController.text),
|
||||||
|
password: _passwordController.text,
|
||||||
|
telephone: normalizePhone(_telephoneController.text),
|
||||||
|
relaisId: _selectedRelaisId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
_isEditMode
|
||||||
|
? (widget.adminMode
|
||||||
|
? 'Administrateur modifié avec succès.'
|
||||||
|
: 'Gestionnaire modifié avec succès.')
|
||||||
|
: (widget.adminMode
|
||||||
|
? 'Administrateur créé avec succès.'
|
||||||
|
: 'Gestionnaire créé avec succès.'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(e.toString().replaceFirst('Exception: ', '')),
|
||||||
|
backgroundColor: Colors.red.shade700,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _delete() async {
|
||||||
|
if (widget.readOnly) return;
|
||||||
|
if (!_canDeleteTarget) return;
|
||||||
|
if (!_isEditMode || _isSubmitting) return;
|
||||||
|
|
||||||
|
final name = widget.initialUser!.fullName.isEmpty
|
||||||
|
? widget.initialUser!.email
|
||||||
|
: widget.initialUser!.fullName;
|
||||||
|
final confirmed = await showSuppressionConfirmDialog(
|
||||||
|
context,
|
||||||
|
title: widget.adminMode
|
||||||
|
? 'Supprimer l\'administrateur'
|
||||||
|
: 'Supprimer le gestionnaire',
|
||||||
|
people: [
|
||||||
|
widget.adminMode
|
||||||
|
? SuppressionPersonLine.administrateur(name)
|
||||||
|
: SuppressionPersonLine.gestionnaire(name),
|
||||||
|
],
|
||||||
|
footnotes: const [
|
||||||
|
'Le compte sera définitivement supprimé.',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
try {
|
||||||
|
await UserService.deleteUser(widget.initialUser!.id);
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
widget.adminMode
|
||||||
|
? 'Administrateur supprimé.'
|
||||||
|
: 'Gestionnaire supprimé.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(e.toString().replaceFirst('Exception: ', '')),
|
||||||
|
backgroundColor: Colors.red.shade700,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _headerTitle() {
|
||||||
|
if (!_isEditMode) {
|
||||||
|
return widget.adminMode
|
||||||
|
? 'Créer un administrateur'
|
||||||
|
: 'Créer un gestionnaire';
|
||||||
|
}
|
||||||
|
final prenom = (_prenomController.text.trim().isNotEmpty
|
||||||
|
? _prenomController.text
|
||||||
|
: (widget.initialUser?.prenom ?? ''))
|
||||||
|
.trim();
|
||||||
|
final nom = (_nomController.text.trim().isNotEmpty
|
||||||
|
? _nomController.text
|
||||||
|
: (widget.initialUser?.nom ?? ''))
|
||||||
|
.trim();
|
||||||
|
final full = '$prenom $nom'.trim();
|
||||||
|
if (full.isNotEmpty) return full;
|
||||||
|
return widget.initialUser?.email ?? _targetRoleLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFooter() {
|
||||||
|
if (widget.readOnly) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Fermer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isEditMode) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ValidationModalTheme.primaryElevatedStyle,
|
||||||
|
onPressed: _isSubmitting ? null : _submit,
|
||||||
|
child: _isSubmitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text('Créer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (_canDeleteTarget)
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: _isSubmitting ? null : _delete,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red.shade700,
|
||||||
|
),
|
||||||
|
child: const Text('Supprimer'),
|
||||||
|
),
|
||||||
|
if (_canDeleteTarget) const SizedBox(width: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Fermer'),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ValidationModalTheme.primaryElevatedStyle,
|
||||||
|
onPressed: !_dirty || _isSubmitting ? null : _submit,
|
||||||
|
child: _isSubmitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(_dirty ? 'Sauvegarder' : 'Aucune modification'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _namedField({
|
||||||
|
required String label,
|
||||||
|
required TextEditingController controller,
|
||||||
|
required String requiredLabel,
|
||||||
|
bool enabled = true,
|
||||||
|
}) {
|
||||||
|
return ValidationLabeledField(
|
||||||
|
label: label,
|
||||||
|
field: SizedBox(
|
||||||
|
height: ValidationFormMetrics.fieldHeight,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
enabled: enabled,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
inputFormatters: const [PersonNameInputFormatter()],
|
||||||
|
style: ValidationFormMetrics.fieldTextStyle,
|
||||||
|
decoration: ValidationFieldDecoration.input(),
|
||||||
|
validator: (!enabled || widget.readOnly)
|
||||||
|
? null
|
||||||
|
: (v) => _required(v, requiredLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _passwordField() {
|
||||||
|
return ValidationLabeledField(
|
||||||
|
label: _isEditMode ? 'Nouveau mot de passe' : 'Mot de passe',
|
||||||
|
field: SizedBox(
|
||||||
|
height: ValidationFormMetrics.fieldHeight,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _passwordController,
|
||||||
|
enabled: _fieldsEnabled,
|
||||||
|
obscureText: _obscurePassword,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
autofillHints: _isEditMode
|
||||||
|
? const <String>[]
|
||||||
|
: const [AutofillHints.newPassword],
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
style: ValidationFormMetrics.fieldTextStyle,
|
||||||
|
decoration: ValidationFieldDecoration.input().copyWith(
|
||||||
|
suffixIcon: widget.readOnly
|
||||||
|
? null
|
||||||
|
: ExcludeFocus(
|
||||||
|
child: IconButton(
|
||||||
|
focusNode: _passwordToggleFocusNode,
|
||||||
|
onPressed: !_fieldsEnabled
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
_obscurePassword = !_obscurePassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
_obscurePassword
|
||||||
|
? Icons.visibility_off
|
||||||
|
: Icons.visibility,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorStyle: TextStyle(color: Colors.red.shade700, fontSize: 11),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
),
|
||||||
|
validator: widget.readOnly ? null : _validatePassword,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _relaisField() {
|
||||||
|
final selectedValue = _selectedRelaisId != null &&
|
||||||
|
_relais.any((relais) => relais.id == _selectedRelaisId)
|
||||||
|
? _selectedRelaisId
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return ValidationLabeledField(
|
||||||
|
label: 'Relais principal',
|
||||||
|
field: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: ValidationFormMetrics.fieldHeight,
|
||||||
|
child: DropdownButtonFormField<String?>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: selectedValue,
|
||||||
|
decoration: ValidationFieldDecoration.input(),
|
||||||
|
style: ValidationFormMetrics.fieldTextStyle,
|
||||||
|
items: [
|
||||||
|
const DropdownMenuItem<String?>(
|
||||||
|
value: null,
|
||||||
|
child: Text('Aucun relais'),
|
||||||
|
),
|
||||||
|
..._relais.map(
|
||||||
|
(relais) => DropdownMenuItem<String?>(
|
||||||
|
value: relais.id,
|
||||||
|
child: Text(relais.nom),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (_isLoadingRelais || !_fieldsEnabled)
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedRelaisId = value;
|
||||||
|
if (_isEditMode) {
|
||||||
|
_dirty = _computeDirty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isLoadingRelais) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const LinearProgressIndicator(minHeight: 2),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final nameEnabled =
|
||||||
|
_fieldsEnabled && !_isLockedAdminIdentity;
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: _modalWidth),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(18, 16, 4, 10),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 2, right: 10),
|
||||||
|
child: Icon(
|
||||||
|
_targetRoleIcon,
|
||||||
|
size: 22,
|
||||||
|
color: ValidationModalTheme.primaryActionBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_headerTitle(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isEditMode) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_targetRoleLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 40,
|
||||||
|
minHeight: 40,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: _isSubmitting
|
||||||
|
? null
|
||||||
|
: () => Navigator.of(context).pop(false),
|
||||||
|
tooltip: 'Fermer',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _namedField(
|
||||||
|
label: 'Prénom',
|
||||||
|
controller: _prenomController,
|
||||||
|
requiredLabel: 'Prénom',
|
||||||
|
enabled: nameEnabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: _namedField(
|
||||||
|
label: 'Nom',
|
||||||
|
controller: _nomController,
|
||||||
|
requiredLabel: 'Nom',
|
||||||
|
enabled: nameEnabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: ValidationFormMetrics.rowGapBelow),
|
||||||
|
ValidationLabeledField(
|
||||||
|
label: 'Email',
|
||||||
|
field: IgnorePointer(
|
||||||
|
ignoring: !_fieldsEnabled,
|
||||||
|
child: ValidationEmailField(
|
||||||
|
controller: _emailController,
|
||||||
|
hintText: 'ex. nom@domaine.fr',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: ValidationFormMetrics.rowGapBelow),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _passwordField()),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: ValidationLabeledField(
|
||||||
|
label: 'Téléphone',
|
||||||
|
field: IgnorePointer(
|
||||||
|
ignoring: !_fieldsEnabled,
|
||||||
|
child: ValidationPhoneField(
|
||||||
|
controller: _telephoneController,
|
||||||
|
hintText: '06 12 34 56 78',
|
||||||
|
allowEmpty: _isEditMode,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (widget.withRelais) ...[
|
||||||
|
SizedBox(height: ValidationFormMetrics.rowGapBelow),
|
||||||
|
_relaisField(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 16, 14),
|
||||||
|
child: _buildFooter(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Gélule de sélection du statut utilisateur (fiches admin parent / AM).
|
/// Gélule de sélection du statut utilisateur (fiches admin parent / AM).
|
||||||
class AdminStatusCapsule extends StatelessWidget {
|
class StatusCapsule extends StatelessWidget {
|
||||||
final String statut;
|
final String statut;
|
||||||
final ValueChanged<String>? onChanged;
|
final ValueChanged<String>? onChanged;
|
||||||
|
|
||||||
static const statuts = ['actif', 'en_attente', 'suspendu', 'refuse'];
|
static const statuts = ['actif', 'en_attente', 'suspendu', 'refuse'];
|
||||||
|
|
||||||
const AdminStatusCapsule({
|
const StatusCapsule({
|
||||||
super.key,
|
super.key,
|
||||||
required this.statut,
|
required this.statut,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
+4
-4
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
|
|
||||||
class AdminUserCard extends StatefulWidget {
|
class UserCard extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final List<String> subtitleLines;
|
final List<String> subtitleLines;
|
||||||
final String? avatarUrl;
|
final String? avatarUrl;
|
||||||
@@ -21,7 +21,7 @@ class AdminUserCard extends StatefulWidget {
|
|||||||
final EdgeInsetsGeometry? margin;
|
final EdgeInsetsGeometry? margin;
|
||||||
final EdgeInsetsGeometry? contentPadding;
|
final EdgeInsetsGeometry? contentPadding;
|
||||||
|
|
||||||
const AdminUserCard({
|
const UserCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.subtitleLines,
|
required this.subtitleLines,
|
||||||
@@ -41,10 +41,10 @@ class AdminUserCard extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AdminUserCard> createState() => _AdminUserCardState();
|
State<UserCard> createState() => _UserCardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdminUserCardState extends State<AdminUserCard> {
|
class _UserCardState extends State<UserCard> {
|
||||||
bool _isHovered = false;
|
bool _isHovered = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AdminListState extends StatelessWidget {
|
class UserListState extends StatelessWidget {
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final String? error;
|
final String? error;
|
||||||
final bool isEmpty;
|
final bool isEmpty;
|
||||||
final String emptyMessage;
|
final String emptyMessage;
|
||||||
final Widget list;
|
final Widget list;
|
||||||
|
|
||||||
const AdminListState({
|
const UserListState({
|
||||||
super.key,
|
super.key,
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.error,
|
required this.error,
|
||||||
+13
-13
@@ -1,15 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create.dart';
|
import 'package:p_tits_pas/widgets/dashboard/staff_user_form_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/am_dossier_create_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_dossier_create_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
import 'package:p_tits_pas/widgets/dashboard/assistante_maternelle_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/child_detail_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
import 'package:p_tits_pas/widgets/dashboard/user_management_sub_bar.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dossiers_management_widget.dart';
|
import 'package:p_tits_pas/widgets/dashboard/dossiers_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/enfant_management_widget.dart';
|
import 'package:p_tits_pas/widgets/dashboard/enfant_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
|
import 'package:p_tits_pas/widgets/dashboard/gestionnaire_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_dossier_create_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_dossier_create_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_managmant_widget.dart';
|
||||||
|
|
||||||
class UserManagementPanel extends StatefulWidget {
|
class UserManagementPanel extends StatefulWidget {
|
||||||
/// Afficher l'onglet Administrateurs (sinon sans Administrateurs).
|
/// Afficher l'onglet Administrateurs (sinon sans Administrateurs).
|
||||||
@@ -348,7 +348,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
return AdminChildDetailModal.create(
|
return ChildDetailModal.create(
|
||||||
onSaved: () {
|
onSaved: () {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _enfantRefreshTick++);
|
setState(() => _enfantRefreshTick++);
|
||||||
@@ -385,7 +385,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
return const AdminUserFormDialog();
|
return const StaffUserFormModal();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -403,7 +403,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (dialogContext) {
|
builder: (dialogContext) {
|
||||||
return const AdminUserFormDialog(
|
return const StaffUserFormModal(
|
||||||
adminMode: true,
|
adminMode: true,
|
||||||
withRelais: false,
|
withRelais: false,
|
||||||
);
|
);
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/am_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_dossier_wizard.dart';
|
||||||
|
|
||||||
/// Wrapper historique (#107) — délègue à [AmDossierWizard.review].
|
/// Wrapper historique (#107) — délègue à [AmDossierWizard.review].
|
||||||
class ValidationAmWizard extends StatelessWidget {
|
class ValidationAmWizard extends StatelessWidget {
|
||||||
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/am_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_dossier_wizard.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_dossier_wizard.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_am_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_am_wizard.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_family_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_family_wizard.dart';
|
||||||
|
|
||||||
/// Modale (dialog) : charge le dossier par numéro puis affiche le wizard AM ou Famille.
|
/// Modale (dialog) : charge le dossier par numéro puis affiche le wizard AM ou Famille.
|
||||||
/// Ticket #107 / #119 (review), #135 (`openAsEdit`).
|
/// Ticket #107 / #119 (review), #135 (`openAsEdit`).
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_dossier_wizard.dart';
|
||||||
|
|
||||||
/// Wrapper historique (#107) — délègue à [ParentDossierWizard.review].
|
/// Wrapper historique (#107) — délègue à [ParentDossierWizard.review].
|
||||||
class ValidationFamilyWizard extends StatelessWidget {
|
class ValidationFamilyWizard extends StatelessWidget {
|
||||||
Reference in New Issue
Block a user