added past chits and updated labels

This commit is contained in:
Deep Koluguri 2025-11-05 23:31:01 -05:00
parent 38fdc84128
commit 2213a6524c
13 changed files with 1737 additions and 188 deletions

View File

@ -13,7 +13,11 @@ const createChitGroup = async (req, res) => {
foreman_commission_amount, foreman_commission_amount,
foreman_commission_type = 'fixed', foreman_commission_type = 'fixed',
foreman_commission_rate, 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; } = req.body;
const managerId = req.user.id; 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 // Create chit group
const chitGroup = await ChitGroup.create({ const chitGroup = await ChitGroup.create({
name, name,
@ -88,21 +100,30 @@ const createChitGroup = async (req, res) => {
foreman_commission_rate, foreman_commission_rate,
draw_date, draw_date,
manager_id: managerId, manager_id: managerId,
status: 'forming', status: groupStatus,
start_date: new Date(), start_date: groupStartDate,
end_date: new Date(Date.now() + duration_months * 30 * 24 * 60 * 60 * 1000) 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({ res.status(201).json({
success: true, success: true,
message: 'Chit group created successfully', message,
data: chitGroup data: {
...chitGroup.toJSON(),
current_month: current_month || 1,
is_imported: is_import || false
}
}); });
} catch (error) { } catch (error) {
console.error('Create chit group error:', error); console.error('Create chit group error:', error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Internal server error' message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
}); });
} }
}; };

View File

@ -29,7 +29,7 @@ const generateProvablyFairResult = (serverSeed, clientSeed, nonce, eligibleMembe
// Create a new monthly draw // Create a new monthly draw
const createMonthlyDraw = async (req, res) => { const createMonthlyDraw = async (req, res) => {
try { 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 // Convert month and year to integers
const monthInt = parseInt(month, 10); const monthInt = parseInt(month, 10);
@ -117,39 +117,56 @@ const createMonthlyDraw = async (req, res) => {
!wonMemberIds.includes(member.user_id) !wonMemberIds.includes(member.user_id)
); );
if (availableMembers.length === 0) { if (availableMembers.length === 0 && !is_past_draw) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'No eligible members available for draw' message: 'No eligible members available for draw'
}); });
} }
// Generate server seed and nonce // Determine winner
const serverSeed = crypto.randomBytes(32).toString('hex'); let selectedWinnerId;
const nonce = Date.now(); let selectedWinner;
let serverSeed;
let resultHash;
let nonce;
// Generate provably fair result if (is_past_draw && winner_id) {
const result = generateProvablyFairResult( // Past draw - manual winner selection
serverSeed, selectedWinnerId = winner_id;
client_seed || 'default', serverSeed = `PAST_DRAW_${Date.now()}`;
nonce, nonce = Date.now();
availableMembers 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 totalMembers = eligibleMembers.length;
const subscriptionPerMonth = chitGroup.total_value / chitGroup.duration_months; const calculatedPrizeAmount = prize_amount || chitGroup.total_value; // Use provided or default
const installmentPerMonth = subscriptionPerMonth + chitGroup.foreman_commission_amount;
// 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 // Create monthly draw
const monthlyDraw = await MonthlyDraw.create({ const monthlyDraw = await MonthlyDraw.create({
group_id, group_id,
@ -161,15 +178,17 @@ const createMonthlyDraw = async (req, res) => {
name: member.User.full_name, name: member.User.full_name,
mobile: member.User.mobile_number mobile: member.User.mobile_number
})), })),
winner_id: result.winner.user_id, winner_id: selectedWinnerId,
prize_amount: winnerPayout, prize_amount: calculatedPrizeAmount,
server_seed: serverSeed, server_seed: serverSeed,
server_seed_hash: crypto.createHash('sha256').update(serverSeed).digest('hex'), 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, nonce,
result_hash: result.hash, result_hash: resultHash,
status: 'completed', 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({ res.status(201).json({

View File

@ -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<AddPastDrawDialog> createState() => _AddPastDrawDialogState();
}
class _AddPastDrawDialogState extends State<AddPastDrawDialog> {
final _formKey = GlobalKey<FormState>();
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<void> _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<ChitGroupService>().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<Color>(Colors.white),
),
)
: Text(
'Add Draw',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
),
),
),
],
),
],
),
),
),
);
}
}

View File

@ -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<AddPastPaymentsDialog> createState() => _AddPastPaymentsDialogState();
}
class _AddPastPaymentsDialogState extends State<AddPastPaymentsDialog> {
final _apiService = ApiService();
final _chitGroupService = Get.find<ChitGroupService>();
Map<String, bool> _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<void> _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<Color>(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]},',
);
}
}

View File

@ -259,7 +259,7 @@ class _CreateGroupDialogState extends State<CreateGroupDialog> {
SizedBox(height: 16.h), SizedBox(height: 16.h),
// Financial Details Section // Financial Details Section
_buildSectionTitle('Financial Details'), _buildSectionTitle('Chitfund Details'),
SizedBox(height: 16.h), SizedBox(height: 16.h),
Row( Row(

View File

@ -188,16 +188,28 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
Widget _buildBasicInfoStep() { Widget _buildBasicInfoStep() {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( TextFormField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Chit Fund Name',
hintText: 'e.g., Family Chit Fund', hintText: 'e.g., Family Chit Fund',
prefixIcon: const Icon(Icons.group), prefixIcon: const Icon(Icons.group),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
), ),
filled: true,
fillColor: Colors.grey.shade50,
), ),
validator: (value) { validator: (value) {
if (value == null || value.trim().isEmpty) { if (value == null || value.trim().isEmpty) {
@ -211,72 +223,100 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
), ),
SizedBox(height: 20.h), SizedBox(height: 20.h),
Row( // Duration
children: [ Text(
Expanded( 'Duration (Months)',
child: TextFormField( style: TextStyle(
controller: _durationMonthsController, fontSize: 16.sp,
decoration: InputDecoration( fontWeight: FontWeight.w600,
labelText: 'Duration (Months)', color: Colors.grey.shade800,
hintText: 'e.g., 20', ),
prefixIcon: const Icon(Icons.calendar_today), ),
border: OutlineInputBorder( SizedBox(height: 8.h),
borderRadius: BorderRadius.circular(12.r), TextFormField(
), controller: _durationMonthsController,
), decoration: InputDecoration(
keyboardType: TextInputType.number, hintText: 'e.g., 20',
onChanged: (_) => _calculateMonthlyPayment(), prefixIcon: const Icon(Icons.calendar_today),
validator: (value) { helperText: 'Total number of months (6-60)',
if (value == null || value.isEmpty) { border: OutlineInputBorder(
return 'Required'; borderRadius: BorderRadius.circular(12.r),
}
final months = int.tryParse(value);
if (months == null || months < 6 || months > 60) {
return '6-60 months';
}
return null;
},
),
), ),
SizedBox(width: 16.w), filled: true,
Expanded( fillColor: Colors.grey.shade50,
child: TextFormField( ),
controller: _maxMembersController, keyboardType: TextInputType.number,
decoration: InputDecoration( onChanged: (_) => _calculateMonthlyPayment(),
labelText: 'Max Members', validator: (value) {
hintText: 'e.g., 20', if (value == null || value.isEmpty) {
prefixIcon: const Icon(Icons.people), return 'Required';
border: OutlineInputBorder( }
borderRadius: BorderRadius.circular(12.r), final months = int.tryParse(value);
), if (months == null || months < 6 || months > 60) {
), return '6-60 months';
keyboardType: TextInputType.number, }
validator: (value) { return null;
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), 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( TextFormField(
controller: _drawDateController, controller: _drawDateController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Draw Date (Day of Month)',
hintText: 'e.g., 15', hintText: 'e.g., 15',
prefixIcon: const Icon(Icons.casino), prefixIcon: const Icon(Icons.casino),
helperText: 'Day of month for lottery draw (1-31)', helperText: 'Day of month for lottery draw (1-31)',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
), ),
filled: true,
fillColor: Colors.grey.shade50,
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (value) { validator: (value) {
@ -298,16 +338,27 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Chitfund Value
Text(
'Chitfund Value (Fixed)',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 8.h),
TextFormField( TextFormField(
controller: _totalValueController, controller: _totalValueController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Fixed Target Value (Chit Value)',
hintText: 'e.g., 200000', hintText: 'e.g., 200000',
prefixIcon: const Icon(Icons.currency_rupee), prefixIcon: const Icon(Icons.currency_rupee),
helperText: 'Fixed target value of the chit fund', helperText: 'Fixed chitfund value of the group',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
), ),
filled: true,
fillColor: Colors.grey.shade50,
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (_) => _calculateMonthlyPayment(), onChanged: (_) => _calculateMonthlyPayment(),
@ -324,16 +375,27 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
), ),
SizedBox(height: 20.h), 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( TextFormField(
controller: _monthlyContributionController, controller: _monthlyContributionController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Monthly Contribution (Principal)',
hintText: 'e.g., 10000', hintText: 'e.g., 10000',
prefixIcon: const Icon(Icons.payment), prefixIcon: const Icon(Icons.payment),
helperText: 'Amount each member contributes monthly', helperText: 'Amount each member pays monthly',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
), ),
filled: true,
fillColor: Colors.grey.shade50,
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (_) => _calculateMonthlyPayment(), onChanged: (_) => _calculateMonthlyPayment(),
@ -350,35 +412,40 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
), ),
SizedBox(height: 20.h), SizedBox(height: 20.h),
Row( // Monthly Commission/Fee
children: [ Text(
Expanded( 'Monthly Commission/Fee',
child: TextFormField( style: TextStyle(
controller: _monthlyCommissionController, fontSize: 16.sp,
decoration: InputDecoration( fontWeight: FontWeight.w600,
labelText: 'Monthly Commission/Fee', color: Colors.grey.shade800,
hintText: 'e.g., 250', ),
prefixIcon: const Icon(Icons.percent), ),
helperText: 'Manager commission per month', SizedBox(height: 8.h),
border: OutlineInputBorder( TextFormField(
borderRadius: BorderRadius.circular(12.r), controller: _monthlyCommissionController,
), decoration: InputDecoration(
), hintText: 'e.g., 250',
keyboardType: TextInputType.number, prefixIcon: const Icon(Icons.currency_rupee),
onChanged: (_) => _calculateMonthlyPayment(), helperText: 'Chit Manager commission per month',
validator: (value) { border: OutlineInputBorder(
if (value == null || value.isEmpty) { borderRadius: BorderRadius.circular(12.r),
return 'Required';
}
final amount = double.tryParse(value);
if (amount == null || amount < 0) {
return 'Invalid';
}
return null;
},
),
), ),
], 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), SizedBox(height: 20.h),
@ -403,7 +470,7 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
_buildPaymentRow( _buildPaymentRow(
'Monthly Contribution', 'Monthly Payment',
_monthlyContributionController.text.isEmpty _monthlyContributionController.text.isEmpty
? '-' ? '-'
: '${_monthlyContributionController.text}', : '${_monthlyContributionController.text}',
@ -466,7 +533,7 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
_buildSummaryRow('Name', _nameController.text), _buildSummaryRow('Name', _nameController.text),
_buildSummaryRow('Target Value', '${_totalValueController.text}'), _buildSummaryRow('Chitfund Value', '${_totalValueController.text}'),
_buildSummaryRow('Duration', '${_durationMonthsController.text} months'), _buildSummaryRow('Duration', '${_durationMonthsController.text} months'),
_buildSummaryRow('Max Members', _maxMembersController.text), _buildSummaryRow('Max Members', _maxMembersController.text),
_buildSummaryRow('Monthly Payment', _getMonthlyPayment()), _buildSummaryRow('Monthly Payment', _getMonthlyPayment()),

View File

@ -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<ImportExistingGroupDialog> createState() => _ImportExistingGroupDialogState();
}
class _ImportExistingGroupDialogState extends State<ImportExistingGroupDialog> {
final _formKey = GlobalKey<FormState>();
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<void> _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<void> _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<Color>(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,
),
),
),
),
],
),
);
}
}

View File

@ -103,20 +103,26 @@ class _MemberGroupDetailsPageState extends State<MemberGroupDetailsPage>
child: ListView( child: ListView(
padding: EdgeInsets.all(16.w), padding: EdgeInsets.all(16.w),
children: [ children: [
// Group Info Card // Last Draw Winner Card (if exists)
_buildGroupInfoCard(group), 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), SizedBox(height: 16.h),
// My Status Card // My Status Card
if (myMember != null) _buildMyStatusCard(myMember), if (myMember != null) _buildMyStatusCard(myMember),
if (myMember != null) SizedBox(height: 16.h), if (myMember != null) SizedBox(height: 16.h),
// Members Card
_buildMembersCard(members),
SizedBox(height: 16.h),
// Quick Stats // Quick Stats
_buildQuickStats(group, myMember), _buildQuickStats(group, myMember),
SizedBox(height: 16.h),
// Members Card
_buildMembersCard(members),
], ],
), ),
); );
@ -179,12 +185,133 @@ class _MemberGroupDetailsPageState extends State<MemberGroupDetailsPage>
}); });
} }
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( return Card(
elevation: 2, elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)),
child: Padding( child: Padding(
padding: EdgeInsets.all(20.w), padding: EdgeInsets.all(16.w),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -192,9 +319,9 @@ class _MemberGroupDetailsPageState extends State<MemberGroupDetailsPage>
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'Group Details', 'Group Info',
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 16.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey.shade800, color: Colors.grey.shade800,
), ),
@ -202,20 +329,64 @@ class _MemberGroupDetailsPageState extends State<MemberGroupDetailsPage>
_buildStatusBadge(group.status), _buildStatusBadge(group.status),
], ],
), ),
SizedBox(height: 20.h), SizedBox(height: 16.h),
_buildInfoRow('Total Value', '${_formatCurrency(group.totalValue)}', Icons.account_balance_wallet), Row(
_buildInfoRow('Monthly Installment', '${_formatCurrency(group.monthlyInstallment)}', Icons.payment), children: [
_buildInfoRow('Duration', '${group.durationMonths} months', Icons.calendar_today), Expanded(child: _buildCompactInfo('Total Value', '${_formatCurrency(group.totalValue)}', Icons.account_balance_wallet)),
_buildInfoRow('Draw Date', '${group.drawDate}th of each month', Icons.event), SizedBox(width: 12.w),
_buildInfoRow('Members', '${group.currentMemberCount}/${group.maxMembers}', Icons.people), Expanded(child: _buildCompactInfo('Installment', '${_formatCurrency(group.monthlyInstallment)}', Icons.payment)),
if (group.startDate != null) ],
_buildInfoRow('Started On', _formatDate(group.startDate!), Icons.play_circle), ),
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) { Widget _buildMyStatusCard(dynamic member) {
return Card( return Card(
elevation: 2, elevation: 2,
@ -797,41 +968,6 @@ class _MemberGroupDetailsPageState extends State<MemberGroupDetailsPage>
); );
} }
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) { Widget _buildStatusBadge(String status) {
Color color; Color color;
String text; String text;

View File

@ -397,7 +397,7 @@ class _DynamicChitCalculatorState extends State<DynamicChitCalculator> {
Icon(Icons.analytics, color: Colors.orange.shade700, size: 18.w), Icon(Icons.analytics, color: Colors.orange.shade700, size: 18.w),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Text( Text(
'Financial Analysis', 'Chitfund Analysis',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -437,8 +437,8 @@ class _DynamicChitCalculatorState extends State<DynamicChitCalculator> {
Divider(color: Colors.orange.shade300), Divider(color: Colors.orange.shade300),
SizedBox(height: 8.h), SizedBox(height: 8.h),
_buildSummaryRow('Fixed Chit Value', '${widget.chitValue.toStringAsFixed(0)}'), _buildSummaryRow('Chit Amount', '${widget.chitValue.toStringAsFixed(0)}'),
_buildSummaryRow('Monthly Payment (Fixed)', '${totalPayment.toStringAsFixed(0)}'), _buildSummaryRow('Monthly Installment (Fixed)', '${totalPayment.toStringAsFixed(0)}'),
_buildSummaryRow('Total per Member', '${totalPerMember.toStringAsFixed(0)}'), _buildSummaryRow('Total per Member', '${totalPerMember.toStringAsFixed(0)}'),
_buildSummaryRow( _buildSummaryRow(
'Lift Amount Range', 'Lift Amount Range',

View File

@ -387,7 +387,7 @@ class EnhancedMonthlyScheduleTable extends StatelessWidget {
Icon(Icons.summarize_rounded, color: Colors.orange.shade700, size: 18.w), Icon(Icons.summarize_rounded, color: Colors.orange.shade700, size: 18.w),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Text( Text(
'Financial Summary', 'Chitfund Summary',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -466,7 +466,7 @@ class EnhancedMonthlyScheduleTable extends StatelessWidget {
SizedBox(width: 12.w), SizedBox(width: 12.w),
Expanded( Expanded(
child: Text( child: Text(
'Enter financial details to see the month-wise schedule', 'Enter chitfund details to see the month-wise schedule',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.orange.shade700, color: Colors.orange.shade700,
@ -526,7 +526,7 @@ class DetailedMonthlyScheduleTable extends StatelessWidget {
), ),
DataColumn( DataColumn(
label: Text( label: Text(
'Target Value', 'Chitfund Value',
style: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.bold),
), ),
numeric: true, numeric: true,

View File

@ -81,7 +81,7 @@ class FinancialTable extends StatelessWidget {
SizedBox(width: 12.w), SizedBox(width: 12.w),
Expanded( Expanded(
child: Text( child: Text(
'Financial Summary', 'Chitfund Summary',
style: TextStyle( style: TextStyle(
fontSize: 22.sp, fontSize: 22.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -260,7 +260,7 @@ class CompactFinancialTable extends StatelessWidget {
SizedBox(width: 12.w), SizedBox(width: 12.w),
Expanded( Expanded(
child: Text( child: Text(
'Financial Summary', 'Chitfund Summary',
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 18.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@ -293,7 +293,7 @@ class MonthlyScheduleTable extends StatelessWidget {
SizedBox(width: 12.w), SizedBox(width: 12.w),
Expanded( Expanded(
child: Text( 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( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.orange.shade700, color: Colors.orange.shade700,

View File

@ -240,7 +240,7 @@ class FinancialTableTestPage extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
Text( Text(
'Financial Summary Table', 'Chitfund Summary Table',
style: TextStyle( style: TextStyle(
fontSize: 24.sp, fontSize: 24.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,