Merge branch 'master' of bitbucket.org:jubilian/nhance_partner
This commit is contained in:
commit
b28e44341f
File diff suppressed because one or more lines are too long
@ -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') ...[
|
if (role == 'Accounts') ...[
|
||||||
_buildMenuItem(
|
_buildHoverMenuItem(
|
||||||
icon: Icons.payments_outlined,
|
icon: Icons.payments_outlined,
|
||||||
label: "Payout",
|
label: "Payout",
|
||||||
isActive: activeMenu == 'Payout',
|
popupKey: 'Payout',
|
||||||
onTap: () {
|
isActive: activeMenu == 'Payout' ||
|
||||||
_hidePopup();
|
activeMenu == 'PayoutUpload' ||
|
||||||
setState(() => _activeMenu = 'Payout');
|
activeMenu == 'PayoutReport',
|
||||||
context.go(AppRoutes.payoutList);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
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
|
// Partner (agent): Payout Report — same route as Accounts
|
||||||
|
|||||||
@ -730,7 +730,9 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
|
|||||||
final isAgent =
|
final isAgent =
|
||||||
(ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase() ==
|
(ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase() ==
|
||||||
'agent';
|
'agent';
|
||||||
|
final isMobile = ResponsiveLayout.isMobile(context);
|
||||||
return Row(
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (!isAgent) ...[
|
if (!isAgent) ...[
|
||||||
Tooltip(
|
Tooltip(
|
||||||
@ -751,14 +753,12 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
|
|||||||
style: GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w400),
|
style: GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w400),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// Text(
|
if (isMobile)
|
||||||
// 'Retention Rate: $_retentionRate%',
|
Expanded(child: _buildSearch(context, true))
|
||||||
// style: GoogleFonts.poppins(
|
else
|
||||||
// fontSize: 12,
|
_buildSearch(context, false),
|
||||||
// fontWeight: FontWeight.w500,
|
const SizedBox(width: 12),
|
||||||
// color: const Color(0xFF2E7D6E),
|
_buildExportButtonWidget(),
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1006,18 +1006,9 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
dropdownRto(),
|
dropdownRto(),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildSearch(context, isMobile),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
searchBtn(),
|
searchBtn(),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
resetBtn(),
|
resetBtn(),
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
_buildExportButtonWidget(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: Row(
|
: Row(
|
||||||
@ -1044,10 +1035,6 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
|
||||||
_buildSearch(context, isMobile),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_buildExportButtonWidget(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -558,40 +558,108 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final maxBody = 1200.0;
|
final mobile = ResponsiveLayout.isMobile(context);
|
||||||
final hPad = ResponsiveLayout.isMobile(context) ? 12.0 : 20.0;
|
final hPad = mobile ? 12.0 : 20.0;
|
||||||
|
final vPad = mobile ? 12.0 : 16.0;
|
||||||
final viewW = constraints.hasBoundedWidth && constraints.maxWidth.isFinite
|
final viewW = constraints.hasBoundedWidth && constraints.maxWidth.isFinite
|
||||||
? constraints.maxWidth
|
? constraints.maxWidth
|
||||||
: MediaQuery.sizeOf(context).width;
|
: MediaQuery.sizeOf(context).width;
|
||||||
final contentW = viewW > maxBody ? maxBody : viewW;
|
final contentW = (viewW - 2 * hPad).clamp(280.0, double.infinity);
|
||||||
return Align(
|
|
||||||
alignment: Alignment.topCenter,
|
final form = Form(
|
||||||
child: ConstrainedBox(
|
key: _formKey,
|
||||||
constraints: BoxConstraints(maxWidth: maxBody),
|
autovalidateMode: AutovalidateMode.disabled,
|
||||||
child: SingleChildScrollView(
|
child: mobile
|
||||||
padding: EdgeInsets.symmetric(
|
? Column(
|
||||||
horizontal: hPad,
|
|
||||||
vertical: ResponsiveLayout.isMobile(context) ? 12 : 16,
|
|
||||||
),
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
autovalidateMode: AutovalidateMode.disabled,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_pageHeaderBar(context, pageTitle),
|
_pageHeaderBar(context, pageTitle),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_partnerDetailsCard(context, contentW),
|
Expanded(
|
||||||
const SizedBox(height: 24),
|
child: SingleChildScrollView(
|
||||||
_retentionSection(context),
|
padding: EdgeInsets.only(bottom: vPad),
|
||||||
SizedBox(height: ResponsiveLayout.isMobile(context) ? 24 : 16),
|
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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -600,6 +668,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) {
|
Widget _partnerDetailsCard(BuildContext context, double contentW) {
|
||||||
final pad = ResponsiveLayout.isMobile(context) ? 14.0 : 20.0;
|
final pad = ResponsiveLayout.isMobile(context) ? 14.0 : 20.0;
|
||||||
return Container(
|
return Container(
|
||||||
@ -612,15 +691,6 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
'Partner details',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: const Color(0xFF1E293B),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_partnerRows(context, contentW),
|
_partnerRows(context, contentW),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Align(
|
Align(
|
||||||
@ -972,12 +1042,122 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _retentionSection(BuildContext context) {
|
Widget _retentionTableInCard({
|
||||||
final mobile = ResponsiveLayout.isMobile(context);
|
required double viewportHeight,
|
||||||
return Column(
|
required double parentWidth,
|
||||||
|
}) {
|
||||||
|
final tableColumn = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
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(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@ -999,81 +1179,51 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
|
|||||||
onChanged: (_) => setState(() {}),
|
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),
|
const SizedBox(height: 12),
|
||||||
if (mobile)
|
tableArea,
|
||||||
_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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_retentionSaveAllButtonBottom(context),
|
_retentionSaveAllButtonBottom(context),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -967,31 +967,31 @@ class _PayoutListState extends ConsumerState<PayoutList> {
|
|||||||
const SizedBox(width: _actionIconGap),
|
const SizedBox(width: _actionIconGap),
|
||||||
|
|
||||||
// Step 2: Add payment only when balance amount is pending.
|
// Step 2: Add payment only when balance amount is pending.
|
||||||
IconButton(
|
// IconButton(
|
||||||
icon: const Icon(
|
// icon: const Icon(
|
||||||
Icons.currency_rupee,
|
// Icons.currency_rupee,
|
||||||
color: Colors.green,
|
// color: Colors.green,
|
||||||
size: _actionIconSize,
|
// size: _actionIconSize,
|
||||||
),
|
// ),
|
||||||
tooltip: 'Add payment',
|
// tooltip: 'Add payment',
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
final balance = _toDouble(item['balance_amount']);
|
// final balance = _toDouble(item['balance_amount']);
|
||||||
if (balance <= 0) {
|
// if (balance <= 0) {
|
||||||
ToastHelper.showWarningToast(context, 'Invoice already fully paid');
|
// ToastHelper.showWarningToast(context, 'Invoice already fully paid');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
_showAddPaymentDialog(item);
|
// _showAddPaymentDialog(item);
|
||||||
},
|
// },
|
||||||
splashRadius: 16,
|
// splashRadius: 16,
|
||||||
hoverColor: Colors.black12,
|
// hoverColor: Colors.black12,
|
||||||
padding: EdgeInsets.zero,
|
// padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints.tightFor(
|
// constraints: const BoxConstraints.tightFor(
|
||||||
width: _actionButtonSize,
|
// width: _actionButtonSize,
|
||||||
height: _actionButtonSize,
|
// height: _actionButtonSize,
|
||||||
),
|
// ),
|
||||||
visualDensity: VisualDensity.compact,
|
// visualDensity: VisualDensity.compact,
|
||||||
),
|
// ),
|
||||||
const SizedBox(width: _actionIconGap),
|
// const SizedBox(width: _actionIconGap),
|
||||||
|
|
||||||
// Step 3: Show account history modal for this invoice.
|
// Step 3: Show account history modal for this invoice.
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@ -603,6 +603,48 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
|
|||||||
return '-';
|
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) {
|
static num? _parseNum(dynamic v) {
|
||||||
if (v == null) return null;
|
if (v == null) return null;
|
||||||
if (v is num) return v;
|
if (v is num) return v;
|
||||||
@ -722,8 +764,13 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
|
|||||||
|
|
||||||
// [ExcelExporter] reverses the list before writing; pass reversed filtered
|
// [ExcelExporter] reverses the list before writing; pass reversed filtered
|
||||||
// rows so the file order matches the on-screen table (top → bottom).
|
// rows so the file order matches the on-screen table (top → bottom).
|
||||||
final exportInput =
|
final exportInput = _filtered.reversed.map((r) {
|
||||||
_filtered.reversed.map((r) => Map<String, dynamic>.from(r)).toList();
|
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 {
|
try {
|
||||||
await ExcelExporter.exportToExcel(
|
await ExcelExporter.exportToExcel(
|
||||||
@ -955,9 +1002,16 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
|
|||||||
Expanded(flex: 1, child: Text('WEIGHT', style: _headerColumn)),
|
Expanded(flex: 1, child: Text('WEIGHT', style: _headerColumn)),
|
||||||
Expanded(flex: 2, child: Text('FUEL TYPE', 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('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('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)),
|
Expanded(flex: 2, child: Text('RECEIVED', style: _headerColumn)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -976,6 +1030,8 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
|
|||||||
)
|
)
|
||||||
: _dataBold;
|
: _dataBold;
|
||||||
|
|
||||||
|
final pu = _payoutAndUtr(item);
|
||||||
|
|
||||||
final row = Row(
|
final row = Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -1025,41 +1081,36 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topRight,
|
|
||||||
child: Text(
|
|
||||||
_cell(item, ['premium']),
|
|
||||||
style: rowStyle,
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topRight,
|
|
||||||
child: Text(
|
|
||||||
_cell(item, ['commission_amount']),
|
|
||||||
style: rowStyle,
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Text(
|
child: Text(
|
||||||
_cell(item, ['utr_no']),
|
pu.utr,
|
||||||
style: rowStyle,
|
style: rowStyle,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Text(
|
||||||
|
_cell(item, ['premium']),
|
||||||
|
style: rowStyle,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Text(
|
||||||
|
pu.payout,
|
||||||
|
style: rowStyle,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user