682 lines
22 KiB
Dart
682 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)),
|
|
child: Container(
|
|
width: 500.w,
|
|
constraints: BoxConstraints(maxHeight: 0.8.sh),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
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
|
|
Flexible(
|
|
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
|
|
Container(
|
|
padding: EdgeInsets.all(16.w),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(vertical: 12.h),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
),
|
|
),
|
|
child: Text(
|
|
'Close',
|
|
style: TextStyle(fontSize: 14.sp),
|
|
),
|
|
),
|
|
),
|
|
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: 12.h),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.play_arrow, size: 18.w),
|
|
SizedBox(width: 6.w),
|
|
Text(
|
|
'Start Draw',
|
|
style: TextStyle(fontSize: 14.sp),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (_isDrawComplete)
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: _saveDrawResult,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green.shade600,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(vertical: 12.h),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.save, size: 18.w),
|
|
SizedBox(width: 6.w),
|
|
Text(
|
|
'Save Result',
|
|
style: TextStyle(fontSize: 14.sp),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|