712 lines
24 KiB
Dart
712 lines
24 KiB
Dart
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 '../../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<Map<String, dynamic>> 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<DrawAnimationPage> createState() => _DrawAnimationPageState();
|
|
}
|
|
|
|
class _DrawAnimationPageState extends State<DrawAnimationPage>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _fadeController;
|
|
late Animation<double> _fadeAnimation;
|
|
bool _isComplete = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_fadeController = AnimationController(
|
|
duration: const Duration(milliseconds: 800),
|
|
vsync: this,
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(
|
|
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;
|
|
});
|
|
|
|
// Wait a moment to show the winner
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
|
|
// Find winner (avoid firstWhere/orElse RTI mismatch Map<String,String> vs dynamic on web)
|
|
Map<String, dynamic> winner = {'name': 'Unknown', 'mobile': ''};
|
|
for (final raw in widget.eligibleMembers) {
|
|
final m = Map<String, dynamic>.from(raw);
|
|
if (m['id'] == winnerId) {
|
|
winner = m;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Get the bid amount for this month from financial data
|
|
final chitGroupService = Get.find<ChitGroupService>();
|
|
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 {
|
|
// User cancelled, go back without saving
|
|
Get.back(result: false);
|
|
}
|
|
}
|
|
|
|
Future<bool?> _showWinnerConfirmation(Map<String, dynamic> winner, double bidAmount) async {
|
|
return showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
),
|
|
title: Row(
|
|
children: [
|
|
Icon(Icons.emoji_events, color: Colors.amber.shade600, size: 32.w),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: Text(
|
|
'Draw Winner!',
|
|
style: TextStyle(
|
|
fontSize: 22.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green.shade700,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(20.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade50,
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
border: Border.all(color: Colors.green.shade200, width: 2),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
winner['name'] ?? 'Unknown',
|
|
style: TextStyle(
|
|
fontSize: 24.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green.shade900,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
winner['mobile'] ?? '',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
SizedBox(height: 16.h),
|
|
Divider(color: Colors.green.shade200),
|
|
SizedBox(height: 16.h),
|
|
Text(
|
|
'Prize Amount',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
_formatIndianCurrency(bidAmount),
|
|
style: TextStyle(
|
|
fontSize: 32.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 16.h),
|
|
|
|
// Quick Actions - Share & Screenshot
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => _shareViaWhatsApp(winner, bidAmount),
|
|
icon: Icon(Icons.send_rounded, size: 18.w, color: Color(0xFF25D366)),
|
|
label: Text('Share', style: TextStyle(fontSize: 13.sp, color: Color(0xFF25D366))),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(vertical: 10.h),
|
|
side: BorderSide(color: Color(0xFF25D366)),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {
|
|
Get.snackbar(
|
|
'Screenshot Tip',
|
|
'Take a screenshot now to save this result!',
|
|
backgroundColor: Colors.blue,
|
|
colorText: Colors.white,
|
|
duration: Duration(seconds: 3),
|
|
);
|
|
},
|
|
icon: Icon(Icons.screenshot, size: 18.w, color: Colors.blue.shade600),
|
|
label: Text('Capture', style: TextStyle(fontSize: 13.sp, color: Colors.blue.shade600)),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(vertical: 10.h),
|
|
side: BorderSide(color: Colors.blue.shade600),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 20.h),
|
|
Text(
|
|
'Do you want to save this draw result?',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
color: Colors.grey.shade700,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
actionsAlignment: MainAxisAlignment.end,
|
|
actionsPadding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
|
|
actions: [
|
|
// Cancel button
|
|
OutlinedButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 14.h),
|
|
side: BorderSide(color: Colors.grey.shade400),
|
|
),
|
|
child: Text(
|
|
'Cancel',
|
|
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
// Save button
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green.shade600,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 14.h),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.save, size: 20.w),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
'Save Result',
|
|
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _shareViaWhatsApp(Map<String, dynamic> 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<void> _saveDrawResult(String winnerId, double bidAmount) async {
|
|
final chitGroupService = Get.find<ChitGroupService>();
|
|
|
|
try {
|
|
// Show loading
|
|
Get.dialog(
|
|
WillPopScope(
|
|
onWillPop: () async => 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: 'DRAW_${DateTime.now().millisecondsSinceEpoch}',
|
|
prizeAmount: bidAmount,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
Get.back();
|
|
|
|
if (created == null) {
|
|
Get.back(result: false);
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to save draw result',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 4),
|
|
snackPosition: SnackPosition.TOP,
|
|
);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
Get.back(result: 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) Get.back();
|
|
|
|
Get.back(result: false);
|
|
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to save draw result: ${e.toString()}',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 4),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _showPublicResultLinkDialog(String url) async {
|
|
await showDialog<void>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('Public result link'),
|
|
content: SelectableText(
|
|
url,
|
|
style: TextStyle(fontSize: 13.sp),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(),
|
|
child: const Text('Close'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () async {
|
|
await Clipboard.setData(ClipboardData(text: url));
|
|
if (ctx.mounted) Navigator.of(ctx).pop();
|
|
Get.snackbar(
|
|
'Copied',
|
|
'Link copied — anyone can open it in a browser',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
},
|
|
child: const Text('Copy link'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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) {
|
|
return WillPopScope(
|
|
onWillPop: () async {
|
|
// Prevent back button during animation
|
|
if (_isComplete) return true;
|
|
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Cancel Draw?'),
|
|
content: const Text('Are you sure you want to cancel the draw? This action cannot be undone.'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Continue Draw'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Cancel Draw'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
return result ?? false;
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: Colors.black,
|
|
body: SafeArea(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
Colors.purple.shade900,
|
|
Colors.blue.shade900,
|
|
Colors.indigo.shade900,
|
|
],
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Header
|
|
Padding(
|
|
padding: EdgeInsets.all(20.w),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
if (!_isComplete)
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.close,
|
|
color: Colors.white.withOpacity(0.8),
|
|
size: 28.w,
|
|
),
|
|
onPressed: () async {
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Cancel Draw?'),
|
|
content: const Text('Are you sure you want to cancel the draw?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Continue'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Cancel'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (result == true) {
|
|
Get.back(result: false);
|
|
}
|
|
},
|
|
)
|
|
else
|
|
SizedBox(width: 48.w),
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
widget.group.name,
|
|
style: TextStyle(
|
|
fontSize: 24.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 16.w,
|
|
vertical: 6.h,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(20.r),
|
|
border: Border.all(
|
|
color: Colors.white.withOpacity(0.3),
|
|
),
|
|
),
|
|
child: Text(
|
|
'Month ${widget.month}/${widget.year}',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
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: 'DRAW_${DateTime.now().millisecondsSinceEpoch}',
|
|
nonce: widget.nonce,
|
|
animationDuration: const Duration(seconds: 8),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Footer
|
|
Padding(
|
|
padding: EdgeInsets.all(20.w),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(16.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
border: Border.all(
|
|
color: Colors.white.withOpacity(0.2),
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_buildInfoItem(
|
|
'Eligible Members',
|
|
'${widget.eligibleMembers.length}',
|
|
Icons.people,
|
|
),
|
|
Container(
|
|
width: 1,
|
|
height: 30.h,
|
|
color: Colors.white.withOpacity(0.3),
|
|
),
|
|
_buildInfoItem(
|
|
'Total Members',
|
|
'${widget.group.maxMembers}',
|
|
Icons.group,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 12.h),
|
|
Text(
|
|
'🎲 Provably Fair Draw',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: Colors.white.withOpacity(0.7),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoItem(String label, String value, IconData icon) {
|
|
return Column(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: Colors.white.withOpacity(0.8),
|
|
size: 24.w,
|
|
),
|
|
SizedBox(height: 6.h),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 20.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: Colors.white.withOpacity(0.7),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|