import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:qpay/screens/groups/models/group_batch_model.dart'; import 'package:qpay/screens/groups/models/recipient_group_model.dart'; import 'package:qpay/models/responsive_policy.dart'; import 'package:qpay/screens/groups/controllers/batch_controller.dart'; import 'package:qpay/screens/groups/controllers/group_controller.dart'; import 'package:qpay/widgets/status_chip_widget.dart'; class GroupDetailScreen extends StatefulWidget { final RecipientGroup group; const GroupDetailScreen({super.key, required this.group}); @override State createState() => _GroupDetailScreenState(); } class _GroupDetailScreenState extends State { late GroupController groupController; late BatchController batchController; final _searchController = TextEditingController(); final _scrollController = ScrollController(); String? _statusFilter; String? _currencyFilter; static const _statusFilterItems = >[ DropdownMenuItem(value: null, child: Text('All')), DropdownMenuItem(value: 'CREATED', child: Text('Created')), DropdownMenuItem(value: 'PENDING', child: Text('Pending')), DropdownMenuItem(value: 'CONFIRMED_ALL', child: Text('Confirmed')), DropdownMenuItem(value: 'REQUESTED', child: Text('Requested')), DropdownMenuItem(value: 'PROCESSING', child: Text('Processing')), DropdownMenuItem(value: 'COMPLETED', child: Text('Completed')), DropdownMenuItem(value: 'FAILED', child: Text('Failed')), ]; static const _currencyFilterItems = >[ DropdownMenuItem(value: null, child: Text('All')), DropdownMenuItem(value: 'USD', child: Text('USD')), DropdownMenuItem(value: 'ZWL', child: Text('ZWL')), DropdownMenuItem(value: 'ZAR', child: Text('ZAR')), ]; @override void initState() { super.initState(); groupController = GroupController(context); batchController = BatchController(context); _loadData(); _scrollController.addListener(_onScroll); _searchController.addListener(() => setState(() {})); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { batchController.loadNextPage(widget.group.id ?? ''); } } Future _loadData() async { final groupId = widget.group.id ?? ''; await Future.wait([ batchController.listBatches(groupId), groupController.listMembers(groupId), ]); } void _onSearchChanged(String value) { batchController.searchBatches( widget.group.id ?? '', search: value.isNotEmpty ? value : null, status: _statusFilter, currency: _currencyFilter, ); } void _showMembersSheet() { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) => DraggableScrollableSheet( initialChildSize: 0.6, minChildSize: 0.3, maxChildSize: 0.85, expand: false, builder: (context, scrollController) { return Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text( 'Members', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, ), ), const Spacer(), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ], ), const SizedBox(height: 4), Text( '${groupController.model.members.length} member(s)', style: TextStyle(fontSize: 14, color: Colors.grey.shade600), ), const SizedBox(height: 12), const Divider(), Expanded( child: ListenableBuilder( listenable: groupController, builder: (context, _) { if (groupController.model.isLoading) { return const Center(child: CircularProgressIndicator()); } if (groupController.model.members.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.person_outline, size: 48, color: Colors.grey.shade400, ), const SizedBox(height: 16), const Text( 'No members found.', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ], ), ), ); } return ListView.separated( controller: scrollController, itemCount: groupController.model.members.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final member = groupController.model.members[index]; return ListTile( contentPadding: const EdgeInsets.symmetric( vertical: 2, horizontal: 4, ), leading: CircleAvatar( backgroundColor: Theme.of( context, ).colorScheme.primary.withAlpha(30), child: Text( member.recipient.name?.isNotEmpty == true ? member.recipient.name![0].toUpperCase() : '?', style: TextStyle( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w600, ), ), ), title: Text( member.recipient.name ?? '', style: const TextStyle( fontWeight: FontWeight.w500, ), ), subtitle: Text(member.recipient.account ?? ''), trailing: member.recipient.latestProviderLabel != null && member .recipient .latestProviderLabel! .isNotEmpty ? Chip( label: Text( member.recipient.latestProviderLabel!, style: const TextStyle(fontSize: 11), ), visualDensity: VisualDensity.compact, ) : null, ); }, ); }, ), ), ], ), ); }, ), ); } void _onDeleteSelected() async { final userId = widget.group.userId ?? ''; final count = batchController.model.selectedBatchIds.length; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Delete batches'), content: Text('Are you sure you want to delete $count batch(es)?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.of(ctx).pop(true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Delete'), ), ], ), ); if (confirmed == true) { await batchController.deleteSelectedBatches( groupId: widget.group.id ?? '', userId: userId, ); } } void _showFilterSheet() { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) { return StatefulBuilder( builder: (context, setSheetState) { return Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text( 'Filter Batches', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), ), const Spacer(), TextButton( onPressed: () { setSheetState(() { _statusFilter = null; _currencyFilter = null; }); }, child: const Text('Clear All'), ), ], ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: _statusFilter, decoration: InputDecoration( labelText: 'Status', filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), ), items: _statusFilterItems, onChanged: (v) { setSheetState(() => _statusFilter = v); }, ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: _currencyFilter, decoration: InputDecoration( labelText: 'Currency', filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), ), items: _currencyFilterItems, onChanged: (v) { setSheetState(() => _currencyFilter = v); }, ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => _applyFilters(ctx), child: const Text('Apply Filters'), ), ), const SizedBox(height: 8), ], ), ); }, ); }, ); } void _applyFilters(BuildContext ctx) { Navigator.of(ctx).pop(); batchController.searchBatches( widget.group.id ?? '', search: _searchController.text.isNotEmpty ? _searchController.text : null, status: _statusFilter, currency: _currencyFilter, ); } @override void dispose() { _searchController.dispose(); _scrollController.dispose(); groupController.dispose(); batchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.group.name), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.people_outline), tooltip: 'View members', onPressed: _showMembersSheet, ), ], ), body: ListenableBuilder( listenable: batchController, builder: (context, _) { return Column( children: [ Expanded( child: CustomScrollView( controller: _scrollController, slivers: [ SliverToBoxAdapter( child: Center( child: LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth > ResponsivePolicy.md ? ResponsivePolicy.md.toDouble() : constraints.maxWidth; return SizedBox( width: width, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSearchRow(), const SizedBox(height: 16), _buildContent(), ], ), ), ); }, ), ), ), ], ), ), if (batchController.model.selectMode) _buildSelectionBottomBar(), ], ); }, ), ); } Widget _buildSearchRow() { return Row( children: [ Expanded( child: TextFormField( controller: _searchController, decoration: InputDecoration( hintText: 'Search batches by description...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); _onSearchChanged(''); }, ) : null, filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of(context).colorScheme.primary, ), ), ), onChanged: _onSearchChanged, ), ), const SizedBox(width: 8), _buildIconButton( icon: Icons.filter_list, tooltip: 'Filter batches', isActive: _statusFilter != null || _currencyFilter != null, onPressed: _showFilterSheet, ), const SizedBox(width: 8), _buildIconButton( icon: batchController.model.selectMode ? Icons.checklist : Icons.checklist_outlined, tooltip: 'Select batches', isActive: batchController.model.selectMode, onPressed: () => batchController.toggleSelectMode(), ), const SizedBox(width: 8), _buildIconButton( icon: Icons.add, tooltip: 'Create batch', isActive: false, onPressed: () async { final result = await context.push( '/groups/${widget.group.id}/batches/create', extra: widget.group, ); if (result == true) { _loadData(); } }, ), ], ); } Widget _buildIconButton({ required IconData icon, required String tooltip, required bool isActive, required VoidCallback onPressed, }) { return Container( decoration: BoxDecoration( color: isActive ? Theme.of(context).colorScheme.primary : Colors.grey.shade100, borderRadius: BorderRadius.circular(12), ), child: IconButton( icon: Icon(icon, color: isActive ? Colors.white : Colors.grey.shade700), tooltip: tooltip, onPressed: onPressed, ), ); } Widget _buildContent() { if (batchController.model.isLoading) { return const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 60), child: CircularProgressIndicator(), ), ); } if (batchController.model.batches.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.symmetric(vertical: 40), child: Column( children: [ Icon( Icons.receipt_long_outlined, size: 64, color: Colors.grey.shade400, ), const SizedBox(height: 16), const Text( 'No batches yet.', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( 'Tap + to create a new batch payment.', style: TextStyle(fontSize: 14, color: Colors.grey.shade600), ), ], ), ), ); } return Column( children: [ if (batchController.model.selectMode) Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Text( '${batchController.model.selectedBatchIds.length} selected', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey.shade600, ), ), ], ), ), if (_statusFilter != null || _currencyFilter != null) Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ if (_statusFilter != null) _activeFilterChip('Status: $_statusFilter', () { setState(() => _statusFilter = null); _onSearchChanged(_searchController.text); }), if (_currencyFilter != null) const SizedBox(width: 6), if (_currencyFilter != null) _activeFilterChip('Currency: $_currencyFilter', () { setState(() => _currencyFilter = null); _onSearchChanged(_searchController.text); }), ], ), ), ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: batchController.model.batches.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { return _buildBatchCard( context, batchController.model.batches[index], widget.group, ); }, ), if (batchController.model.isLoadingMore) const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ), ], ); } Widget _activeFilterChip(String label, VoidCallback onRemove) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withAlpha(20), borderRadius: BorderRadius.circular(20), border: Border.all( color: Theme.of(context).colorScheme.primary.withAlpha(60), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( label, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 4), InkWell( onTap: onRemove, borderRadius: BorderRadius.circular(10), child: Padding( padding: const EdgeInsets.all(2), child: Icon( Icons.close, size: 14, color: Theme.of(context).colorScheme.primary, ), ), ), ], ), ); } Widget _buildBatchCard( BuildContext context, GroupBatch batch, RecipientGroup group, ) { final isSelected = batchController.model.selectedBatchIds.contains( batch.id, ); final inSelectMode = batchController.model.selectMode; final status = batch.status ?? 'UNKNOWN'; return Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: inSelectMode && isSelected ? Theme.of(context).colorScheme.primary : Colors.black12.withAlpha(30), width: inSelectMode && isSelected ? 2 : 1, ), ), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: inSelectMode ? () => batchController.toggleBatchSelection(batch.id!) : () => context.push('/groups/${group.id}/batches/${batch.id}'), child: Padding( padding: const EdgeInsets.all(10), child: Row( children: [ if (inSelectMode) Padding( padding: const EdgeInsets.only(right: 8), child: Icon( isSelected ? Icons.check_box : Icons.check_box_outline_blank, size: 20, color: isSelected ? Theme.of(context).colorScheme.primary : Colors.grey.shade400, ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (batch.description != null && batch.description!.isNotEmpty) Flexible( child: Text( batch.description!, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 6), StatusChip(status: status), ], ), const SizedBox(height: 4), Text( '${batch.currency ?? 'USD'} ${batch.value?.toStringAsFixed(2) ?? '—'}', style: TextStyle( fontSize: 13, color: Colors.grey.shade600, ), ), if (batch.count != null) Padding( padding: const EdgeInsets.only(top: 4), child: Row( children: [ _countPill('${batch.count}', Colors.blue), const SizedBox(width: 3), _countPill( '${batch.successfulCount ?? 0} ok', Colors.green, ), const SizedBox(width: 3), _countPill( '${batch.failedCount ?? 0} failed', Colors.red, ), ], ), ), ], ), ), if (!inSelectMode) Icon(Icons.chevron_right, color: Colors.grey.shade400), ], ), ), ), ); } Widget _countPill(String label, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: color.withAlpha(20), borderRadius: BorderRadius.circular(4), border: Border.all(color: color.withAlpha(60)), ), child: Text( label, style: TextStyle( fontSize: 11, color: color, fontWeight: FontWeight.w500, ), ), ); } Widget _buildSelectionBottomBar() { final selectedCount = batchController.model.selectedBatchIds.length; return Container( decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, -2), ), ], ), child: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Text( '$selectedCount selected', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), const Spacer(), TextButton.icon( onPressed: selectedCount > 0 ? _onDeleteSelected : null, icon: const Icon(Icons.delete_outline, size: 20), label: const Text('Delete'), style: TextButton.styleFrom(foregroundColor: Colors.red), ), ], ), ), ), ); } }