import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:qpay/auth_state.dart'; import 'package:qpay/models/responsive_policy.dart'; import 'package:qpay/screens/accounts/widgets/account_balance_widget.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/app_snack_bar.dart'; 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) { final isMobile = constraints.maxWidth < ResponsivePolicy.md; 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: [ if (isMobile) SliverAppBar( pinned: false, floating: true, stretch: true, surfaceTintColor: Colors.transparent, backgroundColor: isDark ? const Color(0xFF0D0D0D) : const Color(0xFFF8F9FA), title: Image( image: const AssetImage('assets/velocity.png'), width: 130, ), actions: _buildAppBarActions(theme, isDark), ), 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: [ if (!isMobile) ...[ _buildWebHeader(theme, isDark), const SizedBox(height: 16), ], 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, ), ), ), ], ); }, ), ), ), ), ), ], ), ); }, ), ); }, ); } // ────────────────────────────────────────────────────────────── // App Bar Actions (shared between mobile SliverAppBar and web header) // ────────────────────────────────────────────────────────────── List _buildAppBarActions(ThemeData theme, bool isDark) { return [ 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, ), ], ]; } // ────────────────────────────────────────────────────────────── // Web Header (replaces SliverAppBar on web/tablet) // ────────────────────────────────────────────────────────────── Widget _buildWebHeader(ThemeData theme, bool isDark) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Row( mainAxisSize: MainAxisSize.min, children: _buildAppBarActions(theme, isDark), ), ], ); } // ────────────────────────────────────────────────────────────── // 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), if (_isLoggedIn) const AccountBalanceWidget(), 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) { AppSnackBar.show( context, 'ZWG currency not supported yet', customContent: const Center( child: Text('ZWG currency not supported yet'), ), backgroundColor: theme.colorScheme.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), duration: const Duration(seconds: 3), ); } 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 features', style: TextStyle( fontSize: 14, color: isDark ? Colors.white : Colors.black87, ), ), ], ), ), 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; }); authState.refresh(); setupDataAndTransitions(); }, ), ], ); }, ); } }