import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:qpay/screens/groups/models/recipient_group_model.dart'; import 'package:qpay/models/responsive_policy.dart'; import 'package:qpay/screens/groups/controllers/group_controller.dart'; import 'package:shared_preferences/shared_preferences.dart'; class GroupsScreen extends StatefulWidget { const GroupsScreen({super.key}); @override State createState() => _GroupsScreenState(); } class _GroupsScreenState extends State { late GroupController controller; late SharedPreferences prefs; final _searchController = TextEditingController(); final _scrollController = ScrollController(); @override void initState() { super.initState(); controller = GroupController(context); _loadData(); _scrollController.addListener(_onScroll); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { controller.loadNextPage(userId: prefs.getString('userId') ?? ''); } } Future _loadData() async { prefs = await SharedPreferences.getInstance(); final userId = prefs.getString('userId') ?? ''; await controller.listGroups(userId: userId); } void _onSearchChanged(String value) { final userId = prefs.getString('userId') ?? ''; controller.searchGroups( userId: userId, name: value.isNotEmpty ? value : null, ); } void _onDeleteSelected() async { final userId = prefs.getString('userId') ?? ''; final count = controller.model.selectedGroupIds.length; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Delete groups'), content: Text('Are you sure you want to delete $count group(s)?'), 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 controller.deleteSelectedGroups(userId: userId); } } @override void dispose() { _searchController.dispose(); _scrollController.dispose(); controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Groups', style: TextStyle(fontSize: 20, color: Colors.black), ), centerTitle: true, ), body: ListenableBuilder( listenable: controller, builder: (context, _) { return Column( children: [ Expanded( child: SingleChildScrollView( controller: _scrollController, 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 (controller.model.selectMode) _buildSelectionBottomBar(), ], ); }, ), ); } Widget _buildSearchRow() { return Row( children: [ Expanded( child: TextFormField( controller: _searchController, decoration: InputDecoration( hintText: 'Search groups by name...', 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: controller.model.selectMode ? Icons.checklist : Icons.checklist_outlined, tooltip: 'Select groups', isActive: controller.model.selectMode, onPressed: () => controller.toggleSelectMode(), ), const SizedBox(width: 8), _buildIconButton( icon: Icons.add, tooltip: 'Create new group', isActive: false, onPressed: _createGroup, ), ], ); } _createGroup() async { final response = await context.push('/groups/create'); if (response == 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 (controller.model.isLoading) { return const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 60), child: CircularProgressIndicator(), ), ); } if (controller.model.groups.isEmpty) { return Center(child: _buildEmptyState()); } return Column( children: [ if (controller.model.selectMode) Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Text( '${controller.model.selectedGroupIds.length} selected', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey.shade600, ), ), ], ), ), ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: controller.model.groups.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { return _buildGroupCard(controller.model.groups[index]); }, ), if (controller.model.isLoadingMore) const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ), ], ); } Widget _buildEmptyState() { return Column( children: [ const SizedBox(height: 40), Icon(Icons.group_outlined, size: 64, color: Colors.grey.shade400), const SizedBox(height: 16), const Text( 'No groups yet.', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( 'Create a group to start sending batch payments.', style: TextStyle(fontSize: 14, color: Colors.grey.shade600), textAlign: TextAlign.center, ), ], ); } Widget _buildGroupCard(RecipientGroup group) { final isSelected = controller.model.selectedGroupIds.contains(group.id); final inSelectMode = controller.model.selectMode; 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 ? () => controller.toggleGroupSelection(group.id!) : () => context.push('/groups/${group.id}', extra: group), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ if (inSelectMode) Padding( padding: const EdgeInsets.only(right: 12), child: Icon( isSelected ? Icons.check_box : Icons.check_box_outline_blank, color: isSelected ? Theme.of(context).colorScheme.primary : Colors.grey.shade400, ), ), CircleAvatar( backgroundColor: Theme.of( context, ).colorScheme.primary.withAlpha(30), child: Text( _getGroupAbbreviation(group.name), style: TextStyle( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w600, fontSize: 16, ), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( group.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), if (group.description != null && group.description!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 4), child: Text( group.description!, style: TextStyle( fontSize: 13, color: Colors.grey.shade600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), if (!inSelectMode) Icon(Icons.chevron_right, color: Colors.grey.shade400), ], ), ), ), ); } String _getGroupAbbreviation(String name) { if (name.isEmpty) return '?'; final words = name.trim().split(RegExp(r'\s+')); if (words.length >= 2) { return '${words.first[0]}${words.last[0]}'.toUpperCase(); } return name.length >= 2 ? name.substring(0, 2).toUpperCase() : name[0].toUpperCase(); } Widget _buildSelectionBottomBar() { final selectedCount = controller.model.selectedGroupIds.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), ), ], ), ), ), ); } }