fix_payout

This commit is contained in:
sanjeev.p 2026-04-04 09:14:15 +05:30
parent e65290cc7b
commit 1988ebeca7
3 changed files with 576 additions and 892 deletions

View File

@ -1360,6 +1360,8 @@ class ApiService {
if (_token == null) await _initializeToken(); if (_token == null) await _initializeToken();
final url = Uri.parse('${Env.apiUrl}partner/$id/details'); final url = Uri.parse('${Env.apiUrl}partner/$id/details');
// final url = Uri.parse('http://localhost/nhance_partner_be/partner/$id/details');
final headers = { final headers = {
'Authorization': 'Bearer $_token', 'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature, 'app-signature': Env.App_Signature,
@ -1374,6 +1376,7 @@ class ApiService {
if (_token == null) await _initializeToken(); if (_token == null) await _initializeToken();
final url = Uri.parse('${Env.apiUrl}partner/$id/policies'); final url = Uri.parse('${Env.apiUrl}partner/$id/policies');
// final url = Uri.parse('http://localhost/nhance_partner_be/partner/$id/details/policies');
final headers = { final headers = {
'Authorization': 'Bearer $_token', 'Authorization': 'Bearer $_token',
@ -1393,6 +1396,7 @@ class ApiService {
if (_token == null) await _initializeToken(); if (_token == null) await _initializeToken();
final url = Uri.parse('${Env.apiUrl}partner/$id/renewals?days=$days'); final url = Uri.parse('${Env.apiUrl}partner/$id/renewals?days=$days');
// final url = Uri.parse('http://localhost/nhance_partner_be/partner/$id/renewals?days=$days');
final headers = { final headers = {
'Authorization': 'Bearer $_token', 'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature, 'app-signature': Env.App_Signature,
@ -1401,13 +1405,19 @@ class ApiService {
return response; return response;
} }
/// GET /partner/{id}/earnings /// GET /partner/{id}/earnings?financial_year=2025-2026
/// Returns: list of monthly earning records with /// Monthly rows for that Indian FY: month_key, month_label, premium, policies, payout.
/// month_key, month_label, premium, policies, paid/status Future<Map<String, dynamic>> getPartnerEarnings(
Future<Map<String, dynamic>> getPartnerEarnings(dynamic id) async { dynamic id, {
String? financialYear,
}) async {
if (_token == null) await _initializeToken(); if (_token == null) await _initializeToken();
final url = Uri.parse('${Env.apiUrl}partner/$id/earnings'); final q = (financialYear != null && financialYear.isNotEmpty)
? '?financial_year=${Uri.encodeQueryComponent(financialYear)}'
: '';
final url = Uri.parse('${Env.apiUrl}partner/$id/earnings$q');
// final url = Uri.parse('http://localhost/nhance_partner_be/partner/$id/earnings$q');
final headers = { final headers = {
'Authorization': 'Bearer $_token', 'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature, 'app-signature': Env.App_Signature,
@ -2078,6 +2088,7 @@ class ApiService {
Future<Map<String, dynamic>> getCreateOrUpdate(data) async { Future<Map<String, dynamic>> getCreateOrUpdate(data) async {
final url = Uri.parse('${Env.apiUrl}invoice/create-or-update'); final url = Uri.parse('${Env.apiUrl}invoice/create-or-update');
// final url = Uri.parse('http://localhost/nhance_partner_be/invoice/create-or-update');
// final token = await getToken(); // Fetch token // final token = await getToken(); // Fetch token

View File

@ -43,8 +43,7 @@ class _State extends ConsumerState<PartnerPortalDashboard> {
List<Map<String, dynamic>> allPolicies = []; List<Map<String, dynamic>> allPolicies = [];
List<Map<String, dynamic>> allRenewals = []; List<Map<String, dynamic>> allRenewals = [];
List<Map<String, dynamic>> filteredRenewals = []; List<Map<String, dynamic>> filteredRenewals = [];
int selectedRenewalDays = 10, renewalPage = 0; int selectedRenewalDays = 10;
static const kPerPage = 4;
List<Map<String, dynamic>> allEarnings = []; List<Map<String, dynamic>> allEarnings = [];
String selectedFY = _currentFinYear(); String selectedFY = _currentFinYear();
@ -96,57 +95,48 @@ class _State extends ConsumerState<PartnerPortalDashboard> {
selectedRenewalDays = days; selectedRenewalDays = days;
allRenewals = raw; allRenewals = raw;
filteredRenewals = raw; filteredRenewals = raw;
renewalPage = 0;
}); });
} }
} }
Future<void> _fetchEarnings(dynamic id) async { Future<void> _fetchEarnings(dynamic id, {String? financialYear}) async {
final r = await api.getPartnerEarnings(id); final fy = financialYear ?? selectedFY;
if (r['status'] == 'success') setState(() => allEarnings = List<Map<String, dynamic>>.from((r['data'] ?? []).map((e) => Map<String, dynamic>.from(e)))); final r = await api.getPartnerEarnings(id, financialYear: fy);
if (r['status'] == 'success') {
setState(() {
allEarnings = List<Map<String, dynamic>>.from(
(r['data'] ?? []).map((e) => Map<String, dynamic>.from(e)),
);
});
}
} }
void _applyRenewalFilter(int days) { void _applyRenewalFilter(int days) {
setState(() { setState(() {
selectedRenewalDays = days; selectedRenewalDays = days;
filteredRenewals = allRenewals.where((r) => (r['days_left'] ?? 999) <= days).toList(); filteredRenewals = allRenewals.where((r) => (r['days_left'] ?? 999) <= days).toList();
renewalPage = 0;
}); });
} }
List<Map<String, dynamic>> get _fyEarnings { /// Backend returns months for the requested [financial_year] only.
final s = _fyStart(selectedFY), e = _fyEnd(selectedFY);
return allEarnings.where((row) {
try {
final raw = row['month_key']?.toString() ?? '';
final dt = raw.length == 7 ? DateTime.parse('$raw-01') : DateTime.tryParse(raw);
if (dt == null) return true;
return !dt.isBefore(s) && !dt.isAfter(e);
} catch (_) { return true; }
}).toList();
}
Map<String, dynamic> get _fyAgg { Map<String, dynamic> get _fyAgg {
double pre = 0, com = 0, tds = 0, net = 0; int pol = 0; bool allPaid = true; double premium = 0;
for (final e in _fyEarnings) { double payout = 0;
pre += _d(e['premium']); com += _d(e['commission']); int policies = 0;
tds += _d(e['tds']); net += _d(e['net_payout']); for (final e in allEarnings) {
pol += int.tryParse(e['policies']?.toString() ?? '0') ?? 0; premium += _d(e['premium']);
if ((e['paid']?.toString()) != '1' && e['paid'] != true) allPaid = false; policies += int.tryParse(e['policies']?.toString() ?? '0') ?? 0;
payout += _d(e['payout']);
} }
if (com == 0 && pre > 0) { com = pre * 0.15; tds = com * 0.10; net = com - tds; } return {
return { 'premium': pre, 'commission': com, 'tds': tds, 'netPayout': net, 'policies': pol, 'paid': allPaid && _fyEarnings.isNotEmpty }; 'premium': premium,
'policies': policies,
'payout': payout,
};
} }
double _d(dynamic v) => double.tryParse(v?.toString() ?? '0') ?? 0; double _d(dynamic v) => double.tryParse(v?.toString() ?? '0') ?? 0;
List<Map<String, dynamic>> get _pageRenewals {
final s = renewalPage * kPerPage;
final e = (s + kPerPage).clamp(0, filteredRenewals.length);
return s >= filteredRenewals.length ? [] : filteredRenewals.sublist(s, e);
}
int get _totalPages => (filteredRenewals.length / kPerPage).ceil();
String _inr(dynamic v) { String _inr(dynamic v) {
final n = _d(v); final n = _d(v);
if (n >= 10000000) return 'Rs.${(n/10000000).toStringAsFixed(1)}Cr'; if (n >= 10000000) return 'Rs.${(n/10000000).toStringAsFixed(1)}Cr';
@ -180,14 +170,12 @@ class _State extends ConsumerState<PartnerPortalDashboard> {
_PartnerProfileCard(details: partnerDetails, initials: _initials(partnerDetails['agent_name']?.toString() ?? 'PD')), _PartnerProfileCard(details: partnerDetails, initials: _initials(partnerDetails['agent_name']?.toString() ?? 'PD')),
const SizedBox(height: 14), const SizedBox(height: 14),
_label('Overview'), _label('Overview'),
_OverviewBar(details: partnerDetails, inr: _inr), _OverviewBar(details: partnerDetails, inr: _inr, overviewFinYear: partnerDetails['overview_financial_year']?.toString()),
const SizedBox(height: 14), const SizedBox(height: 14),
// _label('Policy List & Renewal Alerts'), // _label('Policy List & Renewal Alerts'),
_PolicyRenewalRow( _PolicyRenewalRow(
policies: allPolicies, renewals: filteredRenewals,
currentPageRenewals: _pageRenewals, totalRenewalPages: _totalPages, selectedRenewalDays: selectedRenewalDays,
renewalPage: renewalPage, selectedRenewalDays: selectedRenewalDays,
filteredRenewalsCount: filteredRenewals.length,
onRenewalDays: (d) { onRenewalDays: (d) {
final id = _agentId ?? ref.read(userIdProvider); final id = _agentId ?? ref.read(userIdProvider);
if (id != null) { if (id != null) {
@ -196,15 +184,20 @@ class _State extends ConsumerState<PartnerPortalDashboard> {
_applyRenewalFilter(d); _applyRenewalFilter(d);
} }
}, },
onRenewalPageChanged: (p) => setState(() => renewalPage = p),
inr: _inr, inr: _inr,
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
_label('Earning Details'), _label('Earning Details'),
_EarningDetailsSection( _EarningDetailsSection(
allFinYears: _allFinYears(), selectedFinYear: selectedFY, allFinYears: _allFinYears(),
onFinYearChanged: (fy) => setState(() => selectedFY = fy), selectedFinYear: selectedFY,
aggregate: _fyAgg, earningsForFY: _fyEarnings, inr: _inr, onFinYearChanged: (fy) {
final id = _agentId ?? ref.read(userIdProvider);
setState(() => selectedFY = fy);
if (id != null) _fetchEarnings(id, financialYear: fy);
},
aggregate: _fyAgg,
inr: _inr,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
]), ]),
@ -272,24 +265,24 @@ class _PartnerProfileCard extends StatelessWidget {
} }
// //
// OVERVIEW BAR // OVERVIEW BAR metrics scoped to current Indian FY (from API overview_financial_year)
// mapped_policies / active_policies COUNT from partner_policy WHERE agent_id=$id
// total_premium / commission_earned SUM from partner_policy
// enquiry_* COUNT from partner_enquiry WHERE agent_id=$id, is_active=1
// endorsement_* COUNT from partner_endorsement_request WHERE agent_id=$id, is_active=1
// Clients removed. Mapped Policies shows Active dot only.
// //
class _OverviewBar extends StatelessWidget { class _OverviewBar extends StatelessWidget {
final Map<String, dynamic> details; final String Function(dynamic) inr; final Map<String, dynamic> details;
const _OverviewBar({required this.details, required this.inr}); final String Function(dynamic) inr;
/// Same format as [selectedFY], e.g. "2026-2027"; drives FY label under premium.
final String? overviewFinYear;
const _OverviewBar({required this.details, required this.inr, this.overviewFinYear});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final fyTag = _fyShort(overviewFinYear ?? _currentFinYear());
final mp = details['mapped_policies'] ?? '0'; final mp = details['mapped_policies'] ?? '0';
final ip = details['issued_policies'] ?? '0'; // policy_number IS NOT NULL final ip = details['issued_policies'] ?? '0'; // policy_number IS NOT NULL
final pp2 = details['pending_policies'] ?? '0'; // policy_number IS NULL final pp2 = details['pending_policies'] ?? '0'; // policy_number IS NULL
final tp = details['total_premium'] ?? '0'; final tp = details['total_premium'] ?? '0';
final com = details['commission_earned'] ?? '0'; final com = details['commission_earned'] ?? '0';
final cr = details['commission_rate'] ?? '15'; final cpd = details['commission_paid'] ?? '0';
final cup = details['commission_unpaid'] ?? '0';
final eq = details['enquiry_total'] ?? '0'; final eq = details['enquiry_total'] ?? '0';
final eqd = details['enquiry_completed'] ?? '0'; final eqd = details['enquiry_completed'] ?? '0';
final eqp = details['enquiry_pending'] ?? '0'; final eqp = details['enquiry_pending'] ?? '0';
@ -303,16 +296,22 @@ class _OverviewBar extends StatelessWidget {
child: LayoutBuilder(builder: (ctx, box) { child: LayoutBuilder(builder: (ctx, box) {
final wide = box.maxWidth > 700; final wide = box.maxWidth > 700;
if (wide) return IntrinsicHeight(child: Row(children: [ if (wide) return IntrinsicHeight(child: Row(children: [
_sc('Mapped Policies', mp.toString(), const Color(0xFF0ABFA3), [_dot(const Color(0xFF10B981), '$ip Issued'), _dot(const Color(0xFFF97316), '$pp2 Pending')], false), _sc('Policies', mp.toString(), const Color(0xFF0ABFA3), [_dot(const Color(0xFF10B981), '$ip Issued'), _dot(const Color(0xFFF97316), '$pp2 Pending')], false),
_sc('Total Premium', inr(tp), const Color(0xFF10B981), [_dot(Colors.grey, 'FY ${_currentFinYear()}')]), _sc('Total Premium', inr(tp), const Color(0xFF10B981), [_dot(Colors.grey, 'FY $fyTag')]),
_sc('Delegation', inr(com), const Color(0xFFF97316), [_dot(Colors.grey, '$cr% rate')]), _sc('Total Payout', inr(com), const Color(0xFFF97316), [
_dot(const Color(0xFF006B5C), 'Paid ${inr(cpd)}'),
_dot(const Color(0xFF64748B), 'Unpaid ${inr(cup)}'),
]),
_fc('Enquiry', eqd.toString(), eq.toString(), const Color(0xFF3B82F6), [_dot(const Color(0xFF10B981), '$eqd Done'), _dot(const Color(0xFFF97316), '$eqp Pending')]), _fc('Enquiry', eqd.toString(), eq.toString(), const Color(0xFF3B82F6), [_dot(const Color(0xFF10B981), '$eqd Done'), _dot(const Color(0xFFF97316), '$eqp Pending')]),
_fc('Endorsement', end.toString(), en.toString(), const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done'), _dot(const Color(0xFFF97316), '$enp Pending')]), _fc('Endorsement', end.toString(), en.toString(), const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done'), _dot(const Color(0xFFF97316), '$enp Pending')]),
])); ]));
return Wrap(children: [ return Wrap(children: [
_wc('Mapped Policies', mp.toString(), const Color(0xFF0ABFA3), [_dot(const Color(0xFF10B981), '$ip Issued'), _dot(const Color(0xFFF97316), '$pp2 Pending')]), _wc('Policies', mp.toString(), const Color(0xFF0ABFA3), [_dot(const Color(0xFF10B981), '$ip Issued'), _dot(const Color(0xFFF97316), '$pp2 Pending')]),
_wc('Total Premium', inr(tp), const Color(0xFF10B981), [_dot(Colors.grey, 'FY ${_currentFinYear()}')]), _wc('Total Premium', inr(tp), const Color(0xFF10B981), [_dot(Colors.grey, 'FY $fyTag')]),
_wc('Delegation', inr(com), const Color(0xFFF97316), [_dot(Colors.grey, '$cr% rate')]), _wc('Total Payout', inr(com), const Color(0xFFF97316), [
_dot(const Color(0xFF006B5C), 'Paid ${inr(cpd)}'),
_dot(const Color(0xFF64748B), 'Unpaid ${inr(cup)}'),
]),
_wc('Enquiry', '$eqd / $eq', const Color(0xFF3B82F6), [_dot(const Color(0xFF10B981), '$eqd Done')]), _wc('Enquiry', '$eqd / $eq', const Color(0xFF3B82F6), [_dot(const Color(0xFF10B981), '$eqd Done')]),
_wc('Endorsement', '$end / $en', const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done')]), _wc('Endorsement', '$end / $en', const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done')]),
]); ]);
@ -340,23 +339,22 @@ class _OverviewBar extends StatelessWidget {
// POLICY LIST + RENEWAL ALERTS ROW // POLICY LIST + RENEWAL ALERTS ROW
// //
class _PolicyRenewalRow extends StatelessWidget { class _PolicyRenewalRow extends StatelessWidget {
final List<Map<String, dynamic>> policies, currentPageRenewals; final List<Map<String, dynamic>> renewals;
final int totalRenewalPages, renewalPage, selectedRenewalDays, filteredRenewalsCount; final int selectedRenewalDays;
final ValueChanged<int> onRenewalDays, onRenewalPageChanged; final ValueChanged<int> onRenewalDays;
final String Function(dynamic) inr; final String Function(dynamic) inr;
const _PolicyRenewalRow({required this.policies, required this.currentPageRenewals, const _PolicyRenewalRow({
required this.totalRenewalPages, required this.renewalPage, required this.renewals,
required this.selectedRenewalDays, required this.filteredRenewalsCount, required this.selectedRenewalDays,
required this.onRenewalDays, required this.onRenewalPageChanged, required this.inr}); required this.onRenewalDays,
required this.inr,
});
@override @override
Widget build(BuildContext context) => _RenewalAlertsCard( Widget build(BuildContext context) => _RenewalAlertsCard(
currentPageRenewals: currentPageRenewals, renewals: renewals,
totalPages: totalRenewalPages,
currentPage: renewalPage,
selectedDays: selectedRenewalDays, selectedDays: selectedRenewalDays,
totalCount: filteredRenewalsCount, totalCount: renewals.length,
onDays: onRenewalDays, onDays: onRenewalDays,
onPageChanged: onRenewalPageChanged,
inr: inr, inr: inr,
); );
} }
@ -406,18 +404,54 @@ class _PolicyListCard extends StatelessWidget {
} }
class _RenewalAlertsCard extends StatelessWidget { class _RenewalAlertsCard extends StatelessWidget {
final List<Map<String, dynamic>> currentPageRenewals; static const int kVisibleRows = 10;
final int totalPages, currentPage, selectedDays, totalCount; static const double kRowHeight = 44;
final ValueChanged<int> onDays, onPageChanged;
final List<Map<String, dynamic>> renewals;
final int selectedDays, totalCount;
final ValueChanged<int> onDays;
final String Function(dynamic) inr; final String Function(dynamic) inr;
const _RenewalAlertsCard({required this.currentPageRenewals, required this.totalPages, required this.currentPage, required this.selectedDays, required this.totalCount, required this.onDays, required this.onPageChanged, required this.inr});
const _RenewalAlertsCard({
required this.renewals,
required this.selectedDays,
required this.totalCount,
required this.onDays,
required this.inr,
});
@override @override
Widget build(BuildContext context) => _DashCard( Widget build(BuildContext context) => _DashCard(
header: Row(children: [ header: Row(children: [
Container(width:30,height:30,decoration:BoxDecoration(color:const Color(0xFFFFEDED),borderRadius:BorderRadius.circular(8)),child:const Icon(Icons.notifications_active_outlined,size:16,color:Color(0xFFEF4444))), Container(
const SizedBox(width:8), width: 30,
Column(crossAxisAlignment:CrossAxisAlignment.start, children:[Text('Renewal Alerts',style:_chartHeader), Text('At-risk policies',style:GoogleFonts.inter(fontSize:9.5,color:const Color(0xFF8AABB5)))]), height: 30,
decoration: BoxDecoration(
color: const Color(0xFFFFEDED),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.notifications_active_outlined, size: 16, color: Color(0xFFEF4444)),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Renewal Alerts', style: _chartHeader),
Text(
'At-risk policies',
style: GoogleFonts.inter(fontSize: 9.5, color: const Color(0xFF8AABB5)),
),
],
),
const Spacer(), const Spacer(),
if (totalCount > 0)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
'$totalCount total',
style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5)),
),
),
Container( Container(
padding: const EdgeInsets.all(3), padding: const EdgeInsets.all(3),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -458,259 +492,293 @@ class _RenewalAlertsCard extends StatelessWidget {
), ),
]), ]),
body: totalCount == 0 body: totalCount == 0
? const _EmptyState(message:'No renewals due within selected period') ? const _EmptyState(message: 'No renewals due within selected period')
: Padding( : Column(
padding: const EdgeInsets.all(10), crossAxisAlignment: CrossAxisAlignment.stretch,
child: LayoutBuilder( children: [
builder: (context, constraints) { Container(
final w = constraints.maxWidth; padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
final crossAxisCount = w >= 1100 ? 4 : w >= 850 ? 3 : w >= 560 ? 2 : 1; color: const Color(0xFFE3F9F8),
return Column(children: [ child: Row(
GridView.builder( children: [
shrinkWrap: true, SizedBox(width: 88, child: Text('Due', style: _tableHeadStyle)),
physics: const NeverScrollableScrollPhysics(), Expanded(flex: 2, child: Text('Policy No.', style: _tableHeadStyle)),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( Expanded(flex: 2, child: Text('Holder', style: _tableHeadStyle)),
crossAxisCount: crossAxisCount, Expanded(flex: 2, child: Text('Expires', style: _tableHeadStyle)),
crossAxisSpacing: 8, Expanded(
mainAxisSpacing: 8, flex: 2,
mainAxisExtent: 90, child: Text('Premium', textAlign: TextAlign.right, style: _tableHeadStyle),
), ),
itemCount: currentPageRenewals.length,
itemBuilder: (_, i) => _RenewalAlertCard(renewal: currentPageRenewals[i], inr: inr),
),
if (totalPages > 1) ...[
const SizedBox(height: 10),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Text('${currentPage+1} / $totalPages', style: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF8AABB5))),
const SizedBox(width: 8),
_PA(icon: Icons.chevron_left, enabled: currentPage > 0, onTap: () => onPageChanged(currentPage - 1)),
const SizedBox(width: 4),
_PA(icon: Icons.chevron_right, enabled: currentPage < totalPages - 1, onTap: () => onPageChanged(currentPage + 1)),
]),
], ],
]);
},
), ),
), ),
SizedBox(
height: kVisibleRows * kRowHeight,
child: ListView.separated(
physics: const ClampingScrollPhysics(),
itemCount: renewals.length,
separatorBuilder: (_, __) => const Divider(height: 1, color: Color(0xFFD9F0ED)),
itemBuilder: (_, i) => _RenewalTableRow(renewal: renewals[i], inr: inr),
),
),
],
),
); );
} }
// renewal card holder_name=insured_name, premium=premium_amount (both aliased in API) class _RenewalTableRow extends StatelessWidget {
class _RenewalAlertCard extends StatelessWidget { final Map<String, dynamic> renewal;
final Map<String, dynamic> renewal; final String Function(dynamic) inr; final String Function(dynamic) inr;
const _RenewalAlertCard({required this.renewal, required this.inr});
const _RenewalTableRow({required this.renewal, required this.inr});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final d = int.tryParse(renewal['days_left']?.toString()??'0')??0; final d = int.tryParse(renewal['days_left']?.toString() ?? '0') ?? 0;
Color dc, bb, bf; Color chipBg, chipFg;
if (d<=10){dc=const Color(0xFFEF4444);bb=const Color(0xFFFFEDED);bf=const Color(0xFFEF4444);} if (d <= 10) {
else if(d<=20){dc=const Color(0xFFF97316);bb=const Color(0xFFFEF0E7);bf=const Color(0xFFF97316);} chipBg = const Color(0xFFFFEDED);
else{dc=const Color(0xFF94A3B8);bb=const Color(0xFFF1F5F9);bf=const Color(0xFF64748B);} chipFg = const Color(0xFFEF4444);
return Container( } else if (d <= 20) {
padding:const EdgeInsets.symmetric(horizontal:10,vertical:7), chipBg = const Color(0xFFFEF0E7);
decoration:BoxDecoration(color:Colors.white,border:Border.all(color:const Color(0xFFE2E8F0)),borderRadius:BorderRadius.circular(10), chipFg = const Color(0xFFF97316);
boxShadow:const[BoxShadow(color:Color(0x06000000),blurRadius:4,offset:Offset(0,2))]), } else {
child:Column(mainAxisSize: MainAxisSize.min,crossAxisAlignment:CrossAxisAlignment.start,children:[ chipBg = const Color(0xFFF1F5F9);
Row(children:[Container(width:8,height:8,decoration:BoxDecoration(shape:BoxShape.circle,color:dc)),const Spacer(), chipFg = const Color(0xFF64748B);
Container(padding:const EdgeInsets.symmetric(horizontal:7,vertical:2),decoration:BoxDecoration(color:bb,borderRadius:BorderRadius.circular(4)), }
child:Text('DUE IN $d DAYS',style:GoogleFonts.inter(fontSize:9,fontWeight:FontWeight.w700,color:bf,letterSpacing:.3)))]),
const SizedBox(height:4), return SizedBox(
Text(renewal['holder_name']?.toString()??'--',style:GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w600,color:const Color(0xFF0F2D3D)),maxLines:1,overflow:TextOverflow.ellipsis), height: _RenewalAlertsCard.kRowHeight,
const SizedBox(height:1), child: Padding(
Row(mainAxisAlignment:MainAxisAlignment.spaceBetween,children:[ padding: const EdgeInsets.symmetric(horizontal: 12),
Flexible(child:Text('Policy: ${renewal['policy_no']?.toString()??'--'}',style:GoogleFonts.inter(fontSize:9.5,color:const Color(0xFF8AABB5)),overflow:TextOverflow.ellipsis)), child: Row(
Text(inr(renewal['premium']??0),style:GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w700,color:const Color(0xFF0F2D3D))), children: [
]), SizedBox(
])); width: 88,
child: Align(
alignment: Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
color: chipBg,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$d d',
style: GoogleFonts.inter(
fontSize: 9.5,
fontWeight: FontWeight.w700,
color: chipFg,
letterSpacing: 0.2,
),
),
),
),
),
Expanded(
flex: 2,
child: Text(
renewal['policy_no']?.toString() ?? '--',
style: _tableTeal,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 2,
child: Text(
renewal['holder_name']?.toString() ?? '--',
style: _tableData,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 2,
child: Text(
renewal['end_date']?.toString() ?? '--',
style: _tableData,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 2,
child: Text(
inr(renewal['premium'] ?? 0),
style: _tableDataBold,
textAlign: TextAlign.right,
),
),
],
),
),
);
} }
} }
typedef _PA = _PaginationArrow;
class _PaginationArrow extends StatelessWidget {
final IconData icon; final bool enabled; final VoidCallback onTap;
const _PaginationArrow({required this.icon, required this.enabled, required this.onTap});
@override
Widget build(BuildContext context) => GestureDetector(onTap:enabled?onTap:null,child:Container(width:28,height:28,
decoration:BoxDecoration(color:enabled?const Color(0xFF0F2D3D):const Color(0xFFF1F5F9),borderRadius:BorderRadius.circular(6)),
child:Icon(icon,size:18,color:enabled?Colors.white:const Color(0xFFCBD5E1))));
}
// //
// EARNING DETAILS SECTION // EARNING DETAILS FY header + stat cards (Premium, Policies, Total commission)
// Data from GET partner/{id}/earnings, aggregated per selected financial year.
// //
class _EarningDetailsSection extends StatelessWidget { class _EarningDetailsSection extends StatelessWidget {
final List<String> allFinYears; final List<String> allFinYears;
final String selectedFinYear; final String selectedFinYear;
final ValueChanged<String> onFinYearChanged; final ValueChanged<String> onFinYearChanged;
final Map<String, dynamic> aggregate; final Map<String, dynamic> aggregate;
final List<Map<String, dynamic>> earningsForFY;
final String Function(dynamic) inr; final String Function(dynamic) inr;
const _EarningDetailsSection({required this.allFinYears, required this.selectedFinYear, required this.onFinYearChanged, required this.aggregate, required this.earningsForFY, required this.inr});
Widget _hero() { const _EarningDetailsSection({
final revenue = inr(aggregate['premium'] ?? 0); required this.allFinYears,
final netPayout = inr(aggregate['netPayout'] ?? 0); required this.selectedFinYear,
return Container( required this.onFinYearChanged,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), required this.aggregate,
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(12), required this.inr,
boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 4, offset: Offset(0, 2))]), });
child: LayoutBuilder(builder: (ctx, box) {
final wide = box.maxWidth > 500;
final left = Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
// Text('PERFORMANCE DASHBOARD', style: GoogleFonts.inter(fontSize: 9.5, fontWeight: FontWeight.w700, color: const Color(0xFF8AABB5), letterSpacing: 1.2)),
// const SizedBox(height: 6),
Text('Monthly Revenue', style: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))),
const SizedBox(height: 14),
Row(crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [
Text(revenue, style: GoogleFonts.manrope(fontSize: 28, fontWeight: FontWeight.w700, color: const Color(0xFF000666))),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(color: const Color(0xFFE6FAF7), borderRadius: BorderRadius.circular(6)),
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.trending_up, size: 12, color: Color(0xFF0ABFA3)),
const SizedBox(width: 4),
Text('+12.4%', style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.w700, color: const Color(0xFF0ABFA3))),
]),
),
]),
const SizedBox(height: 6),
Text('Accumulated earnings for ${_fyFull(selectedFinYear)}.', style: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF8AABB5))),
]);
final payout = Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
decoration: BoxDecoration(color: const Color(0xFFF8FFFE), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFD9F0ED))),
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.savings_outlined, size: 28, color: Color(0xFF0ABFA3)),
const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('TOTAL PAYOUT TO DATE', style: GoogleFonts.inter(fontSize: 8.5, fontWeight: FontWeight.w700, color: const Color(0xFF8AABB5), letterSpacing: .8)),
const SizedBox(height: 4),
Text(netPayout, style: GoogleFonts.manrope(fontSize: 20, fontWeight: FontWeight.w700, color: const Color(0xFF000666))),
]),
]),
);
return wide
? Row(crossAxisAlignment: CrossAxisAlignment.center, children: [Expanded(child: left), const SizedBox(width: 20), payout])
: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [left, const SizedBox(height: 16), payout]);
}),
);
}
Widget _fySection() { @override
Widget build(BuildContext context) {
final agg = aggregate; final agg = aggregate;
final stats = [ final stats = [
_SI('Financial Year', _fyFull(selectedFinYear), Icons.calendar_today_outlined, const Color(0xFF000666)), _EarningStatItem(
_SI('Premium Collected', inr(agg['premium'] ?? 0), Icons.payments_outlined, const Color(0xFF006B5C)), 'Financial Year',
_SI('Policies', '${agg['policies'] ?? 0}', Icons.policy_outlined, const Color(0xFF1A237E)), _fyFull(selectedFinYear),
_SI('Delegation (15%)', inr(agg['commission'] ?? 0), Icons.percent_outlined, const Color(0xFF059669)), Icons.calendar_today_outlined,
_SI('TDS (10%)', inr(agg['tds'] ?? 0), Icons.account_balance_outlined, const Color(0xFFD97706)), const Color(0xFF000666),
_SI('Net Payout', inr(agg['netPayout'] ?? 0), Icons.savings_outlined, const Color(0xFF059669)), ),
_EarningStatItem(
'Premium Collected',
inr(agg['premium'] ?? 0),
Icons.payments_outlined,
const Color(0xFF006B5C),
),
_EarningStatItem(
'Policies',
'${agg['policies'] ?? 0}',
Icons.policy_outlined,
const Color(0xFF1A237E),
),
_EarningStatItem(
'Total Payout',
inr(agg['payout'] ?? 0),
Icons.account_balance_wallet_outlined,
const Color(0xFFF97316),
),
]; ];
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(12)), decoration: BoxDecoration(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ color: Colors.white,
Row(children: [ border: Border.all(color: const Color(0xFFD9F0ED)),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.currency_rupee_rounded, size: 16, color: Color(0xFF006B5C)), const Icon(Icons.currency_rupee_rounded, size: 16, color: Color(0xFF006B5C)),
const SizedBox(width: 6), const SizedBox(width: 6),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(
Text('Earning Details', style: GoogleFonts.inter(fontSize: 13, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))), child: Column(
Text('${_fyFull(selectedFinYear)} · ${_fyRange(selectedFinYear)}', style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5))), crossAxisAlignment: CrossAxisAlignment.start,
]), children: [
const Spacer(), Text(
_FYTabBar(allFinYears: allFinYears, selected: selectedFinYear, onChanged: onFinYearChanged), 'Earning Details',
]), style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w700,
color: const Color(0xFF0F2D3D),
),
),
Text(
'${_fyFull(selectedFinYear)} · ${_fyRange(selectedFinYear)}',
style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5)),
),
],
),
),
_FYTabBar(
allFinYears: allFinYears,
selected: selectedFinYear,
onChanged: onFinYearChanged,
),
],
),
const SizedBox(height: 14), const SizedBox(height: 14),
LayoutBuilder(builder: (ctx, box) { LayoutBuilder(
final cols = box.maxWidth > 700 ? 6 : box.maxWidth > 450 ? 3 : 2; builder: (ctx, box) {
final w = (box.maxWidth - (cols - 1) * 10) / cols; final w = box.maxWidth;
return Wrap(spacing: 10, runSpacing: 10, children: stats.map((s) => SizedBox(width: w, child: _StatCard(item: s))).toList()); final cols = w > 900 ? 4 : w > 520 ? 2 : 1;
}), final cardW = (w - (cols - 1) * 10) / cols;
]), return Wrap(
); spacing: 10,
} runSpacing: 10,
children: stats
Widget _txnTable() => Container( .map((s) => SizedBox(width: cardW, child: _EarningStatCard(item: s)))
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(10), .toList(),
boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 4, offset: Offset(0, 2))]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFD9F0ED)))),
child: Row(children: [
Text('Recent Transactions', style: GoogleFonts.inter(fontSize: 13, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))),
const SizedBox(width: 8), _CountBadge(earningsForFY.length), const Spacer(),
Text('Showing for ${_fyFull(selectedFinYear)}', style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5))),
]),
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), color: const Color(0xFFF2F4F6),
child: Row(children: ['Month','Premium','Policies','Delegation','Net Payout']
.map((h) => Expanded(flex: 2, child: Text(h, style: _tableHeadStyle))).toList()),
),
earningsForFY.isEmpty
? const SizedBox(
height: 180,
child: Center(
child: _EmptyState(
message: 'No transactions for this financial year',
),
),
)
: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 320),
child: ListView.separated(
shrinkWrap: true, physics: const ClampingScrollPhysics(),
itemCount: earningsForFY.length,
separatorBuilder: (_, __) => const Divider(height: 1, color: Color(0xFFD9F0ED)),
itemBuilder: (_, i) {
final e = earningsForFY[i];
final label = e['month_label']?.toString() ?? e['month_key']?.toString() ?? '--';
double pre = double.tryParse(e['premium']?.toString() ?? '0') ?? 0;
double com = double.tryParse(e['commission']?.toString() ?? '0') ?? (pre * 0.15);
double tds = double.tryParse(e['tds']?.toString() ?? '0') ?? (com * 0.10);
double net = double.tryParse(e['net_payout']?.toString() ?? '0') ?? (com - tds);
return Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
child: Row(children: [
Expanded(flex:2, child: Text(label, style:_tableTeal, overflow:TextOverflow.ellipsis)),
Expanded(flex:2, child: Text(inr(pre), style:_tableDataBold.copyWith(color:const Color(0xFF000666)))),
Expanded(flex:2, child: Text('${e['policies']??0}', style:_tableDataBold)),
Expanded(flex:2, child: Text(inr(com), style:_tableDataBold.copyWith(color:const Color(0xFF059669)))),
Expanded(flex:2, child: Text(inr(net), style:_tableDataBold.copyWith(color:const Color(0xFF059669)))),
]),
); );
}, },
), ),
],
), ),
]),
); );
}
@override
Widget build(BuildContext context) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_hero(), const SizedBox(height: 14), _fySection(), const SizedBox(height: 14), _txnTable(),
]);
} }
typedef _SI = _StatItem; class _EarningStatItem {
class _StatItem { final String label, value; final IconData icon; final Color color; const _StatItem(this.label, this.value, this.icon, this.color); } final String label;
final String value;
final IconData icon;
final Color color;
const _EarningStatItem(this.label, this.value, this.icon, this.color);
}
class _EarningStatCard extends StatelessWidget {
final _EarningStatItem item;
const _EarningStatCard({required this.item});
class _StatCard extends StatelessWidget {
final _StatItem item; const _StatCard({required this.item});
@override @override
Widget build(BuildContext context) => Container( Widget build(BuildContext context) => Container(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0)), decoration: BoxDecoration(
boxShadow: const [BoxShadow(color: Color(0x07000000), blurRadius: 4, offset: Offset(0, 2))]), color: Colors.white,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ borderRadius: BorderRadius.circular(10),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ border: Border.all(color: const Color(0xFFE2E8F0)),
Flexible(child: Text(item.label.toUpperCase(), style: GoogleFonts.inter(fontSize: 8.5, fontWeight: FontWeight.w800, color: const Color(0xFF8AABB5), letterSpacing: .6), overflow: TextOverflow.ellipsis)), boxShadow: const [
BoxShadow(color: Color(0x07000000), blurRadius: 4, offset: Offset(0, 2)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
item.label.toUpperCase(),
style: GoogleFonts.inter(
fontSize: 8.5,
fontWeight: FontWeight.w800,
color: const Color(0xFF8AABB5),
letterSpacing: .6,
),
overflow: TextOverflow.ellipsis,
),
),
Icon(item.icon, size: 16, color: item.color), Icon(item.icon, size: 16, color: item.color),
]), ],
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(item.value, style: GoogleFonts.manrope(fontSize: 18, fontWeight: FontWeight.w700, color: item.color)), Text(
]), item.value,
style: GoogleFonts.manrope(
fontSize: 18,
fontWeight: FontWeight.w700,
color: item.color,
),
),
],
),
); );
} }

View File

@ -1,8 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'package:excel/excel.dart' hide Border;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -70,6 +66,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
'invoiceNo', 'invoiceNo',
'invoiceDate', 'invoiceDate',
'invoiceStatus', 'invoiceStatus',
'utrNumber',
]; ];
// -------------------------- // --------------------------
@ -95,7 +92,6 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
bool isLoading = false; bool isLoading = false;
bool isLoadingEditData = false; bool isLoadingEditData = false;
bool hasFetchedTableData = false; bool hasFetchedTableData = false;
final Map<String, String> _uploadedUtrByPolicyKey = {};
final Set<int> _inlineSavingIds = <int>{}; final Set<int> _inlineSavingIds = <int>{};
late ApiService apiService; late ApiService apiService;
dynamic managerId; dynamic managerId;
@ -516,14 +512,26 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
Future<void> saveInvoice() async { Future<void> saveInvoice() async {
try { try {
if (selectedAgentId == null) { if (selectedAgentId == null) {
ToastHelper.showWarningToast(context, "Please select an agent"); ToastHelper.showWarningToast(
context,
"Please select a partner before saving.",
);
return; return;
} }
if (selectedPolicies.isEmpty) { if (selectedPolicies.isEmpty) {
ToastHelper.showWarningToast( ToastHelper.showWarningToast(
context, context,
"Please select at least 1 policy", "Please select at least one policy before saving.",
);
return;
}
final String utrInput = (controllers['utrNumber']?.text ?? '').trim();
if (utrInput.isEmpty) {
ToastHelper.showWarningToast(
context,
"Please enter the UTR number. It cannot be left empty.",
); );
return; return;
} }
@ -536,17 +544,59 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
'yyyy-MM-dd', 'yyyy-MM-dd',
).format(policyTillDate!); ).format(policyTillDate!);
// Total Commission final List<Map<String, dynamic>> selectedRows = filteredPolicies
final double totalCommission = filteredPolicies .where((p) {
.where((p) => selectedPolicies.contains(int.parse(p["policy_id"]))) final id = int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0;
.fold(0.0, (sum, p) => sum + _parseCommissionValue(p["commission_amount"])); return id != 0 && selectedPolicies.contains(id);
})
.toList();
if (selectedRows.isEmpty) {
ToastHelper.showWarningToast(
context,
"No matching policies found for your selection. Refresh the list and try again.",
);
return;
}
for (final p in selectedRows) {
final policyNo = (p['policy_no'] ?? '-').toString();
final rawPayout = (p['commission_amount'] ?? '').toString().trim();
if (rawPayout.isEmpty) {
ToastHelper.showWarningToast(
context,
"Payout amount is missing for policy $policyNo. Enter an amount for every selected policy.",
);
return;
}
final payout = double.tryParse(rawPayout);
if (payout == null) {
ToastHelper.showWarningToast(
context,
"Payout amount for policy $policyNo is not a valid number. Use digits only (e.g. 1500 or 1500.50).",
);
return;
}
final premium = _parsePremiumAmount(p['premium_amount']);
if (premium != null && payout > premium + 0.001) {
ToastHelper.showWarningToast(
context,
"Payout for policy $policyNo (₹${payout.toStringAsFixed(2)}) cannot be greater than the premium (₹${premium.toStringAsFixed(2)}).",
);
return;
}
}
final double totalPayout = selectedRows.fold(
0.0,
(sum, p) => sum + _parseCommissionValue(p["commission_amount"]),
);
// Build items array // Build items array
final List<Map<String, dynamic>> items = filteredPolicies final List<Map<String, dynamic>> items = selectedRows
.where((p) => selectedPolicies.contains(int.parse(p["policy_id"])))
.map( .map(
(p) => { (p) => {
"policy_id": int.parse(p["policy_id"]), "policy_id": int.parse(p["policy_id"].toString()),
"policy_no": p["policy_no"], "policy_no": p["policy_no"],
"commission_amount": _parseCommissionValue(p["commission_amount"]), "commission_amount": _parseCommissionValue(p["commission_amount"]),
}, },
@ -556,14 +606,35 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
defaultUtrDate: formattedInvoiceDate, defaultUtrDate: formattedInvoiceDate,
); );
if (utrs.isEmpty) {
ToastHelper.showWarningToast(
context,
"UTR details could not be built. Check the UTR number and selected policies.",
);
return;
}
final double totalUtrAmount = _sumUtrAmounts(utrs);
if (!_amountsMatch(totalPayout, totalUtrAmount)) {
ToastHelper.showWarningToast(
context,
"Total payout (₹${totalPayout.toStringAsFixed(2)}) must match the total UTR amount (₹${totalUtrAmount.toStringAsFixed(2)}). "
"Check each row's payout and the UTR field.",
);
return;
}
final int payoutStatus =
_amountsMatch(totalPayout, totalUtrAmount) ? 2 : 1;
final jsondata = { final jsondata = {
"invoice_no": controllers['invoiceNo']?.text, "invoice_no": controllers['invoiceNo']?.text,
"invoice_amount": totalCommission, "invoice_amount": totalPayout,
"agent_id": selectedAgentId, "agent_id": selectedAgentId,
"broker_id": selectedBrokerID, "broker_id": selectedBrokerID,
"invoice_date": formattedInvoiceDate, "invoice_date": formattedInvoiceDate,
"till_date": formattedTillDate, "till_date": formattedTillDate,
"payout_status": 0, "payout_status": payoutStatus,
"pos_id": null, "pos_id": null,
"created_by": userId, "created_by": userId,
"updated_by": userId, "updated_by": userId,
@ -581,14 +652,16 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
print('PD =>API Data response - $response'); print('PD =>API Data response - $response');
selectedPolicies.clear(); selectedPolicies.clear();
controllers['invoiceNo']?.clear(); controllers['invoiceNo']?.clear();
controllers['utrNumber']?.clear();
invoiceDate = DateTime.now(); invoiceDate = DateTime.now();
policyTillDate = DateTime.now(); policyTillDate = DateTime.now();
filteredPolicies = []; filteredPolicies = [];
_uploadedUtrByPolicyKey.clear();
}); });
context.go(AppRoutes.invoiceList); context.go(AppRoutes.payoutList);
} else { } else {
filteredPolicies = []; final msg = (response['message'] ?? response['data'] ?? 'Save failed')
.toString();
ToastHelper.showErrorToast(context, msg);
} }
} catch (e) { } catch (e) {
print('PD =>Exception occurred: $e'); print('PD =>Exception occurred: $e');
@ -599,36 +672,6 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
} }
} }
String _normalizeHeader(dynamic header) {
return header
.toString()
.trim()
.toLowerCase()
.replaceAll(RegExp(r'[^a-z0-9]'), '');
}
String _excelCellValue(Data? cell) {
final value = cell?.value;
if (value == null) return '';
return value.toString().trim();
}
String _normalizePolicyNo(dynamic value) {
var text = (value ?? '').toString().trim();
if (text.isEmpty) return '';
// Remove common Excel text-protection wrappers around policy number.
text = text.replaceAll(RegExp(r"^[`']+"), '').replaceAll(RegExp(r"[`']+$"), '');
// Excel often turns large integer-looking values into "12345.0".
final trailingPointZero = RegExp(r'^(\d+)\.0+$').firstMatch(text);
if (trailingPointZero != null) {
text = trailingPointZero.group(1) ?? text;
}
return text.toLowerCase().replaceAll(RegExp(r'\s+'), '');
}
String _displayText(dynamic value) { String _displayText(dynamic value) {
if (value == null) return '-'; if (value == null) return '-';
final text = value.toString().trim(); final text = value.toString().trim();
@ -636,68 +679,19 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
return text; return text;
} }
double? _parseExcelCommission(String raw) {
final cleaned = raw
.replaceAll('', '')
.replaceAll(',', '')
.replaceAll(RegExp(r'\s+'), '')
.trim();
if (cleaned.isEmpty) return null;
return double.tryParse(cleaned);
}
PlatformFile? _buildSanitizedCommissionUploadFile(
List<Map<String, String>> rows,
) {
if (rows.isEmpty) return null;
final excelOut = Excel.createExcel();
final defaultSheet = excelOut.getDefaultSheet();
final sheet = excelOut[defaultSheet ?? 'Sheet1'];
sheet.appendRow([
TextCellValue('Agent Code'),
TextCellValue('Policy Number'),
TextCellValue('Payout Amount'),
]);
for (final row in rows) {
sheet.appendRow([
TextCellValue((row['agent_code'] ?? '').trim()),
TextCellValue((row['policy_no'] ?? '').trim()),
TextCellValue((row['payout_amount'] ?? '').trim()),
]);
}
final bytes = excelOut.encode();
if (bytes == null || bytes.isEmpty) return null;
return PlatformFile(
name: 'policy_commission_upload_cleaned.xlsx',
size: bytes.length,
bytes: Uint8List.fromList(bytes),
);
}
List<Map<String, dynamic>> _buildUtrPayloadForSelectedPolicies({ List<Map<String, dynamic>> _buildUtrPayloadForSelectedPolicies({
required String defaultUtrDate, required String defaultUtrDate,
}) { }) {
/*
* Build UTR payload from selected policies.
* 1) UTR source is Excel "UTR Number" column captured per policy.
* 2) If same UTR appears in multiple selected rows, amount is aggregated.
* 3) Amount comes from selected policy commission amount.
*/
final Map<String, double> utrAmountMap = {}; final Map<String, double> utrAmountMap = {};
final String enteredUtr = (controllers['utrNumber']?.text ?? '').trim();
for (final policy in filteredPolicies) { for (final policy in filteredPolicies) {
final int policyId = int.tryParse(policy["policy_id"]?.toString() ?? "0") ?? 0; final int policyId = int.tryParse(policy["policy_id"]?.toString() ?? "0") ?? 0;
if (!selectedPolicies.contains(policyId)) continue; if (!selectedPolicies.contains(policyId)) continue;
final String policyKey = _normalizePolicyNo(policy['policy_no']); final String utrNo = enteredUtr.isNotEmpty
final String utrNo = ((_uploadedUtrByPolicyKey[policyKey] ?? ? enteredUtr
policy['utr_no'] ?? : (policy['utr_no'] ?? '').toString().trim();
'')
.toString())
.trim();
if (utrNo.isEmpty) continue; if (utrNo.isEmpty) continue;
final double amount = _parseCommissionValue(policy["commission_amount"]); final double amount = _parseCommissionValue(policy["commission_amount"]);
@ -715,422 +709,6 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
.toList(); .toList();
} }
Widget _excelMatchedPolicyTile(Map<String, String> row) {
return Container(
margin: const EdgeInsets.only(bottom: 6),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E7EB)),
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${row['policy_no']} | ${row['current_commission']} -> ${row['excel_commission']}',
style: GoogleFonts.inter(fontSize: 12),
),
],
),
);
}
/// No Excel row matched the table list all Excel policy numbers; OK only.
Future<void> _showAllPoliciesUnmatchedDialog({
required List<String> excelPolicyNumbersNotMatched,
required int invalidExcelRows,
}) async {
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: Text(
'No policies matched',
style: GoogleFonts.inter(fontWeight: FontWeight.w600),
),
content: SizedBox(
width: 520,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'None of the policy numbers in the Excel file match the policies in the current table.',
style: GoogleFonts.inter(fontSize: 13, height: 1.35),
),
if (excelPolicyNumbersNotMatched.isNotEmpty) ...[
const SizedBox(height: 12),
Text(
'Policy numbers from Excel (not matched):',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
...excelPolicyNumbersNotMatched.map(
(p) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text('$p', style: GoogleFonts.inter(fontSize: 13)),
),
),
],
if (invalidExcelRows > 0) ...[
const SizedBox(height: 12),
Text(
'$invalidExcelRows row(s) in Excel were skipped (invalid policy number or commission).',
style: GoogleFonts.inter(fontSize: 12, color: Colors.grey[700]),
),
],
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('OK', style: GoogleFonts.inter(fontWeight: FontWeight.w600)),
),
],
),
);
}
/// At least one match show matched one-by-one; if Excel has extra policies, list them; Proceed / Cancel.
Future<bool> _showUploadReviewProceedDialog({
required List<Map<String, String>> matchedRows,
required List<String> excelPolicyNumbersNotOnTable,
required int invalidExcelRows,
}) async {
final hasExcelOnly = excelPolicyNumbersNotOnTable.isNotEmpty;
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: Text(
hasExcelOnly ? 'Review Excel vs table' : 'Confirm commission update',
style: GoogleFonts.inter(fontWeight: FontWeight.w600),
),
content: SizedBox(
width: 560,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Matched policies (${matchedRows.length})',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
...matchedRows.map(_excelMatchedPolicyTile),
if (hasExcelOnly) ...[
const SizedBox(height: 16),
Text(
'Not found on current table (${excelPolicyNumbersNotOnTable.length})',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.orange.shade800,
),
),
const SizedBox(height: 6),
Text(
'These policy numbers are in Excel but not in the loaded table:',
style: GoogleFonts.inter(fontSize: 12, height: 1.35),
),
const SizedBox(height: 8),
...excelPolicyNumbersNotOnTable.map(
(p) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'$p',
style: GoogleFonts.inter(fontSize: 13),
),
),
),
],
if (invalidExcelRows > 0) ...[
const SizedBox(height: 12),
Text(
'Note: $invalidExcelRows Excel row(s) skipped (invalid data).',
style: GoogleFonts.inter(fontSize: 12, color: Colors.grey[700]),
),
],
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text('Cancel', style: GoogleFonts.inter()),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text('Proceed', style: GoogleFonts.inter()),
),
],
),
) ??
false;
}
Future<bool> _showMismatchProceedDialog(int mismatchCount) async {
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: const Text('Partner mismatch found'),
content: Text(
'$mismatchCount row(s) are linked to a different partner.\n'
'Proceeding will retroactively update partner mapping in payout records.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Proceed'),
),
],
),
) ??
false;
}
Future<void> _triggerBulkUpload() async {
if (selectedAgentId == null || selectedAgentId!.isEmpty) {
ToastHelper.showWarningToast(context, 'Please select Partner before uploading.');
return;
}
if (selectedAgentId!.length != 1) {
ToastHelper.showWarningToast(context, 'Please select only 1 Partner before uploading.');
return;
}
if (!hasFetchedTableData || filteredPolicies.isEmpty) {
ToastHelper.showWarningToast(
context,
'No policy data loaded. Set dates, Partner, tap Filter, then upload.',
);
return;
}
try {
final picked = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['xlsx', 'xls'],
withData: true,
);
if (picked == null || picked.files.isEmpty) {
ToastHelper.showWarningToast(context, 'No Excel file selected. Please choose a file to upload.');
return;
}
final selectedFile = picked.files.single;
if (selectedFile.bytes == null || selectedFile.bytes!.isEmpty) {
ToastHelper.showWarningToast(context, 'Unable to read the selected Excel file. Try another file.');
return;
}
setState(() => isLoading = true);
final excel = Excel.decodeBytes(Uint8List.fromList(selectedFile.bytes!));
if (excel.tables.isEmpty) {
ToastHelper.showWarningToast(context, 'Invalid Excel: no sheet found in the file.');
return;
}
final rows = excel.tables.values.first.rows;
if (rows.isEmpty) {
ToastHelper.showWarningToast(context, 'Uploaded Excel file is empty.');
return;
}
int headerRowIndex = -1;
int policyNoCol = -1;
int commissionCol = -1;
int agentCodeCol = -1;
final int headerScanLimit = rows.length < 5 ? rows.length : 5;
for (int r = 0; r < headerScanLimit; r++) {
int pCol = -1;
int cCol = -1;
int aCol = -1;
final currentRow = rows[r];
for (int i = 0; i < currentRow.length; i++) {
final normalized = _normalizeHeader(_excelCellValue(currentRow[i]));
if (normalized == 'policynumber') pCol = i;
if (normalized == 'payoutamount' || normalized == 'commissionamount') {
cCol = i;
}
if (normalized == 'agentcode') aCol = i;
}
if (pCol != -1 && cCol != -1) {
headerRowIndex = r;
policyNoCol = pCol;
commissionCol = cCol;
agentCodeCol = aCol;
break;
}
}
if (policyNoCol == -1 || commissionCol == -1) {
ToastHelper.showWarningToast(
context,
'Excel header not found. Required columns: Policy Number and Payout Amount.',
);
return;
}
final Map<String, double> uploadMap = {};
final Map<String, String> uploadPolicyDisplay = {};
final Map<String, String> uploadAgentCodeMap = {};
final List<Map<String, String>> sanitizedRowsForUpload = [];
int invalidRows = 0;
for (int rowIndex = headerRowIndex + 1; rowIndex < rows.length; rowIndex++) {
final row = rows[rowIndex];
final policyRaw = row.length > policyNoCol ? _excelCellValue(row[policyNoCol]) : '';
final commissionRaw = row.length > commissionCol ? _excelCellValue(row[commissionCol]) : '';
final agentCodeRaw = agentCodeCol >= 0 && row.length > agentCodeCol
? _excelCellValue(row[agentCodeCol])
: '';
if (policyRaw.trim().isEmpty && commissionRaw.trim().isEmpty && agentCodeRaw.trim().isEmpty) {
continue;
}
final normalizedPolicy = _normalizePolicyNo(policyRaw);
final parsedCommission = _parseExcelCommission(commissionRaw);
if (normalizedPolicy.isEmpty || parsedCommission == null) {
invalidRows++;
continue;
}
uploadMap[normalizedPolicy] = parsedCommission;
uploadPolicyDisplay[normalizedPolicy] =
policyRaw.trim().isEmpty ? normalizedPolicy : policyRaw.trim();
if (agentCodeRaw.trim().isNotEmpty) {
uploadAgentCodeMap[normalizedPolicy] = agentCodeRaw.trim().toUpperCase();
}
sanitizedRowsForUpload.add({
'agent_code': agentCodeRaw.trim().toUpperCase(),
'policy_no': policyRaw.trim(),
'payout_amount': parsedCommission.toStringAsFixed(2),
});
}
if (uploadMap.isEmpty) {
ToastHelper.showWarningToast(
context,
'No valid data rows in Excel (check policy numbers and payout amounts).',
);
return;
}
final Set<String> screenPolicyKeys = {
for (final p in filteredPolicies) _normalizePolicyNo(p['policy_no']),
};
final List<Map<String, dynamic>> matchedPolicyRefs = [];
final List<Map<String, String>> matchedRowsForDialog = [];
final List<String> excelPoliciesNotOnTable = [];
int partnerMismatchCount = 0;
for (final e in uploadMap.entries) {
final key = e.key;
if (screenPolicyKeys.contains(key)) {
final policy = filteredPolicies.firstWhere(
(p) => _normalizePolicyNo(p['policy_no']) == key,
);
final excelAgentCode = (uploadAgentCodeMap[key] ?? '').trim();
final tableAgentCode =
(policy['agent_code'] ?? '').toString().trim().toUpperCase();
if (excelAgentCode.isNotEmpty &&
tableAgentCode.isNotEmpty &&
excelAgentCode != tableAgentCode) {
partnerMismatchCount++;
}
matchedPolicyRefs.add(policy);
matchedRowsForDialog.add({
'policy_no': (policy['policy_no'] ?? '').toString(),
'current_commission': (policy['commission_amount'] ?? '').toString(),
'excel_commission': e.value.toStringAsFixed(2),
});
} else {
excelPoliciesNotOnTable.add(uploadPolicyDisplay[key] ?? key);
}
}
if (matchedRowsForDialog.isEmpty) {
await _showAllPoliciesUnmatchedDialog(
excelPolicyNumbersNotMatched: excelPoliciesNotOnTable.isNotEmpty
? excelPoliciesNotOnTable
: uploadMap.keys.map((k) => uploadPolicyDisplay[k] ?? k).toList(),
invalidExcelRows: invalidRows,
);
return;
}
final bool proceed = await _showUploadReviewProceedDialog(
matchedRows: matchedRowsForDialog,
excelPolicyNumbersNotOnTable: excelPoliciesNotOnTable,
invalidExcelRows: invalidRows,
);
if (!proceed) {
ToastHelper.showWarningToast(context, 'Commission update cancelled');
return;
}
if (partnerMismatchCount > 0) {
final bool proceedMismatch = await _showMismatchProceedDialog(
partnerMismatchCount,
);
if (!proceedMismatch) {
ToastHelper.showWarningToast(context, 'Cancelled due to partner mismatch.');
return;
}
}
final sanitizedUploadFile = _buildSanitizedCommissionUploadFile(
sanitizedRowsForUpload,
);
final response = await apiService.uploadPolicyCommissionExcel(
id: selectedAgentId!.first,
file: sanitizedUploadFile ?? selectedFile,
proceedPartnerMismatch: partnerMismatchCount > 0,
);
if (response['status'] != 'success' && response['status'] != 200) {
ToastHelper.showErrorToast(
context,
(response['message'] ?? 'Upload failed').toString(),
);
return;
}
setState(() {
for (final policy in matchedPolicyRefs) {
final key = _normalizePolicyNo(policy['policy_no']);
if (uploadMap.containsKey(key)) {
policy['commission_amount'] = uploadMap[key]!.toStringAsFixed(2);
}
}
});
_calculateTotalCommission();
final int tableRowsNotInExcel = filteredPolicies
.where((p) => !uploadMap.containsKey(_normalizePolicyNo(p['policy_no'])))
.length;
final message =
'Updated ${matchedRowsForDialog.length} policy commission(s) from Excel'
'${partnerMismatchCount > 0 ? ' | partner mismatch updated: $partnerMismatchCount' : ''}'
'${excelPoliciesNotOnTable.isNotEmpty ? ' | Excel-only (ignored): ${excelPoliciesNotOnTable.length}' : ''}'
'${tableRowsNotInExcel > 0 ? ' | table rows not in Excel: $tableRowsNotInExcel' : ''}'
'${invalidRows > 0 ? ' | invalid Excel rows: $invalidRows' : ''}';
ToastHelper.showSuccessToast(context, message);
} catch (e) {
ToastHelper.showErrorToast(context, e.toString());
} finally {
if (mounted) setState(() => isLoading = false);
}
}
void showMessage(String msg) { void showMessage(String msg) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
@ -1153,7 +731,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
), ),
), ),
content: Text( content: Text(
"Once saved, changes cannot be edited.\nAre you sure you want to continue?", "Once saved, payout details cannot be edited.\nAre you sure you want to continue?",
style: GoogleFonts.inter(fontSize: 13, color: Colors.grey[700]), style: GoogleFonts.inter(fontSize: 13, color: Colors.grey[700]),
), ),
actions: [ actions: [
@ -1305,25 +883,6 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
if (!isEdit) ...[ if (!isEdit) ...[
const Spacer(), const Spacer(),
OutlinedButton.icon(
onPressed: isLoading ? null : _triggerBulkUpload,
icon: const Icon(Icons.upload_file, size: 16),
label: Text(
"Upload",
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF2E7D6E)),
foregroundColor: const Color(0xFF2E7D6E),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
),
),
if (hasFetchedTableData) ...[ if (hasFetchedTableData) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
Container( Container(
@ -1389,7 +948,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
"TOTAL COMMISSION (PAYOUT) :", "TOTAL PAYOUT AMOUNT :",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -1534,7 +1093,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
"Total Commission (Payout)", "Total Payout Amount",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -1557,6 +1116,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
), ),
Spacer(), Spacer(),
buildUTRNumber(context),
SizedBox(width: 20), SizedBox(width: 20),
// ---------------- BUTTONS ---------------- // ---------------- BUTTONS ----------------
Row( Row(
@ -1577,7 +1137,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
), ),
), ),
child: Text( child: Text(
"Raise Invoice", "Save Payout Details",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
color: Colors.white, color: Colors.white,
fontSize: 12, fontSize: 12,
@ -1637,7 +1197,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
"Total Commission (Payout)", "Total Payout Amount",
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -1744,7 +1304,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
_headerText("PARTNER", colAgent), _headerText("PARTNER", colAgent),
_headerText("CUSTOMER", colCustomer), _headerText("CUSTOMER", colCustomer),
_headerText("PREMIUM", colPremium), _headerText("PREMIUM", colPremium),
_headerText("COMMISSION (PAYOUT)", colCommission), _headerText("PAYOUT", colCommission),
], ],
), ),
); );
@ -1893,6 +1453,31 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
return double.tryParse(value.toString().trim()) ?? 0; return double.tryParse(value.toString().trim()) ?? 0;
} }
/// Premium from API / table (may include or commas).
double? _parsePremiumAmount(dynamic value) {
if (value == null) return null;
final cleaned = value
.toString()
.replaceAll('', '')
.replaceAll(',', '')
.replaceAll(RegExp(r'\s+'), '')
.trim();
if (cleaned.isEmpty) return null;
return double.tryParse(cleaned);
}
double _sumUtrAmounts(List<Map<String, dynamic>> utrs) {
double sum = 0;
for (final row in utrs) {
sum += _parseCommissionValue(row['amount']);
}
return sum;
}
bool _amountsMatch(double a, double b, {double epsilon = 0.01}) {
return (a - b).abs() < epsilon;
}
Future<void> _updateInlineCommission(Map<String, dynamic> row, String value) async { Future<void> _updateInlineCommission(Map<String, dynamic> row, String value) async {
final trimmed = value.trim(); final trimmed = value.trim();
final amount = double.tryParse(trimmed); final amount = double.tryParse(trimmed);
@ -1969,6 +1554,26 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
return total.roundToDouble(); return total.roundToDouble();
} }
Widget buildUTRNumber(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('UTR Number', style: _textStyle),
SizedBox(width: 5),
SizedBox(
height: 35,
width: MediaQuery.of(context).size.width * 0.15,
child: TextFormField(
controller: controllers['utrNumber']!,
inputFormatters: _utrInputFormatter,
decoration: commonInputDecoration(hint: 'UTR Number'),
),
),
],
);
}
InputDecoration commonInputDecoration({required String hint}) { InputDecoration commonInputDecoration({required String hint}) {
return InputDecoration( return InputDecoration(
hintText: hint, hintText: hint,