import 'package:flutter/material.dart'; import 'package:p_tits_pas/models/enfant_admin_model.dart'; import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/utils/enfant_status_utils.dart'; /// Fiche enfant consultation / édition (ticket #138). class AdminChildDetailModal extends StatefulWidget { final EnfantAdminModel enfant; final VoidCallback? onSaved; const AdminChildDetailModal({ super.key, required this.enfant, this.onSaved, }); @override State createState() => _AdminChildDetailModalState(); } class _AdminChildDetailModalState extends State { late final TextEditingController _prenomCtrl; late final TextEditingController _nomCtrl; late final TextEditingController _birthCtrl; late final TextEditingController _dueCtrl; late String _status; late String _gender; late bool _consentPhoto; late bool _isMultiple; bool _dirty = false; bool _saving = false; static const _genders = ['H', 'F', 'Autre']; static const _labelStyle = TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black87, ); @override void initState() { super.initState(); final e = widget.enfant; _prenomCtrl = TextEditingController(text: e.firstName ?? ''); _nomCtrl = TextEditingController(text: e.lastName ?? ''); _birthCtrl = TextEditingController(text: e.birthDate ?? ''); _dueCtrl = TextEditingController(text: e.dueDate ?? ''); _status = normalizeEnfantStatus(e.status); if (!enfantStatusValues.contains(_status)) { _status = 'sans_garde'; } _gender = _normalizeGender(e.gender); _consentPhoto = e.consentPhoto; _isMultiple = e.isMultiple; for (final c in [_prenomCtrl, _nomCtrl, _birthCtrl, _dueCtrl]) { c.addListener(_markDirty); } } @override void dispose() { for (final c in [_prenomCtrl, _nomCtrl, _birthCtrl, _dueCtrl]) { c.dispose(); } super.dispose(); } static String _normalizeGender(String? raw) { final g = (raw ?? '').trim(); if (g == 'M') return 'H'; if (_genders.contains(g)) return g; return 'H'; } void _markDirty() { if (!_dirty) setState(() => _dirty = true); } Future _save() async { if (!_dirty) return; setState(() => _saving = true); try { await UserService.updateEnfant( enfantId: widget.enfant.id, body: { 'first_name': _prenomCtrl.text.trim(), 'last_name': _nomCtrl.text.trim(), 'status': _status, 'gender': _gender, if (_birthCtrl.text.trim().isNotEmpty) 'birth_date': _birthCtrl.text.trim(), if (_dueCtrl.text.trim().isNotEmpty) 'due_date': _dueCtrl.text.trim(), 'consent_photo': _consentPhoto, 'is_multiple': _isMultiple, }, ); if (!mounted) return; setState(() { _dirty = false; _saving = false; }); widget.onSaved?.call(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Fiche enfant enregistrée')), ); } catch (e) { if (!mounted) return; setState(() => _saving = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } } String _parentsLine() { final names = widget.enfant.parentLinks .map((l) { final n = (l.parentName ?? '').trim(); return n.isNotEmpty ? n : 'Parent rattaché'; }) .where((s) => s.isNotEmpty) .toList(); if (names.isEmpty) return ''; return names.join(', '); } @override Widget build(BuildContext context) { final parents = _parentsLine(); final showDueDate = _status == 'a_naitre'; return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: SizedBox( width: 480, child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 640), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.enfant.fullName, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, ), ), if (parents.isNotEmpty) ...[ const SizedBox(height: 4), Text( 'Responsables : $parents', style: const TextStyle( fontSize: 13, color: Colors.black54, ), ), ], ], ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close), tooltip: 'Fermer', ), ], ), const SizedBox(height: 12), const Divider(height: 1), const SizedBox(height: 12), Flexible( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _labeledField( 'Prénom', TextField( controller: _prenomCtrl, decoration: _decoration(), ), ), ), const SizedBox(width: 12), Expanded( child: _labeledField( 'Nom', TextField( controller: _nomCtrl, decoration: _decoration(), ), ), ), ], ), const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _labeledDropdown( 'Statut', _status, enfantStatusValues .map( (s) => MapEntry( s, enfantStatusLabel(s, gender: _gender), ), ) .toList(), (v) => setState(() { _status = v; _dirty = true; }), ), ), const SizedBox(width: 12), Expanded( child: _labeledDropdown( 'Genre', _gender, _genders .map((g) => MapEntry(g, _genderLabel(g))) .toList(), (v) => setState(() { _gender = v; _dirty = true; }), ), ), ], ), const SizedBox(height: 12), if (showDueDate) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _labeledField( 'Date de naissance', TextField( controller: _birthCtrl, decoration: _decoration(hint: 'AAAA-MM-JJ'), ), ), ), const SizedBox(width: 12), Expanded( child: _labeledField( 'Date prévue', TextField( controller: _dueCtrl, decoration: _decoration(hint: 'AAAA-MM-JJ'), ), ), ), ], ) else _labeledField( 'Date de naissance', TextField( controller: _birthCtrl, decoration: _decoration(hint: 'AAAA-MM-JJ'), ), ), const SizedBox(height: 16), _switchRow( 'Consentement photo', _consentPhoto, (v) => setState(() { _consentPhoto = v; _dirty = true; }), ), _switchRow( 'Naissance multiple', _isMultiple, (v) => setState(() { _isMultiple = v; _dirty = true; }), ), ], ), ), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: !_dirty || _saving ? null : _save, icon: _saving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.save), label: const Text('Sauvegarder'), ), ], ), ], ), ), ), ), ); } InputDecoration _decoration({String? hint}) { return InputDecoration( isDense: true, border: const OutlineInputBorder(), hintText: hint, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), ); } Widget _labeledField(String label, Widget field) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(label, style: _labelStyle), const SizedBox(height: 4), field, ], ); } Widget _labeledDropdown( String label, String value, List> items, ValueChanged onChanged, ) { final safeValue = items.any((e) => e.key == value) ? value : items.first.key; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(label, style: _labelStyle), const SizedBox(height: 4), DropdownButtonFormField( value: safeValue, isExpanded: true, decoration: _decoration(), items: items .map((e) => DropdownMenuItem(value: e.key, child: Text(e.value))) .toList(), onChanged: (v) { if (v != null) onChanged(v); }, ), ], ); } Widget _switchRow(String label, bool value, ValueChanged onChanged) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Expanded(child: Text(label, style: _labelStyle)), Switch( value: value, onChanged: onChanged, ), ], ), ); } String _genderLabel(String gender) { switch (gender) { case 'H': return 'Garçon'; case 'F': return 'Fille'; case 'Autre': return 'Autre'; default: return gender; } } }