nhance_partner/lib/presentation/screens/dashboard/partner_portal_dashboard.dart
2026-04-06 17:23:16 +05:30

874 lines
39 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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<String> _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<PartnerPortalDashboard> createState() => _State();
}
class _State extends ConsumerState<PartnerPortalDashboard> {
late ApiService api;
bool loading = false;
bool _isAgentRole = false;
int? _agentId;
Map<String, dynamic> partnerDetails = {};
List<Map<String, dynamic>> allPolicies = [];
List<Map<String, dynamic>> allRenewals = [];
List<Map<String, dynamic>> filteredRenewals = [];
int selectedRenewalDays = 10;
List<Map<String, dynamic>> 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<void> _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<void> _fetchDetails(dynamic id) async {
final r = await api.getPartnerDetails(id);
if (r['status'] == 'success') setState(() => partnerDetails = Map.from(r['data'] ?? {}));
}
Future<void> _fetchPolicies(dynamic id) async {
final r = await api.getPartnerPolicies(id);
if (r['status'] == 'success') setState(() => allPolicies = List<Map<String, dynamic>>.from((r['data'] ?? []).map((e) => Map<String, dynamic>.from(e))));
}
Future<void> _fetchRenewals(dynamic id, {int days = 10}) async {
final r = await api.getPartnerRenewals(id, days: days);
if (r['status'] == 'success') {
final raw = List<Map<String, dynamic>>.from((r['data'] ?? []).map((e) => Map<String, dynamic>.from(e)));
setState(() {
selectedRenewalDays = days;
allRenewals = raw;
filteredRenewals = raw;
});
}
}
Future<void> _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<Map<String, dynamic>>.from(
(r['data'] ?? []).map((e) => Map<String, dynamic>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<Widget> 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<Widget> 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<Widget> 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<Map<String, dynamic>> renewals;
final int selectedRenewalDays;
final ValueChanged<int> 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<Map<String, dynamic>> 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 StatelessWidget {
static const int kVisibleRows = 10;
static const double kRowHeight = 44;
final List<Map<String, dynamic>> renewals;
final int selectedDays, totalCount;
final ValueChanged<int> onDays;
final String Function(dynamic) inr;
const _RenewalAlertsCard({
required this.renewals,
required this.selectedDays,
required this.totalCount,
required this.onDays,
required this.inr,
});
@override
Widget build(BuildContext context) {
final visibleRowCount = renewals.length.clamp(1, kVisibleRows);
final listHeight = visibleRowCount * kRowHeight;
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,
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 = selectedDays == d;
return GestureDetector(
onTap: () => 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: 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('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),
),
],
),
),
SizedBox(
height: listHeight,
child: ListView.separated(
physics: const ClampingScrollPhysics(),
itemCount: renewals.length,
separatorBuilder: (_, __) => const Divider(height: 1, color: Color(0xFFD9F0ED)),
itemBuilder: (_, i) => _RenewalTableRow(
serialNo: i + 1,
renewal: renewals[i],
inr: inr,
),
),
),
],
),
);
}
}
class _RenewalTableRow extends StatelessWidget {
final int serialNo;
final Map<String, dynamic> 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['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<String> allFinYears;
final String selectedFinYear;
final ValueChanged<String> onFinYearChanged;
final Map<String, dynamic> 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<String> allFinYears; final String selected; final ValueChanged<String> 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<String> 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));