// ───────────────────────────────────────────────────────────────────────────── // partner_portal_dashboard.dart – Nhance Partner Portal // ───────────────────────────────────────────────────────────────────────────── import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; import '../../../../core/services/api_service.dart'; import '../../providers/manager_provider.dart'; import '../../providers/userRoleProvider.dart'; import '../../layouts/main_layout.dart'; String _currentFinYear() { final now = DateTime.now(); final s = now.month >= 4 ? now.year : now.year - 1; return '$s-${s + 1}'; } List _allFinYears() { final now = DateTime.now(); final cur = now.month >= 4 ? now.year : now.year - 1; return [for (int y = 2022; y <= cur; y++) '$y-${y + 1}']; } String _fyShort(String fy) { final p = fy.split('-'); return 'FY ${p[0].substring(2)}–${p[1].substring(2)}'; } String _fyFull(String fy) { final p = fy.split('-'); return 'FY ${p[0]}–${p[1]}'; } String _fyRange(String fy) { final p = fy.split('-'); return 'Apr ${p[0]} – Mar ${p[1]}'; } DateTime _fyStart(String fy) => DateTime(int.parse(fy.split('-').first), 4, 1); DateTime _fyEnd(String fy) => DateTime(int.parse(fy.split('-').last), 3, 31); class PartnerPortalDashboard extends ConsumerStatefulWidget { const PartnerPortalDashboard({super.key}); @override ConsumerState createState() => _State(); } class _State extends ConsumerState { late ApiService api; bool loading = false; bool _isAgentRole = false; int? _agentId; Map partnerDetails = {}; List> allPolicies = []; List> allRenewals = []; List> filteredRenewals = []; int selectedRenewalDays = 10; List> allEarnings = []; String selectedFY = _currentFinYear(); @override void initState() { super.initState(); api = ApiService(); Future.microtask(() { final role = (ref.read(userRoleProvider) ?? '').toString().trim().toLowerCase(); _isAgentRole = role == 'agent' || role == 'a'; _agentId = ref.read(userIdProvider); /* * Partner dashboard is only for Agent logins. * Use userIdProvider as agent id (not managerIdProvider). */ if (_isAgentRole && _agentId != null) { _loadAll(_agentId); } }); } Future _loadAll(dynamic id) async { setState(() => loading = true); try { await Future.wait([ _fetchDetails(id), _fetchPolicies(id), _fetchRenewals(id, days: selectedRenewalDays), _fetchEarnings(id), ]); } finally { setState(() => loading = false); } } Future _fetchDetails(dynamic id) async { final r = await api.getPartnerDetails(id); if (r['status'] == 'success') setState(() => partnerDetails = Map.from(r['data'] ?? {})); } Future _fetchPolicies(dynamic id) async { final r = await api.getPartnerPolicies(id); if (r['status'] == 'success') setState(() => allPolicies = List>.from((r['data'] ?? []).map((e) => Map.from(e)))); } Future _fetchRenewals(dynamic id, {int days = 10}) async { final r = await api.getPartnerRenewals(id, days: days); if (r['status'] == 'success') { final raw = List>.from((r['data'] ?? []).map((e) => Map.from(e))); setState(() { selectedRenewalDays = days; allRenewals = raw; filteredRenewals = raw; }); } } Future _fetchEarnings(dynamic id, {String? financialYear}) async { final fy = financialYear ?? selectedFY; final r = await api.getPartnerEarnings(id, financialYear: fy); if (r['status'] == 'success') { setState(() { allEarnings = List>.from( (r['data'] ?? []).map((e) => Map.from(e)), ); }); } } void _applyRenewalFilter(int days) { setState(() { selectedRenewalDays = days; filteredRenewals = allRenewals.where((r) => (r['days_left'] ?? 999) <= days).toList(); }); } /// Backend returns months for the requested [financial_year] only. Map get _fyAgg { double premium = 0; double payout = 0; int policies = 0; for (final e in allEarnings) { premium += _d(e['premium']); policies += int.tryParse(e['policies']?.toString() ?? '0') ?? 0; payout += _d(e['payout']); } return { 'premium': premium, 'policies': policies, 'payout': payout, }; } double _d(dynamic v) => double.tryParse(v?.toString() ?? '0') ?? 0; /// Indian-grouped full rupee amount (no L/Cr/K shorthand). String _inr(dynamic v) { final n = _d(v); if (n == n.roundToDouble()) { return 'Rs.${NumberFormat('#,##,###', 'en_IN').format(n.toInt())}'; } return 'Rs.${NumberFormat('#,##,##0.00', 'en_IN').format(n)}'; } String _initials(String name) { final p = name.trim().split(' '); return p.length >= 2 ? '${p[0][0]}${p[1][0]}'.toUpperCase() : name.substring(0, name.length.clamp(0, 2)).toUpperCase(); } @override Widget build(BuildContext context) { if (!_isAgentRole) { return MainLayout( title: 'Partner Dashboard', body: const SizedBox.shrink(), ); } return MainLayout( title: 'Partner Dashboard', body: SafeArea( child: loading ? const Center(child: CircularProgressIndicator(color: Color(0xFF0ABFA3))) : SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ _label('Partner Details'), _PartnerProfileCard(details: partnerDetails, initials: _initials(partnerDetails['agent_name']?.toString() ?? 'PD')), const SizedBox(height: 14), _label('Overview'), _OverviewBar(details: partnerDetails, inr: _inr, overviewFinYear: partnerDetails['overview_financial_year']?.toString()), const SizedBox(height: 14), // _label('Policy List & Renewal Alerts'), _PolicyRenewalRow( renewals: filteredRenewals, selectedRenewalDays: selectedRenewalDays, onRenewalDays: (d) { final id = _agentId ?? ref.read(userIdProvider); if (id != null) { _fetchRenewals(id, days: d); } else { _applyRenewalFilter(d); } }, inr: _inr, ), const SizedBox(height: 14), _label('Earning Details'), _EarningDetailsSection( allFinYears: _allFinYears(), selectedFinYear: selectedFY, 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), ]), ), ), ); } Widget _label(String text) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row(children: [ Text(text.toUpperCase(), style: GoogleFonts.inter(fontSize: 10.5, fontWeight: FontWeight.w700, color: const Color(0xFF8AABB5), letterSpacing: .8)), const SizedBox(width: 10), const Expanded(child: Divider(color: Color(0xFFD9F0ED), thickness: 1)), ]), ); } // ───────────────────────────────────────────────────────────────────────────── // PARTNER PROFILE CARD // Fields: agent_name, agent_code, mobile, email, manager_name, manager_mobile, status // All come from GET partner/{id}/details (partner_agent + manager join) // ───────────────────────────────────────────────────────────────────────────── class _PartnerProfileCard extends StatelessWidget { final Map details; final String initials; const _PartnerProfileCard({required this.details, required this.initials}); @override Widget build(BuildContext context) { final name = details['agent_name'] ?? '--'; final code = details['agent_code'] ?? '--'; final mobile = details['mobile'] ?? '--'; final email = details['email'] ?? '--'; final manager = details['manager_name'] ?? '--'; final managerMobile = details['manager_mobile'] ?? '--'; final managerEmail = details['manager_email'] ?? '--'; final status = details['status'] ?? 'Active'; return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(12)), child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ Container(width: 48, height: 48, decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xFF0ABFA3), border: Border.all(color: const Color(0x19000000), width: 2)), alignment: Alignment.center, child: Text(initials, style: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white))), const SizedBox(width: 14), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name.toString(), style: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))), const SizedBox(height: 5), Wrap(spacing: 16, runSpacing: 4, children: [ _m('Code', code.toString()), _m('Mobile', mobile.toString()), _m('Email', email.toString()), _m('Manager', manager.toString()), _m('Manager Mobile', managerMobile), _m('Manager Email', managerEmail), ]), ])), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration(color: const Color(0x380ABFA3), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0x4D0ABFA3))), child: Text('• $status', style: GoogleFonts.inter(fontSize: 11, fontWeight: FontWeight.w600, color: const Color(0xFF0B8D77)))), ]), ); } Widget _m(String l, String v) => RichText(text: TextSpan( text: '$l: ', style: GoogleFonts.inter(fontSize: 11.5, color: const Color(0xFF8AABB5)), children: [TextSpan(text: v, style: GoogleFonts.inter(fontSize: 11.5, fontWeight: FontWeight.w500, color: const Color(0xFF0F2D3D)))])); } // ───────────────────────────────────────────────────────────────────────────── // OVERVIEW BAR — metrics scoped to current Indian FY (from API overview_financial_year) // ───────────────────────────────────────────────────────────────────────────── class _OverviewBar extends StatelessWidget { final Map details; 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 Widget build(BuildContext context) { final fyTag = _fyShort(overviewFinYear ?? _currentFinYear()); final mp = details['mapped_policies'] ?? '0'; final ip = details['issued_policies'] ?? '0'; // policy_number IS NOT NULL final pp2 = details['pending_policies'] ?? '0'; // policy_number IS NULL final tp = details['total_premium'] ?? '0'; final com = details['commission_earned'] ?? '0'; final cpd = details['commission_paid'] ?? '0'; final cup = details['commission_unpaid'] ?? '0'; final eq = details['enquiry_total'] ?? '0'; final eqd = details['enquiry_completed'] ?? '0'; final eqp = details['enquiry_pending'] ?? '0'; final en = details['endorsement_total'] ?? '0'; final end = details['endorsement_done'] ?? '0'; final enp = details['endorsement_pending'] ?? '0'; return Container( decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(10), boxShadow: const [BoxShadow(color: Color(0x06000000), blurRadius: 4, offset: Offset(0, 2))]), child: LayoutBuilder(builder: (ctx, box) { final wide = box.maxWidth > 700; if (wide) return IntrinsicHeight(child: Row(children: [ _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 $fyTag')]), _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('Endorsement', end.toString(), en.toString(), const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done'), _dot(const Color(0xFFF97316), '$enp Pending')]), ])); return Wrap(children: [ _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 $fyTag')]), _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('Endorsement', '$end / $en', const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done')]), ]); }), ); } Widget _sc(String l, String v, Color c, List d, [bool lb=true]) => Expanded(child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: lb ? const BoxDecoration(border: Border(left: BorderSide(color: Color(0xFFD9F0ED)))) : null, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(l,style:_labelSmall), const SizedBox(height:4), Text(v,style:_bigNum.copyWith(color:c)), const SizedBox(height:4), Wrap(spacing:8,runSpacing:2,children:d)]))); Widget _fc(String l, String dn, String tot, Color c, List d) => Expanded(child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: const BoxDecoration(border: Border(left: BorderSide(color: Color(0xFFD9F0ED)))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(l,style:_labelSmall), const SizedBox(height:4), Row(crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [Text(dn,style:_bigNum.copyWith(color:c)), Text(' / ',style:GoogleFonts.inter(fontSize:14,fontWeight:FontWeight.w300,color:const Color(0xFFCBD5E1))), Text(tot,style:GoogleFonts.inter(fontSize:14,fontWeight:FontWeight.w600,color:const Color(0xFF94A3B8)))]), const SizedBox(height:4), Wrap(spacing:8,runSpacing:2,children:d)]))); Widget _wc(String l, String v, Color c, List d) => SizedBox(width:160, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFD9F0ED)))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(l,style:_labelSmall), const SizedBox(height:4), Text(v,style:_bigNum.copyWith(color:c)), const SizedBox(height:4), Wrap(spacing:6,children:d)]))); Widget _dot(Color c, String l) => Row(mainAxisSize: MainAxisSize.min, children: [Container(width:7,height:7,decoration:BoxDecoration(shape:BoxShape.circle,color:c)), const SizedBox(width:4), Text(l,style:_subInfo)]); } // ───────────────────────────────────────────────────────────────────────────── // POLICY LIST + RENEWAL ALERTS ROW // ───────────────────────────────────────────────────────────────────────────── class _PolicyRenewalRow extends StatelessWidget { final List> renewals; final int selectedRenewalDays; final ValueChanged onRenewalDays; final String Function(dynamic) inr; const _PolicyRenewalRow({ required this.renewals, required this.selectedRenewalDays, required this.onRenewalDays, required this.inr, }); @override Widget build(BuildContext context) => _RenewalAlertsCard( renewals: renewals, selectedDays: selectedRenewalDays, totalCount: renewals.length, onDays: onRenewalDays, inr: inr, ); } // ── Policy list card // holder_name = partner_policy.insured_name (aliased in API) // product = partner_policy.product column; fallback to vehicle_type class _PolicyListCard extends StatelessWidget { final List> policies; final String Function(dynamic) inr; const _PolicyListCard({required this.policies, required this.inr}); @override Widget build(BuildContext context) => _DashCard( header: Row(children: [Text('Policy List', style: _chartHeader), const SizedBox(width:8), _CountBadge(policies.length)]), body: policies.isEmpty ? const _EmptyState(message:'No policies found for this partner') : Column(children: [ _TableHeader(headers: const ['Policy No.', 'Holder', 'Product', 'Premium']), ConstrainedBox(constraints: const BoxConstraints(maxHeight: 340), child: ListView.separated( shrinkWrap: true, itemCount: policies.length, separatorBuilder: (_,__) => const Divider(height:1, color:Color(0xFFD9F0ED)), itemBuilder: (_,i) { final p = policies[i]; final productLabel = (p['product']?.toString().isNotEmpty == true) ? p['product'].toString() : (p['vehicle_type']?.toString() ?? '--'); return Container( padding: const EdgeInsets.symmetric(vertical:8, horizontal:12), child: Row(children: [ Expanded(flex:2, child: Text(p['policy_no']?.toString() ?? '--', style:_tableTeal, overflow:TextOverflow.ellipsis)), Expanded(flex:2, child: Text(p['holder_name']?.toString() ?? '--', style:_tableData, overflow:TextOverflow.ellipsis)), Expanded( flex: 2, child: Text( productLabel, style: _tableData, overflow: TextOverflow.ellipsis, ), ), Expanded(flex:2, child: Text(inr(p['premium'] ?? 0), style:_tableDataBold)), ]), ); }, )), ]), ); } class _RenewalAlertsCard extends StatefulWidget { static const int kPageSize = 10; static const double kRowHeight = 44; final List> renewals; final int selectedDays, totalCount; final ValueChanged onDays; final String Function(dynamic) inr; const _RenewalAlertsCard({ required this.renewals, required this.selectedDays, required this.totalCount, required this.onDays, required this.inr, }); @override State<_RenewalAlertsCard> createState() => _RenewalAlertsCardState(); } class _RenewalAlertsCardState extends State<_RenewalAlertsCard> { int _currentPage = 1; int get _totalPages { if (widget.renewals.isEmpty) return 1; return ((widget.renewals.length - 1) ~/ _RenewalAlertsCard.kPageSize) + 1; } List> get _pageItems { final start = (_currentPage - 1) * _RenewalAlertsCard.kPageSize; var end = start + _RenewalAlertsCard.kPageSize; if (end > widget.renewals.length) end = widget.renewals.length; if (start >= widget.renewals.length) return const []; return widget.renewals.sublist(start, end); } @override void didUpdateWidget(covariant _RenewalAlertsCard oldWidget) { super.didUpdateWidget(oldWidget); final dataChanged = oldWidget.renewals != widget.renewals || oldWidget.selectedDays != widget.selectedDays; if (dataChanged) { _currentPage = 1; return; } if (_currentPage > _totalPages) { _currentPage = _totalPages; } } @override Widget build(BuildContext context) { final pageItems = _pageItems; final startIndex = (_currentPage - 1) * _RenewalAlertsCard.kPageSize; return _DashCard( 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)), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Renewal Alerts', style: _chartHeader), Text( '', style: GoogleFonts.inter(fontSize: 9.5, color: const Color(0xFF8AABB5)), ), ], ), 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( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: const Color(0xFFF2F4F6), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: Wrap( spacing: 2, runSpacing: 2, children: [10, 20, 30, 40, 50].map((d) { final act = widget.selectedDays == d; return GestureDetector( onTap: () => widget.onDays(d), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: act ? const Color(0xFF000666) : Colors.transparent, borderRadius: BorderRadius.circular(6), border: Border.all( color: act ? const Color(0xFF000666) : Colors.transparent, ), ), child: Text( '$d Days', style: GoogleFonts.inter( fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: .4, color: act ? Colors.white : const Color(0xFF454652), ), ), ), ); }).toList(), ), ), ]), body: widget.totalCount == 0 ? const _EmptyState(message: 'No renewals due within selected period') : Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), color: const Color(0xFFE3F9F8), child: Row( children: [ SizedBox(width: 40, child: Text('S.No', style: _tableHeadStyle)), Expanded(flex: 2, child: Text('Policy No.', style: _tableHeadStyle)), Expanded(flex: 2, child: Text('Vehicle Type', style: _tableHeadStyle)), Expanded(flex: 2, child: Text('Policy Holder Name', style: _tableHeadStyle)), Expanded(flex: 2, child: Text('Expiry Date', style: _tableHeadStyle)), Expanded( flex: 2, child: Text('Premium Amount', textAlign: TextAlign.right, style: _tableHeadStyle), ), ], ), ), ...List.generate(pageItems.length, (i) { final serialNo = startIndex + i + 1; return Column( mainAxisSize: MainAxisSize.min, children: [ _RenewalTableRow( serialNo: serialNo, renewal: pageItems[i], inr: widget.inr, ), if (i != pageItems.length - 1) const Divider(height: 1, color: Color(0xFFD9F0ED)), ], ); }), Container( height: 44, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: const BoxDecoration( border: Border(top: BorderSide(color: Color(0xFFD9F0ED))), ), child: Row( children: [ Text( '${startIndex + 1}-${startIndex + pageItems.length} of ${widget.renewals.length}', style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w500, color: const Color(0xFF64748B), ), ), const Spacer(), IconButton( onPressed: _currentPage > 1 ? () => setState(() => _currentPage--) : null, icon: const Icon(Icons.chevron_left, size: 18), visualDensity: VisualDensity.compact, tooltip: 'Previous', ), Text( '$_currentPage / $_totalPages', style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w600, color: const Color(0xFF334155), ), ), IconButton( onPressed: _currentPage < _totalPages ? () => setState(() => _currentPage++) : null, icon: const Icon(Icons.chevron_right, size: 18), visualDensity: VisualDensity.compact, tooltip: 'Next', ), ], ), ), ], ), ); } } class _RenewalTableRow extends StatelessWidget { final int serialNo; final Map renewal; final String Function(dynamic) inr; const _RenewalTableRow({ required this.serialNo, required this.renewal, required this.inr, }); @override Widget build(BuildContext context) { final d = int.tryParse(renewal['days_left']?.toString() ?? '0') ?? 0; final Color chipFg; if (d <= 10) { chipFg = const Color(0xFFEF4444); } else if (d <= 20) { chipFg = const Color(0xFFF97316); } else { chipFg = const Color(0xFF64748B); } final endDate = renewal['end_date']?.toString() ?? '--'; return SizedBox( height: _RenewalAlertsCard.kRowHeight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 40, child: Text( '$serialNo', style: _tableData, textAlign: TextAlign.center, ), ), Expanded( flex: 2, child: Text( renewal['policy_no']?.toString() ?? '--', style: _tableTeal, overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: Text( renewal['vehicle_type']?.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.rich( TextSpan( style: _tableData, children: [ TextSpan(text: endDate), TextSpan( text: ' ($d d)', style: GoogleFonts.inter( fontSize: 9, fontWeight: FontWeight.w600, color: chipFg, ), ), ], ), overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: Text( inr(renewal['premium'] ?? renewal['premium_amount'] ?? 0), style: _tableDataBold, textAlign: TextAlign.right, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } } // ───────────────────────────────────────────────────────────────────────────── // 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 { final List allFinYears; final String selectedFinYear; final ValueChanged onFinYearChanged; final Map aggregate; final String Function(dynamic) inr; const _EarningDetailsSection({ required this.allFinYears, required this.selectedFinYear, required this.onFinYearChanged, required this.aggregate, required this.inr, }); @override Widget build(BuildContext context) { final agg = aggregate; final stats = [ _EarningStatItem( 'Financial Year', _fyFull(selectedFinYear), Icons.calendar_today_outlined, const Color(0xFF000666), ), _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( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, 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 SizedBox(width: 6), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '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), LayoutBuilder( builder: (ctx, box) { final w = box.maxWidth; final cols = w > 900 ? 4 : w > 520 ? 2 : 1; final cardW = (w - (cols - 1) * 10) / cols; return Wrap( spacing: 10, runSpacing: 10, children: stats .map((s) => SizedBox(width: cardW, child: _EarningStatCard(item: s))) .toList(), ); }, ), ], ), ); } } class _EarningStatItem { 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}); @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0)), 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), ], ), const SizedBox(height: 8), Text( item.value, style: GoogleFonts.manrope( fontSize: 18, fontWeight: FontWeight.w700, color: item.color, ), ), ], ), ); } class _FYTabBar extends StatelessWidget { final List allFinYears; final String selected; final ValueChanged onChanged; const _FYTabBar({required this.allFinYears, required this.selected, required this.onChanged}); @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration(color: const Color(0xFFF2F4F6), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE2E8F0))), child: Wrap(spacing: 2, runSpacing: 2, children: allFinYears.map((fy) { final act = fy == selected; return GestureDetector(onTap: () => onChanged(fy), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration(color: act ? const Color(0xFF000666) : Colors.transparent, borderRadius: BorderRadius.circular(6), border: Border.all(color: act ? const Color(0xFF000666) : Colors.transparent)), child: Text(_fyShort(fy), style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: .4, color: act ? Colors.white : const Color(0xFF454652))), )); }).toList()), ); } // ───────────────────────────────────────────────────────────────────────────── // SHARED SMALL WIDGETS // ───────────────────────────────────────────────────────────────────────────── class _DashCard extends StatelessWidget { final Widget header, body; const _DashCard({required this.header, required this.body}); @override Widget build(BuildContext context) => Container( decoration:BoxDecoration(color:Colors.white,border:Border.all(color:const Color(0xFFD9F0ED)),borderRadius:BorderRadius.circular(10), boxShadow:const[BoxShadow(color:Color(0x08000000),blurRadius:4,offset:Offset(0,2))]), child:Column(crossAxisAlignment:CrossAxisAlignment.stretch,children:[ Container(padding:const EdgeInsets.symmetric(horizontal:14,vertical:11),decoration:const BoxDecoration(border:Border(bottom:BorderSide(color:Color(0xFFD9F0ED)))),child:header), body])); } class _TableHeader extends StatelessWidget { final List headers; const _TableHeader({required this.headers}); @override Widget build(BuildContext context) => Container( padding:const EdgeInsets.symmetric(vertical:8,horizontal:12), color:const Color(0xFFE3F9F8), child:Row(children:headers.map((h)=>Expanded(flex:2,child:Text(h,style:_tableHeadStyle))).toList())); } class _CountBadge extends StatelessWidget { final int count; const _CountBadge(this.count); @override Widget build(BuildContext context) => Container( padding:const EdgeInsets.symmetric(horizontal:8,vertical:2), decoration:BoxDecoration(color:const Color(0xFFE6FAF7),borderRadius:BorderRadius.circular(12)), child:Text('$count',style:GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w600,color:const Color(0xFF0ABFA3)))); } class _EmptyState extends StatelessWidget { final String message; const _EmptyState({required this.message}); @override Widget build(BuildContext context) => Padding(padding:const EdgeInsets.symmetric(vertical:32), child:Column(children:[Text('No records found',style:GoogleFonts.inter(fontSize:13,fontWeight:FontWeight.w500,color:const Color(0xFF8AABB5))), const SizedBox(height:3),Text(message,style:GoogleFonts.inter(fontSize:11.5,color:const Color(0xFFB0C8D0)))])); } class _ProductBadge extends StatelessWidget { final String product; const _ProductBadge(this.product); @override Widget build(BuildContext context) => Container( padding:const EdgeInsets.symmetric(horizontal:8,vertical:2), decoration:BoxDecoration(color:const Color(0xFFE6FAF7),borderRadius:BorderRadius.circular(20)), child:Text(product,style:GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w600,color:const Color(0xFF089886)),overflow:TextOverflow.ellipsis)); } // ─── Text Styles ───────────────────────────────────────────────────────────── final _chartHeader = GoogleFonts.inter(fontSize:12,color:Colors.black,fontWeight:FontWeight.w600); final _labelSmall = GoogleFonts.inter(fontSize:10,fontWeight:FontWeight.w600,color:const Color(0xFF8AABB5),letterSpacing:.7); final _bigNum = GoogleFonts.inter(fontSize:21,fontWeight:FontWeight.w700,color:const Color(0xFF0ABFA3)); final _subInfo = GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w400,color:const Color(0xFF4A6B78)); final _tableHeadStyle = GoogleFonts.inter(fontSize:10.5,fontWeight:FontWeight.w600,color:const Color(0xFF8AABB5),letterSpacing:.5); final _tableData = GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w400,color:const Color(0xFF1A3340)); final _tableDataBold = GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w600,color:const Color(0xFF1A3340)); final _tableTeal = GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w600,color:const Color(0xFF089886));