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

815 lines
27 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
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/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;
ScreenRecordingService? _recordingService;
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();
_initializeAnimations();
// Only initialize recording service on mobile
if (!kIsWeb) {
try {
_recordingService = Get.find<ScreenRecordingService>();
} catch (e) {
// Service not available, recording will be disabled
_recordingService = null;
}
}
// Load eligible members after frame to avoid setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadEligibleMembers();
});
}
@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 {
setState(() {
_isLoading = true;
});
try {
// 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<Map<String, dynamic>>(
(member) => <String, dynamic>{
'id': member.userId,
'name': member.user?.fullName ?? 'Unknown',
'mobile': member.user?.mobileNumber ?? '',
},
)
.toList();
_isLoading = false;
});
// 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,
);
}
} catch (e) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Error',
'Failed to load members: ${e.toString()}',
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
void _startDraw() async {
if (!_formKey.currentState!.validate()) return;
// Check if eligible members are loaded
if (_eligibleMembers.isEmpty) {
Get.snackbar(
'No Eligible Members',
'Please wait for members to load, or all members have already won.',
backgroundColor: Colors.orange,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
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() {
if (_recordingService == null) return;
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) {
final scheme = Theme.of(context).colorScheme;
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: scheme.primary,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.r),
topRight: Radius.circular(16.r),
),
),
child: Row(
children: [
Icon(
Icons.emoji_events_rounded,
color: scheme.onPrimary,
size: 24.w,
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Monthly Draw',
style: TextStyle(
color: scheme.onPrimary,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
Text(
'${_monthController.text}/${_yearController.text}',
style: TextStyle(
color: scheme.onPrimary.withOpacity(0.9),
fontSize: 12.sp,
),
),
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(Icons.close, color: scheme.onPrimary),
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: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
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: scheme.primary,
foregroundColor: scheme.onPrimary,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isLoading)
SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
else
Icon(Icons.play_arrow, size: 20.w),
SizedBox(width: 8.w),
Text(
_isLoading ? 'Loading...' : 'Start Draw',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
),
],
),
),
),
if (_isDrawComplete)
Expanded(
child: FilledButton(
onPressed: _saveDrawResult,
style: FilledButton.styleFrom(
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.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() {
final scheme = Theme.of(context).colorScheme;
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Draw Details
Text(
'Draw Details',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: scheme.onSurface,
),
),
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
Row(
children: [
Text(
'Eligible Members',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(width: 8.w),
if (_isLoading)
SizedBox(
width: 16.w,
height: 16.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(scheme.primary),
),
)
else
Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h),
decoration: BoxDecoration(
color: _eligibleMembers.isEmpty
? Colors.orange.shade100
: scheme.primaryContainer,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'${_eligibleMembers.length}',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: _eligibleMembers.isEmpty
? Colors.orange.shade800
: scheme.onPrimaryContainer,
),
),
),
],
),
SizedBox(height: 12.h),
Container(
height: 150.h,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8.r),
),
child: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 12.h),
Text(
'Loading members...',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
),
),
],
),
)
: _eligibleMembers.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.info_outline,
size: 40.w,
color: Colors.orange.shade400,
),
SizedBox(height: 8.h),
Text(
'No eligible members',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4.h),
Text(
'All members have won',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey.shade500,
),
),
],
),
)
: ListView.builder(
itemCount: _eligibleMembers.length,
itemBuilder: (context, index) {
final member = _eligibleMembers[index];
return ListTile(
dense: true,
leading: CircleAvatar(
backgroundColor: scheme.primaryContainer,
radius: 18.r,
child: Text(
(member['name']?.isNotEmpty == true ? member['name'].substring(0, 1) : 'M').toUpperCase(),
style: TextStyle(
color: scheme.primary,
fontWeight: FontWeight.w600,
fontSize: 14.sp,
),
),
),
title: Text(
member['name'],
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.sp,
),
),
subtitle: Text(
member['mobile'],
style: TextStyle(fontSize: 12.sp),
),
trailing: Icon(
Icons.check_circle_rounded,
color: scheme.primary,
size: 18.w,
),
);
},
),
),
],
),
);
}
Widget _buildDrawInProgress() {
// This method is no longer used - we navigate to full screen immediately
return const SizedBox.shrink();
}
Widget _buildDrawResult() {
final scheme = Theme.of(context).colorScheme;
return Column(
children: [
// Winner Announcement
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: scheme.primaryContainer.withOpacity(0.65),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(color: scheme.primary.withOpacity(0.25)),
),
child: Column(
children: [
Icon(
Icons.emoji_events_rounded,
color: scheme.tertiary,
size: 48.w,
),
SizedBox(height: 16.h),
Text(
'Winner Selected!',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: scheme.primary,
),
),
SizedBox(height: 8.h),
Text(
_winnerData?['name'] ?? 'Unknown',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: scheme.onSurface,
),
),
SizedBox(height: 4.h),
Text(
_winnerData?['mobile'] ?? '',
style: TextStyle(
fontSize: 14.sp,
color: scheme.onSurfaceVariant,
),
),
],
),
),
SizedBox(height: 16.h),
// Quick Info
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: scheme.outlineVariant),
),
child: Row(
children: [
Icon(Icons.verified_user_rounded, color: scheme.primary, size: 18.w),
SizedBox(width: 8.w),
Expanded(
child: Text(
'Draw completed using provably fair system. Recording saved.',
style: TextStyle(
fontSize: 12.sp,
color: scheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
);
}
Future<void> _saveDrawResult() async {
setState(() => _isLoading = true);
try {
final month = int.parse(_monthController.text);
final year = int.parse(_yearController.text);
final created = await ChitGroupService.to.createMonthlyDraw(
widget.group.id,
month,
year,
clientSeed: null,
);
if (created != null) {
Navigator.of(context).pop();
final s = Theme.of(context).colorScheme;
Get.snackbar(
'Success',
'Monthly draw completed successfully!',
backgroundColor: s.primary,
colorText: s.onPrimary,
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);
}
}
}