import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:qpay/models/responsive_policy.dart'; import 'package:qpay/screens/gateway/gateway_controller.dart'; import 'package:qpay/screens/transaction_controller.dart'; import 'package:go_router/go_router.dart'; import 'package:qpay/screens/receipt/receipt_controller.dart'; import 'package:share_plus/share_plus.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:widgets_to_image/widgets_to_image.dart'; class ReceiptScreen extends StatefulWidget { const ReceiptScreen({super.key}); @override State createState() => _ReceiptScreenState(); } class _ReceiptScreenState extends State with TickerProviderStateMixin { late TransactionController transactionController; late GatewayController gatewayController; // Enhanced animation controllers late AnimationController _headerAnimController; late Animation _headerSlideAnimation; late Animation _headerFadeAnimation; late AnimationController _cardAnimController; late Animation _cardSlideAnimation; late Animation _cardFadeAnimation; late AnimationController _contentAnimController; late Animation _contentSlideAnimation; late Animation _contentFadeAnimation; int _selectedTabIndex = 0; late ReceiptController receiptController; final _formKey = GlobalKey(); final controller = WidgetsToImageController(); @override void initState() { super.initState(); transactionController = Provider.of( context, listen: false, ); receiptController = ReceiptController(context); gatewayController = GatewayController(context); receiptController.getReceiptData( transactionController.model.receiptData?["id"], ); // Header animation _headerAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _headerSlideAnimation = Tween(begin: const Offset(0, -0.08), end: Offset.zero).animate( CurvedAnimation( parent: _headerAnimController, curve: Curves.easeOutCubic, ), ); _headerFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _headerAnimController, curve: Curves.easeOut), ); // Card animation _cardAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 700), ); _cardSlideAnimation = Tween(begin: const Offset(0, 0.06), end: Offset.zero).animate( CurvedAnimation( parent: _cardAnimController, curve: Curves.easeOutCubic, ), ); _cardFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _cardAnimController, curve: Curves.easeOut), ); // Content animation _contentAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _contentSlideAnimation = Tween(begin: const Offset(0, 0.08), end: Offset.zero).animate( CurvedAnimation( parent: _contentAnimController, curve: Curves.easeOutCubic, ), ); _contentFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _contentAnimController, curve: Curves.easeOut), ); // Start staggered animations _headerAnimController.forward(); Future.delayed(const Duration(milliseconds: 150), () { if (mounted) _cardAnimController.forward(); }); Future.delayed(const Duration(milliseconds: 350), () { if (mounted) _contentAnimController.forward(); }); } Future _captureImage() async { final image = await controller.capture(); if (image != null) { final params = ShareParams( files: [ XFile.fromData(image.buffer.asUint8List(), mimeType: 'image/png'), ], fileNameOverrides: ['receipt.png'], ); SharePlus.instance.share(params); } } _retry() async { receiptController.setLoading(true); await receiptController.doIntegration( receiptController.model.receiptData?["id"], ); if (receiptController.model.status == "SUCCESS") { receiptController.getReceiptData( receiptController.model.receiptData?["id"], ); } receiptController.setLoading(false); } _check() async { receiptController.setLoading(true); await gatewayController.poll(receiptController.model.receiptData?["id"]); if (gatewayController.model.status == "SUCCESS") { receiptController.getReceiptData( transactionController.model.receiptData?["id"], ); if (transactionController.model.receiptData?["pollingStatus"]) { receiptController.setLoading(false); return; } await _retry(); } receiptController.setLoading(false); } @override void dispose() { _headerAnimController.dispose(); _cardAnimController.dispose(); _contentAnimController.dispose(); receiptController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; return Scaffold( backgroundColor: isDark ? const Color(0xFF0D0D0D) : const Color(0xFFF8F9FA), body: ListenableBuilder( listenable: receiptController, builder: (context, child) { return CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ // Modern app bar SliverAppBar( pinned: false, floating: true, stretch: true, stretchTriggerOffset: 80.0, expandedHeight: 60.0, collapsedHeight: 60.0, surfaceTintColor: Colors.transparent, backgroundColor: isDark ? const Color(0xFF0D0D0D) : const Color(0xFFF8F9FA), leading: context.canPop() ? Padding( padding: const EdgeInsets.only(left: 8), child: Container( margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues( alpha: 0.08, ), borderRadius: BorderRadius.circular(14), ), child: IconButton( onPressed: () => context.pop(), icon: const Icon(Icons.arrow_back_rounded), color: theme.colorScheme.primary, ), ), ) : const SizedBox.shrink(), title: Text( "Receipt", style: TextStyle( fontWeight: FontWeight.w700, fontSize: 20, color: isDark ? Colors.white : Colors.black87, ), ), centerTitle: true, ), // Main content SliverToBoxAdapter( child: WidgetsToImage( controller: controller, child: Container( color: isDark ? const Color(0xFF0D0D0D) : const Color(0xFFF8F9FA), child: Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 32), child: Form( key: _formKey, child: LayoutBuilder( builder: (context, constraints) { return Center( child: SizedBox( width: constraints.maxWidth > ResponsivePolicy.md ? ResponsivePolicy.md.toDouble() : constraints.maxWidth, child: Column( children: [ // Animated header section FadeTransition( opacity: _headerFadeAnimation, child: SlideTransition( position: _headerSlideAnimation, child: _buildReceiptHeader( theme, isDark, ), ), ), const SizedBox(height: 24), // Animated info card section FadeTransition( opacity: _cardFadeAnimation, child: SlideTransition( position: _cardSlideAnimation, child: _buildSegmentedTabBar( theme, isDark, ), ), ), const SizedBox(height: 20), // Animated content section FadeTransition( opacity: _contentFadeAnimation, child: SlideTransition( position: _contentSlideAnimation, child: _buildSelectedTabContent( theme, isDark, ), ), ), ], ), ), ); }, ), ), ), ), ), ), ], ); }, ), ); } // ────────────────────────────────────────────────────────────── // Status Badge Helper // ────────────────────────────────────────────────────────────── Widget _buildStatusBadge(String status, ThemeData theme) { final bool isSuccess = status == 'SUCCESS'; final bool isPending = status == 'PENDING'; final Color bgColor = isSuccess ? const Color(0xFFD1FAE5) : isPending ? const Color(0xFFFEF3C7) : const Color(0xFFFEE2E2); final Color textColor = isSuccess ? const Color(0xFF065F46) : isPending ? const Color(0xFF92400E) : const Color(0xFF991B1B); final Color dotColor = isSuccess ? const Color(0xFF10B981) : isPending ? const Color(0xFFF59E0B) : const Color(0xFFEF4444); final String label = isSuccess ? 'Success' : isPending ? 'Pending' : 'Failed'; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 8, height: 8, decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle), ), const SizedBox(width: 6), Text( label, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: textColor, ), ), ], ), ); } // ────────────────────────────────────────────────────────────── // Receipt Header Card // ────────────────────────────────────────────────────────────── Widget _buildReceiptHeader(ThemeData theme, bool isDark) { final status = transactionController.model.receiptData?["integrationStatus"]; final amount = receiptController.model.receiptData?["amount"]; final currency = receiptController.model.receiptData?["debitCurrency"] ?? ""; final billName = receiptController.model.receiptData?["billName"] ?? ""; final createdAt = receiptController.model.receiptData?["createdAt"]; final primaryColor = theme.colorScheme.primary; return Skeletonizer( enabled: receiptController.model.isLoading, child: Container( width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ primaryColor.withValues(alpha: 0.10), primaryColor.withValues(alpha: 0.04), primaryColor.withValues(alpha: 0.08), ], stops: const [0.0, 0.5, 1.0], ), borderRadius: BorderRadius.circular(24), border: Border.all( color: primaryColor.withValues(alpha: 0.12), width: 1, ), boxShadow: [ BoxShadow( color: primaryColor.withValues(alpha: 0.08), blurRadius: 20, offset: const Offset(0, 6), ), ], ), padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 24), child: Column( children: [ // Status badge if (status != null) ...[ _buildStatusBadge(status, theme), const SizedBox(height: 16), ], // Amount row Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( currency, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, color: isDark ? Colors.white70 : Colors.grey.shade600, ), ), const SizedBox(width: 4), Text( amount?.toStringAsFixed(2) ?? "", style: TextStyle( fontSize: 42, fontWeight: FontWeight.w700, color: isDark ? Colors.white : Colors.black87, height: 1.1, ), ), ], ), const SizedBox(height: 12), // Bill name (if available) if (billName.isNotEmpty) Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 6, ), decoration: BoxDecoration( color: primaryColor.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(20), ), child: Text( billName, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: primaryColor, ), ), ), const SizedBox(height: 6), // Date if (createdAt != null) Text( DateFormat.yMMMd().add_jm().format(DateTime.parse(createdAt)), style: TextStyle( fontSize: 13, fontWeight: FontWeight.w400, color: isDark ? Colors.white54 : Colors.grey.shade500, ), ), const SizedBox(height: 24), // Divider with dots pattern _buildDottedDivider(isDark), const SizedBox(height: 20), // Action buttons row _buildActionButtons(theme), ], ), ), ); } // ────────────────────────────────────────────────────────────── // Dotted Divider // ────────────────────────────────────────────────────────────── Widget _buildDottedDivider(bool isDark) { return Row( children: List.generate( 60, (index) => Expanded( child: Container( height: 1.5, color: index.isEven ? (isDark ? Colors.white24 : Colors.grey.shade300) : Colors.transparent, ), ), ), ); } // ────────────────────────────────────────────────────────────── // Action Buttons Row // ────────────────────────────────────────────────────────────── Widget _buildActionButtons(ThemeData theme) { final integrationStatus = receiptController.model.receiptData?["integrationStatus"]; final paymentStatus = receiptController.model.receiptData?["paymentStatus"]; return Wrap( spacing: 12, runSpacing: 8, alignment: WrapAlignment.center, children: [ _modernActionButton( icon: Icons.repeat_rounded, label: "Repeat", color: theme.colorScheme.primary, isLoading: receiptController.model.isLoading, onPressed: () => receiptController.repeatTransaction(context), ), _modernActionButton( icon: Icons.ios_share_rounded, label: "Share", color: const Color(0xFF6366F1), isLoading: false, onPressed: _captureImage, ), if (integrationStatus != "SUCCESS" && paymentStatus == "SUCCESS") ...[ _modernActionButton( icon: Icons.refresh_rounded, label: "Retry", color: const Color(0xFFF59E0B), isLoading: false, onPressed: _retry, ), ], if (receiptController.model.receiptData?["pollingStatus"] != "SUCCESS") ...[ _modernActionButton( icon: Icons.search_rounded, label: "Check", color: const Color(0xFF10B981), isLoading: false, onPressed: _check, ), ], ], ); } Widget _modernActionButton({ required IconData icon, required String label, required Color color, required bool isLoading, required VoidCallback onPressed, }) { return Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 52, height: 52, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ color.withValues(alpha: 0.15), color.withValues(alpha: 0.05), ], ), border: Border.all(color: color.withValues(alpha: 0.2), width: 1.5), boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: isLoading ? Center( child: SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation(color), ), ), ) : Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(26), onTap: onPressed, child: Center(child: Icon(icon, color: color, size: 22)), ), ), ), const SizedBox(height: 6), Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.grey.shade600, ), ), ], ); } // ────────────────────────────────────────────────────────────── // Segmented Tab Bar (Modern UI) // ────────────────────────────────────────────────────────────── Widget _buildSegmentedTabBar(ThemeData theme, bool isDark) { final segments = ["Provider Details", "Transaction Details"]; return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: isDark ? Colors.white.withValues(alpha: 0.06) : Colors.grey.shade100, borderRadius: BorderRadius.circular(16), ), child: Row( children: List.generate(segments.length, (index) { final isSelected = _selectedTabIndex == index; return Expanded( child: GestureDetector( onTap: () => setState(() => _selectedTabIndex = index), child: AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: isSelected ? theme.colorScheme.primary : Colors.transparent, borderRadius: BorderRadius.circular(12), boxShadow: isSelected ? [ BoxShadow( color: theme.colorScheme.primary.withValues( alpha: 0.3, ), blurRadius: 8, offset: const Offset(0, 2), ), ] : null, ), child: Center( child: Text( segments[index], style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : Colors.grey.shade600, ), ), ), ), ), ); }), ), ); } // ────────────────────────────────────────────────────────────── // Selected Tab Content with AnimatedSwitcher // ────────────────────────────────────────────────────────────── Widget _buildSelectedTabContent(ThemeData theme, bool isDark) { return AnimatedSwitcher( duration: const Duration(milliseconds: 300), switchInCurve: Curves.easeOutCubic, switchOutCurve: Curves.easeInCubic, transitionBuilder: (child, animation) { return FadeTransition( opacity: animation, child: SlideTransition( position: Tween( begin: const Offset(0, 0.04), end: Offset.zero, ).animate(animation), child: child, ), ); }, child: KeyedSubtree( key: ValueKey(_selectedTabIndex), child: _selectedTabIndex == 0 ? _buildProviderDetailsTab(theme, isDark) : _buildTransactionDetailsTab(theme, isDark), ), ); } // ────────────────────────────────────────────────────────────── // Provider Details Tab // ────────────────────────────────────────────────────────────── Widget _buildProviderDetailsTab(ThemeData theme, bool isDark) { final receiptData = receiptController.model.receiptData; final additionalData = receiptData?["additionalData"]; final items = <_DetailItem>[]; // Status items.add( _DetailItem( icon: Icons.circle_outlined, label: "Status", value: transactionController.model.receiptData?["integrationStatus"] ?? "", ), ); // Error message final errorMsg = transactionController.model.receiptData?["errorMessage"]; if (errorMsg != null && errorMsg != "") { items.add( _DetailItem( icon: Icons.warning_amber_rounded, label: "Message", value: errorMsg, isWarning: true, ), ); } // Additional data from provider if (additionalData != null) { for (final data in additionalData) { items.add( _DetailItem( icon: Icons.info_outline_rounded, label: data["name"] ?? "", value: data["value"] ?? "", ), ); } } // Standard fields items.add( _DetailItem( icon: Icons.tag_rounded, label: "Debit Reference", value: transactionController.model.receiptData?["debitRef"] ?? "", ), ); items.add( _DetailItem( icon: Icons.phone_iphone_rounded, label: "From", value: receiptData?["debitPhone"] ?? "", ), ); items.add( _DetailItem( icon: Icons.account_balance_wallet_rounded, label: "To", value: receiptData?["creditAccount"] ?? "", ), ); items.add( _DetailItem( icon: Icons.memory_rounded, label: "Processor", value: receiptData?["paymentProcessorName"] ?? "", ), ); if (receiptData?["billProductName"] != null) { items.add( _DetailItem( icon: Icons.category_rounded, label: "Bill Product", value: receiptData?["billProductName"] ?? "", ), ); } if (additionalData == null && items.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 40), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.inbox_outlined, size: 48, color: Colors.grey.shade400), const SizedBox(height: 12), Text( "No provider details found", style: TextStyle( fontSize: 15, color: Colors.grey.shade500, fontWeight: FontWeight.w500, ), ), ], ), ), ); } return _buildInfoCard( theme: theme, isDark: isDark, icon: Icons.business_rounded, title: "Provider Details", items: items, ); } // ────────────────────────────────────────────────────────────── // Transaction Details Tab // ────────────────────────────────────────────────────────────── Widget _buildTransactionDetailsTab(ThemeData theme, bool isDark) { final receiptData = transactionController.model.receiptData; final amount = receiptData?["amount"] ?? 0; final charge = receiptData?["charge"] ?? 0; final gatewayCharge = receiptData?["gatewayCharge"] ?? 0; final tax = receiptData?["tax"] ?? 0; final totalAmount = receiptData?["totalAmount"] ?? 0; final items = <_DetailItem>[ _DetailItem( icon: Icons.monetization_on_rounded, label: "Amount", value: amount.toStringAsFixed(2), trailing: const Icon( Icons.trending_up_rounded, size: 16, color: Colors.green, ), ), ]; if (charge != 0) { items.add( _DetailItem( icon: Icons.percent_rounded, label: "Our Charge", value: charge.toStringAsFixed(2), ), ); } if (gatewayCharge != 0) { items.add( _DetailItem( icon: Icons.account_balance_rounded, label: "Gateway Charge", value: gatewayCharge.toStringAsFixed(2), ), ); } if (tax != 0) { items.add( _DetailItem( icon: Icons.receipt_rounded, label: "Tax", value: tax.toStringAsFixed(2), ), ); } return _buildInfoCard( theme: theme, isDark: isDark, icon: Icons.receipt_long_rounded, title: "Transaction Details", items: items, totalAmount: totalAmount.toStringAsFixed(2), ); } // ────────────────────────────────────────────────────────────── // Modern Info Card // ────────────────────────────────────────────────────────────── Widget _buildInfoCard({ required ThemeData theme, required bool isDark, required IconData icon, required String title, required List<_DetailItem> items, String? totalAmount, }) { return Container( width: double.infinity, 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, ), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.04), blurRadius: 16, offset: const Offset(0, 4), ), BoxShadow( color: isDark ? Colors.black.withValues(alpha: 0.15) : Colors.black.withValues(alpha: 0.02), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Title row Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: 18, color: theme.colorScheme.primary), ), const SizedBox(width: 12), Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: isDark ? Colors.white : Colors.black87, letterSpacing: 0.5, ), ), ], ), const SizedBox(height: 16), // Divider Container( height: 1, color: isDark ? Colors.white.withValues(alpha: 0.06) : Colors.grey.shade200, ), const SizedBox(height: 16), // Detail items ...items.map( (item) => Padding( padding: const EdgeInsets.only(bottom: 14), child: _buildModernDetailRow( theme: theme, isDark: isDark, item: item, ), ), ), // Total amount divider & total if (totalAmount != null) ...[ Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Container( height: 1, decoration: BoxDecoration( gradient: LinearGradient( colors: [ theme.colorScheme.primary.withValues(alpha: 0.3), Colors.transparent, ], ), ), ), ), const SizedBox(height: 12), Row( children: [ Container( width: 28, height: 28, decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.summarize_rounded, size: 16, color: theme.colorScheme.primary, ), ), const SizedBox(width: 10), Text( "Total", style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87, ), ), const Spacer(), Text( totalAmount, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w800, color: theme.colorScheme.primary, ), ), ], ), ], ], ), ), ); } // ────────────────────────────────────────────────────────────── // Modern Detail Row // ────────────────────────────────────────────────────────────── Widget _buildModernDetailRow({ required ThemeData theme, required bool isDark, required _DetailItem item, }) { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( item.icon, size: 16, color: item.isWarning ? const Color(0xFFF59E0B) : isDark ? Colors.white38 : Colors.grey.shade500, ), const SizedBox(width: 10), Expanded( flex: 2, child: Text( item.label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: isDark ? Colors.white60 : Colors.grey.shade600, ), ), ), if (item.trailing != null) ...[ item.trailing!, const SizedBox(width: 6), ], Expanded( flex: 3, child: Text( item.value, style: TextStyle( fontSize: 14, fontWeight: item.isWarning ? FontWeight.w500 : FontWeight.w600, color: item.isWarning ? const Color(0xFF92400E) : isDark ? Colors.white : Colors.black87, ), textAlign: TextAlign.end, softWrap: true, ), ), ], ); } } // ────────────────────────────────────────────────────────────── // Data class for detail items // ────────────────────────────────────────────────────────────── class _DetailItem { final IconData icon; final String label; final String value; final bool isWarning; final Widget? trailing; _DetailItem({ required this.icon, required this.label, required this.value, this.isWarning = false, this.trailing, }); }