import 'package:flutter/material.dart'; import 'package:intl/intl.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/utils/phone_utils.dart'; import 'package:p_tits_pas/widgets/admin/validation_dossier_modal.dart'; /// Onglet « À valider » : deux listes (AM en attente, familles en attente). Ticket #107. class PendingValidationWidget extends StatefulWidget { final VoidCallback? onRefresh; const PendingValidationWidget({super.key, this.onRefresh}); @override State createState() => _PendingValidationWidgetState(); } class _PendingValidationWidgetState extends State { bool _isLoading = true; String? _error; List _pendingAM = []; List _pendingFamilies = []; @override void initState() { super.initState(); _load(); } Future _load() async { setState(() { _isLoading = true; _error = null; }); try { final am = await UserService.getPendingUsers(role: 'assistante_maternelle'); final families = await UserService.getPendingFamilies(); if (!mounted) return; setState(() { _pendingAM = am; _pendingFamilies = families; _isLoading = false; }); } catch (e) { if (!mounted) return; setState(() { _error = e is Exception ? e.toString().replaceFirst('Exception: ', '') : 'Erreur inconnue'; _isLoading = false; }); } } void _onOpenValidation({String? type, String? id, String? numeroDossier}) { final num = numeroDossier?.trim(); if (num == null || num.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Numéro de dossier manquant.')), ); return; } showDialog( context: context, builder: (context) => ValidationDossierModal( numeroDossier: num, onClose: () => Navigator.of(context).pop(), onSuccess: () { Navigator.of(context).pop(); _load(); widget.onRefresh?.call(); }, ), ); } @override Widget build(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null && _error!.isNotEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(_error!, style: const TextStyle(color: Colors.red)), const SizedBox(height: 16), ElevatedButton( onPressed: _load, child: const Text('Réessayer'), ), ], ), ), ); } final hasAM = _pendingAM.isNotEmpty; final hasFamilies = _pendingFamilies.isNotEmpty; if (!hasAM && !hasFamilies) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.check_circle_outline, size: 64, color: Colors.grey.shade400), const SizedBox(height: 16), Text( 'Aucun dossier en attente de validation', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.grey.shade600, ), ), ], ), ); } return RefreshIndicator( onRefresh: () async { await _load(); widget.onRefresh?.call(); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (hasAM) ...[ _sectionTitle('Assistantes maternelles en attente'), const SizedBox(height: 8), ..._pendingAM.map((u) => _buildAMCard(u)), const SizedBox(height: 24), ], if (hasFamilies) ...[ _sectionTitle('Familles en attente'), const SizedBox(height: 8), ..._pendingFamilies.map((f) => _buildFamilyCard(f)), ], ], ), ), ); } Widget _sectionTitle(String title) { return Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: Colors.black87, ), ); } /// Sous-titre AM : `email - date • tél. • CP ville` (plan affichage lignes À valider). String _amSubtitleLine(AppUser user) { final email = user.email.trim(); final bits = []; bits.add(DateFormat('dd/MM/yyyy').format(user.createdAt.toLocal())); final tel = user.telephone?.trim(); if (tel != null && tel.isNotEmpty) { bits.add(formatPhoneForDisplay(tel)); } final cp = user.codePostal?.trim(); final ville = user.ville?.trim(); final loc = [if (cp != null && cp.isNotEmpty) cp, if (ville != null && ville.isNotEmpty) ville] .join(' ') .trim(); if (loc.isNotEmpty) bits.add(loc); final infos = bits.join(' • '); if (email.isEmpty) return infos; return '$email - $infos'; } Widget _buildAMCard(AppUser user) { final numDossier = user.numeroDossier ?? '–'; final nameBold = user.fullName.isNotEmpty ? user.fullName : (user.email.isNotEmpty ? user.email : '–'); return _PendingValidationRow( icon: Icons.person_outline, title: Text.rich( TextSpan( style: const TextStyle(fontSize: 14, color: Colors.black87), children: [ TextSpan( text: nameBold, style: const TextStyle(fontWeight: FontWeight.w700), ), TextSpan( text: ' - $numDossier', style: const TextStyle(fontWeight: FontWeight.w400), ), ], ), ), subtitle: _amSubtitleLine(user), subtitleStyle: TextStyle( fontSize: 12, fontStyle: FontStyle.italic, color: Colors.grey.shade600, ), onOpen: () => _onOpenValidation( type: 'AM', id: user.id, numeroDossier: user.numeroDossier, ), ); } /// `email, tél., localisation` par parent, puis `date soumission`, puis `nb enfants`. String _familyParentSegment(PendingParentLine p) { final parts = []; final e = p.email?.trim(); if (e != null && e.isNotEmpty) parts.add(e); final t = p.telephone?.trim(); if (t != null && t.isNotEmpty) parts.add(formatPhoneForDisplay(t)); final cp = p.codePostal?.trim(); final v = p.ville?.trim(); final loc = [if (cp != null && cp.isNotEmpty) cp, if (v != null && v.isNotEmpty) v] .join(' ') .trim(); if (loc.isNotEmpty) parts.add(loc); return parts.join(', '); } String _familySubtitleLine(PendingFamily family) { final blocks = family.parentLines .map(_familyParentSegment) .where((s) => s.isNotEmpty) .join(' - '); final tail = []; final date = family.dateSoumission; if (date != null) { tail.add(DateFormat('dd/MM/yyyy').format(date.toLocal())); } if (family.nombreEnfants > 0) { tail.add( family.nombreEnfants > 1 ? '${family.nombreEnfants} enfants' : '1 enfant', ); } final right = tail.join(' - '); if (blocks.isEmpty && right.isEmpty) return ''; if (blocks.isEmpty) return right; if (right.isEmpty) return blocks; return '$blocks - $right'; } Widget _buildFamilyCard(PendingFamily family) { final numDossier = family.numeroDossier ?? '–'; final nameBold = family.libelle.isNotEmpty ? family.libelle : 'Famille'; return _PendingValidationRow( icon: Icons.family_restroom_outlined, title: Text.rich( TextSpan( style: const TextStyle(fontSize: 14, color: Colors.black87), children: [ TextSpan( text: nameBold, style: const TextStyle(fontWeight: FontWeight.w700), ), TextSpan( text: ' - $numDossier', style: const TextStyle(fontWeight: FontWeight.w400), ), ], ), ), subtitle: _familySubtitleLine(family), subtitleStyle: TextStyle( fontSize: 12, fontStyle: FontStyle.italic, color: Colors.grey.shade600, ), onOpen: () => _onOpenValidation( type: 'famille', id: family.parentIds.isNotEmpty ? family.parentIds.first : null, numeroDossier: family.numeroDossier, ), ); } } /// Ligne « À valider » : survol comme [AdminUserCard], icône « Ouvrir » visible au hover uniquement. class _PendingValidationRow extends StatefulWidget { final IconData icon; final Widget title; final String? subtitle; final TextStyle? subtitleStyle; final VoidCallback onOpen; const _PendingValidationRow({ required this.icon, required this.title, this.subtitle, this.subtitleStyle, required this.onOpen, }); @override State<_PendingValidationRow> createState() => _PendingValidationRowState(); } class _PendingValidationRowState extends State<_PendingValidationRow> { bool _isHovered = false; @override Widget build(BuildContext context) { final subStyle = widget.subtitleStyle ?? TextStyle( fontSize: 12, color: Colors.grey.shade600, ); return MouseRegion( onEnter: (_) => setState(() => _isHovered = true), onExit: (_) => setState(() => _isHovered = false), cursor: SystemMouseCursors.click, child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(10), child: InkWell( onTap: widget.onOpen, borderRadius: BorderRadius.circular(10), hoverColor: const Color(0x149CC5C0), child: Card( margin: const EdgeInsets.only(bottom: 12), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: Colors.grey.shade300), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(widget.icon, color: Colors.grey.shade600, size: 28), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ widget.title, if (widget.subtitle != null && widget.subtitle!.isNotEmpty) ...[ const SizedBox(height: 4), Text( widget.subtitle!, style: subStyle, ), ], ], ), ), SizedBox( width: 52, child: Center( child: AnimatedOpacity( duration: const Duration(milliseconds: 120), opacity: _isHovered ? 1 : 0, child: IgnorePointer( ignoring: !_isHovered, child: IconButtonTheme( data: IconButtonThemeData( style: IconButton.styleFrom( padding: const EdgeInsets.all(0), minimumSize: const Size(48, 48), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), child: IconButton( onPressed: widget.onOpen, icon: const Icon(Icons.open_in_new), iconSize: 34, tooltip: 'Ouvrir', ), ), ), ), ), ), ], ), ), ), ), ), ); } }