Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d6261b312 | ||
|
|
9557cf9947 | ||
|
|
939e7777ac | ||
|
|
b474842e19 |
@@ -37,7 +37,9 @@ On garde `Admin*` seulement là où c’est vraiment le rôle administrateur.
|
||||
| `admin_detail_modal` → `AdminDetailModal` / `AdminDetailField` | `detail_modal` → `DetailModal` / `DetailField` |
|
||||
| `dashboard_admin.dart` | `user_management_sub_bar.dart` (`DashboardUserManagementSubBar` inchangé) |
|
||||
|
||||
Dossier `widgets/admin/` conserve les panels métier (`user_management_panel`, wizards, etc.) + `AdminManagementWidget`.
|
||||
Dossier `widgets/admin/` conserve encore les panels métier (`user_management_panel`, wizards, etc.) + `AdminManagementWidget`.
|
||||
|
||||
**Phase 2** (même ticket #155) : déplacer ces panels → `widgets/dashboard/` — voir [155-suite-move-admin-panels-to-dashboard.md](./155-suite-move-admin-panels-to-dashboard.md).
|
||||
|
||||
## Hors scope
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# Mini-spec — Déplacer les panels `widgets/admin/` → `widgets/dashboard/`
|
||||
|
||||
**Ticket** : **#155** (phase 2 — même ticket que le rename `Admin*`)
|
||||
**Phase 1** : widgets `Admin*` → `widgets/dashboard/` (déjà sur `feature/155-rename-admin-prefix-dashboard`)
|
||||
**Branche** : poursuivre / rebaser `feature/155-rename-admin-prefix-dashboard` (ou nouvelle branche depuis `develop` après merge phase 1)
|
||||
**Nature** : rename / move mécanique — **zéro** changement UX / métier
|
||||
|
||||
---
|
||||
|
||||
## Contexte
|
||||
|
||||
Après la phase 1 (#155), la situation est **hybride** :
|
||||
|
||||
| Emplacement | Contenu |
|
||||
|-------------|---------|
|
||||
| `widgets/dashboard/` | Composants partagés sans préfixe `Admin*` (modales, cartes, selects, sub-bar…) |
|
||||
| `widgets/admin/` | **Panels** du dashboard staff (listes, wizards, validation, shell `UserManagementPanel`…) + `AdminManagementWidget` |
|
||||
|
||||
Le dossier `admin/` laisse encore croire « réservé administrateur », alors que **gestionnaire** consomme les mêmes panels (`GestionnaireDashboardScreen` → `UserManagementPanel`).
|
||||
|
||||
Ce ticket **termine l’option C** au niveau dossier : tout le dashboard staff vit sous `widgets/dashboard/`, sauf ce qui est **vraiment** rôle admin.
|
||||
|
||||
---
|
||||
|
||||
## Objectif
|
||||
|
||||
```
|
||||
frontend/lib/widgets/admin/<panels & common partagés>
|
||||
↓ git mv + update imports
|
||||
frontend/lib/widgets/dashboard/…
|
||||
```
|
||||
|
||||
Critère : un nouveau dev ne doit plus ouvrir `widgets/admin/` pour du code partagé admin+gestionnaire.
|
||||
|
||||
---
|
||||
|
||||
## Cible d’arborescence (proposée)
|
||||
|
||||
```
|
||||
widgets/dashboard/
|
||||
├── (déjà là #155) child_detail_modal.dart, am_edit_modal.dart, user_card.dart, …
|
||||
├── user_management_panel.dart ← shell onglets
|
||||
├── user_management_sub_bar.dart ← déjà déplacé #155
|
||||
├── dossiers_management_widget.dart
|
||||
├── dossier_list_card.dart
|
||||
├── parent_management_widget.dart ← corriger le typo managmant au passage ?
|
||||
├── enfant_management_widget.dart
|
||||
├── assistante_maternelle_management_widget.dart
|
||||
├── gestionnaire_management_widget.dart
|
||||
├── pending_validation_widget.dart
|
||||
├── parent_dossier_create_modal.dart
|
||||
├── parent_dossier_wizard.dart
|
||||
├── am_dossier_create_modal.dart
|
||||
├── am_dossier_wizard.dart
|
||||
├── validation_*.dart ← family/am wizards, refus, theme, confirm
|
||||
├── parametres_panel.dart ← utilisé par écran admin (OK dans dashboard)
|
||||
├── relais_management_panel.dart
|
||||
├── common/ ← sous-dossier optionnel
|
||||
│ ├── suppression_confirm_dialog.dart
|
||||
│ ├── user_list.dart
|
||||
│ └── validation_detail_section.dart
|
||||
└── …
|
||||
|
||||
widgets/admin/ ← mince, rôle admin seulement
|
||||
└── admin_management_widget.dart ← onglet Administrateurs
|
||||
```
|
||||
|
||||
### Variante B (plus stricte)
|
||||
|
||||
`AdminManagementWidget` + éventuels helpers purement admin →
|
||||
`screens/administrateurs/widgets/`
|
||||
et **suppression** du dossier `widgets/admin/`.
|
||||
|
||||
**Reco** : **variante A** (garder `widgets/admin/` minimal avec seulement `AdminManagementWidget`) — moins de churn screens, clair.
|
||||
|
||||
---
|
||||
|
||||
## Inventaire à déplacer (état actuel)
|
||||
|
||||
### Racine `widgets/admin/` → `widgets/dashboard/`
|
||||
|
||||
| Fichier actuel | Notes |
|
||||
|----------------|--------|
|
||||
| `user_management_panel.dart` | Shell partagé admin + gestionnaire |
|
||||
| `dossiers_management_widget.dart` | |
|
||||
| `dossier_list_card.dart` | |
|
||||
| `parent_managmant_widget.dart` | Typo historique `managmant` — **option** : renommer → `parent_management_widget.dart` dans le même ticket ou ticket typo séparé |
|
||||
| `enfant_management_widget.dart` | |
|
||||
| `assistante_maternelle_management_widget.dart` | |
|
||||
| `gestionnaire_management_widget.dart` | |
|
||||
| `pending_validation_widget.dart` | |
|
||||
| `parent_dossier_create_modal.dart` | |
|
||||
| `parent_dossier_wizard.dart` | |
|
||||
| `am_dossier_create_modal.dart` | |
|
||||
| `am_dossier_wizard.dart` | |
|
||||
| `validation_am_wizard.dart` | |
|
||||
| `validation_family_wizard.dart` | |
|
||||
| `validation_dossier_modal.dart` | |
|
||||
| `validation_modal_theme.dart` | |
|
||||
| `validation_refus_form.dart` | |
|
||||
| `validation_valider_confirm_dialog.dart` | |
|
||||
| `parametres_panel.dart` | Écran admin seulement, mais pas préfixé Admin — OK dashboard |
|
||||
| `relais_management_panel.dart` | |
|
||||
|
||||
### `widgets/admin/common/` → `widgets/dashboard/common/` (ou plat)
|
||||
|
||||
| Fichier | Notes |
|
||||
|---------|--------|
|
||||
| `suppression_confirm_dialog.dart` | Partagé (y compris `screens/administrateurs/creation/*`) |
|
||||
| `user_list.dart` | |
|
||||
| `validation_detail_section.dart` | |
|
||||
|
||||
### **Ne pas** déplacer
|
||||
|
||||
| Fichier | Destination |
|
||||
|---------|-------------|
|
||||
| `admin_management_widget.dart` | Reste `widgets/admin/` (ou variante B → screens) |
|
||||
|
||||
### Déjà fait (#155) — ne pas retraiter
|
||||
|
||||
Tout ce qui est déjà sous `widgets/dashboard/` (`child_detail_modal`, `am_edit_modal`, `user_card`, `select_*`, `user_management_sub_bar`, …).
|
||||
|
||||
---
|
||||
|
||||
## Consommateurs d’imports (à mettre à jour)
|
||||
|
||||
### Screens
|
||||
- `screens/administrateurs/admin_dashboardScreen.dart` — `UserManagementPanel`, `ParametresPanel`
|
||||
- `screens/gestionnaire/gestionnaire_dashboard_screen.dart` — `UserManagementPanel`
|
||||
- `screens/administrateurs/creation/admin_create.dart` — `suppression_confirm_dialog`
|
||||
- `screens/administrateurs/creation/gestionnaires_create.dart` — idem
|
||||
|
||||
### Widgets déjà en `dashboard/`
|
||||
- `am_edit_modal`, `child_detail_modal`, `parent_edit_modal`, `select_*` — imports vers `widgets/admin/common/*` ou panels
|
||||
|
||||
### Divers
|
||||
- `widgets/common/identity_block.dart` (si import admin)
|
||||
- Tous les fichiers **déplacés** entre eux (imports relatifs / package)
|
||||
|
||||
### Hors scope rename classes
|
||||
Sauf décision explicite sur le typo `parent_managmant_widget` → pas de rename de **classes** métier dans ce ticket (seulement chemins de fichiers + imports).
|
||||
`AdminManagementWidget` **conserve** son nom.
|
||||
|
||||
---
|
||||
|
||||
## Plan d’exécution
|
||||
|
||||
1. Partir de `feature/155-rename-admin-prefix-dashboard` (phase 1) **ou** `develop` si phase 1 déjà mergée
|
||||
2. `git mv` fichiers selon inventaire
|
||||
3. Remplacer globalement
|
||||
`package:p_tits_pas/widgets/admin/` → `package:p_tits_pas/widgets/dashboard/`
|
||||
**sauf** `…/widgets/admin/admin_management_widget.dart`
|
||||
4. Corriger imports relatifs cassés
|
||||
5. Grep de contrôle (ci-dessous)
|
||||
6. Build Flutter web (Docker) + smoke dashboard admin **et** gestionnaire
|
||||
7. Merge → squash master si flux habituel
|
||||
|
||||
---
|
||||
|
||||
## Vérifs
|
||||
|
||||
```bash
|
||||
# Plus de panels partagés sous admin (seul AdminManagement attendu)
|
||||
find frontend/lib/widgets/admin -name '*.dart'
|
||||
|
||||
# Plus d’imports panels vers l’ancien chemin (sauf AdminManagement)
|
||||
rg -n "widgets/admin/(user_management|dossiers_|parent_|enfant_|assistante|gestionnaire|pending|validation_|parametres|relais|am_dossier|parent_dossier|dossier_list|common/)" frontend/lib
|
||||
|
||||
# Screens OK
|
||||
rg -n "widgets/admin/" frontend/lib/screens
|
||||
```
|
||||
|
||||
Attendu screens : **0** hit vers panels ; éventuellement plus aucun hit `widgets/admin/` sauf si import explicite `AdminManagementWidget` depuis `user_management_panel` (chemin `widgets/admin/admin_management_widget.dart`).
|
||||
|
||||
---
|
||||
|
||||
## Hors scope
|
||||
|
||||
- Refonte UX des modales / panels (ticket dédié annoncé)
|
||||
- Rename `EnfantAdminModel`
|
||||
- Rename `screens/administrateurs/`
|
||||
- Rename `AdminUserFormDialog` / `AdminCreateDialog`
|
||||
- Changement API / back
|
||||
- #152 (`est_multiple`) — autre branche
|
||||
|
||||
---
|
||||
|
||||
## Critères d’acceptation
|
||||
|
||||
- [ ] Inventaire déplacé selon tableau
|
||||
- [ ] `widgets/admin/` ne contient plus que `admin_management_widget.dart` (variante A)
|
||||
- [ ] Imports screens + widgets à jour
|
||||
- [ ] Build Flutter OK
|
||||
- [ ] Recette : dashboard **administrateur** et **gestionnaire** (listes, ouverture fiches, validation, création dossier) sans régression
|
||||
- [ ] Aucun changement comportemental volontaire
|
||||
|
||||
---
|
||||
|
||||
## Risques / notes
|
||||
|
||||
- **Conflits de merge** si d’autres features touchent les panels → faire ce ticket quand la surface dashboard est calme (fin 0.1.0 OK)
|
||||
- Typo `parent_managmant_widget` : soit inclus (bonus), soit ticket cleanup 1-ligne séparé
|
||||
- Docs d’archive citant `widgets/admin/…` : pas obligatoire de mettre à jour ; `docs/27_BRIEFING-FRONTEND.md` oui si encore listé
|
||||
@@ -0,0 +1,14 @@
|
||||
# Mini-spec — Uniformisation modale staff (Gestionnaire / Administrateur)
|
||||
|
||||
**Ticket** : **#164** — https://git.ptits-pas.fr/jmartin/petitspas/issues/164
|
||||
**Branche** : `feature/164-staff-modal-uniformisation` (depuis `develop`)
|
||||
**Périmètre** : **front only** — pas d’API / BDD
|
||||
**Milestone** : **0.1.0**
|
||||
|
||||
Voir le corps du ticket #164 pour la spec complète.
|
||||
|
||||
## Livré
|
||||
|
||||
- `frontend/lib/widgets/dashboard/staff_user_form_modal.dart` → `StaffUserFormModal`
|
||||
- Ancien `AdminUserFormDialog` / `gestionnaires_create.dart` retiré
|
||||
- Imports : `user_management_panel`, `gestionnaire_management_widget`, `admin_management_widget`
|
||||
@@ -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/configuration_service.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/admin/user_management_panel.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/parametres_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/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/services/user_service.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/french_phone_field.dart';
|
||||
|
||||
|
||||
@@ -1,675 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/relais_model.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/widgets/email_text_field.dart';
|
||||
import 'package:p_tits_pas/widgets/french_phone_field.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/services/auth_service.dart';
|
||||
import 'package:p_tits_pas/services/relais_service.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/utils/staff_deletion_rights.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
|
||||
class AdminUserFormDialog extends StatefulWidget {
|
||||
final AppUser? initialUser;
|
||||
final bool withRelais;
|
||||
final bool adminMode;
|
||||
final bool readOnly;
|
||||
|
||||
const AdminUserFormDialog({
|
||||
super.key,
|
||||
this.initialUser,
|
||||
this.withRelais = true,
|
||||
this.adminMode = false,
|
||||
this.readOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AdminUserFormDialog> createState() => _AdminUserFormDialogState();
|
||||
}
|
||||
|
||||
class _AdminUserFormDialogState extends State<AdminUserFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nomController = TextEditingController();
|
||||
final _prenomController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _telephoneController = TextEditingController();
|
||||
final _passwordToggleFocusNode =
|
||||
FocusNode(skipTraversal: true, canRequestFocus: false);
|
||||
|
||||
bool _isSubmitting = false;
|
||||
bool _obscurePassword = true;
|
||||
bool _isLoadingRelais = true;
|
||||
List<RelaisModel> _relais = [];
|
||||
String? _selectedRelaisId;
|
||||
String? _currentUserId;
|
||||
String? _currentUserRole;
|
||||
bool get _isEditMode => widget.initialUser != null;
|
||||
bool get _isSuperAdminTarget =>
|
||||
(widget.initialUser?.role ?? '').toLowerCase() == 'super_admin';
|
||||
bool get _isSelfTarget =>
|
||||
_isEditMode &&
|
||||
_currentUserId != null &&
|
||||
widget.initialUser!.id == _currentUserId;
|
||||
/// Gestionnaire : pas de delete staff. Admin+ seulement (#154 / #160).
|
||||
bool get _canDeleteTarget {
|
||||
if (!_isEditMode || widget.readOnly) return false;
|
||||
if (_isSelfTarget || _isSuperAdminTarget) return false;
|
||||
return canDeleteGestionnaire(_currentUserRole);
|
||||
}
|
||||
bool get _isLockedAdminIdentity =>
|
||||
_isEditMode && widget.adminMode && _isSuperAdminTarget;
|
||||
String get _targetRoleKey {
|
||||
if (widget.initialUser != null) {
|
||||
return (widget.initialUser!.role).toLowerCase();
|
||||
}
|
||||
return widget.adminMode ? 'administrateur' : 'gestionnaire';
|
||||
}
|
||||
|
||||
String get _targetRoleLabel {
|
||||
switch (_targetRoleKey) {
|
||||
case 'super_admin':
|
||||
return 'Super administrateur';
|
||||
case 'administrateur':
|
||||
return 'Administrateur';
|
||||
case 'gestionnaire':
|
||||
return 'Gestionnaire';
|
||||
case 'assistante_maternelle':
|
||||
return 'Assistante maternelle';
|
||||
case 'parent':
|
||||
return 'Parent';
|
||||
default:
|
||||
return 'Utilisateur';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get _targetRoleIcon {
|
||||
switch (_targetRoleKey) {
|
||||
case 'super_admin':
|
||||
return Icons.verified_user_outlined;
|
||||
case 'administrateur':
|
||||
return Icons.admin_panel_settings_outlined;
|
||||
case 'gestionnaire':
|
||||
return Icons.assignment_ind_outlined;
|
||||
case 'assistante_maternelle':
|
||||
return Icons.child_care_outlined;
|
||||
case 'parent':
|
||||
return Icons.supervisor_account_outlined;
|
||||
default:
|
||||
return Icons.person_outline;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final user = widget.initialUser;
|
||||
if (user != null) {
|
||||
_nomController.text = user.nom ?? '';
|
||||
_prenomController.text = user.prenom ?? '';
|
||||
_emailController.text = user.email;
|
||||
_telephoneController.text = formatPhoneForDisplay(user.telephone ?? '');
|
||||
// En édition, on ne préremplit jamais le mot de passe.
|
||||
_passwordController.clear();
|
||||
final initialRelaisId = user.relaisId?.trim();
|
||||
_selectedRelaisId =
|
||||
(initialRelaisId == null || initialRelaisId.isEmpty)
|
||||
? null
|
||||
: initialRelaisId;
|
||||
}
|
||||
if (widget.withRelais) {
|
||||
_loadRelais();
|
||||
} else {
|
||||
_isLoadingRelais = false;
|
||||
}
|
||||
_loadCurrentUserId();
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentUserId() async {
|
||||
final cached = await AuthService.getCurrentUser();
|
||||
if (!mounted) return;
|
||||
if (cached != null) {
|
||||
setState(() {
|
||||
_currentUserId = cached.id;
|
||||
_currentUserRole = cached.role;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final refreshed = await AuthService.refreshCurrentUser();
|
||||
if (!mounted || refreshed == null) return;
|
||||
setState(() {
|
||||
_currentUserId = refreshed.id;
|
||||
_currentUserRole = refreshed.role;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nomController.dispose();
|
||||
_prenomController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_telephoneController.dispose();
|
||||
_passwordToggleFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Fallback si GET /relais échoue : conserve le relais déjà connu sur l'utilisateur.
|
||||
List<RelaisModel> _fallbackRelaisFromUser() {
|
||||
final id = _selectedRelaisId?.trim();
|
||||
if (id == null || id.isEmpty) return const [];
|
||||
final nom = (widget.initialUser?.relaisNom ?? '').trim();
|
||||
return [
|
||||
RelaisModel(
|
||||
id: id,
|
||||
nom: nom.isNotEmpty ? nom : 'Relais actuel',
|
||||
adresse: '',
|
||||
actif: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _loadRelais() async {
|
||||
try {
|
||||
final list = await RelaisService.getRelais();
|
||||
if (!mounted) return;
|
||||
final uniqueById = <String, RelaisModel>{};
|
||||
for (final relais in list) {
|
||||
uniqueById[relais.id] = relais;
|
||||
}
|
||||
|
||||
final filtered = uniqueById.values.where((r) => r.actif).toList();
|
||||
if (_selectedRelaisId != null &&
|
||||
!filtered.any((r) => r.id == _selectedRelaisId)) {
|
||||
final selected = uniqueById[_selectedRelaisId!];
|
||||
if (selected != null) {
|
||||
filtered.add(selected);
|
||||
} else {
|
||||
// Garder l'id sélectionné et afficher un item de secours (nom carte).
|
||||
filtered.addAll(_fallbackRelaisFromUser());
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_relais = filtered;
|
||||
_isLoadingRelais = false;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
// Ne pas nullifier _selectedRelaisId (#151) — la carte a déjà le bon libellé.
|
||||
setState(() {
|
||||
_relais = _fallbackRelaisFromUser();
|
||||
_isLoadingRelais = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String? _required(String? value, String field) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '$field est requis';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validateEmail(String? value) {
|
||||
final base = _required(value, 'Email');
|
||||
if (base != null) {
|
||||
return base;
|
||||
}
|
||||
return validateEmail(value, allowEmpty: true);
|
||||
}
|
||||
|
||||
String? _validatePassword(String? value) {
|
||||
if (_isEditMode && (value == null || value.trim().isEmpty)) {
|
||||
return null;
|
||||
}
|
||||
final base = _required(value, 'Mot de passe');
|
||||
if (base != null) return base;
|
||||
if (value!.trim().length < 6) return 'Minimum 6 caractères';
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validatePhone(String? value) {
|
||||
if (_isEditMode && (value == null || value.trim().isEmpty)) {
|
||||
return null;
|
||||
}
|
||||
final base = _required(value, 'Téléphone');
|
||||
if (base != null) {
|
||||
return base;
|
||||
}
|
||||
return validateFrenchNationalPhone(value, allowEmpty: false);
|
||||
}
|
||||
|
||||
String _toTitleCase(String raw) {
|
||||
final trimmed = raw.trim();
|
||||
if (trimmed.isEmpty) return trimmed;
|
||||
final words = trimmed.split(RegExp(r'\s+'));
|
||||
final normalizedWords = words.map(_capitalizeComposedWord).toList();
|
||||
return normalizedWords.join(' ');
|
||||
}
|
||||
|
||||
String _capitalizeComposedWord(String word) {
|
||||
if (word.isEmpty) return word;
|
||||
final lower = word.toLowerCase();
|
||||
final separators = <String>{"-", "'", "’"};
|
||||
final buffer = StringBuffer();
|
||||
var capitalizeNext = true;
|
||||
|
||||
for (var i = 0; i < lower.length; i++) {
|
||||
final char = lower[i];
|
||||
if (capitalizeNext && RegExp(r'[a-zà-öø-ÿ]').hasMatch(char)) {
|
||||
buffer.write(char.toUpperCase());
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
buffer.write(char);
|
||||
capitalizeNext = separators.contains(char);
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (widget.readOnly) return;
|
||||
if (_isSubmitting) return;
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final normalizedNom = _toTitleCase(_nomController.text);
|
||||
final normalizedPrenom = _toTitleCase(_prenomController.text);
|
||||
final normalizedPhone = normalizePhone(_telephoneController.text);
|
||||
final passwordProvided = _passwordController.text.trim().isNotEmpty;
|
||||
|
||||
if (_isEditMode) {
|
||||
if (widget.adminMode) {
|
||||
final lockedNom = _toTitleCase(widget.initialUser!.nom ?? '');
|
||||
final lockedPrenom = _toTitleCase(widget.initialUser!.prenom ?? '');
|
||||
await UserService.updateAdministrateur(
|
||||
adminId: widget.initialUser!.id,
|
||||
nom: _isLockedAdminIdentity ? lockedNom : normalizedNom,
|
||||
prenom: _isLockedAdminIdentity ? lockedPrenom : normalizedPrenom,
|
||||
email: _emailController.text.trim(),
|
||||
telephone: normalizedPhone.isEmpty
|
||||
? normalizePhone(widget.initialUser!.telephone ?? '')
|
||||
: normalizedPhone,
|
||||
password: passwordProvided ? _passwordController.text : null,
|
||||
);
|
||||
} else {
|
||||
final currentUser = widget.initialUser!;
|
||||
final initialNom = _toTitleCase(currentUser.nom ?? '');
|
||||
final initialPrenom = _toTitleCase(currentUser.prenom ?? '');
|
||||
final initialEmail = currentUser.email.trim();
|
||||
final initialPhone = normalizePhone(currentUser.telephone ?? '');
|
||||
|
||||
final onlyRelaisChanged =
|
||||
normalizedNom == initialNom &&
|
||||
normalizedPrenom == initialPrenom &&
|
||||
_emailController.text.trim() == initialEmail &&
|
||||
normalizedPhone == initialPhone &&
|
||||
!passwordProvided;
|
||||
|
||||
if (onlyRelaisChanged) {
|
||||
await UserService.updateGestionnaireRelais(
|
||||
gestionnaireId: currentUser.id,
|
||||
relaisId: _selectedRelaisId,
|
||||
);
|
||||
} else {
|
||||
await UserService.updateGestionnaire(
|
||||
gestionnaireId: currentUser.id,
|
||||
nom: normalizedNom,
|
||||
prenom: normalizedPrenom,
|
||||
email: _emailController.text.trim(),
|
||||
telephone: normalizedPhone.isEmpty ? initialPhone : normalizedPhone,
|
||||
relaisId: _selectedRelaisId,
|
||||
password: passwordProvided ? _passwordController.text : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (widget.adminMode) {
|
||||
await UserService.createAdministrateur(
|
||||
nom: normalizedNom,
|
||||
prenom: normalizedPrenom,
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
telephone: normalizePhone(_telephoneController.text),
|
||||
);
|
||||
} else {
|
||||
await UserService.createGestionnaire(
|
||||
nom: normalizedNom,
|
||||
prenom: normalizedPrenom,
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
telephone: normalizePhone(_telephoneController.text),
|
||||
relaisId: _selectedRelaisId,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
_isEditMode
|
||||
? (widget.adminMode
|
||||
? 'Administrateur modifié avec succès.'
|
||||
: 'Gestionnaire modifié avec succès.')
|
||||
: (widget.adminMode
|
||||
? 'Administrateur créé avec succès.'
|
||||
: 'Gestionnaire créé avec succès.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
e.toString().replaceFirst('Exception: ', ''),
|
||||
),
|
||||
backgroundColor: Colors.red.shade700,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _delete() async {
|
||||
if (widget.readOnly) return;
|
||||
if (!_canDeleteTarget) return;
|
||||
if (!_isEditMode || _isSubmitting) return;
|
||||
|
||||
final name = widget.initialUser!.fullName.isEmpty
|
||||
? widget.initialUser!.email
|
||||
: widget.initialUser!.fullName;
|
||||
final confirmed = await showSuppressionConfirmDialog(
|
||||
context,
|
||||
title: widget.adminMode
|
||||
? 'Supprimer l\'administrateur'
|
||||
: 'Supprimer le gestionnaire',
|
||||
people: [
|
||||
widget.adminMode
|
||||
? SuppressionPersonLine.administrateur(name)
|
||||
: SuppressionPersonLine.gestionnaire(name),
|
||||
],
|
||||
footnotes: const [
|
||||
'Le compte sera définitivement supprimé.',
|
||||
],
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
try {
|
||||
await UserService.deleteUser(widget.initialUser!.id);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Gestionnaire supprimé.')),
|
||||
);
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e.toString().replaceFirst('Exception: ', '')),
|
||||
backgroundColor: Colors.red.shade700,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: const Color(0xFFEDE5FA),
|
||||
child: Icon(
|
||||
_targetRoleIcon,
|
||||
size: 20,
|
||||
color: const Color(0xFF6B3FA0),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_isEditMode
|
||||
? (widget.readOnly
|
||||
? 'Consulter un "$_targetRoleLabel"'
|
||||
: 'Modifier un "$_targetRoleLabel"')
|
||||
: 'Créer un "$_targetRoleLabel"',
|
||||
),
|
||||
),
|
||||
if (_isEditMode && !widget.readOnly)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: 'Fermer',
|
||||
onPressed: _isSubmitting
|
||||
? null
|
||||
: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 620,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildPrenomField()),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildNomField()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildEmailField(),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildPasswordField()),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildTelephoneField()),
|
||||
],
|
||||
),
|
||||
if (widget.withRelais) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildRelaisField(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (widget.readOnly) ...[
|
||||
FilledButton(
|
||||
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
] else if (_isEditMode) ...[
|
||||
if (_canDeleteTarget)
|
||||
OutlinedButton(
|
||||
onPressed: _isSubmitting ? null : _delete,
|
||||
style: OutlinedButton.styleFrom(foregroundColor: Colors.red.shade700),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: _isSubmitting ? null : _submit,
|
||||
icon: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.edit),
|
||||
label: Text(_isSubmitting ? 'Modification...' : 'Modifier'),
|
||||
),
|
||||
] else ...[
|
||||
OutlinedButton(
|
||||
onPressed:
|
||||
_isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: _isSubmitting ? null : _submit,
|
||||
icon: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.person_add_alt_1),
|
||||
label: Text(_isSubmitting ? 'Création...' : 'Créer'),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNomField() {
|
||||
return TextFormField(
|
||||
controller: _nomController,
|
||||
readOnly: widget.readOnly || _isLockedAdminIdentity,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (widget.readOnly || _isLockedAdminIdentity)
|
||||
? null
|
||||
: (v) => _required(v, 'Nom'),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrenomField() {
|
||||
return TextFormField(
|
||||
controller: _prenomController,
|
||||
readOnly: widget.readOnly || _isLockedAdminIdentity,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Prénom',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (widget.readOnly || _isLockedAdminIdentity)
|
||||
? null
|
||||
: (v) => _required(v, 'Prénom'),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailField() {
|
||||
return EmailTextFormField(
|
||||
controller: _emailController,
|
||||
readOnly: widget.readOnly,
|
||||
label: 'Email',
|
||||
validator: widget.readOnly ? null : _validateEmail,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordField() {
|
||||
return TextFormField(
|
||||
controller: _passwordController,
|
||||
readOnly: widget.readOnly,
|
||||
obscureText: _obscurePassword,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
autofillHints: _isEditMode
|
||||
? const <String>[]
|
||||
: const [AutofillHints.newPassword],
|
||||
decoration: InputDecoration(
|
||||
labelText: _isEditMode
|
||||
? 'Nouveau mot de passe'
|
||||
: 'Mot de passe',
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: widget.readOnly
|
||||
? null
|
||||
: ExcludeFocus(
|
||||
child: IconButton(
|
||||
focusNode: _passwordToggleFocusNode,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: widget.readOnly ? null : _validatePassword,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTelephoneField() {
|
||||
return FrenchPhoneTextFormField(
|
||||
controller: _telephoneController,
|
||||
readOnly: widget.readOnly,
|
||||
label: 'Téléphone (ex: 06 12 34 56 78)',
|
||||
validator: widget.readOnly ? null : _validatePhone,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRelaisField() {
|
||||
final selectedValue = _selectedRelaisId != null &&
|
||||
_relais.any((relais) => relais.id == _selectedRelaisId)
|
||||
? _selectedRelaisId
|
||||
: null;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
value: selectedValue,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Relais principal',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('Aucun relais'),
|
||||
),
|
||||
..._relais.map(
|
||||
(relais) => DropdownMenuItem<String?>(
|
||||
value: relais.id,
|
||||
child: Text(relais.nom),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (_isLoadingRelais || widget.readOnly)
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_selectedRelaisId = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_isLoadingRelais) ...[
|
||||
const SizedBox(height: 8),
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/user.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/dashboard/dashboard_bandeau.dart';
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/user.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/user_service.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/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||
|
||||
class AdminManagementWidget extends StatefulWidget {
|
||||
final String searchQuery;
|
||||
@@ -100,7 +100,7 @@ class _AdminManagementWidgetState extends State<AdminManagementWidget> {
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return AdminUserFormDialog(
|
||||
return StaffUserFormModal(
|
||||
initialUser: user,
|
||||
adminMode: true,
|
||||
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/utils/name_format_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.
|
||||
class IdentityValues {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
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].
|
||||
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/widgets/dashboard/am_photo_frame.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/admin/validation_modal_theme.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_refus_form.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_valider_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';
|
||||
import 'package:p_tits_pas/widgets/dashboard/validation_refus_form.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/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/select_enfant_modal.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/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.
|
||||
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/widgets/dashboard/am_edit_modal.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/admin/common/user_list.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||
|
||||
class AssistanteMaternelleManagementWidget extends StatefulWidget {
|
||||
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/select_am_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/admin/common/validation_detail_section.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.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';
|
||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||
|
||||
/// Fiche enfant consultation / édition (#138) ou création (#132).
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.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.
|
||||
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/user_service.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/admin/pending_validation_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_dossier_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/dossier_list_card.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/pending_validation_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/validation_dossier_modal.dart';
|
||||
|
||||
/// Onglet permanent « Dossiers » (#153) : pending en haut + liste unifiée en bas.
|
||||
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/widgets/dashboard/child_detail_modal.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/admin/common/user_list.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||
|
||||
/// Onglet liste globale des enfants (doc 28 §6.2, ticket #137).
|
||||
class EnfantManagementWidget extends StatefulWidget {
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.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/user_service.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/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/user_list.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||
|
||||
class GestionnaireManagementWidget extends StatefulWidget {
|
||||
final String searchQuery;
|
||||
@@ -74,7 +74,7 @@ class _GestionnaireManagementWidgetState
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return AdminUserFormDialog(initialUser: user);
|
||||
return StaffUserFormModal(initialUser: user);
|
||||
},
|
||||
);
|
||||
if (changed == true) {
|
||||
+1
-1
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:p_tits_pas/services/configuration_service.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é.
|
||||
class ParametresPanel extends StatefulWidget {
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
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].
|
||||
class ParentDossierCreateModal extends StatefulWidget {
|
||||
+4
-4
@@ -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/postal_utils.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/admin/validation_modal_theme.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_refus_form.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_valider_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';
|
||||
import 'package:p_tits_pas/widgets/dashboard/validation_refus_form.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/identity_block.dart';
|
||||
|
||||
@@ -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/status_capsule.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).
|
||||
/// 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/widgets/dashboard/parent_edit_modal.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/admin/common/user_list.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/user_list.dart';
|
||||
|
||||
class ParentManagementWidget extends StatefulWidget {
|
||||
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/pending_family.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/admin/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_dossier_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/dossier_list_card.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/common/suppression_confirm_dialog.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/validation_dossier_modal.dart';
|
||||
|
||||
/// Section « dossiers à valider » (liste unifiée AM + familles). Ticket #107 / #153.
|
||||
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/utils/phone_utils.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 {
|
||||
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/select_list_modal.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) {
|
||||
final lines = <String>[];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.
|
||||
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: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/am_dossier_create_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/am_dossier_create_modal.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/user_management_sub_bar.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/dossiers_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/enfant_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/parent_dossier_create_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/dossiers_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/enfant_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/gestionnaire_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/parent_dossier_create_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/parent_managmant_widget.dart';
|
||||
|
||||
class UserManagementPanel extends StatefulWidget {
|
||||
/// Afficher l'onglet Administrateurs (sinon sans Administrateurs).
|
||||
@@ -385,7 +385,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return const AdminUserFormDialog();
|
||||
return const StaffUserFormModal();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -403,7 +403,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return const AdminUserFormDialog(
|
||||
return const StaffUserFormModal(
|
||||
adminMode: true,
|
||||
withRelais: false,
|
||||
);
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.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].
|
||||
class ValidationAmWizard extends StatelessWidget {
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/dossier_unifie.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/admin/parent_dossier_wizard.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_am_wizard.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/validation_family_wizard.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/am_dossier_wizard.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/parent_dossier_wizard.dart';
|
||||
import 'package:p_tits_pas/widgets/dashboard/validation_am_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.
|
||||
/// Ticket #107 / #119 (review), #135 (`openAsEdit`).
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.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].
|
||||
class ValidationFamilyWizard extends StatelessWidget {
|
||||
Reference in New Issue
Block a user