chitfund/luckychit/lib/interfaces/manager/combined_draw_dialog.dart

692 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:math' as math;
import '../../core/services/chit_group_service.dart';
import '../../core/models/chit_group.dart';
import '../../shared/widgets/draw_animation_selector.dart';
import '../../shared/widgets/recording_overlay.dart';
import '../../core/services/screen_recording_service.dart';
import 'draw_animation_page.dart';
class CombinedDrawDialog extends StatefulWidget {
final ChitGroup group;
const CombinedDrawDialog({
super.key,
required this.group,
});
@override
State<CombinedDrawDialog> createState() => _CombinedDrawDialogState();
}
class _CombinedDrawDialogState extends State<CombinedDrawDialog>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _monthController = TextEditingController();
final _yearController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
final ScreenRecordingService _recordingService = Get.find<ScreenRecordingService>();
bool _isLoading = false;
bool _isDrawStarted = false;
bool _isDrawComplete = false;
Map<String, dynamic>? _winnerData;
String? _serverSeed;
int? _nonce;
List<Map<String, dynamic>> _eligibleMembers = [];
@override
void initState() {
super.initState();
_initializeForm();
_loadEligibleMembers();
_initializeAnimations();
}
@override
void dispose() {
_monthController.dispose();
_yearController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
void _initializeForm() {
final now = DateTime.now();
_monthController.text = now.month.toString();
_yearController.text = now.year.toString();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
_fadeController.forward();
_slideController.forward();
}
void _loadEligibleMembers() async {
// Load real members from the API
final chitGroupService = Get.find<ChitGroupService>();
// Load group members if not already loaded
await chitGroupService.loadGroupMembers(widget.group.id);
// Get active members who haven't won yet
final allMembers = chitGroupService.groupMembers;
final pastDraws = chitGroupService.monthlyDraws;
final wonMemberIds = pastDraws.map((d) => d.winnerId).toList();
final eligible = allMembers.where((member) {
return member.status.toLowerCase() == 'active' &&
!wonMemberIds.contains(member.userId);
}).toList();
setState(() {
_eligibleMembers = eligible.map((member) => {
'id': member.userId,
'name': member.user?.fullName ?? 'Unknown',
'mobile': member.user?.mobileNumber ?? '',
}).toList();
});
// Show message if no eligible members
if (_eligibleMembers.isEmpty) {
Get.snackbar(
'No Eligible Members',
'All active members have already won, or no members exist in this group.',
backgroundColor: Colors.orange.shade100,
colorText: Colors.orange.shade900,
);
}
}
void _startDraw() async {
if (!_formKey.currentState!.validate()) return;
// Generate seeds before navigation
_generateSeeds();
final month = int.parse(_monthController.text);
final year = int.parse(_yearController.text);
// Close this dialog and navigate to full-screen draw animation
Navigator.of(context).pop();
final result = await Get.to(
() => DrawAnimationPage(
group: widget.group,
month: month,
year: year,
serverSeed: _serverSeed!,
nonce: _nonce!,
eligibleMembers: _eligibleMembers,
),
transition: Transition.fadeIn,
duration: const Duration(milliseconds: 500),
);
// If draw was successful, reload draws in parent
if (result == true) {
final chitGroupService = Get.find<ChitGroupService>();
await chitGroupService.loadGroupMonthlyDraws(widget.group.id);
}
}
void _generateSeeds() {
// Generate server seed
final random = math.Random();
_serverSeed = List.generate(32, (index) => random.nextInt(256))
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join();
// Generate nonce
_nonce = random.nextInt(1000000);
}
void _onDrawComplete(String winnerId, Map<String, dynamic> winnerData) {
setState(() {
_isDrawComplete = true;
_winnerData = winnerData;
});
// Stop recording
_stopRecordingWithDetails();
}
void _startRecording() {
_recordingService.startRecording();
}
void _stopRecording() {
_recordingService.stopRecording();
}
void _stopRecordingWithDetails() {
final month = int.parse(_monthController.text);
final year = int.parse(_yearController.text);
_recordingService.saveRecordingWithDetails(
groupName: widget.group.name,
month: month.toString(),
year: year.toString(),
serverSeed: _serverSeed ?? '',
clientSeed: '',
nonce: _nonce ?? 0,
winnerName: _winnerData?['name'] ?? 'Unknown',
);
}
@override
Widget build(BuildContext context) {
return RecordingOverlay(
showRecordingIndicator: true,
child: Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)),
insetPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 40.h),
child: Container(
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
// Header
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.purple.shade600,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.r),
topRight: Radius.circular(16.r),
),
),
child: Row(
children: [
Icon(
Icons.casino,
color: Colors.white,
size: 24.w,
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Monthly Draw',
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
Text(
'${_monthController.text}/${_yearController.text}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12.sp,
),
),
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
),
// Content - Scrollable
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
children: [
if (!_isDrawStarted) _buildDrawForm(),
if (_isDrawStarted && !_isDrawComplete) _buildDrawInProgress(),
if (_isDrawComplete) _buildDrawResult(),
],
),
),
),
),
),
// Action Buttons - Always visible at bottom
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(color: Colors.grey.shade200, width: 1),
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(16.r),
bottomRight: Radius.circular(16.r),
),
),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
'Cancel',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
),
),
),
SizedBox(width: 12.w),
if (!_isDrawStarted)
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _startDraw,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.play_arrow, size: 20.w),
SizedBox(width: 8.w),
Text(
'Start Draw',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
),
],
),
),
),
if (_isDrawComplete)
Expanded(
child: ElevatedButton(
onPressed: _saveDrawResult,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.save, size: 20.w),
SizedBox(width: 8.w),
Text(
'Save Result',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
),
],
),
),
),
],
),
),
],
),
),
),
);
}
Widget _buildDrawForm() {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Draw Details
Text(
'Draw Details',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 16.h),
Row(
children: [
Expanded(
child: TextFormField(
controller: _monthController,
decoration: InputDecoration(
labelText: 'Month *',
hintText: 'e.g., 12',
prefixIcon: const Icon(Icons.calendar_month),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Month is required';
}
final month = int.tryParse(value);
if (month == null || month < 1 || month > 12) {
return 'Please enter a valid month (1-12)';
}
return null;
},
),
),
SizedBox(width: 16.w),
Expanded(
child: TextFormField(
controller: _yearController,
decoration: InputDecoration(
labelText: 'Year *',
hintText: 'e.g., 2024',
prefixIcon: const Icon(Icons.calendar_today),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Year is required';
}
final year = int.tryParse(value);
if (year == null || year < 2020 || year > 2030) {
return 'Please enter a valid year';
}
return null;
},
),
),
],
),
SizedBox(height: 16.h),
// Quick Info
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.grey.shade600, size: 16.w),
SizedBox(width: 8.w),
Expanded(
child: Text(
'Draw will use provably fair system with animated selection',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey.shade700,
),
),
),
],
),
),
SizedBox(height: 16.h),
// Eligible Members
Text(
'Eligible Members (${_eligibleMembers.length})',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 12.h),
Container(
height: 150.h,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8.r),
),
child: ListView.builder(
itemCount: _eligibleMembers.length,
itemBuilder: (context, index) {
final member = _eligibleMembers[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.purple.shade100,
child: Text(
(member['name']?.isNotEmpty == true ? member['name'].substring(0, 1) : 'M').toUpperCase(),
style: TextStyle(
color: Colors.purple.shade700,
fontWeight: FontWeight.w600,
),
),
),
title: Text(
member['name'],
style: TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(member['mobile']),
trailing: Icon(
Icons.check_circle,
color: Colors.green.shade600,
size: 20.w,
),
);
},
),
),
],
),
);
}
Widget _buildDrawInProgress() {
return Column(
children: [
Text(
'Choose Your Draw Animation',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: Colors.purple.shade700,
),
),
SizedBox(height: 16.h),
Text(
'Select the animation type that works best for your group size',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
height: 1.4,
),
),
SizedBox(height: 24.h),
Center(
child: DrawAnimationSelector(
members: _eligibleMembers,
onDrawComplete: (winnerId) {
final winner = _eligibleMembers.firstWhere((m) => m['id'] == winnerId);
_onDrawComplete(winnerId, winner);
},
serverSeed: _serverSeed,
clientSeed: null,
nonce: _nonce,
animationDuration: const Duration(seconds: 4),
),
),
],
);
}
Widget _buildDrawResult() {
return Column(
children: [
// Winner Announcement
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.green.shade200),
),
child: Column(
children: [
Icon(
Icons.emoji_events,
color: Colors.amber.shade600,
size: 48.w,
),
SizedBox(height: 16.h),
Text(
'Winner Selected!',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: Colors.green.shade800,
),
),
SizedBox(height: 8.h),
Text(
_winnerData?['name'] ?? 'Unknown',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.green.shade700,
),
),
SizedBox(height: 4.h),
Text(
_winnerData?['mobile'] ?? '',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
),
),
],
),
),
SizedBox(height: 16.h),
// Quick Info
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
children: [
Icon(Icons.security, color: Colors.blue.shade600, size: 16.w),
SizedBox(width: 8.w),
Expanded(
child: Text(
'Draw completed using provably fair system. Recording saved.',
style: TextStyle(
fontSize: 12.sp,
color: Colors.blue.shade700,
),
),
),
],
),
),
],
);
}
Future<void> _saveDrawResult() async {
setState(() => _isLoading = true);
try {
final month = int.parse(_monthController.text);
final year = int.parse(_yearController.text);
final success = await ChitGroupService.to.createMonthlyDraw(
widget.group.id,
month,
year,
clientSeed: null,
);
if (success) {
Navigator.of(context).pop();
Get.snackbar(
'Success',
'Monthly draw completed successfully!',
backgroundColor: Colors.green.shade600,
colorText: Colors.white,
snackPosition: SnackPosition.TOP,
);
} else {
Get.snackbar(
'Error',
'Failed to save draw result. Please try again.',
backgroundColor: Colors.red.shade600,
colorText: Colors.white,
snackPosition: SnackPosition.TOP,
);
}
} catch (e) {
Get.snackbar(
'Error',
'An error occurred: $e',
backgroundColor: Colors.red.shade600,
colorText: Colors.white,
snackPosition: SnackPosition.TOP,
);
} finally {
setState(() => _isLoading = false);
}
}
}