import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/enfant_admin_model.dart'; import 'package:p_tits_pas/utils/phone_utils.dart'; import 'package:p_tits_pas/models/parent_child_summary.dart'; import 'package:p_tits_pas/models/parent_model.dart'; import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart'; import 'package:p_tits_pas/widgets/admin/common/admin_enfant_user_card.dart'; import 'package:p_tits_pas/widgets/common/identity_block.dart'; import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart'; /// Fiche parent éditable (doc 28 §6.1, tickets #131 / #138). /// Shell et typo alignés sur [ValidationDossierModal] / wizards validation. class AdminParentEditModal extends StatefulWidget { final ParentModel parent; final VoidCallback? onSaved; const AdminParentEditModal({ super.key, required this.parent, this.onSaved, }); @override State createState() => _AdminParentEditModalState(); } class _AdminParentEditModalState extends State { late final TextEditingController _nomCtrl; late final TextEditingController _prenomCtrl; late final TextEditingController _emailCtrl; late final TextEditingController _telCtrl; late final TextEditingController _adresseCtrl; late final TextEditingController _villeCtrl; late final TextEditingController _cpCtrl; late String _statut; late List _children; late final ScrollController _childrenScrollCtrl; bool _saving = false; bool _dirty = false; static const _statuts = ['actif', 'en_attente', 'suspendu', 'refuse']; static const double _modalWidth = 930; /// Hauteur d'une ligne enfant (carte + marge) — viewport = 2,5 lignes visibles. static const double _childListItemHeight = 58; static const double _childrenListViewportHeight = _childListItemHeight * 2.5 + 8; @override void initState() { super.initState(); final u = widget.parent.user; _nomCtrl = TextEditingController(text: u.nom ?? ''); _prenomCtrl = TextEditingController(text: u.prenom ?? ''); _emailCtrl = TextEditingController(text: u.email); _telCtrl = TextEditingController( text: formatPhoneForDisplay(u.telephone ?? ''), ); _adresseCtrl = TextEditingController(text: u.adresse ?? ''); _villeCtrl = TextEditingController(text: u.ville ?? ''); _cpCtrl = TextEditingController(text: u.codePostal ?? ''); _statut = u.statut ?? 'en_attente'; _children = List.of(widget.parent.children); _childrenScrollCtrl = ScrollController(); for (final c in [ _nomCtrl, _prenomCtrl, _emailCtrl, _telCtrl, _adresseCtrl, _villeCtrl, _cpCtrl, ]) { c.addListener(_markDirty); } _reloadChildren(); } @override void dispose() { for (final c in [ _nomCtrl, _prenomCtrl, _emailCtrl, _telCtrl, _adresseCtrl, _villeCtrl, _cpCtrl, ]) { c.dispose(); } _childrenScrollCtrl.dispose(); super.dispose(); } void _markDirty() { if (!_dirty) setState(() => _dirty = true); } String _displayStatus(String status) { switch (status) { case 'actif': return 'Actif'; case 'en_attente': return 'En attente'; case 'suspendu': return 'Suspendu'; case 'refuse': return 'Refusé'; default: return status; } } Future _save() async { if (!_dirty) return; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Confirmer'), content: const Text( 'Enregistrer les modifications de la fiche parent ?', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Sauvegarder'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _saving = true); try { await UserService.updateParentFiche( parentUserId: widget.parent.user.id, body: { 'nom': _nomCtrl.text.trim(), 'prenom': _prenomCtrl.text.trim(), 'email': _emailCtrl.text.trim(), 'telephone': normalizePhone(_telCtrl.text), 'adresse': _adresseCtrl.text.trim(), 'ville': _villeCtrl.text.trim(), 'code_postal': _cpCtrl.text.trim(), 'statut': _statut, }, ); if (!mounted) return; setState(() { _dirty = false; _saving = false; }); widget.onSaved?.call(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Fiche parent enregistrée')), ); } catch (e) { if (!mounted) return; setState(() => _saving = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } } Future _openChild(ParentChildSummary child) async { try { final enfant = await UserService.getEnfant(child.id); if (!mounted) return; await showDialog( context: context, builder: (ctx) => AdminChildDetailModal( enfant: enfant, onSaved: _reloadChildren, ), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } } Future _reloadChildren() async { try { final refreshed = await UserService.getParent(widget.parent.user.id); final all = await UserService.getEnfants(); final byId = {for (final e in all) e.id: e}; final kids = refreshed.children.map((c) { final full = byId[c.id]; if (full != null) return ParentChildSummary.fromEnfant(full); return c; }).toList(); if (!mounted) return; setState(() => _children = kids); } catch (_) {} } Future _detachChild(ParentChildSummary child) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Détacher l\'enfant'), content: Text( 'Retirer ${child.fullName} de la fiche de ce parent ?\n' '(L\'enfant ne sera pas supprimé.)', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.pop(ctx, true), style: ElevatedButton.styleFrom( backgroundColor: Colors.red.shade700, ), child: const Text('Détacher'), ), ], ), ); if (confirmed != true || !mounted) return; try { await UserService.detachEnfantFromParent( parentUserId: widget.parent.user.id, enfantId: child.id, ); if (!mounted) return; await _reloadChildren(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Enfant détaché')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } } Future _attachChild() async { List all; try { all = await UserService.getEnfants(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); return; } final linkedIds = _children.map((c) => c.id).toSet(); final candidates = all.where((e) => !linkedIds.contains(e.id)).toList(); if (candidates.isEmpty) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Aucun enfant disponible à rattacher')), ); return; } if (!mounted) return; final selected = await showDialog( context: context, builder: (ctx) => SimpleDialog( title: const Text('Rattacher un enfant'), children: candidates .map( (e) => SimpleDialogOption( onPressed: () => Navigator.pop(ctx, e), child: Text(e.fullName), ), ) .toList(), ), ); if (selected == null || !mounted) return; try { await UserService.attachEnfantToParent( parentUserId: widget.parent.user.id, enfantId: selected.id, ); if (!mounted) return; await _reloadChildren(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Enfant rattaché')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } } Widget _statusDropdown() { return Container( height: 34, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), border: Border.all(color: Colors.black26), ), padding: const EdgeInsets.symmetric(horizontal: 12), child: DropdownButtonHideUnderline( child: DropdownButton( value: _statuts.contains(_statut) ? _statut : _statuts.first, isExpanded: true, isDense: true, style: const TextStyle(fontSize: 13, color: Colors.black87), icon: const Icon(Icons.arrow_drop_down, size: 20), items: _statuts .map( (s) => DropdownMenuItem( value: s, child: Text(_displayStatus(s)), ), ) .toList(), onChanged: (v) { if (v == null) return; setState(() { _statut = v; _dirty = true; }); }, ), ), ); } Widget _identityFields() { return IdentityBlock.editable( nomController: _nomCtrl, prenomController: _prenomCtrl, telephoneController: _telCtrl, emailController: _emailCtrl, adresseController: _adresseCtrl, codePostalController: _cpCtrl, villeController: _villeCtrl, ); } Widget _childrenPanel() { return Container( height: _childrenListViewportHeight, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), clipBehavior: Clip.antiAlias, child: _children.isEmpty ? const Center( child: Text( 'Aucun enfant rattaché', style: TextStyle(fontSize: 14, color: Colors.black54), ), ) : ListView.builder( controller: _childrenScrollCtrl, padding: const EdgeInsets.fromLTRB(8, 8, 8, 4), itemExtent: _childListItemHeight, itemCount: _children.length, itemBuilder: (_, i) { final c = _children[i]; return AdminEnfantUserCard.fromSummary( c, actions: [ IconButton( icon: const Icon(Icons.visibility_outlined), tooltip: 'Voir / modifier', onPressed: () => _openChild(c), ), IconButton( tooltip: 'Détacher', icon: Icon(Icons.link_off, color: Colors.orange.shade800), onPressed: () => _detachChild(c), ), ], ); }, ), ); } Widget _buildFooter() { return Row( children: [ TextButton( onPressed: _saving ? null : () => Navigator.of(context).pop(), child: const Text('Fermer'), ), const Spacer(), TextButton.icon( onPressed: _attachChild, icon: const Icon(Icons.link, size: 18), label: const Text('Rattacher un enfant'), ), const SizedBox(width: 12), ElevatedButton( style: ValidationModalTheme.primaryElevatedStyle, onPressed: !_dirty || _saving ? null : _save, child: _saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text(_dirty ? 'Sauvegarder' : 'Aucune modification'), ), ], ); } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ConstrainedBox( constraints: BoxConstraints( maxWidth: _modalWidth, maxHeight: MediaQuery.of(context).size.height * 0.85, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Padding( padding: EdgeInsets.fromLTRB(18, 16, 0, 10), child: Text( 'Fiche parent', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700), ), ), const Spacer(), SizedBox(width: 160, child: _statusDropdown()), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), tooltip: 'Fermer', ), ], ), const Divider(height: 1), Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _identityFields(), const SizedBox(height: 12), Text( 'Nombre d\'enfants : ${_children.length}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87, ), ), const SizedBox(height: 8), _childrenPanel(), const SizedBox(height: 16), _buildFooter(), ], ), ), ], ), ), ); } }