Compare commits

..
Author SHA1 Message Date
jmartinandCursor f745079f0a feat(#164): uniformisation modale staff gestionnaire / admin (squash develop).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 17:34:25 +02:00
jmartin 5b83102a59 feat(#152/#155): cleanup est_multiple + dashboard sans préfixe Admin (squash develop).
Suppression complète grossesse multiple / est_multiple (BDD, API, front).
Rename option C : widgets partagés et panels staff sous widgets/dashboard/,
AdminManagementWidget seul restant dans widgets/admin/.
2026-09-14 12:52:22 +02:00
jmartin eb5e4aa915 feat(#161): admin peut créer gestionnaire et administrateur (squash develop).
Ouvre POST/PATCH /gestionnaires et POST /users/admin aux administrateurs
(en plus du super_admin). Garde-fou front : pas de création staff sur le
dashboard gestionnaire.
2026-09-10 23:00:35 +02:00
jmartin d6d8b299dd feat(#159/#160): suppressions métier dashboard — API + UI (squash develop).
Matrice PO #154 : supprimer dossiers famille/AM, parents, enfants et comptes
staff avec règles métier (placements clôturés, sans_enfant, cascade foyer,
droits gestionnaire/admin). Poubelle en liste, dialogues de confirmation,
garde-fou gestionnaire→gestionnaire. Inclut polish hauteur modale fiche AM.
2026-09-10 22:43:33 +02:00
jmartinandCursor ea0e97d930 feat(#135): mode édition dossier + ajout co-parent (squash develop).
Wizard edit famille/AM, POST co-parent, PATCH enfants avec photo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 17:32:06 +02:00
jmartinandCursor 84e46162fd feat(#153): onglet permanent Dossiers — liste unifiée + pending.
Remplace l’onglet conditionnel « À valider » par un onglet Dossiers
(pending en haut, familles + AM en dessous), avec GET /dossiers,
recherche, et libellés pending NOM Prénom.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 19:41:44 +02:00
jmartinandCursor 14580c34e0 feat(#129): création dossier famille staff — API + wizard dashboard.
Permet au dashboard de créer un dossier parent complet (pivot, co-parent
optionnel, enfants, n° dossier) déjà actif, avec e-mail de création de
mot de passe — miroir du parcours AM (#156), sans passer par l’inscription
publique en attente.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 13:16:02 +02:00
jmartinandCursor ae610733cc feat(#156): création dossier AM staff — API + wizard dashboard.
Squash depuis develop : POST /assistantes-maternelles/dossier (actif +
mail MDP), AmDossierWizard create/review, validations inscription.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 16:53:18 +02:00
jmartinandCursor 846afed86c feat(#158): affiliation enfant au foyer — attach/detach pivot + co-parent.
Squash depuis develop : un POST/DELETE propage les liens à tout le foyer ;
front un seul appel API ; doc empreinte.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 18:31:15 +02:00
jmartinandCursor 99a6c17c23 feat(#157): enfants sans responsable — alerte liste et détach dernier parent.
Squash depuis develop : détachement dernier parent autorisé, flag
sans_responsable, vigilance liste Enfants, rattachement foyer depuis
la fiche, polish détach et compteur foyer interim (#158 à suivre).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 19:43:58 +02:00
jmartinandCursor 04f49cb62f feat(#132): création enfant staff (onglet Enfants) — front + back.
Squash depuis develop : POST /enfants staff + parent_user_id, photo
multipart, modale création + sélection famille, fixes UX/birth_date.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 19:00:12 +02:00
jmartinandCursor 3c7f4f6e16 fix(#151): autoriser GET /relais pour gestionnaire + robustesse combo
Permet au gestionnaire de peupler la liste Relais (création/édition).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:54:17 +02:00
jmartinandCursor dcd407a3da feat(#146–#149): sélection enfant/AM, capacité max et case libre.
Modales de rattachement partagées, filtre Libre/Sans garde, recalcul des places à l'attach, désactivation si capacité pleine, clic sur case libre.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:25:42 +02:00
jmartinandCursor fde63f8e72 feat(#145): lien co-parent cliquable dans la fiche parent.
Squash merge develop → master.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 14:56:45 +02:00
jmartinandCursor 1f8f1b9507 feat(#138,#143,#144): fiche enfant admin, consentement photo et fix gestionnaire.
Squash merge develop → master.
- #138 : modale enfant paysage, zone AM/scolarisation, liens responsables
- #144 : persistance consent_photo à l'inscription enfant
- #143 : masquer Supprimer sur la propre fiche gestionnaire

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 13:21:43 +02:00
63 changed files with 966 additions and 829 deletions
-1
View File
@@ -33,7 +33,6 @@ model Child {
dateOfBirth DateTime dateOfBirth DateTime
photoUrl String? photoUrl String?
photoConsent Boolean @default(false) photoConsent Boolean @default(false)
isMultiple Boolean @default(false)
isUnborn Boolean @default(false) isUnborn Boolean @default(false)
parentId String parentId String
parent Parent @relation(fields: [parentId], references: [id]) parent Parent @relation(fields: [parentId], references: [id])
-3
View File
@@ -63,9 +63,6 @@ export class Children {
@Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' }) @Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' })
consent_photo_at?: Date; consent_photo_at?: Date;
@Column({ default: false, name: 'est_multiple', type: 'boolean' })
is_multiple: boolean;
// Lien via table de jointure enfants_parents // Lien via table de jointure enfants_parents
@OneToMany(() => ParentsChildren, pc => pc.child) @OneToMany(() => ParentsChildren, pc => pc.child)
parentLinks: ParentsChildren[]; parentLinks: ParentsChildren[];
-4
View File
@@ -564,7 +564,6 @@ export class AuthService {
enfant.status = enfantDto.date_naissance ? StatutEnfantType.SANS_GARDE : StatutEnfantType.A_NAITRE; enfant.status = enfantDto.date_naissance ? StatutEnfantType.SANS_GARDE : StatutEnfantType.A_NAITRE;
enfant.consent_photo = !!enfantDto.consent_photo; enfant.consent_photo = !!enfantDto.consent_photo;
enfant.consent_photo_at = enfant.consent_photo ? new Date() : null!; enfant.consent_photo_at = enfant.consent_photo ? new Date() : null!;
enfant.is_multiple = enfantDto.grossesse_multiple || false;
const enfantEnregistre = await manager.save(Children, enfant); const enfantEnregistre = await manager.save(Children, enfant);
enfantsEnregistres.push(enfantEnregistre); enfantsEnregistres.push(enfantEnregistre);
@@ -1387,9 +1386,6 @@ export class AuthService {
enfant.status = StatutEnfantType.A_NAITRE; enfant.status = StatutEnfantType.A_NAITRE;
} }
} }
if (enfantDto.grossesse_multiple !== undefined) {
enfant.is_multiple = enfantDto.grossesse_multiple;
}
if (enfantDto.consent_photo !== undefined) { if (enfantDto.consent_photo !== undefined) {
enfant.consent_photo = !!enfantDto.consent_photo; enfant.consent_photo = !!enfantDto.consent_photo;
enfant.consent_photo_at = enfant.consent_photo enfant.consent_photo_at = enfant.consent_photo
@@ -55,11 +55,6 @@ export class EnfantInscriptionDto {
@IsString() @IsString()
photo_filename?: string; photo_filename?: string;
@ApiProperty({ example: false, required: false, description: 'Grossesse multiple (jumeaux, triplés, etc.)' })
@IsOptional()
@IsBoolean()
grossesse_multiple?: boolean;
@ApiProperty({ @ApiProperty({
example: true, example: true,
required: false, required: false,
@@ -74,11 +74,6 @@ export class CreateEnfantsDto {
@IsDateString() @IsDateString()
consent_photo_at?: string; consent_photo_at?: string;
@ApiProperty({ default: false })
@Transform(toBoolean)
@IsBoolean()
is_multiple: boolean;
/** /**
* Parent pivot du foyer — obligatoire pour staff (gestionnaire/admin). * Parent pivot du foyer — obligatoire pour staff (gestionnaire/admin).
* Ignoré / interdit en externe pour un PARENT (ticket #132). * Ignoré / interdit en externe pour un PARENT (ticket #132).
@@ -29,9 +29,6 @@ export class EnfantResponseDto {
@ApiProperty({ example: false }) @ApiProperty({ example: false })
consent_photo: boolean; consent_photo: boolean;
@ApiProperty({ example: false })
is_multiple: boolean;
@ApiProperty({ example: 'UUID-parent' }) @ApiProperty({ example: 'UUID-parent' })
parent_id: string; parent_id: string;
} }
@@ -92,7 +92,6 @@ export class EnfantsService {
photo_url: photoUrl, photo_url: photoUrl,
consent_photo: !!dto.consent_photo, consent_photo: !!dto.consent_photo,
consent_photo_at: consentAt, consent_photo_at: consentAt,
is_multiple: !!dto.is_multiple,
}); });
await this.childrenRepository.save(child); await this.childrenRepository.save(child);
@@ -54,9 +54,6 @@ export class DossierFamilleEnfantDto {
description: 'Consentement affichage photo (colonne consentement_photo)', description: 'Consentement affichage photo (colonne consentement_photo)',
}) })
consent_photo?: boolean; consent_photo?: boolean;
@ApiProperty({ required: false, description: 'Grossesse multiple (est_multiple)' })
est_multiple?: boolean;
} }
/** Réponse GET /parents/dossier-famille/:numeroDossier dossier famille complet. Ticket #119 */ /** Réponse GET /parents/dossier-famille/:numeroDossier dossier famille complet. Ticket #119 */
@@ -370,7 +370,6 @@ export class ParentsService {
status: child.status, status: child.status,
photo_url: child.photo_url ?? undefined, photo_url: child.photo_url ?? undefined,
consent_photo: child.consent_photo, consent_photo: child.consent_photo,
est_multiple: child.is_multiple,
}; };
} }
+1 -2
View File
@@ -174,8 +174,7 @@ CREATE TABLE enfants (
date_prevue_naissance DATE, date_prevue_naissance DATE,
photo_url TEXT, photo_url TEXT,
consentement_photo BOOLEAN DEFAULT false, consentement_photo BOOLEAN DEFAULT false,
date_consentement_photo TIMESTAMPTZ, date_consentement_photo TIMESTAMPTZ
est_multiple BOOLEAN DEFAULT false
); );
-- ========================================================== -- ==========================================================
+10 -10
View File
@@ -1,10 +1,10 @@
"id","statut","prenom","nom","genre","date_naissance","date_prevue_naissance","photo_url","consentement_photo","date_consentement_photo","est_multiple" "id","statut","prenom","nom","genre","date_naissance","date_prevue_naissance","photo_url","consentement_photo","date_consentement_photo"
"5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","sans_garde","Emma","Dupont","F","2020-06-01",,,False,,False "5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","sans_garde","Emma","Dupont","F","2020-06-01",,,False,
"a5c3268e-07eb-41a4-9f6c-2f9f16f37c3d","sans_garde",,,,"2020-01-01","2025-01-01",,False,,False "a5c3268e-07eb-41a4-9f6c-2f9f16f37c3d","sans_garde",,,,"2020-01-01","2025-01-01",,False,
"e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c","sans_garde","Emma","Martin",,"2023-02-15",,,False,,False "e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c","sans_garde","Emma","Martin",,"2023-02-15",,,False,
"e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d","sans_garde","Noah","Martin",,"2023-02-15",,,False,,False "e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d","sans_garde","Noah","Martin",,"2023-02-15",,,False,
"e3c4d5e6-f7a8-4b9c-1d2e-3f4a5b6c7d8e","sans_garde","Léa","Martin",,"2023-02-15",,,False,,False "e3c4d5e6-f7a8-4b9c-1d2e-3f4a5b6c7d8e","sans_garde","Léa","Martin",,"2023-02-15",,,False,
"e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f","sans_garde","Chloé","Rousseau",,"2022-04-20",,,False,,False "e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f","sans_garde","Chloé","Rousseau",,"2022-04-20",,,False,
"e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a","sans_garde","Hugo","Rousseau",,"2024-03-10",,,False,,False "e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a","sans_garde","Hugo","Rousseau",,"2024-03-10",,,False,
"e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b","sans_garde","Maxime","Lecomte",,"2023-04-15",,,False,,False "e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b","sans_garde","Maxime","Lecomte",,"2023-04-15",,,False,
"edd19cd1-bb67-4f14-8a37-c66b75c94537","scolarise","Lucas","Durand","H","2018-09-15",,,False,,False "edd19cd1-bb67-4f14-8a37-c66b75c94537","scolarise","Lucas","Durand","H","2018-09-15",,,False,
1 id statut prenom nom genre date_naissance date_prevue_naissance photo_url consentement_photo date_consentement_photo est_multiple
2 5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf sans_garde Emma Dupont F 2020-06-01 False False
3 a5c3268e-07eb-41a4-9f6c-2f9f16f37c3d sans_garde 2020-01-01 2025-01-01 False False
4 e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c sans_garde Emma Martin 2023-02-15 False False
5 e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d sans_garde Noah Martin 2023-02-15 False False
6 e3c4d5e6-f7a8-4b9c-1d2e-3f4a5b6c7d8e sans_garde Léa Martin 2023-02-15 False False
7 e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f sans_garde Chloé Rousseau 2022-04-20 False False
8 e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a sans_garde Hugo Rousseau 2024-03-10 False False
9 e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b sans_garde Maxime Lecomte 2023-04-15 False False
10 edd19cd1-bb67-4f14-8a37-c66b75c94537 scolarise Lucas Durand H 2018-09-15 False False
@@ -0,0 +1,2 @@
-- #152 — Suppression grossesse multiple / est_multiple
ALTER TABLE enfants DROP COLUMN IF EXISTS est_multiple;
+4 -4
View File
@@ -69,12 +69,12 @@ ON CONFLICT (id_utilisateur) DO NOTHING;
-- - child B : à naître (statut = 'a_naitre' et date_prevue_naissance requise) -- - child B : à naître (statut = 'a_naitre' et date_prevue_naissance requise)
-- ------------------------------------------------------------ -- ------------------------------------------------------------
INSERT INTO enfants (id, prenom, nom, statut, date_naissance, jumeau_multiple) INSERT INTO enfants (id, prenom, nom, statut, date_naissance)
VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Léo', 'Parent', 'sans_garde', '2022-04-12', false) VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Léo', 'Parent', 'sans_garde', '2022-04-12')
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO enfants (id, prenom, nom, statut, date_prevue_naissance, jumeau_multiple) INSERT INTO enfants (id, prenom, nom, statut, date_prevue_naissance)
VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Mila', 'Parent', 'a_naitre', '2026-02-15', false) VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Mila', 'Parent', 'a_naitre', '2026-02-15')
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
-- ------------------------------------------------------------ -- ------------------------------------------------------------
+7 -7
View File
@@ -49,14 +49,14 @@ VALUES
ON CONFLICT (id_utilisateur) DO NOTHING; ON CONFLICT (id_utilisateur) DO NOTHING;
-- ========== ENFANTS ========== -- ========== ENFANTS ==========
INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut, est_multiple) INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut)
VALUES VALUES
('e0000001-0001-0001-0001-000000000001', 'Emma', 'MARTIN', 'F', '2023-02-15', 'sans_garde', true), ('e0000001-0001-0001-0001-000000000001', 'Emma', 'MARTIN', 'F', '2023-02-15', 'sans_garde'),
('e0000002-0002-0002-0002-000000000002', 'Noah', 'MARTIN', 'H', '2023-02-15', 'sans_garde', true), ('e0000002-0002-0002-0002-000000000002', 'Noah', 'MARTIN', 'H', '2023-02-15', 'sans_garde'),
('e0000003-0003-0003-0003-000000000003', 'Léa', 'MARTIN', 'F', '2023-02-15', 'sans_garde', true), ('e0000003-0003-0003-0003-000000000003', 'Léa', 'MARTIN', 'F', '2023-02-15', 'sans_garde'),
('e0000004-0004-0004-0004-000000000004', 'Chloé', 'ROUSSEAU', 'F', '2022-04-20', 'sans_garde', false), ('e0000004-0004-0004-0004-000000000004', 'Chloé', 'ROUSSEAU', 'F', '2022-04-20', 'sans_garde'),
('e0000005-0005-0005-0005-000000000005', 'Hugo', 'ROUSSEAU', 'H', '2024-03-10', 'sans_garde', false), ('e0000005-0005-0005-0005-000000000005', 'Hugo', 'ROUSSEAU', 'H', '2024-03-10', 'sans_garde'),
('e0000006-0006-0006-0006-000000000006', 'Maxime', 'LECOMTE', 'H', '2023-04-15', 'sans_garde', false) ('e0000006-0006-0006-0006-000000000006', 'Maxime', 'LECOMTE', 'H', '2023-04-15', 'sans_garde')
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
-- ========== ENFANTS_PARENTS (liaison N:N) ========== -- ========== ENFANTS_PARENTS (liaison N:N) ==========
-1
View File
@@ -117,7 +117,6 @@ Table des enfants pris en charge.
| `photo_url` | TEXT | | URL de la photo | | `photo_url` | TEXT | | URL de la photo |
| `consentement_photo` | BOOLEAN | DEFAULT false | Consentement photo | | `consentement_photo` | BOOLEAN | DEFAULT false | Consentement photo |
| `date_consentement_photo` | TIMESTAMPTZ | | Date du consentement | | `date_consentement_photo` | TIMESTAMPTZ | | Date du consentement |
| `est_multiple` | BOOLEAN | DEFAULT false | Indique si grossesse multiple |
--- ---
-3
View File
@@ -276,9 +276,6 @@ export class Enfants {
@Column({ name: 'consentement_photo', type: 'boolean', default: false }) @Column({ name: 'consentement_photo', type: 'boolean', default: false })
consentementPhoto: boolean; consentementPhoto: boolean;
@Column({ name: 'est_multiple', type: 'boolean', default: false })
estMultiple: boolean;
@Column({ @Column({
type: 'enum', type: 'enum',
enum: StatutEnfantType, enum: StatutEnfantType,
@@ -0,0 +1,65 @@
# Mini-spec — Suppression complète grossesse multiple / `est_multiple`
**Ticket** : **#152** — https://git.ptits-pas.fr/jmartin/petitspas/issues/152
**Branche** : `feature/152-remove-est-multiple` (depuis `develop`)
**Périmètre** : **full stack** — BDD + back + front + scripts + docs. **Aucun fantôme.**
---
## Décision
On **supprime tout**. Pas de DTO « ignorés », pas de compat payload.
`forbidNonWhitelisted: true` ⇒ back et front **partent ensemble** (même feature / même déploiement).
---
## Alias retirés
`est_multiple` · `is_multiple` · `grossesse_multiple` · `multipleBirth` · `estMultiple` · `isMultiple` · `jumeau_multiple`
---
## Back / BDD (fait sur la branche)
- [x] Migration `database/migrations/2026_drop_enfants_est_multiple.sql`
- [x] `BDD.sql`, seeds, CSV test
- [x] Entity `Children` sans colonne
- [x] DTO create/inscription/réponse/dossier famille **sans** le champ
- [x] Services auth / enfants / parents : plus de mapping
- [x] Prisma legacy `isMultiple` retiré
## Front (fait sur la branche)
- [x] Modèles admin / dossier / inscription
- [x] Payloads inscription + reprise
- [x] Modale enfant + wizard dossier famille
- [x] Step3 inscription parent
## Scripts / docs
- [x] `tests/scripts/register-parent-*.mjs`
- [x] `docs/10_DATABASE.md`, `docs/99_REGLES-CODAGE.md`
- Docs tmp/archive #112 : mentions historiques OK (archive)
---
## Déploiement
1. Appliquer la migration SQL sur la BDD vivante
2. Deploy back **et** front de cette branche
3. Smoke : création enfant staff, inscription parent, reprise, wizard famille
## Vérif
```bash
rg -n 'est_multiple|is_multiple|grossesse_multiple|multipleBirth|estMultiple|isMultiple|jumeau_multiple' \
backend/src frontend/lib database tests/scripts docs/10_DATABASE.md docs/99_REGLES-CODAGE.md
```
**0** hit (hors ce fichier mini-spec / archives).
## Hors scope
- Métier futur « fratrie / jumeaux » → nouveau ticket
- #155 rename Admin*
- Ticket modales staff
@@ -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 dAPI / 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`
-4
View File
@@ -185,7 +185,6 @@ class EnfantDossier {
final String? dueDate; final String? dueDate;
final String? photoUrl; final String? photoUrl;
final bool consentPhoto; final bool consentPhoto;
final bool estMultiple;
EnfantDossier({ EnfantDossier({
required this.id, required this.id,
@@ -197,7 +196,6 @@ class EnfantDossier {
this.dueDate, this.dueDate,
this.photoUrl, this.photoUrl,
this.consentPhoto = false, this.consentPhoto = false,
this.estMultiple = false,
}); });
String get fullName => '${firstName ?? ''} ${lastName ?? ''}'.trim(); String get fullName => '${firstName ?? ''} ${lastName ?? ''}'.trim();
@@ -231,8 +229,6 @@ class EnfantDossier {
photoUrl: resolvedPhoto, photoUrl: resolvedPhoto,
consentPhoto: consentPhoto:
json['consent_photo'] == true || json['consentPhoto'] == true, json['consent_photo'] == true || json['consentPhoto'] == true,
estMultiple:
json['est_multiple'] == true || json['estMultiple'] == true,
); );
} }
} }
@@ -12,7 +12,6 @@ class EnfantAdminModel {
final String status; final String status;
final String? photoUrl; final String? photoUrl;
final bool consentPhoto; final bool consentPhoto;
final bool isMultiple;
final List<EnfantParentLink> parentLinks; final List<EnfantParentLink> parentLinks;
/// Flag API #157 (sinon déduit de [parentLinks]). /// Flag API #157 (sinon déduit de [parentLinks]).
final bool? sansResponsable; final bool? sansResponsable;
@@ -27,7 +26,6 @@ class EnfantAdminModel {
required this.status, required this.status,
this.photoUrl, this.photoUrl,
this.consentPhoto = false, this.consentPhoto = false,
this.isMultiple = false,
this.parentLinks = const [], this.parentLinks = const [],
this.sansResponsable, this.sansResponsable,
}); });
@@ -56,7 +54,6 @@ class EnfantAdminModel {
String? status, String? status,
String? photoUrl, String? photoUrl,
bool? consentPhoto, bool? consentPhoto,
bool? isMultiple,
List<EnfantParentLink>? parentLinks, List<EnfantParentLink>? parentLinks,
bool? sansResponsable, bool? sansResponsable,
}) { }) {
@@ -70,7 +67,6 @@ class EnfantAdminModel {
status: status ?? this.status, status: status ?? this.status,
photoUrl: photoUrl ?? this.photoUrl, photoUrl: photoUrl ?? this.photoUrl,
consentPhoto: consentPhoto ?? this.consentPhoto, consentPhoto: consentPhoto ?? this.consentPhoto,
isMultiple: isMultiple ?? this.isMultiple,
parentLinks: parentLinks ?? this.parentLinks, parentLinks: parentLinks ?? this.parentLinks,
sansResponsable: sansResponsable ?? this.sansResponsable, sansResponsable: sansResponsable ?? this.sansResponsable,
); );
@@ -111,8 +107,6 @@ class EnfantAdminModel {
), ),
photoUrl: photoUrl, photoUrl: photoUrl,
consentPhoto: consentPhoto, consentPhoto: consentPhoto,
isMultiple: _parseBool(json['is_multiple']) ||
_parseBool(json['est_multiple']),
parentLinks: links, parentLinks: links,
sansResponsable: sansResponsable, sansResponsable: sansResponsable,
); );
@@ -127,7 +121,6 @@ class EnfantAdminModel {
if (birthDate != null && birthDate!.isNotEmpty) 'birth_date': birthDate, if (birthDate != null && birthDate!.isNotEmpty) 'birth_date': birthDate,
if (dueDate != null && dueDate!.isNotEmpty) 'due_date': dueDate, if (dueDate != null && dueDate!.isNotEmpty) 'due_date': dueDate,
'consent_photo': consentPhoto, 'consent_photo': consentPhoto,
'is_multiple': isMultiple,
}; };
} }
@@ -30,7 +30,6 @@ class ChildData {
String lastName; String lastName;
String dob; // Date de naissance ou prévisionnelle String dob; // Date de naissance ou prévisionnelle
bool photoConsent; bool photoConsent;
bool multipleBirth;
bool isUnbornChild; bool isUnbornChild;
File? imageFile; File? imageFile;
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
@@ -40,7 +39,6 @@ class ChildData {
this.lastName = '', this.lastName = '',
this.dob = '', this.dob = '',
this.photoConsent = false, this.photoConsent = false,
this.multipleBirth = false,
this.isUnbornChild = false, this.isUnbornChild = false,
this.imageFile, this.imageFile,
required this.cardColor, // Rendre requis dans le constructeur required this.cardColor, // Rendre requis dans le constructeur
@@ -38,7 +38,6 @@ class ChildData {
/// Valeurs API : `H`, `F`, `Autre` (GenreType backend). Vide tant que non choisi. /// Valeurs API : `H`, `F`, `Autre` (GenreType backend). Vide tant que non choisi.
String genre; String genre;
bool photoConsent; bool photoConsent;
bool multipleBirth;
bool isUnbornChild; bool isUnbornChild;
File? imageFile; File? imageFile;
/// Octets de la photo (fiable à lenvoi API ; [imageFile] peut être absent sur le web). /// Octets de la photo (fiable à lenvoi API ; [imageFile] peut être absent sur le web).
@@ -55,7 +54,6 @@ class ChildData {
this.dob = '', this.dob = '',
this.genre = '', this.genre = '',
this.photoConsent = false, this.photoConsent = false,
this.multipleBirth = false,
this.isUnbornChild = false, this.isUnbornChild = false,
this.imageFile, this.imageFile,
this.imageBytes, this.imageBytes,
@@ -70,7 +68,6 @@ class ChildData {
String? dob, String? dob,
String? genre, String? genre,
bool? photoConsent, bool? photoConsent,
bool? multipleBirth,
bool? isUnbornChild, bool? isUnbornChild,
Object? imageFile = _unsetImage, Object? imageFile = _unsetImage,
Object? imageBytes = _unsetImageBytes, Object? imageBytes = _unsetImageBytes,
@@ -84,7 +81,6 @@ class ChildData {
dob: dob ?? this.dob, dob: dob ?? this.dob,
genre: genre ?? this.genre, genre: genre ?? this.genre,
photoConsent: photoConsent ?? this.photoConsent, photoConsent: photoConsent ?? this.photoConsent,
multipleBirth: multipleBirth ?? this.multipleBirth,
isUnbornChild: isUnbornChild ?? this.isUnbornChild, isUnbornChild: isUnbornChild ?? this.isUnbornChild,
imageFile: identical(imageFile, _unsetImage) ? this.imageFile : imageFile as File?, imageFile: identical(imageFile, _unsetImage) ? this.imageFile : imageFile as File?,
imageBytes: imageBytes:
@@ -3,8 +3,8 @@ 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/dashboard/user_management_sub_bar.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),
],
],
);
}
}
@@ -121,7 +121,6 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
dob: '', dob: '',
isUnbornChild: false, isUnbornChild: false,
photoConsent: false, photoConsent: false,
multipleBirth: false,
cardColor: cardColor, cardColor: cardColor,
); );
registrationData.addChild(newChild); registrationData.addChild(newChild);
@@ -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';
@@ -166,7 +166,6 @@ class ParentRegistrationPayload {
static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) { static Map<String, dynamic> _childToJson(ChildData c, int index, String parentNom) {
final map = <String, dynamic>{ final map = <String, dynamic>{
'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre', 'genre': apiGenres.contains(c.genre) ? c.genre : 'Autre',
'grossesse_multiple': c.multipleBirth,
'consent_photo': c.photoConsent, 'consent_photo': c.photoConsent,
}; };
-1
View File
@@ -44,7 +44,6 @@ class RepriseMapper {
dob: dob, dob: dob,
genre: e.gender ?? '', genre: e.gender ?? '',
photoConsent: e.consentPhoto, photoConsent: e.consentPhoto,
multipleBirth: e.estMultiple,
isUnbornChild: isUnborn, isUnbornChild: isUnborn,
cardColor: _childCardColors[index % _childCardColors.length], cardColor: _childCardColors[index % _childCardColors.length],
repriseChildId: e.id, repriseChildId: e.id,
@@ -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/dashboard/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,
@@ -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 {
@@ -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 {
@@ -16,10 +16,10 @@ 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/dashboard/am_photo_frame.dart'; import 'package:p_tits_pas/widgets/dashboard/am_photo_frame.dart';
import 'package:p_tits_pas/widgets/dashboard/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';
@@ -12,9 +12,9 @@ import 'package:p_tits_pas/widgets/dashboard/am_photo_frame.dart';
import 'package:p_tits_pas/widgets/dashboard/child_detail_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/child_detail_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/select_enfant_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/select_enfant_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/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 AmEditModal extends StatefulWidget { class AmEditModal extends StatefulWidget {
@@ -7,8 +7,8 @@ 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/dashboard/am_edit_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/am_edit_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/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;
@@ -16,9 +16,9 @@ import 'package:p_tits_pas/widgets/dashboard/am_photo_frame.dart';
import 'package:p_tits_pas/widgets/dashboard/parent_edit_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/parent_edit_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/select_am_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/select_am_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/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).
@@ -55,7 +55,6 @@ class _ChildDetailModalState extends State<ChildDetailModal> {
late String _status; late String _status;
late String? _gender; late String? _gender;
late bool _consentPhoto; late bool _consentPhoto;
late bool _isMultiple;
bool _dirty = false; bool _dirty = false;
bool _saving = false; bool _saving = false;
bool _deleting = false; bool _deleting = false;
@@ -142,7 +141,6 @@ class _ChildDetailModalState extends State<ChildDetailModal> {
_gender = _normalizeGender(e?.gender, allowUnknown: _isUnborn); _gender = _normalizeGender(e?.gender, allowUnknown: _isUnborn);
} }
_consentPhoto = e?.consentPhoto ?? false; _consentPhoto = e?.consentPhoto ?? false;
_isMultiple = e?.isMultiple ?? false;
for (final c in [_prenomCtrl, _nomCtrl]) { for (final c in [_prenomCtrl, _nomCtrl]) {
c.addListener(_onNameChanged); c.addListener(_onNameChanged);
} }
@@ -383,7 +381,6 @@ class _ChildDetailModalState extends State<ChildDetailModal> {
else if (_dateToIso(_birthCtrl.text) != null) else if (_dateToIso(_birthCtrl.text) != null)
'birth_date': _dateToIso(_birthCtrl.text), 'birth_date': _dateToIso(_birthCtrl.text),
'consent_photo': _consentPhoto, 'consent_photo': _consentPhoto,
'is_multiple': _isMultiple,
}, },
); );
if (!mounted) return; if (!mounted) return;
@@ -460,7 +457,6 @@ class _ChildDetailModalState extends State<ChildDetailModal> {
'birth_date': _dateToIso(_birthCtrl.text), 'birth_date': _dateToIso(_birthCtrl.text),
}, },
'consent_photo': _consentPhoto, 'consent_photo': _consentPhoto,
'is_multiple': _isMultiple,
}, },
); );
if (!mounted) return; if (!mounted) return;
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:p_tits_pas/widgets/dashboard/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 licône. /// Carte dossier unifiée (#153) — fond neutre, accent couleur sur licône.
class DossierListCard extends StatelessWidget { class DossierListCard extends StatelessWidget {
@@ -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 {
@@ -7,8 +7,8 @@ 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/dashboard/child_detail_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/child_detail_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/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 {
@@ -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/dashboard/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) {
@@ -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,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 {
@@ -15,10 +15,10 @@ 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/dashboard/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';
@@ -1466,7 +1466,6 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
final map = <String, dynamic>{ final map = <String, dynamic>{
'genre': c.genre, 'genre': c.genre,
'consent_photo': true, 'consent_photo': true,
'grossesse_multiple': false,
}; };
if (prenom.length >= 2) { if (prenom.length >= 2) {
@@ -1723,7 +1722,6 @@ class _ParentDossierWizardState extends State<ParentDossierWizard> {
'status': status, 'status': status,
'gender': gender, 'gender': gender,
'consent_photo': true, 'consent_photo': true,
'is_multiple': false,
}; };
if (prenom.length >= 2) map['first_name'] = prenom; if (prenom.length >= 2) map['first_name'] = prenom;
if (nom.length >= 2) map['last_name'] = nom; if (nom.length >= 2) map['last_name'] = nom;
@@ -9,7 +9,7 @@ import 'package:p_tits_pas/widgets/dashboard/children_affiliation_panel.dart';
import 'package:p_tits_pas/widgets/dashboard/select_enfant_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/select_enfant_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/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.
@@ -6,8 +6,8 @@ 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/dashboard/parent_edit_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/parent_edit_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/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;
@@ -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 {
@@ -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});
@@ -6,7 +6,7 @@ import 'package:p_tits_pas/utils/am_vigilance.dart';
import 'package:p_tits_pas/widgets/dashboard/am_edit_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/am_edit_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/select_list_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/select_list_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/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>[];
@@ -1,5 +1,5 @@
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 SelectToggleFilter<T> { class SelectToggleFilter<T> {
@@ -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(),
),
],
),
),
),
);
}
}
@@ -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/dashboard/child_detail_modal.dart'; import 'package:p_tits_pas/widgets/dashboard/child_detail_modal.dart';
import 'package:p_tits_pas/widgets/dashboard/user_management_sub_bar.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).
@@ -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,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 {
@@ -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,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 {
@@ -74,8 +74,7 @@ try {
date_naissance: '2022-04-20', date_naissance: '2022-04-20',
genre: 'F', genre: 'F',
photo_base64: toDataUri(chloeJpg), photo_base64: toDataUri(chloeJpg),
photo_filename: 'chloe_rousseau.jpg', photo_filename: 'chloe_rousseau.jpg'
grossesse_multiple: false,
}, },
{ {
prenom: 'Hugo', prenom: 'Hugo',
@@ -83,8 +82,7 @@ try {
date_naissance: '2024-03-10', date_naissance: '2024-03-10',
genre: 'H', genre: 'H',
photo_base64: toDataUri(hugoJpg), photo_base64: toDataUri(hugoJpg),
photo_filename: 'hugo_rousseau.jpg', photo_filename: 'hugo_rousseau.jpg'
grossesse_multiple: false,
}, },
], ],
presentation_dossier: presentationDossier, presentation_dossier: presentationDossier,
@@ -40,8 +40,7 @@ const body = {
date_naissance: '2023-04-15', date_naissance: '2023-04-15',
genre: 'H', genre: 'H',
photo_base64: toDataUri(path.join(photosDir, 'lecomte-maxime.png')), photo_base64: toDataUri(path.join(photosDir, 'lecomte-maxime.png')),
photo_filename: 'maxime_lecomte.png', photo_filename: 'maxime_lecomte.png'
grossesse_multiple: false,
}, },
], ],
presentation_dossier: presentationDossier, presentation_dossier: presentationDossier,
@@ -46,8 +46,7 @@ const body = {
date_naissance: '2023-02-15', date_naissance: '2023-02-15',
genre: 'F', genre: 'F',
photo_base64: toDataUri(path.join(photosDir, 'martin-emma.png')), photo_base64: toDataUri(path.join(photosDir, 'martin-emma.png')),
photo_filename: 'emma_martin.png', photo_filename: 'emma_martin.png'
grossesse_multiple: true,
}, },
{ {
prenom: 'Noah', prenom: 'Noah',
@@ -55,8 +54,7 @@ const body = {
date_naissance: '2023-02-15', date_naissance: '2023-02-15',
genre: 'H', genre: 'H',
photo_base64: toDataUri(path.join(photosDir, 'martin-noah.png')), photo_base64: toDataUri(path.join(photosDir, 'martin-noah.png')),
photo_filename: 'noah_martin.png', photo_filename: 'noah_martin.png'
grossesse_multiple: true,
}, },
{ {
prenom: 'Léa', prenom: 'Léa',
@@ -64,8 +62,7 @@ const body = {
date_naissance: '2023-02-15', date_naissance: '2023-02-15',
genre: 'F', genre: 'F',
photo_base64: toDataUri(path.join(photosDir, 'martin-lea.png')), photo_base64: toDataUri(path.join(photosDir, 'martin-lea.png')),
photo_filename: 'lea_martin.png', photo_filename: 'lea_martin.png'
grossesse_multiple: true,
}, },
], ],
presentation_dossier: presentationDossier, presentation_dossier: presentationDossier,