diff --git a/backend/src/controllers/chitGroupController.js b/backend/src/controllers/chitGroupController.js index c7efec7..b6366f2 100644 --- a/backend/src/controllers/chitGroupController.js +++ b/backend/src/controllers/chitGroupController.js @@ -13,7 +13,11 @@ const createChitGroup = async (req, res) => { foreman_commission_amount, foreman_commission_type = 'fixed', foreman_commission_rate, - draw_date + draw_date, + start_date, // Optional: for importing existing groups + current_month, // Optional: current month number for existing groups + is_import, // Flag to indicate this is an existing group + status // Optional: can be 'active' for imported groups } = req.body; const managerId = req.user.id; @@ -76,6 +80,14 @@ const createChitGroup = async (req, res) => { }); } + // Determine start date and status + const groupStartDate = is_import && start_date ? new Date(start_date) : new Date(); + const groupStatus = is_import && status === 'active' ? 'active' : 'forming'; + + // Calculate end date from start date + const endDate = new Date(groupStartDate); + endDate.setMonth(endDate.getMonth() + duration_months); + // Create chit group const chitGroup = await ChitGroup.create({ name, @@ -88,21 +100,30 @@ const createChitGroup = async (req, res) => { foreman_commission_rate, draw_date, manager_id: managerId, - status: 'forming', - start_date: new Date(), - end_date: new Date(Date.now() + duration_months * 30 * 24 * 60 * 60 * 1000) + status: groupStatus, + start_date: groupStartDate, + end_date: endDate }); + const message = is_import + ? `Existing group imported successfully. Started: ${groupStartDate.toLocaleDateString()}. Current month: ${current_month || 1}` + : 'Chit group created successfully'; + res.status(201).json({ success: true, - message: 'Chit group created successfully', - data: chitGroup + message, + data: { + ...chitGroup.toJSON(), + current_month: current_month || 1, + is_imported: is_import || false + } }); } catch (error) { console.error('Create chit group error:', error); res.status(500).json({ success: false, - message: 'Internal server error' + message: 'Internal server error', + error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; diff --git a/backend/src/controllers/monthlyDrawController.js b/backend/src/controllers/monthlyDrawController.js index d75ad06..146054b 100644 --- a/backend/src/controllers/monthlyDrawController.js +++ b/backend/src/controllers/monthlyDrawController.js @@ -29,7 +29,7 @@ const generateProvablyFairResult = (serverSeed, clientSeed, nonce, eligibleMembe // Create a new monthly draw const createMonthlyDraw = async (req, res) => { try { - const { group_id, month, year, client_seed } = req.body; + const { group_id, month, year, client_seed, winner_id, prize_amount, is_past_draw } = req.body; // Convert month and year to integers const monthInt = parseInt(month, 10); @@ -117,39 +117,56 @@ const createMonthlyDraw = async (req, res) => { !wonMemberIds.includes(member.user_id) ); - if (availableMembers.length === 0) { + if (availableMembers.length === 0 && !is_past_draw) { return res.status(400).json({ success: false, message: 'No eligible members available for draw' }); } - // Generate server seed and nonce - const serverSeed = crypto.randomBytes(32).toString('hex'); - const nonce = Date.now(); + // Determine winner + let selectedWinnerId; + let selectedWinner; + let serverSeed; + let resultHash; + let nonce; - // Generate provably fair result - const result = generateProvablyFairResult( - serverSeed, - client_seed || 'default', - nonce, - availableMembers - ); + if (is_past_draw && winner_id) { + // Past draw - manual winner selection + selectedWinnerId = winner_id; + serverSeed = `PAST_DRAW_${Date.now()}`; + nonce = Date.now(); + resultHash = crypto.createHash('sha256').update(`${serverSeed}_${winner_id}_${nonce}`).digest('hex'); + + // Get winner details + selectedWinner = eligibleMembers.find(m => m.user_id === winner_id); + if (!selectedWinner) { + return res.status(400).json({ + success: false, + message: 'Selected winner is not a member of this group' + }); + } + } else { + // Regular draw - provably fair random selection + serverSeed = crypto.randomBytes(32).toString('hex'); + nonce = Date.now(); - // Calculate prize amount using generic formula + const result = generateProvablyFairResult( + serverSeed, + client_seed || 'default', + nonce, + availableMembers + ); + + selectedWinnerId = result.winner.user_id; + selectedWinner = result.winner; + resultHash = result.hash; + } + + // Calculate prize amount const totalMembers = eligibleMembers.length; - const subscriptionPerMonth = chitGroup.total_value / chitGroup.duration_months; - const installmentPerMonth = subscriptionPerMonth + chitGroup.foreman_commission_amount; + const calculatedPrizeAmount = prize_amount || chitGroup.total_value; // Use provided or default - // Calculate dividend for this month (linear schedule) - const maxDividend = chitGroup.foreman_commission_amount * 2; // Default to 2x commission - const step = (2 * maxDividend) / (chitGroup.duration_months - 1); - const currentMonth = monthInt; // Assuming month is 1-based - const dividend = maxDividend - ((currentMonth - 1) * step); - - const winnerPayout = chitGroup.total_value - dividend; // Winner gets bid amount - const dividendPerMember = dividend / (totalMembers - 1); // Dividend distributed to others - // Create monthly draw const monthlyDraw = await MonthlyDraw.create({ group_id, @@ -161,15 +178,17 @@ const createMonthlyDraw = async (req, res) => { name: member.User.full_name, mobile: member.User.mobile_number })), - winner_id: result.winner.user_id, - prize_amount: winnerPayout, + winner_id: selectedWinnerId, + prize_amount: calculatedPrizeAmount, server_seed: serverSeed, server_seed_hash: crypto.createHash('sha256').update(serverSeed).digest('hex'), - client_seed: client_seed || 'default', + client_seed: client_seed || (is_past_draw ? 'PAST_DRAW' : 'default'), nonce, - result_hash: result.hash, + result_hash: resultHash, status: 'completed', - notes: `Winner selected: ${result.winner.User.full_name}` + notes: is_past_draw + ? `Past draw result (imported) - Winner: ${selectedWinner.User.full_name}` + : `Winner selected: ${selectedWinner.User.full_name}` }); res.status(201).json({ diff --git a/luckychit/lib/interfaces/manager/add_past_draw_dialog.dart b/luckychit/lib/interfaces/manager/add_past_draw_dialog.dart new file mode 100644 index 0000000..062c748 --- /dev/null +++ b/luckychit/lib/interfaces/manager/add_past_draw_dialog.dart @@ -0,0 +1,331 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../core/services/chit_group_service.dart'; +import '../../core/services/api_service.dart'; +import '../../core/models/chit_group.dart'; +import '../../core/models/group_member.dart'; +import '../../core/utils/snackbar_util.dart'; + +class AddPastDrawDialog extends StatefulWidget { + final ChitGroup group; + final int monthNumber; + + const AddPastDrawDialog({ + super.key, + required this.group, + required this.monthNumber, + }); + + @override + State createState() => _AddPastDrawDialogState(); +} + +class _AddPastDrawDialogState extends State { + final _formKey = GlobalKey(); + final _prizeAmountController = TextEditingController(); + final _apiService = ApiService(); + + String? _selectedMemberId; + String? _selectedMemberName; + bool _isLoading = false; + int _selectedMonth = 1; + int _selectedYear = DateTime.now().year; + + @override + void initState() { + super.initState(); + _selectedMonth = widget.monthNumber; + _calculateMonthYear(); + _prizeAmountController.text = widget.group.monthlyInstallment.toStringAsFixed(0); + } + + void _calculateMonthYear() { + if (widget.group.startDate != null) { + final startDate = widget.group.startDate!; + final targetDate = DateTime( + startDate.year, + startDate.month + widget.monthNumber - 1, + startDate.day, + ); + _selectedMonth = targetDate.month; + _selectedYear = targetDate.year; + } + } + + @override + void dispose() { + _prizeAmountController.dispose(); + super.dispose(); + } + + Future _handleSubmit() async { + if (_formKey.currentState!.validate()) { + if (_selectedMemberId == null) { + SnackbarUtil.showError('Please select a winner'); + return; + } + + setState(() => _isLoading = true); + + try { + final response = await _apiService.createMonthlyDraw({ + 'group_id': widget.group.id, + 'month': _selectedMonth, + 'year': _selectedYear, + 'winner_id': _selectedMemberId, + 'prize_amount': double.parse(_prizeAmountController.text), + 'client_seed': 'PAST_DRAW_${DateTime.now().millisecondsSinceEpoch}', + 'is_past_draw': true, // Flag for past draw + }); + + if (response['success']) { + SnackbarUtil.showSuccess( + 'Past draw result added successfully', + title: 'Success', + ); + Get.back(result: true); + } else { + SnackbarUtil.showError( + response['message'] ?? 'Failed to add draw', + title: 'Error', + ); + } + } catch (e) { + SnackbarUtil.showError('Error: ${e.toString()}'); + } finally { + setState(() => _isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.r)), + child: Container( + width: 500.w, + padding: EdgeInsets.all(24.w), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Icon(Icons.history, color: Colors.blue.shade600, size: 28.w), + SizedBox(width: 12.w), + Expanded( + child: Text( + 'Add Past Draw Result', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Get.back(), + ), + ], + ), + SizedBox(height: 24.h), + + // Month Info + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: Colors.blue.shade200), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Month ${widget.monthNumber}', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + Text( + '$_selectedMonth/$_selectedYear', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.blue.shade700, + ), + ), + ], + ), + ), + SizedBox(height: 20.h), + + // Select Winner + Text( + 'Who Won This Draw?', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 12.h), + Obx(() { + final members = Get.find().groupMembers; + + if (members.isEmpty) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: Colors.orange.shade200), + ), + child: Text( + 'Please add members to the group first', + style: TextStyle( + color: Colors.orange.shade900, + fontSize: 14.sp, + ), + ), + ); + } + + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + children: members.map((member) { + final isSelected = member.id == _selectedMemberId; + return InkWell( + onTap: () { + setState(() { + _selectedMemberId = member.userId; + _selectedMemberName = member.user?.fullName ?? 'Unknown'; + }); + }, + child: Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isSelected ? Colors.green.shade50 : null, + border: Border( + bottom: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Row( + children: [ + Icon( + isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: isSelected ? Colors.green.shade600 : Colors.grey.shade400, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + member.user?.fullName ?? 'Unknown', + style: TextStyle( + fontSize: 16.sp, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + if (isSelected) + Icon(Icons.check_circle, color: Colors.green.shade600), + ], + ), + ), + ); + }).toList(), + ), + ); + }), + SizedBox(height: 20.h), + + // Prize Amount + Text( + 'Prize Amount', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8.h), + TextFormField( + controller: _prizeAmountController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Prize amount won', + prefixText: '₹ ', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.r)), + filled: true, + fillColor: Colors.grey.shade50, + ), + validator: (value) { + if (value?.isEmpty ?? true) return 'Required'; + if (double.tryParse(value!) == null) return 'Invalid amount'; + return null; + }, + ), + SizedBox(height: 24.h), + + // Actions + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: _isLoading ? null : () => Get.back(), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 14.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: Text( + 'Cancel', + style: TextStyle(fontSize: 16.sp), + ), + ), + ), + SizedBox(width: 16.w), + Expanded( + child: ElevatedButton( + onPressed: _isLoading ? null : _handleSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 14.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: _isLoading + ? SizedBox( + height: 20.h, + width: 20.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + 'Add Draw', + style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} + diff --git a/luckychit/lib/interfaces/manager/add_past_payments_dialog.dart b/luckychit/lib/interfaces/manager/add_past_payments_dialog.dart new file mode 100644 index 0000000..70cdc81 --- /dev/null +++ b/luckychit/lib/interfaces/manager/add_past_payments_dialog.dart @@ -0,0 +1,370 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../core/services/payment_service.dart'; +import '../../core/services/chit_group_service.dart'; +import '../../core/services/api_service.dart'; +import '../../core/models/chit_group.dart'; +import '../../core/models/group_member.dart'; +import '../../core/utils/snackbar_util.dart'; + +class AddPastPaymentsDialog extends StatefulWidget { + final ChitGroup group; + final int monthNumber; + + const AddPastPaymentsDialog({ + super.key, + required this.group, + required this.monthNumber, + }); + + @override + State createState() => _AddPastPaymentsDialogState(); +} + +class _AddPastPaymentsDialogState extends State { + final _apiService = ApiService(); + final _chitGroupService = Get.find(); + + Map _memberPaymentStatus = {}; // memberId: paid + bool _isLoading = false; + int _selectedMonth = 1; + int _selectedYear = DateTime.now().year; + + @override + void initState() { + super.initState(); + _calculateMonthYear(); + _initializePaymentStatus(); + } + + void _calculateMonthYear() { + if (widget.group.startDate != null) { + final startDate = widget.group.startDate!; + final targetDate = DateTime( + startDate.year, + startDate.month + widget.monthNumber - 1, + startDate.day, + ); + _selectedMonth = targetDate.month; + _selectedYear = targetDate.year; + } + } + + void _initializePaymentStatus() { + final members = _chitGroupService.groupMembers; + for (var member in members) { + _memberPaymentStatus[member.userId] = false; + } + } + + Future _handleSubmit() async { + final paidMembers = _memberPaymentStatus.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + + if (paidMembers.isEmpty) { + SnackbarUtil.showWarning('Please select at least one member who paid'); + return; + } + + setState(() => _isLoading = true); + + try { + int successCount = 0; + int errorCount = 0; + + for (var userId in paidMembers) { + final response = await _apiService.recordPayment({ + 'group_id': widget.group.id, + 'user_id': userId, + 'month': _selectedMonth, + 'year': _selectedYear, + 'amount': widget.group.monthlyInstallment, + 'payment_method': 'cash', + 'status': 'success', + 'paid_at': DateTime.now().toIso8601String(), + 'notes': 'Past payment - Month ${widget.monthNumber}', + }); + + if (response['success']) { + successCount++; + } else { + errorCount++; + } + } + + if (successCount > 0) { + SnackbarUtil.showSuccess( + 'Added $successCount payment(s) successfully${errorCount > 0 ? ". $errorCount failed." : ""}', + title: 'Payments Added', + ); + Get.back(result: true); + } else { + SnackbarUtil.showError('Failed to add payments. Please try again.'); + } + } catch (e) { + SnackbarUtil.showError('Error: ${e.toString()}'); + } finally { + setState(() => _isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.r)), + child: Container( + width: 500.w, + constraints: BoxConstraints(maxHeight: 600.h), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.green.shade600, Colors.green.shade700], + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.r), + topRight: Radius.circular(20.r), + ), + ), + child: Row( + children: [ + Icon(Icons.payment, color: Colors.white, size: 28.w), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Add Past Payments', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Text( + 'Month ${widget.monthNumber} ($_selectedMonth/$_selectedYear)', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.9), + ), + ), + ], + ), + ), + IconButton( + icon: Icon(Icons.close, color: Colors.white, size: 24.w), + onPressed: () => Get.back(), + ), + ], + ), + ), + + // Member List + Expanded( + child: ListView( + padding: EdgeInsets.all(24.w), + children: [ + // Info + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue.shade700, size: 20.w), + SizedBox(width: 12.w), + Expanded( + child: Text( + 'Select members who made their payment for this month', + style: TextStyle( + fontSize: 14.sp, + color: Colors.blue.shade900, + ), + ), + ), + ], + ), + ), + SizedBox(height: 20.h), + + // Members + Text( + 'Members (₹${_formatCurrency(widget.group.monthlyInstallment)} each)', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 12.h), + + Obx(() { + final members = _chitGroupService.groupMembers; + + if (members.isEmpty) { + return Container( + padding: EdgeInsets.all(24.w), + child: Center( + child: Text( + 'No members in group', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14.sp, + ), + ), + ), + ); + } + + return Column( + children: members.map((member) { + final isPaid = _memberPaymentStatus[member.userId] ?? false; + return CheckboxListTile( + value: isPaid, + onChanged: (value) { + setState(() { + _memberPaymentStatus[member.userId] = value ?? false; + }); + }, + title: Text( + member.user?.fullName ?? 'Unknown', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + member.user?.mobileNumber ?? '', + style: TextStyle(fontSize: 14.sp), + ), + secondary: CircleAvatar( + backgroundColor: isPaid ? Colors.green.shade600 : Colors.grey.shade300, + child: Text( + member.user?.fullName?.substring(0, 1).toUpperCase() ?? 'M', + style: TextStyle(color: Colors.white), + ), + ), + activeColor: Colors.green.shade600, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)), + tileColor: isPaid ? Colors.green.shade50 : null, + ); + }).toList(), + ); + }), + + SizedBox(height: 16.h), + + // Summary + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Selected:', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + Text( + '${_memberPaymentStatus.values.where((v) => v).length} members', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + ], + ), + ), + ], + ), + ), + + // Actions + Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(20.r), + bottomRight: Radius.circular(20.r), + ), + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: _isLoading ? null : () => Get.back(), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 14.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: Text( + 'Cancel', + style: TextStyle(fontSize: 16.sp), + ), + ), + ), + SizedBox(width: 16.w), + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _isLoading ? null : _handleSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade600, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 14.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: _isLoading + ? SizedBox( + height: 20.h, + width: 20.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + 'Add Payments', + style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + String _formatCurrency(double amount) { + final amountStr = amount.toStringAsFixed(0); + return amountStr.replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},', + ); + } +} + diff --git a/luckychit/lib/interfaces/manager/create_group_dialog.dart b/luckychit/lib/interfaces/manager/create_group_dialog.dart index a9b82e6..8ef7851 100644 --- a/luckychit/lib/interfaces/manager/create_group_dialog.dart +++ b/luckychit/lib/interfaces/manager/create_group_dialog.dart @@ -259,7 +259,7 @@ class _CreateGroupDialogState extends State { SizedBox(height: 16.h), // Financial Details Section - _buildSectionTitle('Financial Details'), + _buildSectionTitle('Chitfund Details'), SizedBox(height: 16.h), Row( diff --git a/luckychit/lib/interfaces/manager/create_group_page.dart b/luckychit/lib/interfaces/manager/create_group_page.dart index 798f79c..de9828c 100644 --- a/luckychit/lib/interfaces/manager/create_group_page.dart +++ b/luckychit/lib/interfaces/manager/create_group_page.dart @@ -188,16 +188,28 @@ class _CreateGroupPageState extends State { Widget _buildBasicInfoStep() { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Chit Fund Name + Text( + 'Chit Fund Name', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 8.h), TextFormField( controller: _nameController, decoration: InputDecoration( - labelText: 'Chit Fund Name', hintText: 'e.g., Family Chit Fund', prefixIcon: const Icon(Icons.group), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12.r), ), + filled: true, + fillColor: Colors.grey.shade50, ), validator: (value) { if (value == null || value.trim().isEmpty) { @@ -211,72 +223,100 @@ class _CreateGroupPageState extends State { ), SizedBox(height: 20.h), - Row( - children: [ - Expanded( - child: TextFormField( - controller: _durationMonthsController, - decoration: InputDecoration( - labelText: 'Duration (Months)', - hintText: 'e.g., 20', - prefixIcon: const Icon(Icons.calendar_today), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.r), - ), - ), - keyboardType: TextInputType.number, - onChanged: (_) => _calculateMonthlyPayment(), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Required'; - } - final months = int.tryParse(value); - if (months == null || months < 6 || months > 60) { - return '6-60 months'; - } - return null; - }, - ), + // Duration + Text( + 'Duration (Months)', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 8.h), + TextFormField( + controller: _durationMonthsController, + decoration: InputDecoration( + hintText: 'e.g., 20', + prefixIcon: const Icon(Icons.calendar_today), + helperText: 'Total number of months (6-60)', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), ), - SizedBox(width: 16.w), - Expanded( - child: TextFormField( - controller: _maxMembersController, - decoration: InputDecoration( - labelText: 'Max Members', - hintText: 'e.g., 20', - prefixIcon: const Icon(Icons.people), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.r), - ), - ), - keyboardType: TextInputType.number, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Required'; - } - final members = int.tryParse(value); - if (members == null || members < 2 || members > 100) { - return '2-100 members'; - } - return null; - }, - ), - ), - ], + filled: true, + fillColor: Colors.grey.shade50, + ), + keyboardType: TextInputType.number, + onChanged: (_) => _calculateMonthlyPayment(), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Required'; + } + final months = int.tryParse(value); + if (months == null || months < 6 || months > 60) { + return '6-60 months'; + } + return null; + }, ), SizedBox(height: 20.h), + // Max Members + Text( + 'Max Participants', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 8.h), + TextFormField( + controller: _maxMembersController, + decoration: InputDecoration( + hintText: 'e.g., 20', + prefixIcon: const Icon(Icons.people), + helperText: 'Maximum number of members (2-100)', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Required'; + } + final members = int.tryParse(value); + if (members == null || members < 2 || members > 100) { + return '2-100 members'; + } + return null; + }, + ), + SizedBox(height: 20.h), + + // Draw Date + Text( + 'Draw Date (Day of Month)', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 8.h), TextFormField( controller: _drawDateController, decoration: InputDecoration( - labelText: 'Draw Date (Day of Month)', hintText: 'e.g., 15', prefixIcon: const Icon(Icons.casino), helperText: 'Day of month for lottery draw (1-31)', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12.r), ), + filled: true, + fillColor: Colors.grey.shade50, ), keyboardType: TextInputType.number, validator: (value) { @@ -298,16 +338,27 @@ class _CreateGroupPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Chitfund Value + Text( + 'Chitfund Value (Fixed)', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 8.h), TextFormField( controller: _totalValueController, decoration: InputDecoration( - labelText: 'Fixed Target Value (Chit Value)', hintText: 'e.g., 200000', prefixIcon: const Icon(Icons.currency_rupee), - helperText: 'Fixed target value of the chit fund', + helperText: 'Fixed chitfund value of the group', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12.r), ), + filled: true, + fillColor: Colors.grey.shade50, ), keyboardType: TextInputType.number, onChanged: (_) => _calculateMonthlyPayment(), @@ -324,16 +375,27 @@ class _CreateGroupPageState extends State { ), SizedBox(height: 20.h), + // Monthly Payment (Principal) + Text( + 'Monthly Payment (Principal)', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 8.h), TextFormField( controller: _monthlyContributionController, decoration: InputDecoration( - labelText: 'Monthly Contribution (Principal)', hintText: 'e.g., 10000', prefixIcon: const Icon(Icons.payment), - helperText: 'Amount each member contributes monthly', + helperText: 'Amount each member pays monthly', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12.r), ), + filled: true, + fillColor: Colors.grey.shade50, ), keyboardType: TextInputType.number, onChanged: (_) => _calculateMonthlyPayment(), @@ -350,35 +412,40 @@ class _CreateGroupPageState extends State { ), SizedBox(height: 20.h), - Row( - children: [ - Expanded( - child: TextFormField( - controller: _monthlyCommissionController, - decoration: InputDecoration( - labelText: 'Monthly Commission/Fee', - hintText: 'e.g., 250', - prefixIcon: const Icon(Icons.percent), - helperText: 'Manager commission per month', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12.r), - ), - ), - keyboardType: TextInputType.number, - onChanged: (_) => _calculateMonthlyPayment(), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Required'; - } - final amount = double.tryParse(value); - if (amount == null || amount < 0) { - return 'Invalid'; - } - return null; - }, - ), + // Monthly Commission/Fee + Text( + 'Monthly Commission/Fee', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 8.h), + TextFormField( + controller: _monthlyCommissionController, + decoration: InputDecoration( + hintText: 'e.g., 250', + prefixIcon: const Icon(Icons.currency_rupee), + helperText: 'Chit Manager commission per month', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), ), - ], + filled: true, + fillColor: Colors.grey.shade50, + ), + keyboardType: TextInputType.number, + onChanged: (_) => _calculateMonthlyPayment(), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Required'; + } + final amount = double.tryParse(value); + if (amount == null || amount < 0) { + return 'Invalid'; + } + return null; + }, ), SizedBox(height: 20.h), @@ -403,7 +470,7 @@ class _CreateGroupPageState extends State { ), SizedBox(height: 12.h), _buildPaymentRow( - 'Monthly Contribution', + 'Monthly Payment', _monthlyContributionController.text.isEmpty ? '-' : '₹${_monthlyContributionController.text}', @@ -466,7 +533,7 @@ class _CreateGroupPageState extends State { ), SizedBox(height: 16.h), _buildSummaryRow('Name', _nameController.text), - _buildSummaryRow('Target Value', '₹${_totalValueController.text}'), + _buildSummaryRow('Chitfund Value', '₹${_totalValueController.text}'), _buildSummaryRow('Duration', '${_durationMonthsController.text} months'), _buildSummaryRow('Max Members', _maxMembersController.text), _buildSummaryRow('Monthly Payment', _getMonthlyPayment()), diff --git a/luckychit/lib/interfaces/manager/import_existing_group_dialog.dart b/luckychit/lib/interfaces/manager/import_existing_group_dialog.dart new file mode 100644 index 0000000..a1f94b5 --- /dev/null +++ b/luckychit/lib/interfaces/manager/import_existing_group_dialog.dart @@ -0,0 +1,605 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../core/services/chit_group_service.dart'; +import '../../core/utils/snackbar_util.dart'; +import 'package:intl/intl.dart'; + +class ImportExistingGroupDialog extends StatefulWidget { + const ImportExistingGroupDialog({super.key}); + + @override + State createState() => _ImportExistingGroupDialogState(); +} + +class _ImportExistingGroupDialogState extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _totalValueController = TextEditingController(); + final _monthlyInstallmentController = TextEditingController(); + final _durationMonthsController = TextEditingController(); + final _maxMembersController = TextEditingController(); + final _foremanCommissionController = TextEditingController(); + final _drawDateController = TextEditingController(); + final _currentMonthController = TextEditingController(); + + DateTime? _selectedStartDate; + bool _isLoading = false; + int _step = 1; // Multi-step wizard + + @override + void initState() { + super.initState(); + _drawDateController.text = '15'; + _foremanCommissionController.text = '250'; + _currentMonthController.text = '1'; + // Default to 6 months ago + _selectedStartDate = DateTime.now().subtract(const Duration(days: 180)); + } + + @override + void dispose() { + _nameController.dispose(); + _totalValueController.dispose(); + _monthlyInstallmentController.dispose(); + _durationMonthsController.dispose(); + _maxMembersController.dispose(); + _foremanCommissionController.dispose(); + _drawDateController.dispose(); + _currentMonthController.dispose(); + super.dispose(); + } + + Future _selectStartDate() async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedStartDate ?? DateTime.now().subtract(const Duration(days: 180)), + firstDate: DateTime(2020), + lastDate: DateTime.now(), + helpText: 'Select when the group started', + ); + if (picked != null) { + setState(() { + _selectedStartDate = picked; + _calculateCurrentMonth(); + }); + } + } + + void _calculateCurrentMonth() { + if (_selectedStartDate != null) { + final now = DateTime.now(); + final diff = now.difference(_selectedStartDate!); + final monthsPassed = (diff.inDays / 30).floor() + 1; + _currentMonthController.text = monthsPassed.toString(); + } + } + + void _calculateMonthlyInstallment() { + final totalValue = double.tryParse(_totalValueController.text); + final durationMonths = int.tryParse(_durationMonthsController.text); + final commission = double.tryParse(_foremanCommissionController.text) ?? 250.0; + + if (totalValue != null && durationMonths != null) { + final subscriptionPerMonth = totalValue / durationMonths; + final monthlyInstallment = subscriptionPerMonth + commission; + _monthlyInstallmentController.text = monthlyInstallment.toStringAsFixed(0); + } + } + + Future _handleImport() async { + if (_formKey.currentState!.validate()) { + if (_selectedStartDate == null) { + SnackbarUtil.showError('Please select a start date'); + return; + } + + setState(() => _isLoading = true); + + try { + final data = { + 'name': _nameController.text.trim(), + 'total_value': double.parse(_totalValueController.text), + 'monthly_installment': double.parse(_monthlyInstallmentController.text), + 'duration_months': int.parse(_durationMonthsController.text), + 'max_members': int.parse(_maxMembersController.text), + 'foreman_commission_amount': double.parse(_foremanCommissionController.text), + 'foreman_commission_type': 'fixed', + 'draw_date': int.parse(_drawDateController.text), + 'start_date': _selectedStartDate!.toIso8601String(), + 'current_month': int.parse(_currentMonthController.text), + 'is_import': true, // Flag to indicate this is an existing group + 'status': 'active', // Start as active directly + }; + + final success = await ChitGroupService.to.createChitGroup(data); + + if (success) { + SnackbarUtil.showSuccess( + 'Existing group imported successfully! You can now add members and past draw results.', + title: 'Import Successful', + ); + Get.back(result: true); + } else { + SnackbarUtil.showError('Failed to import group. Please try again.'); + } + } catch (e) { + SnackbarUtil.showError('Error: ${e.toString()}'); + } finally { + setState(() => _isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.r)), + child: Container( + width: 600.w, + constraints: BoxConstraints(maxHeight: 700.h), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade600, Colors.blue.shade700], + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.r), + topRight: Radius.circular(20.r), + ), + ), + child: Row( + children: [ + Icon(Icons.upload, color: Colors.white, size: 28.w), + SizedBox(width: 12.w), + Expanded( + child: Text( + 'Import Existing Chit Group', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + IconButton( + icon: Icon(Icons.close, color: Colors.white, size: 24.w), + onPressed: () => Get.back(), + ), + ], + ), + ), + + // Form + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(24.w), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Info Box + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: Colors.blue.shade200), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue.shade700, size: 20.w), + SizedBox(width: 12.w), + Expanded( + child: Text( + 'Import a chit group that has already started. You can add past draw results and payments later.', + style: TextStyle( + fontSize: 14.sp, + color: Colors.blue.shade900, + ), + ), + ), + ], + ), + ), + SizedBox(height: 24.h), + + // Group Name + Text( + 'Group Name', + style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), + ), + SizedBox(height: 8.h), + TextFormField( + controller: _nameController, + decoration: InputDecoration( + hintText: 'e.g., January 2024 Batch', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.r)), + filled: true, + fillColor: Colors.grey.shade50, + ), + validator: (value) => value?.isEmpty ?? true ? 'Required' : null, + ), + SizedBox(height: 20.h), + + // Start Date + Text( + 'When Did This Group Start?', + style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), + ), + SizedBox(height: 8.h), + InkWell( + onTap: _selectStartDate, + child: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12.r), + color: Colors.grey.shade50, + ), + child: Row( + children: [ + Icon(Icons.calendar_today, color: Colors.blue.shade600), + SizedBox(width: 12.w), + Text( + _selectedStartDate != null + ? DateFormat('dd/MM/yyyy').format(_selectedStartDate!) + : 'Select start date', + style: TextStyle(fontSize: 16.sp), + ), + ], + ), + ), + ), + SizedBox(height: 20.h), + + // Current Month + Text( + 'Current Month Number', + style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), + ), + SizedBox(height: 8.h), + TextFormField( + controller: _currentMonthController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'e.g., 6 (if 6 months have passed)', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.r)), + filled: true, + fillColor: Colors.grey.shade50, + suffixText: 'months elapsed', + ), + validator: (value) { + if (value?.isEmpty ?? true) return 'Required'; + final month = int.tryParse(value!); + if (month == null || month < 1) return 'Must be at least 1'; + return null; + }, + ), + SizedBox(height: 20.h), + + // Financial Details Header + Divider(), + SizedBox(height: 12.h), + Text( + 'Chitfund Details', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 16.h), + + // Total Value + Text('Total Value', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)), + SizedBox(height: 8.h), + TextFormField( + controller: _totalValueController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'e.g., 100000', + prefixText: '₹ ', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.r)), + filled: true, + fillColor: Colors.grey.shade50, + ), + onChanged: (_) => _calculateMonthlyInstallment(), + validator: (value) { + if (value?.isEmpty ?? true) return 'Required'; + if (double.tryParse(value!) == null) return 'Invalid number'; + return null; + }, + ), + SizedBox(height: 16.h), + + // Duration + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Duration (Months)', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)), + SizedBox(height: 8.h), + TextFormField( + controller: _durationMonthsController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'e.g., 20', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.r)), + filled: true, + fillColor: Colors.grey.shade50, + ), + onChanged: (_) => _calculateMonthlyInstallment(), + validator: (value) { + if (value?.isEmpty ?? true) return 'Required'; + if (int.tryParse(value!) == null) return 'Invalid'; + return null; + }, + ), + ], + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Max Members', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)), + SizedBox(height: 8.h), + TextFormField( + controller: _maxMembersController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'e.g., 20', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.r)), + filled: true, + fillColor: Colors.grey.shade50, + ), + validator: (value) { + if (value?.isEmpty ?? true) return 'Required'; + if (int.tryParse(value!) == null) return 'Invalid'; + return null; + }, + ), + ], + ), + ), + ], + ), + SizedBox(height: 16.h), + + // Monthly Installment & Commission + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Monthly Installment', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)), + SizedBox(height: 8.h), + TextFormField( + controller: _monthlyInstallmentController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'e.g., 5250', + prefixText: '₹ ', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.r)), + filled: true, + fillColor: Colors.grey.shade50, + ), + validator: (value) { + if (value?.isEmpty ?? true) return 'Required'; + if (double.tryParse(value!) == null) return 'Invalid'; + return null; + }, + ), + ], + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Commission', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)), + SizedBox(height: 8.h), + TextFormField( + controller: _foremanCommissionController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'e.g., 250', + prefixText: '₹ ', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.r)), + filled: true, + fillColor: Colors.grey.shade50, + ), + onChanged: (_) => _calculateMonthlyInstallment(), + validator: (value) { + if (value?.isEmpty ?? true) return 'Required'; + if (double.tryParse(value!) == null) return 'Invalid'; + return null; + }, + ), + ], + ), + ), + ], + ), + SizedBox(height: 16.h), + + // Draw Date + Text('Draw Date (Day of Month)', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)), + SizedBox(height: 8.h), + TextFormField( + controller: _drawDateController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'e.g., 15 (15th of every month)', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.r)), + filled: true, + fillColor: Colors.grey.shade50, + ), + validator: (value) { + if (value?.isEmpty ?? true) return 'Required'; + final date = int.tryParse(value!); + if (date == null || date < 1 || date > 31) return '1-31 only'; + return null; + }, + ), + SizedBox(height: 24.h), + + // Summary Box + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.info_outline, color: Colors.green.shade700, size: 20.w), + SizedBox(width: 8.w), + Text( + 'Next Steps After Import', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.green.shade800, + ), + ), + ], + ), + SizedBox(height: 12.h), + _buildNextStep('1', 'Add all members to the group'), + _buildNextStep('2', 'Record past draw results (if any)'), + _buildNextStep('3', 'Record past payments for each member'), + _buildNextStep('4', 'Continue with regular monthly operations'), + ], + ), + ), + ], + ), + ), + ), + ), + + // Actions + Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(20.r), + bottomRight: Radius.circular(20.r), + ), + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: _isLoading ? null : () => Get.back(), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: Text( + 'Cancel', + style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), + ), + ), + ), + SizedBox(width: 16.w), + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _isLoading ? null : _handleImport, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + elevation: 2, + ), + child: _isLoading + ? SizedBox( + height: 20.h, + width: 20.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + 'Import Group', + style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildNextStep(String number, String text) { + return Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: Colors.green.shade600, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + number, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Padding( + padding: EdgeInsets.only(top: 2.h), + child: Text( + text, + style: TextStyle( + fontSize: 14.sp, + color: Colors.green.shade900, + ), + ), + ), + ), + ], + ), + ); + } +} + diff --git a/luckychit/lib/interfaces/member/member_group_details_page.dart b/luckychit/lib/interfaces/member/member_group_details_page.dart index 65d163f..16ef558 100644 --- a/luckychit/lib/interfaces/member/member_group_details_page.dart +++ b/luckychit/lib/interfaces/member/member_group_details_page.dart @@ -103,20 +103,26 @@ class _MemberGroupDetailsPageState extends State child: ListView( padding: EdgeInsets.all(16.w), children: [ - // Group Info Card - _buildGroupInfoCard(group), + // Last Draw Winner Card (if exists) + if (_chitGroupService.monthlyDraws.isNotEmpty) + _buildLastDrawWinnerCard(_chitGroupService.monthlyDraws.first), + if (_chitGroupService.monthlyDraws.isNotEmpty) + SizedBox(height: 16.h), + + // Group Info Card (Compact) + _buildCompactGroupInfoCard(group), SizedBox(height: 16.h), // My Status Card if (myMember != null) _buildMyStatusCard(myMember), if (myMember != null) SizedBox(height: 16.h), - // Members Card - _buildMembersCard(members), - SizedBox(height: 16.h), - // Quick Stats _buildQuickStats(group, myMember), + SizedBox(height: 16.h), + + // Members Card + _buildMembersCard(members), ], ), ); @@ -179,12 +185,133 @@ class _MemberGroupDetailsPageState extends State }); } - Widget _buildGroupInfoCard(ChitGroup group) { + Widget _buildLastDrawWinnerCard(MonthlyDraw lastDraw) { + final isWinner = lastDraw.winnerId == _authService.currentUser.value?.id; + + return Card( + elevation: 3, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)), + child: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: isWinner + ? [Colors.amber.shade400, Colors.amber.shade600] + : [Colors.purple.shade400, Colors.purple.shade600], + ), + borderRadius: BorderRadius.circular(16.r), + ), + child: Column( + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(8.w), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.3), + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon( + isWinner ? Icons.emoji_events : Icons.casino, + color: Colors.white, + size: 24.w, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isWinner ? '🎉 You Won!' : 'Latest Draw Winner', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Text( + 'Month ${lastDraw.month}, ${lastDraw.year}', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white.withOpacity(0.9), + ), + ), + ], + ), + ), + ], + ), + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + Text( + lastDraw.winner?.fullName ?? 'Unknown', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + SizedBox(height: 4.h), + Text( + 'Winner', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + Container( + width: 1, + height: 40.h, + color: Colors.grey.shade300, + ), + Column( + children: [ + Text( + '₹${_formatCurrency(lastDraw.prizeAmount)}', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + SizedBox(height: 4.h), + Text( + 'Prize', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildCompactGroupInfoCard(ChitGroup group) { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)), child: Padding( - padding: EdgeInsets.all(20.w), + padding: EdgeInsets.all(16.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -192,9 +319,9 @@ class _MemberGroupDetailsPageState extends State mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Group Details', + 'Group Info', style: TextStyle( - fontSize: 18.sp, + fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.grey.shade800, ), @@ -202,20 +329,64 @@ class _MemberGroupDetailsPageState extends State _buildStatusBadge(group.status), ], ), - SizedBox(height: 20.h), - _buildInfoRow('Total Value', '₹${_formatCurrency(group.totalValue)}', Icons.account_balance_wallet), - _buildInfoRow('Monthly Installment', '₹${_formatCurrency(group.monthlyInstallment)}', Icons.payment), - _buildInfoRow('Duration', '${group.durationMonths} months', Icons.calendar_today), - _buildInfoRow('Draw Date', '${group.drawDate}th of each month', Icons.event), - _buildInfoRow('Members', '${group.currentMemberCount}/${group.maxMembers}', Icons.people), - if (group.startDate != null) - _buildInfoRow('Started On', _formatDate(group.startDate!), Icons.play_circle), + SizedBox(height: 16.h), + Row( + children: [ + Expanded(child: _buildCompactInfo('Total Value', '₹${_formatCurrency(group.totalValue)}', Icons.account_balance_wallet)), + SizedBox(width: 12.w), + Expanded(child: _buildCompactInfo('Installment', '₹${_formatCurrency(group.monthlyInstallment)}', Icons.payment)), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded(child: _buildCompactInfo('Duration', '${group.durationMonths} months', Icons.calendar_today)), + SizedBox(width: 12.w), + Expanded(child: _buildCompactInfo('Draw Date', '${group.drawDate}th', Icons.event)), + ], + ), + if (group.startDate != null) ...[ + SizedBox(height: 12.h), + _buildCompactInfo('Started On', _formatDate(group.startDate!), Icons.play_circle), + ], ], ), ), ); } + Widget _buildCompactInfo(String label, String value, IconData icon) { + return Row( + children: [ + Icon(icon, size: 18.w, color: Colors.green.shade600), + SizedBox(width: 8.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey.shade600, + ), + ), + SizedBox(height: 2.h), + Text( + value, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + ], + ), + ), + ], + ); + } + Widget _buildMyStatusCard(dynamic member) { return Card( elevation: 2, @@ -797,41 +968,6 @@ class _MemberGroupDetailsPageState extends State ); } - Widget _buildInfoRow(String label, String value, IconData icon) { - return Padding( - padding: EdgeInsets.only(bottom: 16.h), - child: Row( - children: [ - Icon(icon, size: 20.w, color: Colors.green.shade600), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - fontSize: 14.sp, - color: Colors.grey.shade600, - ), - ), - SizedBox(height: 4.h), - Text( - value, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.grey.shade800, - ), - ), - ], - ), - ), - ], - ), - ); - } - Widget _buildStatusBadge(String status) { Color color; String text; diff --git a/luckychit/lib/shared/widgets/dynamic_chit_calculator.dart b/luckychit/lib/shared/widgets/dynamic_chit_calculator.dart index b1fd608..cdd849f 100644 --- a/luckychit/lib/shared/widgets/dynamic_chit_calculator.dart +++ b/luckychit/lib/shared/widgets/dynamic_chit_calculator.dart @@ -397,7 +397,7 @@ class _DynamicChitCalculatorState extends State { Icon(Icons.analytics, color: Colors.orange.shade700, size: 18.w), SizedBox(width: 8.w), Text( - 'Financial Analysis', + 'Chitfund Analysis', style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.bold, @@ -437,8 +437,8 @@ class _DynamicChitCalculatorState extends State { Divider(color: Colors.orange.shade300), SizedBox(height: 8.h), - _buildSummaryRow('Fixed Chit Value', '₹${widget.chitValue.toStringAsFixed(0)}'), - _buildSummaryRow('Monthly Payment (Fixed)', '₹${totalPayment.toStringAsFixed(0)}'), + _buildSummaryRow('Chit Amount', '₹${widget.chitValue.toStringAsFixed(0)}'), + _buildSummaryRow('Monthly Installment (Fixed)', '₹${totalPayment.toStringAsFixed(0)}'), _buildSummaryRow('Total per Member', '₹${totalPerMember.toStringAsFixed(0)}'), _buildSummaryRow( 'Lift Amount Range', diff --git a/luckychit/lib/shared/widgets/enhanced_monthly_schedule.dart b/luckychit/lib/shared/widgets/enhanced_monthly_schedule.dart index 2245e78..a82b28b 100644 --- a/luckychit/lib/shared/widgets/enhanced_monthly_schedule.dart +++ b/luckychit/lib/shared/widgets/enhanced_monthly_schedule.dart @@ -387,7 +387,7 @@ class EnhancedMonthlyScheduleTable extends StatelessWidget { Icon(Icons.summarize_rounded, color: Colors.orange.shade700, size: 18.w), SizedBox(width: 8.w), Text( - 'Financial Summary', + 'Chitfund Summary', style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.bold, @@ -466,7 +466,7 @@ class EnhancedMonthlyScheduleTable extends StatelessWidget { SizedBox(width: 12.w), Expanded( child: Text( - 'Enter financial details to see the month-wise schedule', + 'Enter chitfund details to see the month-wise schedule', style: TextStyle( fontSize: 14.sp, color: Colors.orange.shade700, @@ -526,7 +526,7 @@ class DetailedMonthlyScheduleTable extends StatelessWidget { ), DataColumn( label: Text( - 'Target Value', + 'Chitfund Value', style: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.bold), ), numeric: true, diff --git a/luckychit/lib/shared/widgets/financial_table.dart b/luckychit/lib/shared/widgets/financial_table.dart index ac9738a..ce5eed9 100644 --- a/luckychit/lib/shared/widgets/financial_table.dart +++ b/luckychit/lib/shared/widgets/financial_table.dart @@ -81,7 +81,7 @@ class FinancialTable extends StatelessWidget { SizedBox(width: 12.w), Expanded( child: Text( - 'Financial Summary', + 'Chitfund Summary', style: TextStyle( fontSize: 22.sp, fontWeight: FontWeight.w600, @@ -260,7 +260,7 @@ class CompactFinancialTable extends StatelessWidget { SizedBox(width: 12.w), Expanded( child: Text( - 'Financial Summary', + 'Chitfund Summary', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, diff --git a/luckychit/lib/shared/widgets/monthly_schedule_table.dart b/luckychit/lib/shared/widgets/monthly_schedule_table.dart index 537abe3..512498b 100644 --- a/luckychit/lib/shared/widgets/monthly_schedule_table.dart +++ b/luckychit/lib/shared/widgets/monthly_schedule_table.dart @@ -293,7 +293,7 @@ class MonthlyScheduleTable extends StatelessWidget { SizedBox(width: 12.w), Expanded( child: Text( - 'Enter financial details above to see the month-wise payment schedule', + 'Enter chitfund details above to see the month-wise payment schedule', style: TextStyle( fontSize: 14.sp, color: Colors.orange.shade700, diff --git a/luckychit/lib/test_financial_table.dart b/luckychit/lib/test_financial_table.dart index 9ecc163..8ba2ec4 100644 --- a/luckychit/lib/test_financial_table.dart +++ b/luckychit/lib/test_financial_table.dart @@ -240,7 +240,7 @@ class FinancialTableTestPage extends StatelessWidget { child: Column( children: [ Text( - 'Financial Summary Table', + 'Chitfund Summary Table', style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.bold,