This commit is contained in:
Surendiran 2026-04-04 17:38:00 +05:30
parent c35d42f39c
commit 30e81109e6
7 changed files with 412 additions and 210 deletions

File diff suppressed because one or more lines are too long

View File

@ -362,6 +362,35 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
},
),
],
if (key == 'Payout' && role == 'Accounts') ...[
_buildPopupItem(
label: "Payout",
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'Payout');
context.go(AppRoutes.payoutList);
},
),
const SizedBox(height: 2),
_buildPopupItem(
label: "Payout upload",
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'PayoutUpload');
context.go(AppRoutes.payoutUpload);
},
),
const SizedBox(height: 2),
_buildPopupItem(
label: "Payout Reports",
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'PayoutReport');
context.go(AppRoutes.payoutReport);
},
),
],
],
),
),
@ -503,40 +532,17 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
],
// Payout (Accounts only) single top-level item, no submenu
// Payout (Accounts only) one top-level menu with submenu
if (role == 'Accounts') ...[
_buildMenuItem(
_buildHoverMenuItem(
icon: Icons.payments_outlined,
label: "Payout",
isActive: activeMenu == 'Payout',
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'Payout');
context.go(AppRoutes.payoutList);
},
popupKey: 'Payout',
isActive: activeMenu == 'Payout' ||
activeMenu == 'PayoutUpload' ||
activeMenu == 'PayoutReport',
),
const SizedBox(width: 8),
_buildMenuItem(
icon: Icons.upload_file_outlined,
label: "Payout Upload",
isActive: activeMenu == 'PayoutUpload',
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'PayoutUpload');
context.go(AppRoutes.payoutUpload);
},
),
const SizedBox(width: 8),
_buildMenuItem(
icon: Icons.table_chart_outlined,
label: "Payout Report",
isActive: activeMenu == 'PayoutReport',
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'PayoutReport');
context.go(AppRoutes.payoutReport);
},
),
],
// Partner (agent): Payout Report same route as Accounts

View File

@ -730,7 +730,9 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
final isAgent =
(ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase() ==
'agent';
final isMobile = ResponsiveLayout.isMobile(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (!isAgent) ...[
Tooltip(
@ -751,14 +753,12 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
style: GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w400),
),
const Spacer(),
// Text(
// 'Retention Rate: $_retentionRate%',
// style: GoogleFonts.poppins(
// fontSize: 12,
// fontWeight: FontWeight.w500,
// color: const Color(0xFF2E7D6E),
// ),
// ),
if (isMobile)
Expanded(child: _buildSearch(context, true))
else
_buildSearch(context, false),
const SizedBox(width: 12),
_buildExportButtonWidget(),
],
);
}
@ -1006,18 +1006,9 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
const SizedBox(height: 10),
dropdownRto(),
const SizedBox(height: 10),
_buildSearch(context, isMobile),
const SizedBox(height: 10),
searchBtn(),
const SizedBox(height: 10),
resetBtn(),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildExportButtonWidget(),
],
),
],
)
: Row(
@ -1044,10 +1035,6 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
),
),
),
const SizedBox(width: 20),
_buildSearch(context, isMobile),
const SizedBox(width: 12),
_buildExportButtonWidget(),
],
),
),

View File

@ -518,40 +518,108 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
child: Container(
color: Colors.white,
width: double.infinity,
height: double.infinity,
child: LayoutBuilder(
builder: (context, constraints) {
final maxBody = 1200.0;
final hPad = ResponsiveLayout.isMobile(context) ? 12.0 : 20.0;
final mobile = ResponsiveLayout.isMobile(context);
final hPad = mobile ? 12.0 : 20.0;
final vPad = mobile ? 12.0 : 16.0;
final viewW = constraints.hasBoundedWidth && constraints.maxWidth.isFinite
? constraints.maxWidth
: MediaQuery.sizeOf(context).width;
final contentW = viewW > maxBody ? maxBody : viewW;
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxBody),
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: hPad,
vertical: ResponsiveLayout.isMobile(context) ? 12 : 16,
),
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
final contentW = (viewW - 2 * hPad).clamp(280.0, double.infinity);
final form = Form(
key: _formKey,
autovalidateMode: AutovalidateMode.disabled,
child: mobile
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_pageHeaderBar(context, pageTitle),
const SizedBox(height: 12),
_partnerDetailsCard(context, contentW),
const SizedBox(height: 24),
_retentionSection(context),
SizedBox(height: ResponsiveLayout.isMobile(context) ? 24 : 16),
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.only(bottom: vPad),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_partnerDetailsHeading(),
const SizedBox(height: 14),
_partnerDetailsCard(context, contentW),
const SizedBox(height: 24),
_retentionSection(context, expandTableHeight: false),
SizedBox(height: mobile ? 24 : 16),
],
),
),
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_pageHeaderBar(context, pageTitle),
const SizedBox(height: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 5,
child: _partnerDetailsHeading(),
),
const SizedBox(width: 16),
Expanded(
flex: 6,
child: _retentionTitleSearchRow(),
),
],
),
const SizedBox(height: 12),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 5,
child: SingleChildScrollView(
padding: const EdgeInsets.only(right: 10),
child: LayoutBuilder(
builder: (ctx, c) {
final w = c.maxWidth.isFinite && c.maxWidth > 0
? c.maxWidth
: contentW * 0.48;
return _partnerDetailsCard(context, w);
},
),
),
),
const SizedBox(width: 16),
Expanded(
flex: 6,
child: _retentionSection(
context,
expandTableHeight: true,
showSectionTitleRow: false,
),
),
],
),
),
],
),
),
],
),
),
),
),
);
return Padding(
padding: EdgeInsets.symmetric(horizontal: hPad, vertical: vPad),
child: form,
);
},
),
@ -560,6 +628,17 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
);
}
Widget _partnerDetailsHeading() {
return Text(
'Partner Details',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
),
);
}
Widget _partnerDetailsCard(BuildContext context, double contentW) {
final pad = ResponsiveLayout.isMobile(context) ? 14.0 : 20.0;
return Container(
@ -572,15 +651,6 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Partner details',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
),
),
const SizedBox(height: 16),
_partnerRows(context, contentW),
const SizedBox(height: 20),
Align(
@ -932,12 +1002,122 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
);
}
Widget _retentionSection(BuildContext context) {
final mobile = ResponsiveLayout.isMobile(context);
return Column(
Widget _retentionTableInCard({
required double viewportHeight,
required double parentWidth,
}) {
final tableColumn = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (mobile)
_retentionTableHeader(),
..._filteredVehicleTypes.map(_retentionDataRow),
],
);
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: SizedBox(
height: viewportHeight,
width: double.infinity,
child: Scrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
primary: false,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: parentWidth),
child: tableColumn,
),
),
),
),
),
);
}
/// Table shell with a fixed header row; only data rows scroll (web two-column layout).
Widget _retentionTableStickyHeaderCard() {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_retentionTableHeader(),
Expanded(
child: Scrollbar(
thumbVisibility: true,
child: ListView.builder(
padding: EdgeInsets.zero,
primary: false,
itemCount: _filteredVehicleTypes.length,
itemBuilder: (context, i) =>
_retentionDataRow(_filteredVehicleTypes[i]),
),
),
),
],
),
),
);
}
Widget _retentionTitleSearchRow() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
'Retention rate by vehicle type',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
),
),
),
SizedBox(
width: 220,
child: ThemedSearchField(
hintText: 'Search vehicle type',
backgroundColor: Colors.white,
txtHeight: 34,
controller: _retentionSearchCtrl,
onChanged: (_) => setState(() {}),
),
),
],
);
}
/// [expandTableHeight]: when true (web two-column layout), the table fills remaining
/// height beside Partner Details. When false (mobile), uses list/cards and vertical stack.
///
/// [showSectionTitleRow]: when false, the title + search row is omitted (shown beside
/// Partner Details in the parent [Row]).
Widget _retentionSection(
BuildContext context, {
bool expandTableHeight = false,
bool showSectionTitleRow = true,
}) {
final mobile = ResponsiveLayout.isMobile(context);
if (mobile) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -959,81 +1139,51 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
onChanged: (_) => setState(() {}),
),
],
)
else
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
'Retention rate by vehicle type',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
),
),
),
SizedBox(
width: 220,
child: ThemedSearchField(
hintText: 'Search vehicle type',
backgroundColor: Colors.white,
txtHeight: 34,
controller: _retentionSearchCtrl,
onChanged: (_) => setState(() {}),
),
),
],
),
const SizedBox(height: 12),
_retentionMobileList(context),
const SizedBox(height: 12),
_retentionSaveAllButtonBottom(context),
],
);
}
if (!showSectionTitleRow) {
assert(expandTableHeight, 'showSectionTitleRow false is only used with expandTableHeight');
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: _retentionTableStickyHeaderCard()),
const SizedBox(height: 12),
_retentionSaveAllButtonBottom(context),
],
);
}
final Widget tableArea;
if (expandTableHeight) {
tableArea = Expanded(
child: _retentionTableStickyHeaderCard(),
);
} else {
tableArea = LayoutBuilder(
builder: (context, constraints) {
final parentW = constraints.maxWidth.isFinite
? constraints.maxWidth
: MediaQuery.sizeOf(context).width;
final screenH = MediaQuery.sizeOf(context).height;
final viewportH = math.min(520.0, math.max(240.0, screenH * 0.42));
return _retentionTableInCard(viewportHeight: viewportH, parentWidth: parentW);
},
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_retentionTitleSearchRow(),
const SizedBox(height: 12),
if (mobile)
_retentionMobileList(context)
else
LayoutBuilder(
builder: (context, constraints) {
final parentW = constraints.maxWidth.isFinite
? constraints.maxWidth
: MediaQuery.sizeOf(context).width;
final screenH = MediaQuery.sizeOf(context).height;
final viewportH = math.min(520.0, math.max(240.0, screenH * 0.42));
final tableColumn = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_retentionTableHeader(),
..._filteredVehicleTypes.map(_retentionDataRow),
],
);
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: SizedBox(
height: viewportH,
width: double.infinity,
child: Scrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
primary: false,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: parentW),
child: tableColumn,
),
),
),
),
),
);
},
),
tableArea,
const SizedBox(height: 12),
_retentionSaveAllButtonBottom(context),
],

View File

@ -1030,31 +1030,31 @@ class _PayoutListState extends ConsumerState<PayoutList> {
const SizedBox(width: _actionIconGap),
// Step 2: Add payment only when balance amount is pending.
IconButton(
icon: const Icon(
Icons.currency_rupee,
color: Colors.green,
size: _actionIconSize,
),
tooltip: 'Add payment',
onPressed: () {
final balance = _toDouble(item['balance_amount']);
if (balance <= 0) {
ToastHelper.showWarningToast(context, 'Invoice already fully paid');
return;
}
_showAddPaymentDialog(item);
},
splashRadius: 16,
hoverColor: Colors.black12,
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(
width: _actionButtonSize,
height: _actionButtonSize,
),
visualDensity: VisualDensity.compact,
),
const SizedBox(width: _actionIconGap),
// IconButton(
// icon: const Icon(
// Icons.currency_rupee,
// color: Colors.green,
// size: _actionIconSize,
// ),
// tooltip: 'Add payment',
// onPressed: () {
// final balance = _toDouble(item['balance_amount']);
// if (balance <= 0) {
// ToastHelper.showWarningToast(context, 'Invoice already fully paid');
// return;
// }
// _showAddPaymentDialog(item);
// },
// splashRadius: 16,
// hoverColor: Colors.black12,
// padding: EdgeInsets.zero,
// constraints: const BoxConstraints.tightFor(
// width: _actionButtonSize,
// height: _actionButtonSize,
// ),
// visualDensity: VisualDensity.compact,
// ),
// const SizedBox(width: _actionIconGap),
// Step 3: Show account history modal for this invoice.
IconButton(

View File

@ -595,6 +595,48 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
return '-';
}
String? _firstNonEmptyRaw(Map<String, dynamic> row, List<String> keys) {
for (final k in keys) {
final v = row[k];
if (v != null && v.toString().trim().isNotEmpty) {
return v.toString();
}
}
return null;
}
/// API sometimes returns payout + UTR in one string (e.g. `5000.00UTR20260401164749`).
/// Normalizes into separate display values for PAYOUT and UTR NO columns (and export).
({String payout, String utr}) _payoutAndUtr(Map<String, dynamic> row) {
const utrKeys = ['utr_no', 'utr', 'utr_number'];
const payoutKeys = ['commission_amount', 'payout', 'commission'];
String utrStr = (_firstNonEmptyRaw(row, utrKeys) ?? '').trim();
String payoutRaw = (_firstNonEmptyRaw(row, payoutKeys) ?? '').trim();
String stripTrailingPlaceholder(String s) =>
s.replaceAll(RegExp(r'[\s\-–—]+$'), '').trim();
if (payoutRaw.isNotEmpty) {
final upper = payoutRaw.toUpperCase();
final idx = upper.indexOf('UTR');
if (idx > 0) {
final gluedUtr = payoutRaw.substring(idx).trim();
payoutRaw = stripTrailingPlaceholder(payoutRaw.substring(0, idx));
if (utrStr.isEmpty || utrStr == '-') {
utrStr = gluedUtr;
}
} else {
payoutRaw = stripTrailingPlaceholder(payoutRaw);
}
}
if (payoutRaw.isEmpty) payoutRaw = '-';
if (utrStr.isEmpty) utrStr = '-';
return (payout: payoutRaw, utr: utrStr);
}
static num? _parseNum(dynamic v) {
if (v == null) return null;
if (v is num) return v;
@ -714,8 +756,13 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
// [ExcelExporter] reverses the list before writing; pass reversed filtered
// rows so the file order matches the on-screen table (top bottom).
final exportInput =
_filtered.reversed.map((r) => Map<String, dynamic>.from(r)).toList();
final exportInput = _filtered.reversed.map((r) {
final m = Map<String, dynamic>.from(r);
final pu = _payoutAndUtr(m);
m['commission_amount'] = pu.payout;
m['utr_no'] = pu.utr;
return m;
}).toList();
try {
await ExcelExporter.exportToExcel(
@ -944,9 +991,16 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
Expanded(flex: 1, child: Text('WEIGHT', style: _headerColumn)),
Expanded(flex: 2, child: Text('FUEL TYPE', style: _headerColumn)),
Expanded(flex: 2, child: Text('VEHICLE TYPE', style: _headerColumn)),
Expanded(flex: 2, child: Text('PREMIUM', style: _headerColumn)),
Expanded(flex: 2, child: Text('PAYOUT', style: _headerColumn)),
Expanded(flex: 2, child: Text('UTR NO', style: _headerColumn)),
Expanded(
flex: 2,
child: Text('PREMIUM', style: _headerColumn, textAlign: TextAlign.right),
),
Expanded(
flex: 2,
child: Text('PAYOUT', style: _headerColumn, textAlign: TextAlign.right),
),
const SizedBox(width: 15),
Expanded(flex: 2, child: Text('RECEIVED', style: _headerColumn)),
],
),
@ -965,6 +1019,8 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
)
: _dataBold;
final pu = _payoutAndUtr(item);
final row = Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -1014,11 +1070,21 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 2,
child: Text(
pu.utr,
style: rowStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 2,
child: Text(
_cell(item, ['premium']),
style: rowStyle,
textAlign: TextAlign.right,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
@ -1026,21 +1092,14 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
Expanded(
flex: 2,
child: Text(
_cell(item, ['commission_amount']),
style: rowStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 2,
child: Text(
_cell(item, ['utr_no']),
pu.payout,
style: rowStyle,
textAlign: TextAlign.right,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 15),
Expanded(
flex: 2,
child: Text(

View File

@ -117,10 +117,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.1"
version: "1.4.0"
charcode:
dependency: transitive
description:
@ -780,26 +780,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.19"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.13.0"
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.16.0"
mime:
dependency: transitive
description:
@ -1297,10 +1297,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
version: "0.7.6"
toastification:
dependency: "direct main"
description:
@ -1502,5 +1502,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.0-0 <4.0.0"
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.32.0"