import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:qpay/models/responsive_policy.dart'; import 'package:qpay/screens/receipt/receipt_controller.dart'; import 'package:qpay/screens/transaction_controller.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:qpay/screens/history/history_controller.dart'; import 'package:qpay/widgets/app_snack_bar.dart'; import 'package:qpay/widgets/transaction_item_widget.dart'; import 'package:qpay/screens/transactions/transaction_model.dart' as txn; class HistoryScreen extends StatefulWidget { const HistoryScreen({super.key}); @override State createState() => _HistoryScreenState(); } class _HistoryScreenState extends State with TickerProviderStateMixin { late HistoryController controller; late SharedPreferences prefs; late TransactionController transactionController; final TextEditingController _searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); late final AnimationController _entryController; late final Animation _entryFade; late final Animation _entrySlide; @override void initState() { super.initState(); controller = HistoryController(context); transactionController = Provider.of( context, listen: false, ); // Short, subtle entry animation (replaces the old 3-second stagger) _entryController = AnimationController( duration: const Duration(milliseconds: 220), vsync: this, ); _entryFade = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation(parent: _entryController, curve: Curves.easeOut)); _entrySlide = Tween( begin: const Offset(0, 0.02), end: Offset.zero, ).animate(CurvedAnimation(parent: _entryController, curve: Curves.easeOut)); _entryController.forward(); // Listen to controller changes to rebuild when transactions are loaded controller.addListener(() => setState(() {})); _searchController.addListener(() => setState(() {})); _scrollController.addListener(_onScroll); setupData(); } Future setupData() async { prefs = await SharedPreferences.getInstance(); final userId = prefs.getString("userId") ?? ''; await controller.searchTransactions(userId); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { final userId = prefs.getString("userId") ?? ''; if (userId.isNotEmpty) { controller.loadNextPage(userId); } } } /// Handles the repeat action for a transaction. Future _repeatTransaction(Map item) async { final receiptController = ReceiptController(context); final transaction = txn.TransactionModel.fromJson(item); await receiptController.repeatTransaction(transaction); if (mounted) { context.push('/make-payment'); } } Future _onDeleteSelected() async { final count = controller.model.selectedTransactionIds.length; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Delete transactions'), content: Text('Remove $count transaction(s) from this list?'), 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) { controller.deleteSelectedTransactions(); if (mounted) { AppSnackBar.show( context, 'Transactions removed from this list', duration: const Duration(seconds: 2), ); } } } 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 Transactions', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), ), const Spacer(), TextButton( onPressed: () { setSheetState(() { controller.model.statusFilter = null; controller.model.typeFilter = null; }); }, child: const Text('Clear All'), ), ], ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: controller.model.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: const [ DropdownMenuItem(value: null, child: Text('All')), DropdownMenuItem( value: 'SUCCESS', child: Text('Success'), ), DropdownMenuItem( value: 'PENDING', child: Text('Pending'), ), DropdownMenuItem(value: 'FAILED', child: Text('Failed')), ], onChanged: (v) { setSheetState(() => controller.model.statusFilter = v); }, ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: controller.model.typeFilter, decoration: InputDecoration( labelText: 'Type', filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), ), items: const [ DropdownMenuItem(value: null, child: Text('All')), DropdownMenuItem( value: 'REQUEST', child: Text('Request'), ), DropdownMenuItem( value: 'PAYMENT', child: Text('Payment'), ), DropdownMenuItem(value: 'REFUND', child: Text('Refund')), ], onChanged: (v) { setSheetState(() => controller.model.typeFilter = v); }, ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { Navigator.of(ctx).pop(); final userId = prefs.getString("userId") ?? ''; controller.searchTransactions( userId, search: _searchController.text.isNotEmpty ? _searchController.text : null, status: controller.model.statusFilter, type: controller.model.typeFilter, ); }, child: const Text('Apply Filters'), ), ), const SizedBox(height: 8), ], ), ); }, ); }, ); } @override void dispose() { _searchController.dispose(); _scrollController.dispose(); _entryController.dispose(); controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('History'), centerTitle: true), body: Column( children: [ Expanded( child: CustomScrollView( controller: _scrollController, slivers: [ SliverToBoxAdapter( child: FadeTransition( opacity: _entryFade, child: SlideTransition( position: _entrySlide, 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: 12), _buildContent(), ], ), ), ); }, ), ), ), ), ), ], ), ), if (controller.model.selectMode) _buildSelectionBottomBar(), ], ), ); } Widget _buildSearchRow() { final hasFilters = controller.model.statusFilter != null || controller.model.typeFilter != null; return Row( children: [ Expanded( child: TextFormField( controller: _searchController, decoration: InputDecoration( hintText: 'Search by name, account, email, phone', prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); final userId = prefs.getString("userId") ?? ''; controller.searchTransactions( userId, status: controller.model.statusFilter, type: controller.model.typeFilter, ); }, ) : 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, ), ), ), textInputAction: TextInputAction.search, onFieldSubmitted: (value) { final userId = prefs.getString("userId") ?? ''; controller.searchTransactions( userId, search: value.isNotEmpty ? value : null, status: controller.model.statusFilter, type: controller.model.typeFilter, ); }, ), ), const SizedBox(width: 8), _buildIconButton( icon: Icons.filter_list, tooltip: 'Filter transactions', isActive: hasFilters, onPressed: _showFilterSheet, ), const SizedBox(width: 8), _buildIconButton( icon: controller.model.selectMode ? Icons.checklist : Icons.checklist_outlined, tooltip: 'Select transactions', isActive: controller.model.selectMode, onPressed: controller.toggleSelectMode, ), const SizedBox(width: 8), _buildIconButton( icon: Icons.refresh, tooltip: 'Refresh transactions', isActive: false, onPressed: () async { await setupData(); if (mounted) { AppSnackBar.show( context, 'Transactions refreshed', duration: const Duration(seconds: 1), ); } }, ), const SizedBox(width: 8), _buildIconButton( icon: Icons.add, tooltip: 'New payment', isActive: false, onPressed: () => context.push('/recipients'), ), ], ); } 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.transactions.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 transactions yet.', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( 'Tap + to make a new payment.', style: TextStyle(fontSize: 14, color: Colors.grey.shade600), ), ], ), ), ); } final hasFilters = controller.model.statusFilter != null || controller.model.typeFilter != null; return Column( children: [ if (controller.model.selectMode) Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Text( '${controller.model.selectedTransactionIds.length} selected', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey.shade600, ), ), ], ), ), if (hasFilters) Padding( padding: const EdgeInsets.only(bottom: 8), child: Wrap( spacing: 6, runSpacing: 6, children: [ if (controller.model.statusFilter != null) _activeFilterChip( 'Status: ${controller.model.statusFilter}', () { controller.model.statusFilter = null; final userId = prefs.getString("userId") ?? ''; controller.searchTransactions( userId, search: _searchController.text.isNotEmpty ? _searchController.text : null, type: controller.model.typeFilter, ); }, ), if (controller.model.typeFilter != null) _activeFilterChip('Type: ${controller.model.typeFilter}', () { controller.model.typeFilter = null; final userId = prefs.getString("userId") ?? ''; controller.searchTransactions( userId, search: _searchController.text.isNotEmpty ? _searchController.text : null, status: controller.model.statusFilter, ); }), ], ), ), ...controller.model.transactions.map( (e) => _buildTransactionItem(e, context), ), if (controller.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 _buildTransactionItem(Map e, BuildContext context) { final id = e['id'] as String? ?? ''; final isSelected = controller.model.selectedTransactionIds.contains(id); final inSelectMode = controller.model.selectMode; return Stack( children: [ TransactionItemWidget( transaction: e, enabled: controller.model.isLoading, onRepeat: inSelectMode ? null : () async { await _repeatTransaction(e); }, ), if (inSelectMode) Positioned.fill( child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () => controller.toggleTransactionSelection(id), child: Container( decoration: BoxDecoration( color: isSelected ? Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.08) : Colors.transparent, borderRadius: BorderRadius.circular(16), border: Border.all( color: isSelected ? Theme.of(context).colorScheme.primary : Colors.transparent, width: 2, ), ), child: Align( alignment: Alignment.topLeft, child: Padding( padding: const EdgeInsets.all(8), child: Icon( isSelected ? Icons.check_box : Icons.check_box_outline_blank, size: 22, color: isSelected ? Theme.of(context).colorScheme.primary : Colors.grey.shade500, ), ), ), ), ), ), ), ], ); } Widget _buildSelectionBottomBar() { final selectedCount = controller.model.selectedTransactionIds.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: () => controller.toggleSelectMode(), icon: const Icon(Icons.close, size: 20), label: const Text('Cancel'), ), const SizedBox(width: 4), 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), ), ], ), ), ), ); } }