624 lines
21 KiB
Dart
624 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../core/utils/snackbar_util.dart';
|
|
|
|
/// Dynamic Chit Fund Calculator with adjustable parameters
|
|
class DynamicChitCalculator extends StatefulWidget {
|
|
final double chitValue;
|
|
final int durationMonths;
|
|
final double subscriptionAmount;
|
|
final double commissionAmount;
|
|
final ValueChanged<Map<String, dynamic>>? onCalculationsChanged;
|
|
|
|
const DynamicChitCalculator({
|
|
super.key,
|
|
required this.chitValue,
|
|
required this.durationMonths,
|
|
required this.subscriptionAmount,
|
|
required this.commissionAmount,
|
|
this.onCalculationsChanged,
|
|
});
|
|
|
|
@override
|
|
State<DynamicChitCalculator> createState() => _DynamicChitCalculatorState();
|
|
}
|
|
|
|
class _DynamicChitCalculatorState extends State<DynamicChitCalculator> {
|
|
// Adjustable parameters
|
|
double _startingPercentage = 87.65; // First month lifter gets 87.65%
|
|
double _endingPercentage = 112.35; // Last month lifter gets 112.35% (more than chit value!)
|
|
String _calculationMode = 'linear'; // linear, custom, accelerated
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Calculator controls
|
|
_buildCalculatorControls(),
|
|
SizedBox(height: 16.h),
|
|
|
|
// Month-wise table
|
|
_buildCompleteScheduleTable(),
|
|
SizedBox(height: 16.h),
|
|
|
|
// Summary & insights
|
|
_buildSummaryCard(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCalculatorControls() {
|
|
return Card(
|
|
elevation: 0,
|
|
color: Colors.blue.shade50,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
side: BorderSide(color: Colors.blue.shade200),
|
|
),
|
|
child: ExpansionTile(
|
|
leading: Icon(Icons.tune, color: Colors.blue.shade700),
|
|
title: Text(
|
|
'Adjust Lift Amount Calculations',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.blue.shade800,
|
|
),
|
|
),
|
|
subtitle: Text(
|
|
'Customize how much lifters get each month',
|
|
style: TextStyle(fontSize: 12.sp),
|
|
),
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.all(16.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Starting percentage
|
|
Text(
|
|
'First Month Lifter Gets:',
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Slider(
|
|
value: _startingPercentage,
|
|
min: 70,
|
|
max: 95,
|
|
divisions: 50,
|
|
label: '${_startingPercentage.toStringAsFixed(1)}%',
|
|
activeColor: Colors.green.shade600,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_startingPercentage = value;
|
|
_notifyChanges();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 100.w,
|
|
child: Text(
|
|
'${_startingPercentage.toStringAsFixed(1)}% = ₹${_calculateLiftAmount(1).toStringAsFixed(0)}',
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.green.shade700,
|
|
),
|
|
textAlign: TextAlign.right,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 16.h),
|
|
|
|
// Ending percentage
|
|
Text(
|
|
'Last Month Lifter Gets:',
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Slider(
|
|
value: _endingPercentage,
|
|
min: 95,
|
|
max: 120,
|
|
divisions: 50,
|
|
label: '${_endingPercentage.toStringAsFixed(1)}%',
|
|
activeColor: Colors.purple.shade600,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_endingPercentage = value;
|
|
_notifyChanges();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 100.w,
|
|
child: Text(
|
|
'${_endingPercentage.toStringAsFixed(1)}% = ₹${_calculateLiftAmount(widget.durationMonths).toStringAsFixed(0)}',
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.purple.shade700,
|
|
),
|
|
textAlign: TextAlign.right,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 16.h),
|
|
|
|
// Calculation mode
|
|
Text(
|
|
'Progression Mode:',
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Wrap(
|
|
spacing: 8.w,
|
|
children: [
|
|
ChoiceChip(
|
|
label: const Text('Linear'),
|
|
selected: _calculationMode == 'linear',
|
|
onSelected: (selected) {
|
|
if (selected) {
|
|
setState(() {
|
|
_calculationMode = 'linear';
|
|
_notifyChanges();
|
|
});
|
|
}
|
|
},
|
|
selectedColor: Colors.green.shade200,
|
|
),
|
|
ChoiceChip(
|
|
label: const Text('Accelerated'),
|
|
selected: _calculationMode == 'accelerated',
|
|
onSelected: (selected) {
|
|
if (selected) {
|
|
setState(() {
|
|
_calculationMode = 'accelerated';
|
|
_notifyChanges();
|
|
});
|
|
}
|
|
},
|
|
selectedColor: Colors.orange.shade200,
|
|
),
|
|
ChoiceChip(
|
|
label: const Text('Custom'),
|
|
selected: _calculationMode == 'custom',
|
|
onSelected: (selected) {
|
|
if (selected) {
|
|
setState(() {
|
|
_calculationMode = 'custom';
|
|
_notifyChanges();
|
|
});
|
|
}
|
|
},
|
|
selectedColor: Colors.purple.shade200,
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 12.h),
|
|
|
|
// Preset buttons
|
|
Wrap(
|
|
spacing: 8.w,
|
|
runSpacing: 8.h,
|
|
children: [
|
|
OutlinedButton.icon(
|
|
onPressed: () => _applyPreset('conservative'),
|
|
icon: Icon(Icons.shield, size: 16.w),
|
|
label: const Text('Conservative'),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
|
),
|
|
),
|
|
OutlinedButton.icon(
|
|
onPressed: () => _applyPreset('balanced'),
|
|
icon: Icon(Icons.balance, size: 16.w),
|
|
label: const Text('Balanced'),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
|
),
|
|
),
|
|
OutlinedButton.icon(
|
|
onPressed: () => _applyPreset('aggressive'),
|
|
icon: Icon(Icons.trending_up, size: 16.w),
|
|
label: const Text('Aggressive'),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
|
),
|
|
),
|
|
OutlinedButton.icon(
|
|
onPressed: () => _applyPreset('sample'),
|
|
icon: Icon(Icons.file_copy, size: 16.w),
|
|
label: const Text('Your Sample'),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCompleteScheduleTable() {
|
|
return Card(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Container(
|
|
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width - 32.w),
|
|
child: SingleChildScrollView(
|
|
child: DataTable(
|
|
headingRowHeight: 56.h,
|
|
headingRowColor: MaterialStateProperty.all(Colors.green.shade600),
|
|
columnSpacing: 12.w,
|
|
horizontalMargin: 12.w,
|
|
dataRowMinHeight: 48.h,
|
|
dataRowMaxHeight: 56.h,
|
|
columns: [
|
|
_buildColumn('Month'),
|
|
_buildColumn('Chit\nValue'),
|
|
_buildColumn('Lifter\nGets', tooltip: 'Net amount paid to lifter'),
|
|
_buildColumn('Sub.', tooltip: 'Monthly subscription'),
|
|
_buildColumn('Fee'),
|
|
_buildColumn('Total\nPayment'),
|
|
_buildColumn('Dividend', tooltip: 'Chit Value - Lift Amount'),
|
|
],
|
|
rows: List.generate(widget.durationMonths, (index) {
|
|
return _buildDataRow(index + 1);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
DataColumn _buildColumn(String label, {String? tooltip}) {
|
|
return DataColumn(
|
|
label: Tooltip(
|
|
message: tooltip ?? label,
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 11.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
DataRow _buildDataRow(int month) {
|
|
final monthDate = _getMonthDate(month);
|
|
final liftAmount = _calculateLiftAmount(month);
|
|
final dividendAmount = widget.chitValue - liftAmount;
|
|
final totalPayment = widget.subscriptionAmount + widget.commissionAmount;
|
|
|
|
final isEarly = month <= (widget.durationMonths * 0.3).ceil();
|
|
final isLast = month == widget.durationMonths;
|
|
|
|
final rowColor = isLast
|
|
? Colors.purple.shade50
|
|
: isEarly
|
|
? Colors.green.shade50
|
|
: null;
|
|
|
|
return DataRow(
|
|
color: MaterialStateProperty.all(rowColor),
|
|
cells: [
|
|
_buildCell('$month\n$monthDate', isHeader: true),
|
|
_buildCell('₹${widget.chitValue.toStringAsFixed(0)}'),
|
|
_buildCell(
|
|
'₹${liftAmount.toStringAsFixed(0)}',
|
|
color: Colors.green.shade700,
|
|
bold: true,
|
|
),
|
|
_buildCell('₹${widget.subscriptionAmount.toStringAsFixed(0)}'),
|
|
_buildCell('₹${widget.commissionAmount.toStringAsFixed(0)}'),
|
|
_buildCell(
|
|
'₹${totalPayment.toStringAsFixed(0)}',
|
|
bold: true,
|
|
),
|
|
_buildCell(
|
|
'${dividendAmount >= 0 ? "+" : ""}₹${dividendAmount.toStringAsFixed(0)}',
|
|
color: dividendAmount >= 0 ? Colors.blue.shade700 : Colors.red.shade700,
|
|
bold: true,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
DataCell _buildCell(String text, {bool isHeader = false, Color? color, bool bold = false}) {
|
|
return DataCell(
|
|
Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontSize: isHeader ? 11.sp : 12.sp,
|
|
fontWeight: bold || isHeader ? FontWeight.w600 : FontWeight.normal,
|
|
color: color ?? Colors.grey.shade800,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSummaryCard() {
|
|
final totalPayment = widget.subscriptionAmount + widget.commissionAmount;
|
|
final totalPerMember = totalPayment * widget.durationMonths;
|
|
final firstMonthLift = _calculateLiftAmount(1);
|
|
final lastMonthLift = _calculateLiftAmount(widget.durationMonths);
|
|
final firstMonthDividend = widget.chitValue - firstMonthLift;
|
|
final lastMonthDividend = widget.chitValue - lastMonthLift;
|
|
|
|
return Card(
|
|
elevation: 0,
|
|
color: Colors.orange.shade50,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
side: BorderSide(color: Colors.orange.shade200),
|
|
),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.analytics, color: Colors.orange.shade700, size: 18.w),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
'Financial Analysis',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.orange.shade800,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 12.h),
|
|
|
|
// Key metrics
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildMetricCard(
|
|
'First Month',
|
|
'Lifter: ₹${firstMonthLift.toStringAsFixed(0)}',
|
|
'Dividend: +₹${firstMonthDividend.toStringAsFixed(0)}',
|
|
Colors.green.shade600,
|
|
Icons.rocket_launch,
|
|
),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: _buildMetricCard(
|
|
'Last Month',
|
|
'Lifter: ₹${lastMonthLift.toStringAsFixed(0)}',
|
|
'Dividend: ${lastMonthDividend >= 0 ? "+" : ""}₹${lastMonthDividend.toStringAsFixed(0)}',
|
|
Colors.purple.shade600,
|
|
Icons.flag,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 12.h),
|
|
|
|
Divider(color: Colors.orange.shade300),
|
|
SizedBox(height: 8.h),
|
|
|
|
_buildSummaryRow('Fixed Chit Value', '₹${widget.chitValue.toStringAsFixed(0)}'),
|
|
_buildSummaryRow('Monthly Payment (Fixed)', '₹${totalPayment.toStringAsFixed(0)}'),
|
|
_buildSummaryRow('Total per Member', '₹${totalPerMember.toStringAsFixed(0)}'),
|
|
_buildSummaryRow(
|
|
'Lift Amount Range',
|
|
'₹${firstMonthLift.toStringAsFixed(0)} → ₹${lastMonthLift.toStringAsFixed(0)}',
|
|
),
|
|
_buildSummaryRow(
|
|
'Monthly Increment',
|
|
'₹${_calculateMonthlyIncrement().toStringAsFixed(0)}',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMetricCard(String title, String value1, String value2, Color color, IconData icon) {
|
|
return Container(
|
|
padding: EdgeInsets.all(12.w),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
border: Border.all(color: color.withOpacity(0.3)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(icon, color: color, size: 16.w),
|
|
SizedBox(width: 6.w),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 11.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: color,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 6.h),
|
|
Text(
|
|
value1,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
Text(
|
|
value2,
|
|
style: TextStyle(
|
|
fontSize: 11.sp,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSummaryRow(String label, String value) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 4.h),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Calculate lift amount for a specific month
|
|
double _calculateLiftAmount(int month) {
|
|
if (widget.durationMonths <= 1) return widget.chitValue;
|
|
|
|
final double startPercentage = _startingPercentage / 100;
|
|
final double endPercentage = _endingPercentage / 100;
|
|
|
|
double percentage;
|
|
|
|
switch (_calculationMode) {
|
|
case 'linear':
|
|
// Linear progression
|
|
final double incrementPerMonth = (endPercentage - startPercentage) / (widget.durationMonths - 1);
|
|
percentage = startPercentage + (incrementPerMonth * (month - 1));
|
|
break;
|
|
|
|
case 'accelerated':
|
|
// Accelerated - grows faster in later months
|
|
final double progress = (month - 1) / (widget.durationMonths - 1);
|
|
final double accelerationFactor = progress * progress; // Quadratic
|
|
percentage = startPercentage + ((endPercentage - startPercentage) * accelerationFactor);
|
|
break;
|
|
|
|
case 'custom':
|
|
// Custom - could add more complex formulas here
|
|
final double incrementPerMonth = (endPercentage - startPercentage) / (widget.durationMonths - 1);
|
|
percentage = startPercentage + (incrementPerMonth * (month - 1));
|
|
break;
|
|
|
|
default:
|
|
percentage = startPercentage;
|
|
}
|
|
|
|
return widget.chitValue * percentage;
|
|
}
|
|
|
|
double _calculateMonthlyIncrement() {
|
|
if (widget.durationMonths <= 1) return 0;
|
|
final first = _calculateLiftAmount(1);
|
|
final second = _calculateLiftAmount(2);
|
|
return second - first;
|
|
}
|
|
|
|
String _getMonthDate(int monthNumber) {
|
|
final now = DateTime.now();
|
|
final futureMonth = DateTime(now.year, now.month + monthNumber - 1);
|
|
return DateFormat('MMM-yy').format(futureMonth);
|
|
}
|
|
|
|
void _applyPreset(String preset) {
|
|
setState(() {
|
|
switch (preset) {
|
|
case 'conservative':
|
|
_startingPercentage = 90.0;
|
|
_endingPercentage = 100.0;
|
|
_calculationMode = 'linear';
|
|
break;
|
|
case 'balanced':
|
|
_startingPercentage = 85.0;
|
|
_endingPercentage = 105.0;
|
|
_calculationMode = 'linear';
|
|
break;
|
|
case 'aggressive':
|
|
_startingPercentage = 80.0;
|
|
_endingPercentage = 115.0;
|
|
_calculationMode = 'accelerated';
|
|
break;
|
|
case 'sample':
|
|
// Match your exact sample data
|
|
_startingPercentage = 87.65;
|
|
_endingPercentage = 112.35;
|
|
_calculationMode = 'linear';
|
|
break;
|
|
}
|
|
_notifyChanges();
|
|
});
|
|
|
|
SnackbarUtil.showSuccess('Preset "$preset" applied!');
|
|
}
|
|
|
|
void _notifyChanges() {
|
|
if (widget.onCalculationsChanged != null) {
|
|
widget.onCalculationsChanged!({
|
|
'startingPercentage': _startingPercentage,
|
|
'endingPercentage': _endingPercentage,
|
|
'calculationMode': _calculationMode,
|
|
'firstMonthLift': _calculateLiftAmount(1),
|
|
'lastMonthLift': _calculateLiftAmount(widget.durationMonths),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|