added past chits and updated labels
This commit is contained in:
parent
38fdc84128
commit
2213a6524c
|
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,18 +117,40 @@ 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;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
// Generate provably fair result
|
|
||||||
const result = generateProvablyFairResult(
|
const result = generateProvablyFairResult(
|
||||||
serverSeed,
|
serverSeed,
|
||||||
client_seed || 'default',
|
client_seed || 'default',
|
||||||
|
|
@ -136,19 +158,14 @@ const createMonthlyDraw = async (req, res) => {
|
||||||
availableMembers
|
availableMembers
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate prize amount using generic formula
|
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({
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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]},',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,18 +223,27 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
|
||||||
),
|
),
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 20.h),
|
||||||
|
|
||||||
Row(
|
// Duration
|
||||||
children: [
|
Text(
|
||||||
Expanded(
|
'Duration (Months)',
|
||||||
child: TextFormField(
|
style: TextStyle(
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8.h),
|
||||||
|
TextFormField(
|
||||||
controller: _durationMonthsController,
|
controller: _durationMonthsController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Duration (Months)',
|
|
||||||
hintText: 'e.g., 20',
|
hintText: 'e.g., 20',
|
||||||
prefixIcon: const Icon(Icons.calendar_today),
|
prefixIcon: const Icon(Icons.calendar_today),
|
||||||
|
helperText: 'Total number of months (6-60)',
|
||||||
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(),
|
||||||
|
|
@ -237,18 +258,29 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
|
||||||
|
// Max Members
|
||||||
|
Text(
|
||||||
|
'Max Participants',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
),
|
),
|
||||||
SizedBox(width: 16.w),
|
),
|
||||||
Expanded(
|
SizedBox(height: 8.h),
|
||||||
child: TextFormField(
|
TextFormField(
|
||||||
controller: _maxMembersController,
|
controller: _maxMembersController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Max Members',
|
|
||||||
hintText: 'e.g., 20',
|
hintText: 'e.g., 20',
|
||||||
prefixIcon: const Icon(Icons.people),
|
prefixIcon: const Icon(Icons.people),
|
||||||
|
helperText: 'Maximum number of members (2-100)',
|
||||||
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) {
|
||||||
|
|
@ -262,21 +294,29 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 20.h),
|
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,19 +412,27 @@ 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(
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8.h),
|
||||||
|
TextFormField(
|
||||||
controller: _monthlyCommissionController,
|
controller: _monthlyCommissionController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Monthly Commission/Fee',
|
|
||||||
hintText: 'e.g., 250',
|
hintText: 'e.g., 250',
|
||||||
prefixIcon: const Icon(Icons.percent),
|
prefixIcon: const Icon(Icons.currency_rupee),
|
||||||
helperText: 'Manager commission per month',
|
helperText: 'Chit Manager commission per month',
|
||||||
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(),
|
||||||
|
|
@ -377,9 +447,6 @@ class _CreateGroupPageState extends State<CreateGroupPage> {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 20.h),
|
||||||
|
|
||||||
// Calculated monthly payment display
|
// Calculated monthly payment display
|
||||||
|
|
@ -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()),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue