Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d6261b312 | ||
|
|
9557cf9947 | ||
|
|
939e7777ac | ||
|
|
b474842e19 | ||
|
|
a312d9c0fa | ||
|
|
b5b32062b3 | ||
|
|
946d8edcd2 |
@@ -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])
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
-- #152 — Suppression grossesse multiple / est_multiple
|
||||||
|
ALTER TABLE enfants DROP COLUMN IF EXISTS est_multiple;
|
||||||
@@ -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;
|
||||||
|
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
|
|||||||
@@ -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) ==========
|
||||||
|
|||||||
@@ -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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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 d’API / BDD
|
||||||
|
**Milestone** : **0.1.0**
|
||||||
|
|
||||||
|
Voir le corps du ticket #164 pour la spec complète.
|
||||||
|
|
||||||
|
## Livré
|
||||||
|
|
||||||
|
- `frontend/lib/widgets/dashboard/staff_user_form_modal.dart` → `StaffUserFormModal`
|
||||||
|
- Ancien `AdminUserFormDialog` / `gestionnaires_create.dart` retiré
|
||||||
|
- Imports : `user_management_panel`, `gestionnaire_management_widget`, `admin_management_widget`
|
||||||
@@ -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 à l’envoi API ; [imageFile] peut être absent sur le web).
|
/// Octets de la photo (fiable à l’envoi 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
-1
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/am_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_dossier_wizard.dart';
|
||||||
|
|
||||||
/// Modale de création dossier AM (#156) — même shell que [ValidationDossierModal].
|
/// Modale de création dossier AM (#156) — même shell que [ValidationDossierModal].
|
||||||
class AmDossierCreateModal extends StatefulWidget {
|
class AmDossierCreateModal extends StatefulWidget {
|
||||||
+4
-4
@@ -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 {
|
||||||
|
|||||||
+2
-2
@@ -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
-1
@@ -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 l’icône.
|
/// Carte dossier unifiée (#153) — fond neutre, accent couleur sur l’icône.
|
||||||
class DossierListCard extends StatelessWidget {
|
class DossierListCard extends StatelessWidget {
|
||||||
+4
-4
@@ -3,10 +3,10 @@ import 'package:p_tits_pas/models/dossier_list_item.dart';
|
|||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dossier_list_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/dossier_list_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/pending_validation_widget.dart';
|
import 'package:p_tits_pas/widgets/dashboard/pending_validation_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_dossier_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_dossier_modal.dart';
|
||||||
|
|
||||||
/// Onglet permanent « Dossiers » (#153) : pending en haut + liste unifiée en bas.
|
/// Onglet permanent « Dossiers » (#153) : pending en haut + liste unifiée en bas.
|
||||||
class DossiersManagementWidget extends StatefulWidget {
|
class DossiersManagementWidget extends StatefulWidget {
|
||||||
+2
-2
@@ -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 {
|
||||||
+4
-4
@@ -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) {
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:p_tits_pas/services/configuration_service.dart';
|
import 'package:p_tits_pas/services/configuration_service.dart';
|
||||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/relais_management_panel.dart';
|
import 'package:p_tits_pas/widgets/dashboard/relais_management_panel.dart';
|
||||||
|
|
||||||
/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé.
|
/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé.
|
||||||
class ParametresPanel extends StatefulWidget {
|
class ParametresPanel extends StatefulWidget {
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_dossier_wizard.dart';
|
||||||
|
|
||||||
/// Modale de création dossier famille (#129) — même shell que [AmDossierCreateModal].
|
/// Modale de création dossier famille (#129) — même shell que [AmDossierCreateModal].
|
||||||
class ParentDossierCreateModal extends StatefulWidget {
|
class ParentDossierCreateModal extends StatefulWidget {
|
||||||
+4
-6
@@ -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.
|
||||||
|
|||||||
+2
-2
@@ -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
-3
@@ -3,9 +3,9 @@ import 'package:p_tits_pas/models/dossier_list_item.dart';
|
|||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/models/pending_family.dart';
|
import 'package:p_tits_pas/models/pending_family.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dossier_list_card.dart';
|
import 'package:p_tits_pas/widgets/dashboard/dossier_list_card.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_dossier_modal.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_dossier_modal.dart';
|
||||||
|
|
||||||
/// Section « dossiers à valider » (liste unifiée AM + familles). Ticket #107 / #153.
|
/// Section « dossiers à valider » (liste unifiée AM + familles). Ticket #107 / #153.
|
||||||
class PendingValidationWidget extends StatefulWidget {
|
class PendingValidationWidget extends StatefulWidget {
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:p_tits_pas/models/relais_model.dart';
|
import 'package:p_tits_pas/models/relais_model.dart';
|
||||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||||
import 'package:p_tits_pas/services/relais_service.dart';
|
import 'package:p_tits_pas/services/relais_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||||
|
|
||||||
class RelaisManagementPanel extends StatefulWidget {
|
class RelaisManagementPanel extends StatefulWidget {
|
||||||
const RelaisManagementPanel({super.key});
|
const RelaisManagementPanel({super.key});
|
||||||
@@ -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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-10
@@ -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
-1
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/am_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_dossier_wizard.dart';
|
||||||
|
|
||||||
/// Wrapper historique (#107) — délègue à [AmDossierWizard.review].
|
/// Wrapper historique (#107) — délègue à [AmDossierWizard.review].
|
||||||
class ValidationAmWizard extends StatelessWidget {
|
class ValidationAmWizard extends StatelessWidget {
|
||||||
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/am_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/am_dossier_wizard.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_dossier_wizard.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_am_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_am_wizard.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_family_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/validation_family_wizard.dart';
|
||||||
|
|
||||||
/// Modale (dialog) : charge le dossier par numéro puis affiche le wizard AM ou Famille.
|
/// Modale (dialog) : charge le dossier par numéro puis affiche le wizard AM ou Famille.
|
||||||
/// Ticket #107 / #119 (review), #135 (`openAsEdit`).
|
/// Ticket #107 / #119 (review), #135 (`openAsEdit`).
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_dossier_wizard.dart';
|
import 'package:p_tits_pas/widgets/dashboard/parent_dossier_wizard.dart';
|
||||||
|
|
||||||
/// Wrapper historique (#107) — délègue à [ParentDossierWizard.review].
|
/// Wrapper historique (#107) — délègue à [ParentDossierWizard.review].
|
||||||
class ValidationFamilyWizard extends StatelessWidget {
|
class ValidationFamilyWizard extends StatelessWidget {
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user