import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../core/services/chit_group_service.dart'; import '../../core/models/chit_group.dart'; import '../../core/themes/draw_slot_theme.dart'; import '../../shared/widgets/draw_animation_selector.dart'; import '../../core/utils/whatsapp_util.dart'; class DrawAnimationPage extends StatefulWidget { final ChitGroup group; final int month; final int year; final String serverSeed; final int nonce; final List> eligibleMembers; const DrawAnimationPage({ super.key, required this.group, required this.month, required this.year, required this.serverSeed, required this.nonce, required this.eligibleMembers, }); @override State createState() => _DrawAnimationPageState(); } class _DrawAnimationPageState extends State with SingleTickerProviderStateMixin { late AnimationController _fadeController; late Animation _fadeAnimation; bool _isComplete = false; /// True after the draw is successfully persisted (safe to leave without prompt). bool _drawRecorded = false; /// Single client seed for the whole page so animation + API save use the same value. late final String _animationClientSeed; void _exitDrawScreen({required bool recorded}) { if (!mounted) return; Navigator.of(context).pop(recorded); } /// System back / header โ€” always offers a way out. Future _handleLeaveIntent() async { if (!_isComplete) { final cancel = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Cancel draw?'), content: const Text( 'The draw is still in progress. Stop and go back?', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Keep drawing'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom( backgroundColor: Colors.red.shade700, foregroundColor: Colors.white, ), child: const Text('Stop'), ), ], ), ); if (cancel == true && mounted) { _exitDrawScreen(recorded: false); } return; } if (_drawRecorded) { _exitDrawScreen(recorded: true); return; } final leave = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Leave without saving?'), content: const Text( 'This winner is not recorded yet. If you leave now, you will need to run the draw again.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Stay'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom( backgroundColor: Colors.red.shade700, foregroundColor: Colors.white, ), child: const Text('Leave anyway'), ), ], ), ); if (leave == true && mounted) { _exitDrawScreen(recorded: false); } } @override void initState() { super.initState(); _animationClientSeed = 'DRAW_${DateTime.now().millisecondsSinceEpoch}'; _fadeController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _fadeController, curve: Curves.easeInOut, )); _fadeController.forward(); } @override void dispose() { _fadeController.dispose(); super.dispose(); } void _onDrawComplete(String winnerId) async { if (_isComplete) return; setState(() { _isComplete = true; }); // Let the slot settle on the winner before the confirmation sheet await Future.delayed(const Duration(milliseconds: 900)); // Find winner (avoid firstWhere/orElse RTI mismatch Map vs dynamic on web) Map winner = {'name': 'Unknown', 'mobile': ''}; for (final raw in widget.eligibleMembers) { final m = Map.from(raw); if ('${m['id']}' == '$winnerId') { winner = m; break; } } // Get the bid amount for this month from financial data final chitGroupService = Get.find(); await chitGroupService.loadGroupFinancialData(widget.group.id); // Find the financial data for this specific month final monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; final monthKey = '${monthNames[widget.month - 1]}-${widget.year.toString().substring(2)}'; final financialEntry = chitGroupService.financialData.firstWhereOrNull( (entry) => entry.monthYear == monthKey, ); final bidAmount = financialEntry?.bidAmount ?? widget.group.monthlyInstallment; // Show confirmation dialog with winner and bid amount final shouldSave = await _showWinnerConfirmation(winner, bidAmount); if (shouldSave == true) { await _saveDrawResult(winnerId, bidAmount); } else { _exitDrawScreen(recorded: false); } } Future _showWinnerConfirmation(Map winner, double bidAmount) async { await HapticFeedback.mediumImpact(); final monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; final periodLabel = '${monthNames[widget.month - 1]} ${widget.year} ยท ${widget.group.name}'; return showDialog( context: context, barrierDismissible: false, builder: (dialogContext) => TweenAnimationBuilder( tween: Tween(begin: 0.88, end: 1.0), duration: const Duration(milliseconds: 420), curve: Curves.easeOutCubic, builder: (context, scale, child) => Transform.scale(scale: scale, child: child), child: AlertDialog( insetPadding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 24.h), backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20.r), ), title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: EdgeInsets.all(10.w), decoration: BoxDecoration( color: Colors.amber.shade50, shape: BoxShape.circle, ), child: Icon( Icons.emoji_events_rounded, color: Colors.amber.shade800, size: 28.w, ), ), SizedBox(width: 12.w), Expanded( child: Text( 'Winner', style: TextStyle( fontSize: 22.sp, fontWeight: FontWeight.w800, color: Colors.grey.shade900, letterSpacing: -0.5, ), ), ), IconButton( tooltip: 'Close', onPressed: () => Navigator.pop(dialogContext, false), icon: Icon(Icons.close, color: Colors.grey.shade700), ), ], ), SizedBox(height: 8.h), Text( periodLabel, style: TextStyle( fontSize: 12.sp, fontWeight: FontWeight.w500, color: Colors.grey.shade600, height: 1.3, ), ), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 22.h), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.green.shade50, Colors.teal.shade50.withOpacity(0.85), ], ), borderRadius: BorderRadius.circular(16.r), border: Border.all(color: Colors.green.shade200, width: 1.5), ), child: Column( children: [ Text( winner['name']?.toString() ?? 'Unknown', style: TextStyle( fontSize: 26.sp, fontWeight: FontWeight.w800, color: Colors.green.shade900, height: 1.15, ), textAlign: TextAlign.center, ), if ((winner['mobile']?.toString() ?? '').isNotEmpty) ...[ SizedBox(height: 10.h), Text( winner['mobile']?.toString() ?? '', style: TextStyle( fontSize: 15.sp, color: Colors.grey.shade700, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), ], SizedBox(height: 18.h), Container( height: 1, color: Colors.green.shade200.withOpacity(0.9), ), SizedBox(height: 14.h), Text( 'Prize amount', style: TextStyle( fontSize: 13.sp, color: Colors.grey.shade600, fontWeight: FontWeight.w600, letterSpacing: 0.3, ), ), SizedBox(height: 6.h), Text( _formatIndianCurrency(bidAmount), style: TextStyle( fontSize: 30.sp, fontWeight: FontWeight.w800, color: Colors.green.shade800, ), ), ], ), ), SizedBox(height: 16.h), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => _shareViaWhatsApp(winner, bidAmount), icon: Icon(Icons.send_rounded, size: 18.w, color: const Color(0xFF25D366)), label: Text( 'Share', style: TextStyle( fontSize: 13.sp, color: const Color(0xFF25D366)), ), style: OutlinedButton.styleFrom( padding: EdgeInsets.symmetric(vertical: 12.h), side: const BorderSide(color: Color(0xFF25D366)), ), ), ), SizedBox(width: 8.w), Expanded( child: OutlinedButton.icon( onPressed: () { Get.snackbar( 'Screenshot', 'Capture this screen if you want a local copy.', backgroundColor: Colors.blue.shade700, colorText: Colors.white, duration: const Duration(seconds: 3), ); }, icon: Icon(Icons.photo_camera_outlined, size: 18.w, color: Colors.blue.shade700), label: Text( 'Capture', style: TextStyle( fontSize: 13.sp, color: Colors.blue.shade700), ), style: OutlinedButton.styleFrom( padding: EdgeInsets.symmetric(vertical: 12.h), side: BorderSide(color: Colors.blue.shade700), ), ), ), ], ), SizedBox(height: 12.h), Text( 'Save to record this result and get a shareable public link.', style: TextStyle( fontSize: 14.sp, color: Colors.grey.shade700, fontWeight: FontWeight.w500, height: 1.35, ), textAlign: TextAlign.center, ), ], ), ), actionsPadding: EdgeInsets.fromLTRB(20.w, 0, 20.w, 16.h), actions: [ Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.pop(dialogContext, false), style: OutlinedButton.styleFrom( padding: EdgeInsets.symmetric(vertical: 14.h), side: BorderSide(color: Colors.grey.shade400), ), child: Text( 'Discard', style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.w600, color: Colors.grey.shade800, ), ), ), ), SizedBox(width: 10.w), Expanded( flex: 2, child: FilledButton.icon( onPressed: () => Navigator.pop(dialogContext, true), style: FilledButton.styleFrom( backgroundColor: Colors.green.shade700, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 14.h), ), icon: Icon(Icons.check_circle_outline, size: 20.w), label: Text( 'Save result', style: TextStyle( fontSize: 15.sp, fontWeight: FontWeight.w700, ), ), ), ), ], ), ], ), ), ); } void _shareViaWhatsApp(Map winner, double bidAmount) { final monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; final monthName = monthNames[widget.month - 1]; final message = ''' ๐ŸŽ‰ *${widget.group.name}* ๐ŸŽ‰ ๐Ÿ“… *Monthly Draw Result* Month: $monthName ${widget.year} ๐Ÿ† *WINNER* ${winner['name']} ${winner['mobile']} ๐Ÿ’ฐ *Prize Amount* ${_formatIndianCurrency(bidAmount)} โœจ This draw was conducted using our provably fair system for complete transparency. _Congratulations to the winner!_ '''; // Share to all members or specific number WhatsAppUtil.shareText(message); } Future _saveDrawResult(String winnerId, double bidAmount) async { final chitGroupService = Get.find(); try { // Show loading Get.dialog( PopScope( canPop: false, child: Center( child: Container( padding: EdgeInsets.all(32.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16.r), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 16.h), Text( 'Saving draw result...', style: TextStyle(fontSize: 16.sp), ), ], ), ), ), ), barrierDismissible: false, ); final created = await chitGroupService.createMonthlyDraw( widget.group.id, widget.month, widget.year, clientSeed: _animationClientSeed, prizeAmount: bidAmount, animationWinnerUserId: winnerId, serverSeed: widget.serverSeed, nonce: widget.nonce, ); if (!mounted) return; Get.back(); if (created == null) { Get.snackbar( 'Error', 'Failed to save draw result', backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 4), snackPosition: SnackPosition.TOP, ); return; } setState(() { _drawRecorded = true; }); final drawId = created['id']?.toString(); if (drawId != null) { final publicUrl = await WhatsAppUtil.getDrawPublicShareUrl(drawId); if (mounted && publicUrl != null && publicUrl.isNotEmpty) { await _showPublicResultLinkDialog(publicUrl); } else if (mounted && (publicUrl == null || publicUrl.isEmpty)) { Get.snackbar( 'Draw saved', 'Public link unavailable โ€” set PUBLIC_BASE_URL on the server to share results.', backgroundColor: Colors.orange.shade700, colorText: Colors.white, duration: const Duration(seconds: 5), snackPosition: SnackPosition.TOP, ); } } if (!mounted) return; _exitDrawScreen(recorded: true); Get.snackbar( 'Draw Saved! ๐ŸŽ‰', 'Winner has been recorded. Share the public link so anyone can view results.', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 3), snackPosition: SnackPosition.TOP, ); } catch (e) { if (mounted) { try { Get.back(); } catch (_) {} } if (mounted) { Get.snackbar( 'Error', 'Failed to save draw result: ${e.toString()}', backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 4), ); } } } Future _showPublicResultLinkDialog(String url) async { await showDialog( context: context, barrierDismissible: true, builder: (ctx) => AlertDialog( title: Row( children: [ Expanded(child: const Text('Public result link')), IconButton( tooltip: 'Close', onPressed: () => Navigator.of(ctx).pop(), icon: const Icon(Icons.close), ), ], ), content: SingleChildScrollView( child: SelectableText( url, style: TextStyle(fontSize: 13.sp), ), ), actions: [ TextButton( onPressed: () async { await Clipboard.setData(ClipboardData(text: url)); if (ctx.mounted) { Get.snackbar( 'Copied', 'Link copied โ€” anyone can open it in a browser', snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 3), ); } }, child: const Text('Copy only'), ), FilledButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Done'), ), ], ), ); } String _formatIndianCurrency(double amount) { int intAmount = amount.round(); String amountStr = intAmount.toString(); String formatted = ''; if (amountStr.length <= 3) { formatted = amountStr; } else { int remaining = amountStr.length; int start = 0; if (remaining > 3) { formatted = amountStr.substring(amountStr.length - 3); remaining -= 3; start = amountStr.length - 3; } else { formatted = amountStr; remaining = 0; } while (remaining > 0) { int groupSize = remaining >= 2 ? 2 : remaining; int groupStart = start - groupSize; String group = amountStr.substring(groupStart, start); formatted = group + ',' + formatted; start = groupStart; remaining -= groupSize; } } return 'โ‚น$formatted'; } @override Widget build(BuildContext context) { const monthLabels = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; final drawPeriodShort = '${monthLabels[widget.month - 1]} ${widget.year}'; final theme = Theme.of(context); final d = DrawSlotTheming(theme.colorScheme, theme.brightness); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (didPop || !mounted) return; await _handleLeaveIntent(); }, child: Scaffold( backgroundColor: Colors.transparent, body: SafeArea( child: FadeTransition( opacity: _fadeAnimation, child: Container( width: double.infinity, height: double.infinity, decoration: BoxDecoration( gradient: d.pageBackdrop, ), child: Column( children: [ // Header Padding( padding: EdgeInsets.fromLTRB(8.w, 12.h, 12.w, 8.h), child: Column( children: [ Row( children: [ IconButton( tooltip: _drawRecorded ? 'Done' : (_isComplete ? 'Close' : 'Cancel draw'), icon: Icon( _drawRecorded ? Icons.check_circle_outline_rounded : Icons.close_rounded, color: d.pageOnText, size: 28.w, ), onPressed: () => _handleLeaveIntent(), ), Expanded( child: Column( children: [ Text( widget.group.name, style: theme.textTheme.headlineSmall?.copyWith( color: d.pageOnText, fontWeight: FontWeight.w800, letterSpacing: -0.3, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), SizedBox(height: 10.h), Container( padding: EdgeInsets.symmetric( horizontal: 14.w, vertical: 8.h, ), decoration: BoxDecoration( color: d.chipFill, borderRadius: BorderRadius.circular(20.r), border: Border.all(color: d.chipBorder), ), child: Text( drawPeriodShort, style: theme.textTheme.labelLarge?.copyWith( color: d.pageOnText, fontWeight: FontWeight.w600, letterSpacing: 0.2, ), ), ), ], ), ), SizedBox(width: 48.w), ], ), ], ), ), // Animation Area - Scrollable Expanded( child: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h), child: DrawAnimationSelector( members: widget.eligibleMembers, onDrawComplete: _onDrawComplete, serverSeed: widget.serverSeed, clientSeed: _animationClientSeed, nonce: widget.nonce, animationDuration: const Duration(seconds: 8), allowReplay: !_isComplete, ), ), ), // Footer Padding( padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h), child: Column( children: [ Container( padding: EdgeInsets.symmetric( horizontal: 12.w, vertical: 14.h, ), decoration: BoxDecoration( color: d.chipFill, borderRadius: BorderRadius.circular(18.r), border: Border.all(color: d.chipBorder), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildInfoItem( d, 'In this draw', '${widget.eligibleMembers.length}', Icons.how_to_reg_rounded, ), Container( width: 1, height: 36.h, color: d.chipBorder.withOpacity(0.6), ), _buildInfoItem( d, 'Group size', '${widget.group.maxMembers}', Icons.groups_rounded, ), ], ), ), SizedBox(height: 14.h), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.verified_user_rounded, size: 16.w, color: theme.colorScheme.tertiary, ), SizedBox(width: 8.w), Text( 'Provably fair ยท transparent draw', style: theme.textTheme.bodyMedium?.copyWith( color: d.pageMuted, fontWeight: FontWeight.w600, ), ), ], ), ], ), ), ], ), ), ), ), ), ); } Widget _buildInfoItem( DrawSlotTheming d, String label, String value, IconData icon, ) { return Column( children: [ Icon( icon, color: d.pageMuted, size: 22.w, ), SizedBox(height: 6.h), Text( value, style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.w800, color: d.pageOnText, ), ), Text( label, style: TextStyle( fontSize: 11.sp, fontWeight: FontWeight.w600, color: d.pageMuted, letterSpacing: 0.2, ), ), ], ); } }