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/home/home_controller.dart'; import 'package:qpay/screens/receipt/receipt_controller.dart'; import 'package:qpay/screens/transaction_controller.dart'; import 'package:qpay/screens/transactions/transaction_model.dart' as txn; import 'package:qpay/widgets/transaction_item_widget.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State with TickerProviderStateMixin { late HomeController homeController; late TransactionController transactionController; late SharedPreferences prefs; late final router = GoRouter.of(context); bool _isLoggedIn = false; bool _isZWGSelected = false; String initials = ''; late AnimationController _providersAnimController; late Animation _providersSlideAnimation; late Animation _providersFadeAnimation; late AnimationController _transactionsAnimController; late Animation _transactionsSlideAnimation; late Animation _transactionsFadeAnimation; // ── Vibrant category color map ────────────────────────────── static const Map _categoryColorMap = { 'ECONET': Color(0xFF1A73E8), // Deep green 'NETONE': Color.fromARGB(255, 230, 127, 0), // Bold blue 'TELECEL': Color(0xFF7B1FA2), // Vibrant purple 'ZESA': Color.fromARGB(255, 230, 58, 0), // Fiery orange }; @override void initState() { super.initState(); _providersAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _providersSlideAnimation = Tween(begin: const Offset(0, 0.06), end: Offset.zero).animate( CurvedAnimation( parent: _providersAnimController, curve: Curves.easeOutCubic, ), ); _providersFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _providersAnimController, curve: Curves.easeOut), ); _transactionsAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 700), ); _transactionsSlideAnimation = Tween(begin: const Offset(0, 0.06), end: Offset.zero).animate( CurvedAnimation( parent: _transactionsAnimController, curve: Curves.easeOutCubic, ), ); _transactionsFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _transactionsAnimController, curve: Curves.easeOut, ), ); setupDataAndTransitions(); Future.delayed(const Duration(milliseconds: 120), () { if (mounted) _providersAnimController.forward(); }); Future.delayed(const Duration(milliseconds: 280), () { if (mounted) _transactionsAnimController.forward(); }); } Future setupDataAndTransitions() async { router.routerDelegate.addListener(_onRouteChanged); transactionController = Provider.of( context, listen: false, ); transactionController.model = TransactionModel(); homeController = HomeController(context); await homeController.getAccounts(); await homeController.getCategories(); await homeController.getProviders(_isZWGSelected ? "ZWG" : "USD"); prefs = await SharedPreferences.getInstance(); if (prefs.getString("token") != null) { setState(() { _isLoggedIn = true; initials = prefs.getString("initials")!; }); } if (prefs.getString("userId") == null) { prefs.setString("userId", Uuid().v4()); } await homeController.getTransactions(prefs.getString("userId")!); } Future _onRefresh() async { await homeController.getAccounts(); await homeController.getCategories(); await homeController.getProviders(_isZWGSelected ? "ZWG" : "USD"); prefs = await SharedPreferences.getInstance(); if (prefs.getString("userId") != null) { await homeController.getTransactions(prefs.getString("userId")!); } } void _onRouteChanged() async { final location = router.routerDelegate.currentConfiguration.uri.toString(); if (location.startsWith('/home')) { prefs = await SharedPreferences.getInstance(); if (prefs.getString("userId") == null) { prefs.setString("userId", Uuid().v4()); } await homeController.getTransactions(prefs.getString("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'); } } @override void dispose() { _providersAnimController.dispose(); _transactionsAnimController.dispose(); homeController.dispose(); router.routerDelegate.removeListener(_onRouteChanged); super.dispose(); } /// Derives a vibrant accent colour from the provider's category. Color _resolveProviderColor(BillProvider provider) { // Try exact match on category final exact = _categoryColorMap[provider.category.toUpperCase()]; if (exact != null) return exact; // Try matching on label final labelMatch = _categoryColorMap[provider.label.toUpperCase()]; if (labelMatch != null) return labelMatch; // Try matching on provider name keywords final name = provider.name.toUpperCase(); for (final entry in _categoryColorMap.entries) { if (name.contains(entry.key)) return entry.value; } // Hash-based fallback – deterministic vibrant hue final hash = provider.clientId.hashCode; final hue = (hash % 360).toDouble(); return HSVColor.fromAHSV(1.0, hue, 0.75, 0.85).toColor(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return LayoutBuilder( builder: (context, constraints) { return Scaffold( backgroundColor: isDark ? const Color(0xFF0D0D0D) : const Color(0xFFF8F9FA), body: ListenableBuilder( listenable: homeController, builder: (context, child) { return RefreshIndicator( onRefresh: _onRefresh, child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ SliverAppBar( pinned: false, floating: true, stretch: true, surfaceTintColor: Colors.transparent, backgroundColor: isDark ? const Color(0xFF0D0D0D) : const Color(0xFFF8F9FA), title: constraints.maxWidth < ResponsivePolicy.md ? Image( image: const AssetImage('assets/velocity.png'), width: 130, ) : null, actions: [ if (_isLoggedIn) ...[ Container( width: 30, height: 30, margin: const EdgeInsets.only(right: 4), decoration: BoxDecoration( border: Border.all( color: isDark ? Colors.white.withValues(alpha: 0.12) : Colors.black87.withAlpha(20), ), shape: BoxShape.circle, gradient: LinearGradient( colors: [ theme.colorScheme.primary.withValues( alpha: 0.15, ), theme.colorScheme.tertiary.withValues( alpha: 0.05, ), ], stops: const [0.3, 0.7], begin: Alignment.topRight, end: Alignment.bottomLeft, ), ), child: Center( child: Text( initials, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black87, ), ), ), ), IconButton( icon: Icon( Icons.logout_rounded, color: isDark ? Colors.white54 : Colors.black54, size: 20, ), onPressed: _showLogoutDialog, ), ], ], ), SliverToBoxAdapter( child: Center( child: SizedBox( width: constraints.maxWidth > 600 ? 600 : constraints.maxWidth, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), child: LayoutBuilder( builder: (context, innerConstraints) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FadeTransition( opacity: _providersFadeAnimation, child: SlideTransition( position: _providersSlideAnimation, child: _buildProvidersSection( theme, isDark, innerConstraints, ), ), ), const SizedBox(height: 24), FadeTransition( opacity: _transactionsFadeAnimation, child: SlideTransition( position: _transactionsSlideAnimation, child: _buildTransactionsSection( theme, isDark, innerConstraints, ), ), ), ], ); }, ), ), ), ), ), ], ), ); }, ), ); }, ); } // ────────────────────────────────────────────────────────────── // Providers Section // ────────────────────────────────────────────────────────────── Widget _buildProvidersSection( ThemeData theme, bool isDark, BoxConstraints constraints, ) { final containerWidth = constraints.maxWidth.isFinite ? constraints.maxWidth : 340.0; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!_isLoggedIn) _buildLoginPrompt(theme, isDark), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildSectionLabel(theme, isDark, Icons.grid_view_rounded, "Pay"), Row( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E1E1E) : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: isDark ? Colors.white.withValues(alpha: 0.12) : Colors.black.withValues(alpha: 0.08), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( 'USD', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: !_isZWGSelected ? theme.colorScheme.primary : isDark ? Colors.white54 : Colors.black45, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: GestureDetector( onTap: () { setState(() { _isZWGSelected = !_isZWGSelected; }); if (_isZWGSelected) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Center( child: const Text( 'ZWG currency not supported yet', ), ), behavior: SnackBarBehavior.floating, backgroundColor: theme.colorScheme.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), duration: const Duration(seconds: 3), width: 300, ), ); } Future.delayed( const Duration(milliseconds: 1000), () { setState(() { _isZWGSelected = !_isZWGSelected; }); }, ); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 32, height: 18, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: _isZWGSelected ? theme.colorScheme.primary : Colors.grey.shade300, ), child: AnimatedAlign( duration: const Duration(milliseconds: 200), alignment: _isZWGSelected ? Alignment.centerRight : Alignment.centerLeft, child: Container( width: 14, height: 14, margin: const EdgeInsets.all(2), decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ), ), ), ), Text( 'ZWG', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _isZWGSelected ? theme.colorScheme.primary : isDark ? Colors.white54 : Colors.black45, ), ), ], ), ), const SizedBox(width: 4), IconButton( icon: Icon( Icons.refresh_rounded, color: isDark ? Colors.white54 : Colors.black54, size: 20, ), onPressed: _onRefresh, tooltip: 'Refresh', ), ], ), ], ), const SizedBox(height: 12), Skeletonizer( enabled: homeController.model.isLoading, child: Wrap( spacing: 12, runSpacing: 12, children: [ ...homeController.model.filterableProviders.map( (provider) => _buildProviderCard(provider, theme, isDark, containerWidth), ), ], ), ), ], ); } // ────────────────────────────────────────────────────────────── // Provider Card — flex-based 2-column grid // ────────────────────────────────────────────────────────────── Widget _buildProviderCard( BillProvider provider, ThemeData theme, bool isDark, double availableWidth, ) { final cardWidth = (availableWidth / 2) - 6; final accent = _resolveProviderColor(provider); return SizedBox( width: cardWidth, child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () { transactionController.clearFormData(); homeController.updateSelectedProvider(provider); transactionController.updateSelectedProvider(provider); context.push("/recipients"); }, child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E1E1E) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: accent.withValues(alpha: 0.15), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( width: 40, height: 40, padding: const EdgeInsets.all(1), decoration: BoxDecoration( color: accent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Image( image: AssetImage("assets/${provider.image}"), errorBuilder: (_, __, ___) => Icon( Icons.payment_rounded, size: 20, color: accent, ), ), ), Icon( Icons.chevron_right_rounded, size: 20, color: accent.withValues(alpha: 0.55), ), ], ), const SizedBox(height: 12), Text( provider.name, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: isDark ? Colors.white : Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( provider.description, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w400, color: isDark ? Colors.white54 : Colors.grey.shade500, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ), ), ); } // ────────────────────────────────────────────────────────────── // Transactions Section // ────────────────────────────────────────────────────────────── Widget _buildTransactionsSection( ThemeData theme, bool isDark, BoxConstraints constraints, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionLabel( theme, isDark, Icons.history_rounded, "Recent Activity", ), const SizedBox(height: 12), if (homeController.model.transactions.isEmpty) _buildEmptyTransactions(isDark), if (homeController.model.transactions.isNotEmpty) ...[ ...homeController.model.transactions.map( (e) => TransactionItemWidget( transaction: e, enabled: homeController.model.isLoading, onRepeat: () => _repeatTransaction(e), ), ), const SizedBox(height: 8), if (homeController.model.transactions.length > 2) Center( child: OutlinedButton.icon( icon: const Icon(Icons.arrow_forward_rounded, size: 16), label: const Text('View All'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), side: BorderSide( color: theme.colorScheme.primary.withValues(alpha: 0.3), ), ), onPressed: () { context.go('/history'); }, ), ), ], ], ); } // ────────────────────────────────────────────────────────────── // Empty Transactions Placeholder // ────────────────────────────────────────────────────────────── Widget _buildEmptyTransactions(bool isDark) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), decoration: BoxDecoration( color: isDark ? const Color(0xFF1A1A2E) : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: isDark ? Colors.white.withValues(alpha: 0.06) : Colors.grey.shade200, width: 1, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.receipt_long_outlined, size: 48, color: isDark ? Colors.white24 : Colors.grey.shade300, ), const SizedBox(height: 12), Text( 'No transactions yet', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: isDark ? Colors.white54 : Colors.grey.shade500, ), ), const SizedBox(height: 4), Text( 'Make a payment to see your recent activity.', style: TextStyle( fontSize: 13, color: isDark ? Colors.white38 : Colors.grey.shade400, ), ), ], ), ); } // ────────────────────────────────────────────────────────────── // Section Label // ────────────────────────────────────────────────────────────── Widget _buildSectionLabel( ThemeData theme, bool isDark, IconData icon, String label, ) { return Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: 16, color: theme.colorScheme.primary), ), const SizedBox(width: 10), Text( label, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: isDark ? Colors.white : Colors.black87, letterSpacing: 0.3, ), ), ], ); } // ────────────────────────────────────────────────────────────── // Login Prompt // ────────────────────────────────────────────────────────────── Widget _buildLoginPrompt(ThemeData theme, bool isDark) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ theme.colorScheme.primary.withValues(alpha: 0.12), theme.colorScheme.primary.withValues(alpha: 0.04), ], ), borderRadius: BorderRadius.circular(16), border: Border.all( color: theme.colorScheme.primary.withValues(alpha: 0.15), width: 1, ), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Register or Login for more', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87, ), ), const SizedBox(height: 2), InkWell( onTap: () => _showFeaturesPopup(context), borderRadius: BorderRadius.circular(4), child: Text( 'features', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, decoration: TextDecoration.underline, decorationColor: theme.colorScheme.primary, color: theme.colorScheme.primary, ), ), ), ], ), ), ElevatedButton( onPressed: () { context.push('/onboarding/landing'); }, style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 0, ), child: const Text( 'Get Started', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), ), ), ], ), ); } // ────────────────────────────────────────────────────────────── // Logout Dialog // ────────────────────────────────────────────────────────────── void _showLogoutDialog() { showDialog( context: context, builder: (BuildContext dialogContext) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: const Text('Confirm Logout'), content: const Text('Are you sure you want to log out?'), actions: [ TextButton( child: const Text('Cancel'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), TextButton( child: const Text('Logout'), onPressed: () { Navigator.of(dialogContext).pop(); prefs.clear(); setState(() { _isLoggedIn = false; }); setupDataAndTransitions(); }, ), ], ); }, ); } // ────────────────────────────────────────────────────────────── // Features Popup // ────────────────────────────────────────────────────────────── void _showFeaturesPopup(BuildContext context) { showDialog( context: context, builder: (BuildContext context) { return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), elevation: 0, backgroundColor: Colors.transparent, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 600), child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( shape: BoxShape.rectangle, color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: const Image( image: AssetImage("assets/favicon.png"), width: 45, ), ), const SizedBox(height: 16), Text( 'Unlock Additional Features', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'Register or login to access these great features', style: TextStyle( fontSize: 14, color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.7), ), textAlign: TextAlign.center, ), const SizedBox(height: 24), _buildFeatureItem( Icons.account_balance_wallet, 'Dual Currency Wallets', 'USD and ZWG wallets for prepayments that can be utilized later', ), const SizedBox(height: 16), _buildFeatureItem( Icons.history, 'Transaction History', 'All transactions are saved for reference from any device', ), const SizedBox(height: 16), _buildFeatureItem( Icons.people, 'Recipient Management', 'Save and manage recipient details for future use', ), const SizedBox(height: 16), _buildFeatureItem( Icons.schedule, 'Smart Payments', 'Schedule payments and split bills with friends', ), const SizedBox(height: 24), Row( children: [ Expanded( child: TextButton( onPressed: () => Navigator.of(context).pop(), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text( 'Maybe Later', style: TextStyle( color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.6), fontSize: 16, ), ), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( onPressed: () { Navigator.of(context).pop(); context.push('/onboarding/landing'); }, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), backgroundColor: Theme.of( context, ).colorScheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text( 'Get Started', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ], ), ], ), ), ), ); }, ); } Widget _buildFeatureItem(IconData icon, String title, String description) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( icon, size: 20, color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface, ), ), const SizedBox(height: 8), Text( description, style: TextStyle( fontSize: 14, color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.7), height: 1.4, ), ), ], ), ), ], ); } }