import 'package:flutter/material.dart'; import '../../core/service_locator.dart'; import '../../models/member.dart'; import '../../services/member_repository.dart'; class MembersScreen extends StatefulWidget { const MembersScreen({super.key}); @override State createState() => _MembersScreenState(); } class _MembersScreenState extends State { final _service = memberRepository(); final _searchKey = GlobalKey<_MemberSearchState>(); Member? _member; bool _loading = true; String? _error; @override void initState() { super.initState(); _load(_service.first()); } void _handleAction(String action) { final member = _member; if (member == null) return; switch (action) { case 'receipts': _doPaidCaution(member); case 'retain': _doRetener(member); case 'unregister': _doDarDeBaja(member); case 'recover': _doReactivar(member); default: ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('$action — pendiente de implementar'), duration: const Duration(seconds: 2)), ); } } Future _doPaidCaution(Member member) async { try { final updated = await _service.paidCaution(member.id); if (!mounted) return; setState(() => _member = updated); } catch (e) { if (!mounted) return; _showError('Error: $e'); } } Future _doRetener(Member member) async { final cancelar = member.retenido; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(cancelar ? 'Cancelar retención' : 'Retener abonado'), content: Text(cancelar ? '¿Cancelar la retención del socio ${member.formattedId}?' : '¿Marcar como retenido al socio ${member.formattedId}?'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar')), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: Text(cancelar ? 'Cancelar retención' : 'Retener'), ), ], ), ); if (confirmed != true || !mounted) return; try { final updated = cancelar ? await _service.retenerCancel(member.id) : await _service.retener(member.id); if (!mounted) return; setState(() => _member = updated); } catch (e) { if (!mounted) return; _showError('Error: $e'); } } Future _doDarDeBaja(Member member) async { Map preCheck; try { preCheck = await _service.preDelete(member.id); } catch (e) { if (!mounted) return; _showError('Error al verificar: $e'); return; } if (!mounted) return; final result = await showDialog<(String, String)>( context: context, builder: (_) => _BajaDialog(member: member, preCheck: preCheck), ); if (result == null || !mounted) return; try { final updated = await _service.delete(member.id, result.$1, result.$2); if (!mounted) return; setState(() => _member = updated); } catch (e) { if (!mounted) return; _showError('Error al dar de baja: $e'); } } Future _doReactivar(Member member) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Reactivar abonado'), content: Text('¿Reactivar al socio ${member.formattedId}?'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar')), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Reactivar'), ), ], ), ); if (confirmed != true || !mounted) return; try { final updated = await _service.recover(member.id); if (!mounted) return; setState(() => _member = updated); } catch (e) { if (!mounted) return; _showError('Error al reactivar: $e'); } } void _showError(String msg) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg), duration: const Duration(seconds: 3)), ); } Future _load(Future future) async { setState(() { _loading = true; _error = null; }); try { final m = await future; setState(() { _member = m; _loading = false; }); } catch (e) { setState(() { _error = e.toString(); _loading = false; }); } } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _NavBar( searchKey: _searchKey, member: _member, loading: _loading, service: _service, onFirst: () { _searchKey.currentState?.clear(); _load(_service.first()); }, onPrev: () { _searchKey.currentState?.clear(); if (_member != null) _load(_service.prev(_member!.id)); }, onNext: () { _searchKey.currentState?.clear(); if (_member != null) _load(_service.next(_member!.id)); }, onLast: () { _searchKey.currentState?.clear(); _load(_service.last()); }, onSearch: (id) => _load(_service.show(id)), onAction: _handleAction, ), const Divider(height: 1), Expanded( child: _loading ? const Center(child: CircularProgressIndicator()) : _error != null ? _ErrorView(error: _error!) : _member == null ? const Center(child: Text('Sin datos')) : _MemberBody(member: _member!), ), ], ); } } // ── Nav bar ────────────────────────────────────────────────────────────────── class _NavBar extends StatelessWidget { final GlobalKey<_MemberSearchState> searchKey; final Member? member; final bool loading; final MemberRepository service; final VoidCallback onFirst, onPrev, onNext, onLast; final void Function(int id) onSearch; final void Function(String action) onAction; const _NavBar({ required this.searchKey, required this.member, required this.loading, required this.service, required this.onFirst, required this.onPrev, required this.onNext, required this.onLast, required this.onSearch, required this.onAction, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final disabled = loading || member == null; return Container( color: cs.surfaceContainerLow, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ Expanded( child: _MemberSearch(key: searchKey, service: service, onSelected: onSearch), ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.first_page), onPressed: loading ? null : onFirst, tooltip: 'Primero', ), IconButton( icon: const Icon(Icons.chevron_left), onPressed: loading ? null : onPrev, tooltip: 'Anterior', ), IconButton( icon: const Icon(Icons.chevron_right), onPressed: loading ? null : onNext, tooltip: 'Siguiente', ), IconButton( icon: const Icon(Icons.last_page), onPressed: loading ? null : onLast, tooltip: 'Último', ), const VerticalDivider(width: 16, indent: 8, endIndent: 8), PopupMenuButton( icon: const Icon(Icons.more_vert), tooltip: 'Acciones', enabled: !disabled, onSelected: onAction, itemBuilder: (context) { final m = member; final isActive = m?.isActive ?? true; final retenido = m?.retenido ?? false; return [ const PopupMenuItem( value: 'receipts', child: ListTile( leading: Icon(Icons.receipt_long_outlined), title: Text('Recibos anteriores'), dense: true, ), ), const PopupMenuItem( value: 'edit', child: ListTile( leading: Icon(Icons.edit_outlined), title: Text('Editar abonado'), dense: true, ), ), const PopupMenuItem( value: 'add_family', child: ListTile( leading: Icon(Icons.person_add_outlined), title: Text('Añadir familiar'), dense: true, ), ), if (isActive) PopupMenuItem( value: 'retain', child: ListTile( leading: Icon(retenido ? Icons.play_circle_outline : Icons.pause_circle_outline), title: Text(retenido ? 'Cancelar retención' : 'Retener abonado'), dense: true, ), ), const PopupMenuDivider(), if (!isActive) const PopupMenuItem( value: 'recover', child: ListTile( leading: Icon(Icons.person_add_alt_1_outlined), title: Text('Reactivar abonado'), dense: true, ), ), if (isActive) PopupMenuItem( value: 'unregister', child: ListTile( leading: Icon(Icons.person_remove_outlined, color: cs.error), title: Text('Dar de baja', style: TextStyle(color: cs.error)), dense: true, ), ), ]; }, ), ], ), ); } } class _MemberSearch extends StatefulWidget { final MemberRepository service; final void Function(int id) onSelected; const _MemberSearch({super.key, required this.service, required this.onSelected}); @override State<_MemberSearch> createState() => _MemberSearchState(); } class _MemberSearchState extends State<_MemberSearch> { TextEditingController? _controller; void clear() => _controller?.clear(); @override Widget build(BuildContext context) { return Autocomplete>( displayStringForOption: (entry) => entry.value, optionsBuilder: (textValue) async { if (textValue.text.length < 2) return const []; final results = await widget.service.search(textValue.text); return results.entries.toList(); }, onSelected: (entry) { // key format: 5-digit member_id + 2-digit family index final memberId = int.parse(entry.key.substring(0, entry.key.length - 2)); widget.onSelected(memberId); _controller?.clear(); }, fieldViewBuilder: (context, controller, focusNode, onSubmit) { _controller = controller; return TextField( controller: controller, focusNode: focusNode, onTap: () => controller.clear(), onSubmitted: (text) { final n = int.tryParse(text.trim()); if (n != null) { widget.onSelected(n); controller.clear(); } else { onSubmit(); } }, decoration: const InputDecoration( hintText: 'Buscar abonado...', prefixIcon: Icon(Icons.search), isDense: true, border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), ), ); }, optionsViewBuilder: (context, onSelected, options) { return Align( alignment: Alignment.topLeft, child: Material( elevation: 4, borderRadius: BorderRadius.circular(8), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400, maxHeight: 280), child: ListView.builder( shrinkWrap: true, itemCount: options.length, itemBuilder: (context, i) { final entry = options.elementAt(i); return ListTile( dense: true, title: Text(entry.value), onTap: () => onSelected(entry), ); }, ), ), ), ); }, ); } } // ── Error ───────────────────────────────────────────────────────────────────── class _ErrorView extends StatelessWidget { final String error; const _ErrorView({required this.error}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.error_outline, size: 48, color: Theme.of(context).colorScheme.error), const SizedBox(height: 8), Text(error, style: TextStyle(color: Theme.of(context).colorScheme.error)), ], ), ); } } // ── Member body ─────────────────────────────────────────────────────────────── class _MemberBody extends StatelessWidget { final Member member; const _MemberBody({required this.member}); @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ LayoutBuilder(builder: (context, constraints) { if (constraints.maxWidth >= 700) { return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded(flex: 2, child: _InfoCard(member: member)), const SizedBox(width: 12), Expanded(flex: 3, child: _CommentsCard(member: member)), ], ), ); } return Column( children: [ _InfoCard(member: member), const SizedBox(height: 12), _CommentsCard(member: member), ], ); }), const SizedBox(height: 12), _FamiliarsCard(people: member.people), const SizedBox(height: 12), _FichaCard(member: member), ], ), ); } } // ── Info card ───────────────────────────────────────────────────────────────── class _InfoCard extends StatelessWidget { final Member member; const _InfoCard({required this.member}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Card.outlined( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Información', style: Theme.of(context).textTheme.titleMedium), const Divider(height: 20), // Fila principal: Número | Fecha alta | Tipo+Estado | Cuota Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Número Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Número', style: Theme.of(context) .textTheme .labelSmall ?.copyWith(color: cs.outline)), Text(member.formattedId, style: Theme.of(context).textTheme.headlineSmall), ], ), const SizedBox(width: 20), // Fecha de alta Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Fecha de alta', style: Theme.of(context) .textTheme .labelSmall ?.copyWith(color: cs.outline)), Text(_fmtDate(member.regDate), style: Theme.of(context).textTheme.bodyLarge), ], ), const SizedBox(width: 20), // Tipo + Estado Expanded( child: Wrap( spacing: 6, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: [ if (member.type.isNotEmpty) _TypeChip(label: member.type), member.isActive ? _StatusChip( label: 'Activo', color: Colors.green, icon: Icons.check_circle_outline) : _StatusChip( label: 'Baja', color: cs.error, icon: Icons.cancel_outlined), if (member.retenido) _StatusChip( label: 'Retenido', color: Colors.orange, icon: Icons.pause_circle_outline), if (member.remSpecial) _StatusChip( label: 'Remesa especial', color: cs.primary, icon: Icons.star_outline), if (member.paid) _StatusChip( label: 'Debe recibos', color: cs.error, icon: Icons.warning_amber_rounded), ], ), ), // Cuota if (member.fee != null) ...[ const SizedBox(width: 20), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('Cuota', style: Theme.of(context) .textTheme .labelSmall ?.copyWith(color: cs.outline)), Text( '${member.fee!.toStringAsFixed(2)} €', style: Theme.of(context) .textTheme .titleMedium ?.copyWith(color: cs.primary), ), ], ), ], ], ), const SizedBox(height: 14), // Franjas de años _YearStrips(data: member.monthlyFees), // Info de baja si está dado de baja if (!member.isActive && member.unregDate != null) ...[ const SizedBox(height: 12), _InfoRow(label: 'Fecha de baja', value: _fmtDate(member.unregDate)), if (member.unregReason.isNotEmpty) _InfoRow(label: 'Motivo', value: member.unregReason), ], ], ), ), ); } } // ── Year strips ─────────────────────────────────────────────────────────────── enum _MonthStatus { unknown, paid, unpaid } class _YearStrips extends StatelessWidget { /// year → lista de 12 valores: -1 amarillo, 1 verde, 0 o 4 rojo. final Map> data; const _YearStrips({this.data = const {}}); static const _months = [ 'Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic', ]; static _MonthStatus _toStatus(int v) => switch (v) { 1 => _MonthStatus.paid, 0 || 4 => _MonthStatus.unpaid, _ => _MonthStatus.unknown, }; @override Widget build(BuildContext context) { final now = DateTime.now(); final years = [now.year - 1, now.year, now.year + 1, now.year + 2]; return Column( children: years.map((year) { final months = data[year] ?? []; return Padding( padding: const EdgeInsets.only(bottom: 3), child: Row( children: [ SizedBox( width: 34, child: Text('$year', style: TextStyle( fontSize: 10, color: Theme.of(context).colorScheme.outline)), ), ...List.generate(12, (i) { final status = i < months.length ? _toStatus(months[i]) : _MonthStatus.unknown; return Expanded(child: _MonthCell(month: _months[i], status: status)); }), ], ), ); }).toList(), ); } } class _MonthCell extends StatelessWidget { final String month; final _MonthStatus status; const _MonthCell({required this.month, required this.status}); Color _bg(BuildContext context) => switch (status) { _MonthStatus.paid => Colors.green.shade400, _MonthStatus.unpaid => Theme.of(context).colorScheme.error, _MonthStatus.unknown => Colors.amber.shade400, }; @override Widget build(BuildContext context) { return Container( height: 20, margin: const EdgeInsets.only(right: 2), decoration: BoxDecoration( color: _bg(context), borderRadius: BorderRadius.circular(3), ), alignment: Alignment.center, child: Text( month, style: const TextStyle( fontSize: 8, color: Colors.white, fontWeight: FontWeight.w700, height: 1), ), ); } } class _TypeChip extends StatelessWidget { final String label; const _TypeChip({required this.label}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Chip( label: Text(label, style: TextStyle( color: cs.onPrimaryContainer, fontWeight: FontWeight.bold, fontSize: 12)), backgroundColor: cs.primaryContainer, padding: EdgeInsets.zero, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); } } class _StatusChip extends StatelessWidget { final String label; final Color color; final IconData icon; const _StatusChip( {required this.label, required this.color, required this.icon}); @override Widget build(BuildContext context) { return Chip( avatar: Icon(icon, size: 16, color: color), label: Text(label, style: TextStyle(fontSize: 12, color: color)), side: BorderSide(color: color.withValues(alpha: 0.4)), backgroundColor: color.withValues(alpha: 0.08), padding: EdgeInsets.zero, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); } } // ── Comments card ───────────────────────────────────────────────────────────── class _CommentsCard extends StatelessWidget { final Member member; const _CommentsCard({required this.member}); void _showAddCommentDialog(BuildContext context) { final controller = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Añadir comentario'), content: TextField( controller: controller, maxLines: 4, autofocus: true, decoration: const InputDecoration( hintText: 'Escribe el comentario...', border: OutlineInputBorder(), ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancelar'), ), FilledButton( onPressed: () { Navigator.of(ctx).pop(); // TODO: llamar al endpoint de creación de comentario ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Guardar comentario — pendiente de implementar'), duration: Duration(seconds: 2), ), ); }, child: const Text('Guardar'), ), ], ), ); } bool get _hasContent => member.comment.isNotEmpty || member.comment2.isNotEmpty || member.comments.isNotEmpty; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Card.outlined( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text('Comentarios', style: Theme.of(context).textTheme.titleMedium), ), TextButton.icon( onPressed: () => _showAddCommentDialog(context), icon: const Icon(Icons.add, size: 16), label: const Text('Añadir'), style: TextButton.styleFrom( visualDensity: VisualDensity.compact, ), ), ], ), const Divider(height: 20), if (!_hasContent) Text('Sin comentarios', style: TextStyle(color: cs.outline, fontSize: 13)) else ...[ if (member.comment.isNotEmpty) _CommentBubble( text: member.comment, label: 'Nota heredada', legacy: true), if (member.comment2.isNotEmpty) _CommentBubble(text: member.comment2, label: 'Nota interna'), ...member.comments.map((c) => _CommentBubble( text: c.comment, label: _fmtDate(c.createdAt), )), ], ], ), ), ); } } class _CommentBubble extends StatelessWidget { final String text; final String label; final bool legacy; const _CommentBubble( {required this.text, required this.label, this.legacy = false}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: legacy ? cs.errorContainer.withValues(alpha: 0.4) : cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle( fontSize: 11, color: cs.outline, fontWeight: FontWeight.w500)), const SizedBox(height: 4), Text(text, style: const TextStyle(fontSize: 13)), ], ), ); } } // ── Familiars card ──────────────────────────────────────────────────────────── class _FamiliarsCard extends StatelessWidget { final List people; const _FamiliarsCard({required this.people}); @override Widget build(BuildContext context) { return Card.outlined( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Familiares asociados', style: Theme.of(context).textTheme.titleMedium), const Divider(height: 20), if (people.isEmpty) Text('Sin familiares', style: TextStyle( color: Theme.of(context).colorScheme.outline, fontSize: 13)) else LayoutBuilder(builder: (context, constraints) { return SingleChildScrollView( scrollDirection: Axis.horizontal, child: ConstrainedBox( constraints: BoxConstraints(minWidth: constraints.maxWidth), child: DataTable( headingRowHeight: 36, dataRowMinHeight: 52, dataRowMaxHeight: 60, columnSpacing: 12, columns: const [ DataColumn(label: Text('Foto')), DataColumn(label: Text('FF')), DataColumn(label: Text('Nombre')), DataColumn(label: Text('DNI')), DataColumn(label: Text('Móvil')), DataColumn(label: Text('Email')), DataColumn(label: Text('Fecha nac.')), DataColumn(label: Tooltip(message: 'Acepta LOPD', child: Icon(Icons.privacy_tip_outlined, size: 16))), DataColumn(label: Tooltip(message: 'Recibir noticias', child: Icon(Icons.mail_outline, size: 16))), DataColumn(label: Tooltip(message: 'Cuestionario de salud', child: Icon(Icons.health_and_safety_outlined, size: 16))), DataColumn(label: Tooltip(message: 'Autorización 1', child: Icon(Icons.verified_user_outlined, size: 16))), DataColumn(label: Tooltip(message: 'Autorización 2', child: Icon(Icons.how_to_reg_outlined, size: 16))), DataColumn(label: Text('')), ], rows: people.map((p) => _personRow(context, p)).toList(), ), ), ); }), ], ), ), ); } DataRow _personRow(BuildContext context, Person p) { final cs = Theme.of(context).colorScheme; final isPrincipal = p.family == 1; return DataRow(cells: [ // Foto DataCell( p.photoPath != null ? CircleAvatar( radius: 18, backgroundImage: NetworkImage(p.photoPath!), ) : CircleAvatar( radius: 18, backgroundColor: isPrincipal ? cs.primary : cs.surfaceContainerHigh, child: Text(p.initials, style: TextStyle( fontSize: 12, color: isPrincipal ? cs.onPrimary : cs.onSurface)), ), ), // FF — número de familiar DataCell( Tooltip( message: isPrincipal ? 'Familiar principal' : 'Familiar ${p.family}', child: Text( p.family.toString(), style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: isPrincipal ? cs.primary : null, ), ), ), ), DataCell(Text(p.fullName, style: const TextStyle(fontSize: 13))), DataCell(Text(p.dni.isEmpty ? '—' : p.dni, style: const TextStyle(fontSize: 13))), DataCell(Text(p.phone.isEmpty ? '—' : p.phone, style: const TextStyle(fontSize: 13))), DataCell(Text(p.email.isEmpty ? '—' : p.email, style: const TextStyle(fontSize: 13))), DataCell(Text(_fmtDate(p.birthDate.isEmpty ? null : p.birthDate), style: const TextStyle(fontSize: 13))), DataCell(_BoolIcon(p.lopd, 'Acepta LOPD')), DataCell(_BoolIcon(p.newsletter, 'Recibir noticias')), DataCell(_BoolIcon(p.bHealth, 'Cuestionario de salud')), DataCell(_BoolIcon(p.authorized, 'Autorización 1')), DataCell(_BoolIcon(p.authorized2, 'Autorización 2')), // Acciones DataCell( PopupMenuButton( icon: const Icon(Icons.more_vert, size: 18), tooltip: 'Acciones', padding: EdgeInsets.zero, onSelected: (value) { // TODO: implementar acciones de familiar ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$value — pendiente de implementar'), duration: const Duration(seconds: 2), ), ); }, itemBuilder: (context) => [ const PopupMenuItem( value: 'edit', child: ListTile(leading: Icon(Icons.edit_outlined), title: Text('Editar familiar'), dense: true), ), const PopupMenuItem( value: 'photo', child: ListTile(leading: Icon(Icons.camera_alt_outlined), title: Text('Tomar foto'), dense: true), ), const PopupMenuItem( value: 'card', child: ListTile(leading: Icon(Icons.badge_outlined), title: Text('Ver carnet'), dense: true), ), const PopupMenuItem( value: 'password', child: ListTile(leading: Icon(Icons.lock_reset_outlined), title: Text('Resetear contraseña'), dense: true), ), const PopupMenuDivider(), PopupMenuItem( value: 'delete', child: ListTile( leading: Icon(Icons.delete_outline, color: cs.error), title: Text('Borrar familiar', style: TextStyle(color: cs.error)), dense: true, ), ), ], ), ), ]); } } class _BoolIcon extends StatelessWidget { final bool? value; final String tooltip; const _BoolIcon(this.value, this.tooltip); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; if (value == null) { return Tooltip( message: '$tooltip: desconocido', child: Icon(Icons.help_outline, size: 18, color: cs.outlineVariant), ); } return Tooltip( message: '$tooltip: ${value! ? 'Sí' : 'No'}', child: Icon( value! ? Icons.check_circle : Icons.cancel, size: 18, color: value! ? Colors.green.shade600 : cs.error, ), ); } } // ── Ficha card ──────────────────────────────────────────────────────────────── class _FichaCard extends StatelessWidget { final Member member; const _FichaCard({required this.member}); @override Widget build(BuildContext context) { final hasBank = member.bank.isNotEmpty || member.bankAccount.isNotEmpty; return Card.outlined( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Ficha', style: Theme.of(context).textTheme.titleMedium), const Divider(height: 20), _SectionLabel('Contacto'), const SizedBox(height: 8), Wrap( spacing: 32, runSpacing: 8, children: [ _InfoRow( label: 'Dirección', value: member.address.isEmpty ? '—' : member.address), _InfoRow( label: 'Población', value: member.city.isEmpty ? '—' : member.city), _InfoRow( label: 'C.P.', value: member.zip.isEmpty ? '—' : member.zip), _InfoRow( label: 'Teléfono', value: member.phone.isEmpty ? '—' : member.phone), _InfoRow( label: 'Email', value: member.email.isEmpty ? '—' : member.email), ], ), if (hasBank) ...[ const SizedBox(height: 16), _SectionLabel('Datos bancarios'), const SizedBox(height: 8), Wrap( spacing: 32, runSpacing: 8, children: [ _InfoRow( label: 'Entidad', value: member.bank.isEmpty ? '—' : member.bank), _InfoRow( label: 'Sucursal', value: member.bankOffice.isEmpty ? '—' : member.bankOffice), _InfoRow( label: 'D.C.', value: member.bankSecCode.isEmpty ? '—' : member.bankSecCode), _InfoRow( label: 'Cuenta', value: member.bankAccount.isEmpty ? '—' : member.bankAccount), if (member.bankName.isNotEmpty) _InfoRow(label: 'Nombre', value: member.bankName), if (member.mandate.isNotEmpty) _InfoRow(label: 'Mandato SEPA', value: member.mandate), ], ), ], ], ), ), ); } } class _SectionLabel extends StatelessWidget { final String text; const _SectionLabel(this.text); @override Widget build(BuildContext context) { return Text(text, style: Theme.of(context).textTheme.labelMedium?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold, )); } } // ── Shared helpers ──────────────────────────────────────────────────────────── class _InfoRow extends StatelessWidget { final String label; final String value; const _InfoRow({required this.label, required this.value}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle( fontSize: 11, color: cs.outline, fontWeight: FontWeight.w500)), const SizedBox(height: 2), Text(value, style: const TextStyle(fontSize: 14)), ], ); } } // ── Dar de baja dialog ──────────────────────────────────────────────────────── class _BajaDialog extends StatefulWidget { final Member member; final Map preCheck; const _BajaDialog({required this.member, required this.preCheck}); @override State<_BajaDialog> createState() => _BajaDialogState(); } class _BajaDialogState extends State<_BajaDialog> { late DateTime _date; final _reasonCtrl = TextEditingController(); @override void initState() { super.initState(); _date = DateTime.now(); } @override void dispose() { _reasonCtrl.dispose(); super.dispose(); } String get _dateStr => '${_date.year.toString().padLeft(4, '0')}-' '${_date.month.toString().padLeft(2, '0')}-' '${_date.day.toString().padLeft(2, '0')}'; Future _pickDate() async { final picked = await showDatePicker( context: context, initialDate: _date, firstDate: DateTime(2000), lastDate: DateTime.now().add(const Duration(days: 30)), ); if (picked != null) setState(() => _date = picked); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final pendientes = widget.preCheck['pendientes'] ?? 0; final devueltos = widget.preCheck['devueltos'] ?? 0; final revision = widget.preCheck['revision'] ?? 0; final hasWarning = pendientes > 0 || devueltos > 0 || revision > 0; return AlertDialog( title: Text('Dar de baja — ${widget.member.formattedId}'), content: SizedBox( width: 380, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (hasWarning) ...[ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: cs.errorContainer, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.warning_amber_rounded, size: 16, color: cs.error), const SizedBox(width: 6), Text('Atención', style: TextStyle(fontWeight: FontWeight.bold, color: cs.error)), ]), const SizedBox(height: 4), if (pendientes > 0) Text('• $pendientes recibo(s) pendiente(s)', style: const TextStyle(fontSize: 13)), if (devueltos > 0) Text('• $devueltos recibo(s) devuelto(s)', style: const TextStyle(fontSize: 13)), if (revision > 0) Text('• $revision recibo(s) en revisión', style: const TextStyle(fontSize: 13)), ], ), ), const SizedBox(height: 16), ], // Fecha de baja Row( children: [ Expanded( child: InputDecorator( decoration: const InputDecoration( labelText: 'Fecha de baja', border: OutlineInputBorder(), isDense: true, ), child: Text(_dateStr), ), ), const SizedBox(width: 8), IconButton.outlined( icon: const Icon(Icons.calendar_today, size: 18), onPressed: _pickDate, tooltip: 'Seleccionar fecha', ), ], ), const SizedBox(height: 12), TextField( controller: _reasonCtrl, decoration: const InputDecoration( labelText: 'Motivo (opcional)', border: OutlineInputBorder(), isDense: true, ), maxLines: 2, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancelar'), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: cs.error), onPressed: () => Navigator.pop(context, (_dateStr, _reasonCtrl.text)), child: const Text('Dar de baja'), ), ], ); } } // ── Helpers ─────────────────────────────────────────────────────────────────── String _fmtDate(String? date) { if (date == null || date.isEmpty) return '—'; try { final parts = date.split('-'); if (parts.length == 3) return '${parts[2]}/${parts[1]}/${parts[0]}'; } catch (_) {} return date; }