874 lines
39 KiB
Dart
874 lines
39 KiB
Dart
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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)); |