Merge branch 'master' of bitbucket.org:jubilian/nhance_partner

This commit is contained in:
sanjeev.p 2026-04-07 09:14:35 +05:30
commit cb9c84bf0b
15 changed files with 1293 additions and 578 deletions

File diff suppressed because one or more lines are too long

View File

@ -374,7 +374,7 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
),
const SizedBox(height: 2),
_buildPopupItem(
label: "Payout upload",
label: "Payout Upload",
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'PayoutUpload');

View File

@ -14,7 +14,6 @@ import '../../../data/utils/validators.dart';
import '../../layouts/main_layout.dart';
import '../../layouts/responsive_layout.dart';
import '../../providers/manager_provider.dart';
import '../../themes/indicators/date_field_theme.dart';
import '../../themes/indicators/export_btn.dart';
import '../../themes/indicators/search_field_theme.dart';
import '../../widgets/custom_Stdate_EnDate_Filter.dart';
@ -1077,77 +1076,6 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
);
}
Widget buildStartDate(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Start Date', style: _textStyle),
SizedBox(height: 5),
ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.16,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
// validator: (value) => Validators.requiredField(value, "date"),
controller: controllers['startDate']!,
onDateSelected: (date) {
print("SPicked Date: $date");
controllers['startDate']?.text = DateFormat(
'dd-MM-yyyy',
).format(date);
// controllers['date']?.text = date as String;
},
),
],
);
}
Widget buildEndDate(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('End Date', style: _textStyle),
SizedBox(height: 5),
ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.16,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
validator: (value) {
if (value == null || value.isEmpty) {
return "End Date is required";
}
final fromText = controllers['startDate']?.text ?? '';
if (fromText.isNotEmpty) {
final startDate = DateFormat('dd-MM-yyyy').parse(fromText);
final endDate = DateFormat('dd-MM-yyyy').parse(value);
if (endDate.isBefore(startDate)) {
return "End Date cannot be earlier than Start Date";
}
}
return null; // no error
},
controller: controllers['endDate']!,
lastDate: DateTime.now(),
onDateSelected: (date) {
print("EPicked Date: $date");
controllers['endDate']?.text = DateFormat(
'dd-MM-yyyy',
).format(date);
// controllers['date']?.text = date as String;
},
),
],
);
}
static final _dataBold = GoogleFonts.inter(
fontSize: 12,
@ -1186,9 +1114,4 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
fontWeight: FontWeight.w400,
fontSize: 12,
);
static const _textStyle = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
);
}

View File

@ -15,7 +15,7 @@ import '../../../layouts/main_layout.dart';
import '../../../layouts/responsive_layout.dart';
import '../../../providers/manager_provider.dart';
import '../../../themes/indicators/customizd_file_upload.dart';
import '../../../themes/indicators/date_field_theme.dart';
import '../../../themes/indicators/date_range_picker_field.dart';
import '../../../themes/indicators/export_btn.dart';
import '../../../themes/indicators/input_field_decoration.dart';
import '../../../themes/indicators/search_field_theme.dart';
@ -795,10 +795,7 @@ class endosementState extends ConsumerState<Endorsement> {
scrollDirection: Axis.horizontal,
child: Row(
children: [
buildStartDate(context),
const SizedBox(width: 12),
buildEndDate(context),
buildDateRangeFilter(context),
const SizedBox(width: 12),
buildEndorsementTypeSearch(context),
@ -1448,38 +1445,20 @@ class endosementState extends ConsumerState<Endorsement> {
);
}
Widget buildStartDate(BuildContext context) {
Widget buildDateRangeFilter(BuildContext context) {
return SizedBox(
height: 36,
width: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.10,
child: ThemedDateField(
hintText: "Start Date",
controller: startController,
onDateSelected: (date) {
startController.text =
DateFormat('dd-MM-yyyy').format(date);
},
),
);
}
Widget buildEndDate(BuildContext context) {
return SizedBox(
height: 36,
width: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.10,
child: ThemedDateField(
hintText: "End Date",
controller: endController,
: MediaQuery.of(context).size.width * 0.15,
child: DateRangePickerField(
startController: startController,
endController: endController,
hintText: 'Select date range',
showLabelAboveField: false,
txtheight: 36,
lastDate: DateTime.now(),
onDateSelected: (date) {
endController.text =
DateFormat('dd-MM-yyyy').format(date);
// refresh();
},
useFormField: false,
),
);
}

View File

@ -4396,42 +4396,6 @@ Expanded(
);
}
Widget buildStartDate(BuildContext context) {
return SizedBox(
height: 36,
width: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.10,
child: ThemedDateField(
hintText: "Start Date",
controller: startController,
onDateSelected: (date) {
startController.text =
DateFormat('dd-MM-yyyy').format(date);
},
),
);
}
Widget buildEndDate(BuildContext context) {
return SizedBox(
height: 36,
width: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.10,
child: ThemedDateField(
hintText: "End Date",
controller: endController,
lastDate: DateTime.now(),
onDateSelected: (date) {
endController.text =
DateFormat('dd-MM-yyyy').format(date);
// refresh();
},
),
);
}
Widget buildRTOSearch(BuildContext context) {
final List<Map<String, dynamic>> endorsementOptions =
List<Map<String, dynamic>>.from(filteredEndrosmentData);

View File

@ -286,7 +286,9 @@ class VehicleTypeListState extends ConsumerState<VehicleTypeList> {
),
Expanded(
flex: 2,
child: Text('Action', style: _headerStyle),
child: Center(
child: Text('Action', style: _headerStyle),
),
),
],
),
@ -415,38 +417,34 @@ class VehicleTypeListState extends ConsumerState<VehicleTypeList> {
),
Expanded(
flex: 2,
child: Center( // Add Center wrapper
child: Row(
mainAxisSize: MainAxisSize.min, // Add this
children: [
item['is_active'] == "1"
? Tooltip(
message: 'Edit',
child: IconButton(
icon: Image.asset(
"assets/miscellaneous/Edit.png",
child: Align(
alignment: Alignment.center,
child: item['is_active'] == "1"
? Tooltip(
message: 'Edit',
child: IconButton(
icon: Image.asset(
"assets/miscellaneous/Edit.png",
height: 12,
width: 15,
),
onPressed: () {
handleEdit(item);
},
splashRadius: 20,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
),
)
: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.asset(
"assets/miscellaneous/Edit_muted.png",
height: 12,
width: 15,
),
onPressed: () {
handleEdit(item);
},
splashRadius: 28,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
),
)
: Padding(
padding: const EdgeInsets.all(8.0), // Match IconButton padding
child: Image.asset(
"assets/miscellaneous/Edit_muted.png",
height: 12,
width: 15,
),
),
],
),
),
),
],

View File

@ -427,7 +427,10 @@ class _RenewalAlertsCard extends StatelessWidget {
});
@override
Widget build(BuildContext context) => _DashCard(
Widget build(BuildContext context) {
final visibleRowCount = renewals.length.clamp(1, kVisibleRows);
final listHeight = visibleRowCount * kRowHeight;
return _DashCard(
header: Row(children: [
Container(
width: 30,
@ -519,7 +522,7 @@ class _RenewalAlertsCard extends StatelessWidget {
),
),
SizedBox(
height: kVisibleRows * kRowHeight,
height: listHeight,
child: ListView.separated(
physics: const ClampingScrollPhysics(),
itemCount: renewals.length,
@ -534,6 +537,7 @@ class _RenewalAlertsCard extends StatelessWidget {
],
),
);
}
}
class _RenewalTableRow extends StatelessWidget {

View File

@ -1,14 +1,16 @@
import 'dart:math' show min;
import 'package:dropdown_search/dropdown_search.dart';
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 'package:nhance_partner/data/utils/toastNotification.dart';
import '../../../core/services/api_service.dart';
import '../../layouts/responsive_layout.dart';
import '../../providers/manager_provider.dart';
import '../../providers/userRoleProvider.dart';
import '../../themes/indicators/date_field_theme.dart';
import '../../themes/indicators/date_range_picker_field.dart';
import '../../themes/indicators/input_field_decoration.dart';
class DateFilterRowPayout extends ConsumerStatefulWidget {
@ -28,6 +30,13 @@ class DateFilterRowPayout extends ConsumerStatefulWidget {
final role;
final id;
/// When non-null, toggles filter layout for payout details:
/// - `false` no table rows: show date range only (hide partner + actions).
/// - `true` table has rows: show partner (+ broker if [showBrokerFilter]) and
/// search/reset aligned to the right; hide date range.
/// When `null`, legacy: date range and partner row together.
final bool? policyTableHasRows;
const DateFilterRowPayout({
super.key,
this.showBrokerFilter = true,
@ -45,6 +54,7 @@ class DateFilterRowPayout extends ConsumerStatefulWidget {
required this.id,
this.isMobile = false,
this.dataFrom,
this.policyTableHasRows,
});
@override
@ -224,28 +234,17 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
final spacing = 5.0;
List<Widget> getRowChildren(bool isMobile, double spacing) {
final dateFields = [
buildStartDate(context),
SizedBox(width: spacing),
buildEndDate(context),
SizedBox(width: spacing),
if (widget.showBrokerFilter) ...[
buildBroker(context),
SizedBox(width: spacing),
],
buildPartner(context),
SizedBox(width: spacing),
];
final hasDateRange = widget.startController.text.trim().isNotEmpty &&
widget.endController.text.trim().isNotEmpty;
double partnerMaxWidth() {
final sw = MediaQuery.sizeOf(context).width;
return min(sw * 0.32, 420);
}
final buttons = [
Padding(
padding: const EdgeInsets.symmetric(vertical: 0.0),
// child: GestureDetector(
// onTap: () {
// if (formKey.currentState!.validate()) onFilter();
// },
// child: Icon(Icons.filter_alt_outlined),
// ),
child: Tooltip(
message: 'Filter',
child: IconButton(
@ -254,18 +253,26 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
size: 18,
color: const Color(0xFF94A3B8),
),
onPressed: () {
if (widget.formKey.currentState!.validate()) {
widget.onFilter();
if (widget.formKey.currentState?.validate() != true) return;
final partners = widget.selectedParnter;
final hasPartner = partners != null &&
partners.isNotEmpty &&
partners.any((e) => e.toString().trim().isNotEmpty);
if (!hasPartner) {
ToastHelper.showWarningToast(
context,
'Please select a partner before searching.',
);
return;
}
widget.onFilter();
},
),
),
),
Padding(
padding: const EdgeInsets.all(0.0),
// child: GestureDetector(onTap: onRefresh, child: Icon(Icons.refresh)),
child: Tooltip(
message: 'Refresh',
child: IconButton(
@ -280,17 +287,38 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
),
];
if (isMobile) {
// For mobile: buttons below fields
return [
...dateFields,
SizedBox(height: 10),
Row(mainAxisAlignment: MainAxisAlignment.end, children: buttons),
];
} else {
// For desktop: buttons inline with fields
return [...dateFields, Spacer(), ...buttons];
final partnerActionsRow = <Widget>[
if (widget.showBrokerFilter) ...[
buildBroker(context),
SizedBox(width: spacing),
],
buildPartner(context, maxWidth: partnerMaxWidth()),
SizedBox(width: spacing),
...buttons,
];
if (!hasDateRange) {
return [buildDateRangeFilter(context)];
}
if (isMobile) {
return [
buildDateRangeFilter(context),
const SizedBox(height: 10),
Wrap(
spacing: spacing,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.end,
children: partnerActionsRow,
),
];
}
return [
buildDateRangeFilter(context),
SizedBox(width: spacing),
Spacer(),
...partnerActionsRow,
];
}
return Form(
@ -309,78 +337,19 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
);
}
Widget buildStartDate(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Start Date *', style: _textStyle),
SizedBox(height: 5),
SizedBox(
height: 35,
child: ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.15,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
// validator: (value) => Validators.requiredField(value, "date"),
controller: widget.startController,
onDateSelected: (date) {
print("Picked Date: $date");
widget.startController.text = DateFormat(
'dd-MM-yyyy',
).format(date);
// controllers['date']?.text = date as String;
Future.microtask(() => widget.onFilter());
},
),
),
],
);
}
Widget buildEndDate(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('End Date *', style: _textStyle),
SizedBox(height: 5),
SizedBox(
height: 35,
child: ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.15,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
validator: (value) {
final fromText = widget.startController.text ?? '';
if (fromText.isNotEmpty) {
final startDate = DateFormat('dd-MM-yyyy').parse(fromText);
if (value == null || value.isEmpty) {
return "End Date is required";
}
final endDate = DateFormat('dd-MM-yyyy').parse(value);
if (endDate.isBefore(startDate)) {
return "End Date cannot be earlier than Start Date";
}
}
return null; // no error
},
controller: widget.endController,
// lastDate: DateTime.now(),
onDateSelected: (date) {
print("Picked Date: $date");
widget.endController.text = DateFormat('dd-MM-yyyy').format(date);
Future.microtask(() => widget.onFilter());
// controllers['date']?.text = date as String;
},
),
),
],
Widget buildDateRangeFilter(BuildContext context) {
final w = ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.15;
return DateRangePickerField(
startController: widget.startController,
endController: widget.endController,
hintText: 'Select date range',
txtwidth: w,
txtheight: 32,
lastDate: DateTime.now(),
rangeValidator: DateRangePickerField.defaultFilterValidator,
onRangeSelected: (_) => Future.microtask(() => widget.onFilter()),
);
}
@ -555,8 +524,8 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
);
}
Widget buildPartner(BuildContext context) {
final isReadOnly = false;
Widget buildPartner(BuildContext context, {double? maxWidth}) {
final w = maxWidth ?? MediaQuery.of(context).size.width * 0.4;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -565,7 +534,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
SizedBox(height: 5),
SizedBox(
height: 35,
width: MediaQuery.of(context).size.width * 0.4,
width: w,
child: DropdownSearch<Map<String, dynamic>>.multiSelection(
key: dropDownKeyPartner,
@ -591,6 +560,28 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
"${val['agent_name']} | ${val['unused_commission_amount']} (${val['total_policies']})",
compareFn: (item, selectedItem) =>
item['agent_id'] == selectedItem['agent_id'], // compare by id
dropdownBuilder: (context, selectedItems) {
final safeItems = selectedItems;
final text = safeItems.isEmpty
? 'Select Partner'
: safeItems.length == 1
? "${safeItems.first['agent_name']} | ${safeItems.first['unused_commission_amount']} (${safeItems.first['total_policies']})"
: '${safeItems.length} partners selected';
return Align(
alignment: Alignment.centerLeft,
child: Text(
text,
style: GoogleFonts.inter(
fontSize: 12,
color: safeItems.isEmpty
? const Color(0xFF64748B)
: Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
// validator: (val) {
// if (val == null) {
// return "Required"; // error message
@ -612,8 +603,9 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: "Select Partner",
label: "",
).copyWith(
hintText: null,
hintStyle: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,

View File

@ -5,13 +5,16 @@ import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:nhance_partner/data/utils/toastNotification.dart';
import 'dart:math' show max;
import '../../../data/utils/Pagination.dart';
import '../../../core/routing/routes.dart';
import '../../../core/services/api_service.dart';
import '../../layouts/main_layout.dart';
import '../../layouts/responsive_layout.dart';
import '../../providers/manager_provider.dart';
import '../../providers/userRoleProvider.dart';
import '../../themes/indicators/export_btn.dart';
import 'custom_dateRange.dart';
class PayOutDetails extends ConsumerStatefulWidget {
@ -92,6 +95,8 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
bool isLoading = false;
bool hasFetchedTableData = false;
int currentPage = 1;
int itemsPerPage = 10;
final Set<int> _inlineSavingIds = <int>{};
/// Per-policy payout editors so [setState] rebuilds do not reset focus/text.
final Map<int, TextEditingController> _commissionControllers = {};
@ -184,6 +189,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
return TextFormField(
key: fieldKey,
controller: controller,
textAlign: TextAlign.right,
style: fieldStyle,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: _commissionInputFormatter,
@ -195,6 +201,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
return TextFormField(
key: fieldKey,
initialValue: (p["commission_amount"] ?? '').toString(),
textAlign: TextAlign.right,
style: fieldStyle,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: _commissionInputFormatter,
@ -206,6 +213,9 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
@override
void dispose() {
for (final controller in controllers.values) {
controller.dispose();
}
_disposeCommissionControllers();
super.dispose();
}
@ -217,6 +227,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
print('PD => filteredPolicies1 - $filteredPolicies');
setState(() {
currentPage = 1;
if (query.trim().isEmpty) {
print('PD => filteredPolicies2');
filteredPolicies = List.from(masterPolicies);
@ -253,7 +264,6 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
setState(() {
for (var controller in controllers.values) {
controller.clear();
controller.dispose();
}
selectedBrokerID = '';
@ -262,6 +272,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
totalPolicies = '';
totalCommission = '';
filteredPolicies = [];
currentPage = 1;
_disposeCommissionControllers();
hasFetchedTableData = false;
// dropDownKeyPartner.currentState?.clear();
@ -380,8 +391,9 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
hasFetchedTableData = false;
});
try {
final List<int> agentIds = (selectedAgentId ?? [])
.map((e) => int.parse(e))
final List<String> agentIds = (selectedAgentId ?? [])
.map((e) => e.toString().trim())
.where((e) => e.isNotEmpty)
.toList();
final jsondata = {
@ -400,6 +412,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
filteredPolicies = List<Map<String, dynamic>>.from(response['data']);
_rebuildCommissionControllers(filteredPolicies);
hasFetchedTableData = true;
currentPage = 1;
print('PD =>API Data - $filteredPolicies');
// filteredPolicies = allPolicies.where((p) {
// if (p["agentId"] != selectedAgentId) return false;
@ -414,6 +427,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
filteredPolicies = [];
_rebuildCommissionControllers(filteredPolicies);
hasFetchedTableData = true;
currentPage = 1;
});
}
} catch (e) {
@ -422,6 +436,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
filteredPolicies = [];
_rebuildCommissionControllers(filteredPolicies);
hasFetchedTableData = false;
currentPage = 1;
});
} finally {
setState(() {
@ -450,13 +465,13 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
// Save invoice (simulation)
Future<void> saveInvoice() async {
try {
if (selectedAgentId == null) {
ToastHelper.showWarningToast(
context,
"Please select a partner before saving.",
);
return;
}
// if (selectedAgentId == null) {
// ToastHelper.showWarningToast(
// context,
// "Please select a partner before saving.",
// );
// return;
// }
if (selectedPolicies.isEmpty) {
ToastHelper.showWarningToast(
@ -848,6 +863,30 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
),
),
const SizedBox(width: 10),
ExportBtn(
sheetName: 'Payout Details',
fileName:
'payout_details_${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}',
data: _exportRows,
displayHeaders: const [
'S.NO',
'POLICY NO',
'POLICY DATE',
'PARTNER',
'CUSTOMER',
'PREMIUM (₹)',
'PAYOUT (₹)',
],
keys: const [
'sno',
'policy_no',
'policy_date',
'partner',
'customer',
'premium',
'payout',
],
),
],
],
),
@ -863,6 +902,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
role: roleId,
id: userId,
showBrokerFilter: false,
policyTableHasRows: filteredPolicies.isNotEmpty,
selectedParnter: selectedAgentId,
selectedBroker: selectedBrokerID,
@ -913,7 +953,10 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
secondChild: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: _tableMinWidth,
width: max(
_tableMinWidth,
MediaQuery.of(context).size.width - 32,
),
child: Column(
children: [
_policyTableHeader(), // static header
@ -926,7 +969,6 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
],
),
),
if (selectedPolicies.isNotEmpty) ...[
Container(
padding: const EdgeInsets.all(8),
@ -937,59 +979,59 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ---------------- SELECTED COUNT ----------------
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
// selectedPolicies.length.toString(),
totalPolicies,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.blueGrey,
),
),
const SizedBox(width: 4),
Text(
"policies selected",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.blueGrey,
),
),
const SizedBox(width: 4),
Text(
"|",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.blueGrey,
),
),
const SizedBox(width: 4),
Text(
"Total Payout Amount",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.blueGrey,
),
),
const SizedBox(width: 4),
/// NULL-SAFE TOTAL COMMISSION CALCULATION
Text(
// "${_calculateTotalCommission()}",
"$totalCommission",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.teal,
),
),
],
),
// Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// // selectedPolicies.length.toString(),
// totalPolicies,
// style: GoogleFonts.poppins(
// fontSize: 12,
// fontWeight: FontWeight.w500,
// color: Colors.blueGrey,
// ),
// ),
// const SizedBox(width: 4),
// Text(
// "policies selected",
// style: GoogleFonts.poppins(
// fontSize: 12,
// fontWeight: FontWeight.w500,
// color: Colors.blueGrey,
// ),
// ),
// const SizedBox(width: 4),
// Text(
// "|",
// style: GoogleFonts.poppins(
// fontSize: 12,
// fontWeight: FontWeight.w500,
// color: Colors.blueGrey,
// ),
// ),
// const SizedBox(width: 4),
// Text(
// "Total Payout Amount",
// style: GoogleFonts.poppins(
// fontSize: 12,
// fontWeight: FontWeight.w500,
// color: Colors.blueGrey,
// ),
// ),
// const SizedBox(width: 4),
//
// /// NULL-SAFE TOTAL COMMISSION CALCULATION
// Text(
// // "${_calculateTotalCommission()}",
// "$totalCommission",
// style: GoogleFonts.poppins(
// fontSize: 14,
// fontWeight: FontWeight.w500,
// color: Colors.teal,
// ),
// ),
// ],
// ),
Spacer(),
buildUTRNumber(context),
@ -1036,6 +1078,27 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
),
),
],
if (filteredPolicies.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 2),
child: Align(
alignment: Alignment.centerRight,
child: PaginationControls(
currentPage: currentPage,
itemsPerPage: itemsPerPage,
totalItems: filteredPolicies.length,
onPageChanged: (page) {
setState(() => currentPage = page);
},
onItemsPerPageChanged: (items) {
setState(() {
itemsPerPage = items;
currentPage = 1;
});
},
),
),
),
// ----------------------
// const SizedBox(height: 100),
],
@ -1101,22 +1164,25 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
},
),
),
_headerText("S.NO", colSno, align: TextAlign.center),
_headerText("POLICY NO", colPolicy),
_headerText("POLICY DATE", colDate),
_headerText("PARTNER", colAgent),
_headerText("CUSTOMER", colCustomer),
_headerText("PREMIUM", colPremium),
_headerText("PAYOUT", colCommission),
_headerText("PREMIUM (₹)", colPremium, align: TextAlign.right),
const SizedBox(width: 10),
_headerText("PAYOUT (₹)", colCommission, align: TextAlign.right),
],
),
);
}
Widget _headerText(String text, double width) {
Widget _headerText(String text, double width, {TextAlign align = TextAlign.left}) {
return SizedBox(
width: width,
child: Text(
text,
textAlign: align,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w500,
@ -1128,12 +1194,15 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
}
Widget _policyTableBody() {
final pageRows = _paginatedPolicies;
final startIndex =
filteredPolicies.isEmpty ? 0 : (currentPage - 1) * itemsPerPage;
return SizedBox(
height: MediaQuery.of(context).size.height * 0.48,
child: ListView.builder(
itemCount: filteredPolicies.length,
itemCount: pageRows.length,
itemBuilder: (context, index) {
final p = filteredPolicies[index];
final p = pageRows[index];
final int id = int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0;
final bool isSelected = selectedPolicies.contains(id);
@ -1175,6 +1244,11 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
},
),
),
_cell(
'${startIndex + index + 1}',
colSno,
align: TextAlign.center,
),
_cell(p["policy_no"], colPolicy),
// _cell(p["issued_date"] ?? "-", colDate),
_cell(_formatDate(p['issued_date']), colDate),
@ -1186,20 +1260,16 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
colAgent,
),
_cell(p["customer_name"], colCustomer),
_cell("${p["premium_amount"]}", colPremium),
_cell(
_amountText(p["premium_amount"]),
colPremium,
align: TextAlign.right,
),
const SizedBox(width: 10),
SizedBox(
width: colCommission,
child: Row(
children: [
const Text(
"",
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF009B77),
),
),
const SizedBox(width: 2),
Expanded(
child: _commissionAmountField(
p,
@ -1218,6 +1288,35 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
);
}
String _amountText(dynamic value) {
final raw = (value ?? '').toString().replaceAll('', '').trim();
if (raw.isEmpty || raw.toLowerCase() == 'null') return '-';
return raw;
}
List<Map<String, dynamic>> get _paginatedPolicies {
if (filteredPolicies.isEmpty) return const <Map<String, dynamic>>[];
final start = (currentPage - 1) * itemsPerPage;
final end = (start + itemsPerPage).clamp(0, filteredPolicies.length);
return filteredPolicies.sublist(start, end);
}
List<Map<String, dynamic>> get _exportRows {
return filteredPolicies.map((p) {
return {
'sno': 0,
'policy_no': p['policy_no'],
'policy_date': _formatDate((p['issued_date'] ?? '').toString()),
'partner': (p["agent_code"] != null && p["agent_name"] != null)
? '${p["agent_code"]} - ${p["agent_name"]}'
: (p["agent_name"] ?? '-').toString(),
'customer': p['customer_name'],
'premium': _amountText(p['premium_amount']),
'payout': _amountText(p['commission_amount']),
};
}).toList();
}
double _parseCommissionValue(dynamic value) {
if (value == null) return 0;
return double.tryParse(value.toString().trim()) ?? 0;
@ -1300,11 +1399,16 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
}
}
Widget _cell(String? value, double width) {
Widget _cell(
dynamic value,
double width, {
TextAlign align = TextAlign.left,
}) {
return SizedBox(
width: width,
child: Text(
value ?? "-",
(value ?? "-").toString(),
textAlign: align,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 11),
),
@ -1426,20 +1530,24 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
}
static final double colCheck = 44;
static final double colSno = 60;
static final double colPolicy = 200;
static final double colDate = 200;
static final double colAgent = 200;
static final double colAgent = 400;
static final double colCustomer = 200;
static final double colPremium = 200;
static final double colCommission = 200;
static final double colPremiumPayoutGap = 10;
static final double colCommission = 150;
double get _tableMinWidth =>
colCheck +
colSno +
colPolicy +
colDate +
colAgent +
colCustomer +
colPremium +
colPremiumPayoutGap +
colCommission +
32;
}

View File

@ -5,11 +5,13 @@ import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import '../../../data/utils/Pagination.dart';
import '../../../core/routing/routes.dart';
import '../../../core/services/api_service.dart';
import '../../layouts/main_layout.dart';
import '../../layouts/responsive_layout.dart';
import '../../providers/manager_provider.dart';
import '../../themes/indicators/export_btn.dart';
import '../../themes/indicators/search_field_theme.dart';
/// Read-only payout invoice + policy lines (opened from payout list View).
@ -35,15 +37,8 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
dynamic totalPolicies = '0';
dynamic totalCommission = '0';
static const double colPolicy = 150;
static const double colDate = 120;
static const double colAgent = 200;
static const double colCustomer = 200;
static const double colPremium = 200;
static const double colCommission = 120;
double get _tableMinWidth =>
colPolicy + colDate + colAgent + colCustomer + colPremium + colCommission + 32;
int _rowsPerPage = 10;
int _currentPage = 1;
@override
void initState() {
@ -114,6 +109,7 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
totalCommission = totalCommiss;
masterPolicies = items;
filteredPolicies = List<Map<String, dynamic>>.from(items);
_currentPage = 1;
});
} else {
setState(() {
@ -132,22 +128,21 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
}
void filterPolicyData(String query) {
final lowerQuery = query.toLowerCase();
final q = query.trim();
setState(() {
if (query.trim().isEmpty) {
_currentPage = 1;
if (q.isEmpty) {
filteredPolicies = List.from(masterPolicies);
return;
}
filteredPolicies = masterPolicies.where((item) {
return (item['policy_no'] ?? '').toString().toLowerCase().contains(lowerQuery) ||
(item['customer_name'] ?? '').toString().toLowerCase().contains(lowerQuery) ||
(item['agent_name'] ?? '').toString().toLowerCase().contains(lowerQuery) ||
(item['insurer_name'] ?? '').toString().toLowerCase().contains(lowerQuery) ||
(item['premium_amount'] ?? '').toString().toLowerCase().contains(lowerQuery) ||
(item['commission_amount'] ?? '').toString().toLowerCase().contains(lowerQuery) ||
_formatDate(item['issued_date']?.toString() ?? '')
.toLowerCase()
.contains(lowerQuery);
return (item['policy_no'] ?? '').toString().contains(q) ||
(item['customer_name'] ?? '').toString().contains(q) ||
(item['agent_name'] ?? '').toString().contains(q) ||
(item['insurer_name'] ?? '').toString().contains(q) ||
(item['premium_amount'] ?? '').toString().contains(q) ||
(item['commission_amount'] ?? '').toString().contains(q) ||
_formatDate(item['issued_date']?.toString() ?? '').contains(q);
}).toList();
});
}
@ -182,11 +177,37 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
}
}
Widget _headerText(String text, double width) {
String _amountText(dynamic value) {
final text = _displayText(value);
if (text == '-') return '-';
return text.replaceAll('', '').trim();
}
List<double> _columnWidths(double maxWidth) {
const sidePadding = 32.0; // 16 left + 16 right
final usable = (maxWidth - sidePadding).clamp(720.0, 2000.0);
const totalFlex = 2 + 14 + 10 + 20 + 16 + 11 + 11;
return [
usable * (2 / totalFlex), // S.NO
usable * (14 / totalFlex), // POLICY NO
usable * (10 / totalFlex), // POLICY DATE
usable * (20 / totalFlex), // PARTNER
usable * (16 / totalFlex), // CUSTOMER
usable * (11 / totalFlex), // PREMIUM
usable * (11 / totalFlex), // PAYOUT
];
}
Widget _headerText(
String text,
double width, {
TextAlign align = TextAlign.left,
}) {
return SizedBox(
width: width,
child: Text(
text,
textAlign: align,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w500,
@ -197,18 +218,29 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
);
}
Widget _cell(String? value, double width) {
Widget _cell(
String? value,
double width, {
TextAlign align = TextAlign.left,
Color? color,
FontWeight? fontWeight,
}) {
return SizedBox(
width: width,
child: Text(
value ?? '-',
textAlign: align,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 11),
style: TextStyle(
fontSize: 11,
color: color,
fontWeight: fontWeight,
),
),
);
}
Widget _policyTableHeader() {
Widget _policyTableHeader(List<double> w) {
return Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 16),
@ -218,18 +250,19 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
),
child: Row(
children: [
_headerText('POLICY NO', colPolicy),
_headerText('POLICY DATE', colDate),
_headerText('PARTNER', colAgent),
_headerText('CUSTOMER', colCustomer),
_headerText('PREMIUM', colPremium),
_headerText('PAYOUT', colCommission),
_headerText('S.NO', w[0]),
_headerText('POLICY NO', w[1]),
_headerText('POLICY DATE', w[2]),
_headerText('PARTNER', w[3]),
_headerText('CUSTOMER', w[4]),
_headerText('PREMIUM (₹)', w[5], align: TextAlign.right),
_headerText('PAYOUT (₹)', w[6], align: TextAlign.right),
],
),
);
}
Widget _policyRow(Map<String, dynamic> p) {
Widget _policyRow(Map<String, dynamic> p, int index, List<double> w) {
return Container(
height: 38,
padding: const EdgeInsets.symmetric(horizontal: 16),
@ -239,33 +272,66 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
),
child: Row(
children: [
_cell(p['policy_no']?.toString(), colPolicy),
_cell(_formatDate(p['issued_date']?.toString()), colDate),
_cell('${index + 1}', w[0]),
_cell(p['policy_no']?.toString(), w[1]),
_cell(_formatDate(p['issued_date']?.toString()), w[2]),
_cell(
(p['agent_code'] != null && p['agent_name'] != null)
? '${p['agent_code']} - ${p['agent_name']}'
: (p['agent_name'] ?? '-').toString(),
colAgent,
w[3],
),
_cell(p['customer_name']?.toString(), colCustomer),
_cell('${p['premium_amount']}', colPremium),
SizedBox(
width: colCommission,
child: Text(
'${_displayText(p['commission_amount'])}',
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF009B77),
),
),
_cell(p['customer_name']?.toString(), w[4]),
_cell(
_amountText(p['premium_amount']),
w[5],
align: TextAlign.right,
),
_cell(
_amountText(p['commission_amount']),
w[6],
align: TextAlign.right,
color: const Color(0xFF009B77),
fontWeight: FontWeight.w600,
),
],
),
);
}
int get _totalPages {
if (filteredPolicies.isEmpty) return 1;
return (filteredPolicies.length / _rowsPerPage).ceil();
}
List<Map<String, dynamic>> get _paginatedPolicies {
if (filteredPolicies.isEmpty) return const <Map<String, dynamic>>[];
final start = (_currentPage - 1) * _rowsPerPage;
final end = (start + _rowsPerPage).clamp(0, filteredPolicies.length);
return filteredPolicies.sublist(start, end);
}
void _goToPage(int page) {
if (page < 1 || page > _totalPages) return;
setState(() => _currentPage = page);
}
List<Map<String, dynamic>> get _exportRows {
return filteredPolicies.map((p) {
return {
'sno': 0,
'policy_no': p['policy_no'],
'policy_date': _formatDate(p['issued_date']?.toString()),
'partner': (p['agent_code'] != null && p['agent_name'] != null)
? '${p['agent_code']} - ${p['agent_name']}'
: (p['agent_name'] ?? '-').toString(),
'customer': p['customer_name'],
'premium': _amountText(p['premium_amount']),
'payout': _amountText(p['commission_amount']),
};
}).toList();
}
@override
Widget build(BuildContext context) {
final statusRaw = widget.viewItem['payout_status'];
@ -356,6 +422,31 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
? MediaQuery.of(context).size.width * 0.7
: MediaQuery.of(context).size.width * 0.15,
),
const SizedBox(width: 10),
ExportBtn(
sheetName: 'Payout Details',
fileName:
'payout_details_${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}',
data: _exportRows,
displayHeaders: const [
'S.NO',
'POLICY NO',
'POLICY DATE',
'PARTNER',
'CUSTOMER',
'PREMIUM (₹)',
'PAYOUT (₹)',
],
keys: const [
'sno',
'policy_no',
'policy_date',
'partner',
'customer',
'premium',
'payout',
],
),
],
),
),
@ -377,45 +468,38 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
),
child: LayoutBuilder(
builder: (context, constraints) {
// Same pattern as enquiry list: nested axis scroll views
// avoids broken vertical gestures on web/desktop.
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
primary: false,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
primary: false,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: _tableMinWidth,
minHeight: constraints.maxHeight.isFinite
? constraints.maxHeight
: 0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_policyTableHeader(),
if (filteredPolicies.isEmpty)
const Padding(
padding: EdgeInsets.all(24),
child: Center(
child: Text(
'No policies in this payout',
style: TextStyle(
fontSize: 12,
color: Colors.blueGrey,
),
final widths = _columnWidths(constraints.maxWidth);
final rows = _paginatedPolicies;
final startIdx = (filteredPolicies.isEmpty)
? 0
: ((_currentPage - 1) * _rowsPerPage);
return Column(
children: [
_policyTableHeader(widths),
Expanded(
child: filteredPolicies.isEmpty
? const Center(
child: Text(
'No policies in this payout',
style: TextStyle(
fontSize: 12,
color: Colors.blueGrey,
),
),
)
else
...filteredPolicies.map(_policyRow),
],
),
: ListView.builder(
itemCount: rows.length,
itemBuilder: (context, index) {
return _policyRow(
rows[index],
startIdx + index,
widths,
);
},
),
),
),
],
);
},
),
@ -438,7 +522,7 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
),
const SizedBox(width: 4),
Text(
'policies',
'Policies',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
@ -475,6 +559,25 @@ class _PayOutDetailsViewState extends ConsumerState<PayOutDetailsView> {
],
),
),
if (filteredPolicies.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 2),
child: Align(
alignment: Alignment.centerRight,
child: PaginationControls(
currentPage: _currentPage,
itemsPerPage: _rowsPerPage,
totalItems: filteredPolicies.length,
onPageChanged: (page) => _goToPage(page),
onItemsPerPageChanged: (items) {
setState(() {
_rowsPerPage = items;
_currentPage = 1;
});
},
),
),
),
],
),
),

View File

@ -11,7 +11,7 @@ import '../../layouts/main_layout.dart';
import '../../layouts/responsive_layout.dart';
import '../../providers/manager_provider.dart';
import '../../providers/userRoleProvider.dart';
import '../../themes/indicators/date_field_theme.dart';
import '../../themes/indicators/date_range_picker_field.dart';
import '../../themes/indicators/export_btn.dart';
import '../../themes/indicators/search_field_theme.dart';
@ -69,6 +69,25 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
(s.length > 1 ? s.substring(1).toLowerCase() : '');
}
bool _isAccountsReceivedRole() {
final role = (ref.read(userRoleProvider) ?? '')
.toString()
.trim()
.toLowerCase()
.replaceAll(RegExp(r'[\s_\-]+'), '');
return role == 'accountsreceived' || role == 'accounts';
}
String _receivedDisplayLabel(dynamic v) {
final raw = v?.toString().trim().toLowerCase() ?? '';
if (_isAccountsReceivedRole()) {
if (raw == 'yes' || raw == 'received' || raw == 'paid') {
return 'Paid';
}
}
return _receivedLabel(v);
}
String? _agentIdForApi() {
if (_isAgentRole()) {
final uid = ref.read(userIdProvider);
@ -689,7 +708,7 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
valueColor: const Color(0xFF37474F),
),
_summaryChip(
label: 'Raised',
label: 'Paid',
value: _fmtSummaryAmount(_summaryRaised),
bg: const Color(0xFFE0F2F1),
border: const Color(0xFF80CBC4),
@ -787,7 +806,7 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
'Premium',
'Payout',
'UTR No',
'Received',
'Paid/Received',
],
keys: const [
'sno',
@ -802,7 +821,7 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
'received_or_not',
],
valueFormatters: {
'received_or_not': (v, _) => _receivedLabel(v),
'received_or_not': (v, _) => _receivedDisplayLabel(v),
},
);
if (mounted) {
@ -815,27 +834,6 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
}
}
Widget _buildDateField({
required TextEditingController controller,
required String hintText,
required bool isStart,
}) {
return SizedBox(
height: 35,
child: ThemedDateField(
hintText: hintText,
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.1,
controller: controller,
lastDate: isStart ? null : DateTime.now(),
onDateSelected: (date) {
controller.text = DateFormat('dd-MM-yyyy').format(date);
},
),
);
}
Widget _filterControlsRow(BuildContext context) {
final spacing = 5.0;
final isMobile = ResponsiveLayout.isMobile(context);
@ -845,16 +843,16 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
final fields = Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDateField(
controller: _startController,
hintText: 'Select Date',
isStart: true,
),
SizedBox(width: spacing),
_buildDateField(
controller: _endController,
hintText: 'Select Date',
isStart: false,
DateRangePickerField(
startController: _startController,
endController: _endController,
hintText: 'Select date range',
showLabelAboveField: false,
txtwidth: isMobile
? null
: MediaQuery.of(context).size.width * 0.15,
lastDate: DateTime.now(),
useFormField: false,
),
if (showAgentFilter) ...[
SizedBox(width: spacing),
@ -1012,7 +1010,13 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
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(
_isAccountsReceivedRole() ? 'PAID' : 'RECEIVED',
style: _headerColumn,
),
),
],
),
);
@ -1114,7 +1118,7 @@ class _PayoutReportScreenState extends ConsumerState<PayoutReportScreen> {
Expanded(
flex: 2,
child: Text(
_receivedLabel(item['received_or_not']),
_receivedDisplayLabel(item['received_or_not']),
style: rowStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,

View File

@ -0,0 +1,446 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
/// Compact date-range dialog (centered card, not full-screen on web).
/// From/To header, month navigation, range highlight, Cancel / Apply.
Future<DateTimeRange?> showCompactDateRangePicker({
required BuildContext context,
required DateTime firstDate,
required DateTime lastDate,
DateTimeRange? initialRange,
}) {
return showDialog<DateTimeRange>(
context: context,
barrierDismissible: true,
builder: (ctx) => _CompactDateRangeDialog(
firstDate: DateTime(firstDate.year, firstDate.month, firstDate.day),
lastDate: DateTime(lastDate.year, lastDate.month, lastDate.day),
initialRange: initialRange,
),
);
}
class _CompactDateRangeDialog extends StatefulWidget {
const _CompactDateRangeDialog({
required this.firstDate,
required this.lastDate,
this.initialRange,
});
final DateTime firstDate;
final DateTime lastDate;
final DateTimeRange? initialRange;
@override
State<_CompactDateRangeDialog> createState() => _CompactDateRangeDialogState();
}
class _CompactDateRangeDialogState extends State<_CompactDateRangeDialog> {
static const _primary = Color(0xFF2E7D6E);
static const _primaryLight = Color(0xFFE6F4F1);
static const _muted = Color(0xFF64748B);
late DateTime _focusedMonth;
DateTime? _start;
DateTime? _end;
final _headerFrom = DateFormat('EEE, dd MMM');
final _headerMonth = DateFormat('MMMM yyyy');
@override
void initState() {
super.initState();
final ir = widget.initialRange;
if (ir != null) {
_start = DateTime(ir.start.year, ir.start.month, ir.start.day);
_end = DateTime(ir.end.year, ir.end.month, ir.end.day);
_focusedMonth = DateTime(_start!.year, _start!.month, 1);
} else {
final now = DateTime.now();
final n = DateTime(now.year, now.month, now.day);
_focusedMonth = DateTime(n.year, n.month, 1);
if (n.isBefore(widget.firstDate)) {
_focusedMonth = DateTime(widget.firstDate.year, widget.firstDate.month, 1);
} else if (n.isAfter(widget.lastDate)) {
_focusedMonth = DateTime(widget.lastDate.year, widget.lastDate.month, 1);
}
}
}
bool _sameDay(DateTime a, DateTime b) =>
a.year == b.year && a.month == b.month && a.day == b.day;
DateTime _norm(DateTime d) => DateTime(d.year, d.month, d.day);
bool _inSelectedRange(DateTime d) {
if (_start == null) return false;
final x = _norm(d);
final s = _norm(_start!);
final e = _norm(_end ?? _start!);
return !x.isBefore(s) && !x.isAfter(e);
}
bool _isRangeStart(DateTime d) => _start != null && _sameDay(d, _start!);
bool _isRangeEnd(DateTime d) {
if (_start == null) return false;
final end = _end ?? _start!;
return _sameDay(d, end);
}
void _onDayTap(DateTime day) {
final d = _norm(day);
if (d.isBefore(widget.firstDate) || d.isAfter(widget.lastDate)) return;
setState(() {
if (_start == null || (_start != null && _end != null)) {
_start = d;
_end = null;
} else {
if (d.isBefore(_start!)) {
_end = _start;
_start = d;
} else {
_end = d;
}
}
});
}
bool get _canPrev {
final lastOfPrevMonth = DateTime(_focusedMonth.year, _focusedMonth.month, 0);
return !lastOfPrevMonth.isBefore(widget.firstDate);
}
bool get _canNext {
final firstOfNextMonth = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 1);
return !firstOfNextMonth.isAfter(widget.lastDate);
}
void _prevMonth() {
if (!_canPrev) return;
setState(() {
_focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month - 1, 1);
});
}
void _nextMonth() {
if (!_canNext) return;
setState(() {
_focusedMonth = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 1);
});
}
List<DateTime> _calendarCells() {
final first = DateTime(_focusedMonth.year, _focusedMonth.month, 1);
final daysInMonth = DateTime(_focusedMonth.year, _focusedMonth.month + 1, 0).day;
final leading = first.weekday - 1;
final cells = <DateTime>[];
final prevEnd = DateTime(_focusedMonth.year, _focusedMonth.month, 0);
for (int i = 0; i < leading; i++) {
final dayNum = prevEnd.day - leading + i + 1;
cells.add(DateTime(prevEnd.year, prevEnd.month, dayNum));
}
for (int d = 1; d <= daysInMonth; d++) {
cells.add(DateTime(_focusedMonth.year, _focusedMonth.month, d));
}
var next = 1;
while (cells.length % 7 != 0 || cells.length < 42) {
cells.add(DateTime(_focusedMonth.year, _focusedMonth.month + 1, next));
next++;
}
return cells;
}
void _apply() {
if (_start == null) {
Navigator.of(context).pop();
return;
}
final end = _end ?? _start!;
Navigator.of(context).pop(DateTimeRange(start: _start!, end: end));
}
Widget _dayCell(DateTime d) {
final inMonth = d.month == _focusedMonth.month;
final disabled =
d.isBefore(widget.firstDate) || d.isAfter(widget.lastDate);
if (!inMonth) {
return Center(
child: Text(
'${d.day}',
style: GoogleFonts.inter(
fontSize: 13,
color: const Color(0xFF94A3B8),
fontWeight: FontWeight.w500,
),
),
);
}
final inRange = !disabled && _inSelectedRange(d);
final isStart = !disabled && _isRangeStart(d);
final isEnd = !disabled && _isRangeEnd(d);
final span = _start != null &&
_end != null &&
!_sameDay(_start!, _end!);
BorderRadius? bgRadius;
if (inRange && span) {
if (isStart) {
bgRadius = const BorderRadius.horizontal(left: Radius.circular(20));
} else if (isEnd) {
bgRadius = const BorderRadius.horizontal(right: Radius.circular(20));
} else {
bgRadius = BorderRadius.zero;
}
}
return GestureDetector(
onTap: disabled ? null : () => _onDayTap(d),
behavior: HitTestBehavior.opaque,
child: Container(
decoration: BoxDecoration(
color: (inRange && span) ? _primaryLight : null,
borderRadius: bgRadius,
),
alignment: Alignment.center,
child: Container(
width: 36,
height: 36,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: (isStart || isEnd) ? _primary : Colors.transparent,
border: (isStart || isEnd)
? Border.all(color: Colors.white, width: 2)
: null,
),
child: Text(
'${d.day}',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight:
(isStart || isEnd) ? FontWeight.w700 : FontWeight.w500,
color: disabled
? const Color(0xFFCBD5E1)
: (isStart || isEnd)
? Colors.white
: const Color(0xFF0F172A),
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
final cells = _calendarCells();
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
clipBehavior: Clip.antiAlias,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
IconButton(
icon: const Icon(Icons.close, size: 22),
color: _muted,
onPressed: () => Navigator.of(context).pop(),
visualDensity: VisualDensity.compact,
),
],
),
Container(
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'From',
style: GoogleFonts.inter(
fontSize: 11,
color: _muted,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
_start != null
? _headerFrom.format(_start!)
: '',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w700,
color: const Color(0xFF0F172A),
),
),
],
),
),
),
Container(
width: 1, height: 52, color: const Color(0xFFE2E8F0)),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'To',
style: GoogleFonts.inter(
fontSize: 11,
color: _muted,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
_start != null
? _headerFrom.format(_end ?? _start!)
: '',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w700,
color: const Color(0xFF0F172A),
),
),
],
),
),
),
],
),
),
const SizedBox(height: 14),
Row(
children: [
IconButton(
onPressed: _canPrev ? _prevMonth : null,
icon: const Icon(Icons.chevron_left),
color: _primary,
),
Expanded(
child: Text(
_headerMonth.format(_focusedMonth),
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
),
),
),
IconButton(
onPressed: _canNext ? _nextMonth : null,
icon: const Icon(Icons.chevron_right),
color: _primary,
),
],
),
const SizedBox(height: 6),
Row(
children: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
.map(
(w) => Expanded(
child: Center(
child: Text(
w,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
color: _primary,
),
),
),
),
)
.toList(),
),
const SizedBox(height: 6),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
childAspectRatio: 1.05,
),
itemCount: cells.length,
itemBuilder: (context, i) => _dayCell(cells[i]),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
foregroundColor: _primary,
side: const BorderSide(color: _primary),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: Text(
'Cancel',
style:
GoogleFonts.poppins(fontWeight: FontWeight.w600),
),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton(
onPressed: _start != null ? _apply : null,
style: FilledButton.styleFrom(
backgroundColor: _primary,
disabledBackgroundColor: const Color(0xFFCBD5E1),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: Text(
'Apply',
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,255 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'date_range_picker_dialog.dart';
/// Single UI control for filter **from** / **to** dates.
///
/// Writes [dd-MM-yyyy] to [startController] and [endController] (same as [ThemedDateField]).
/// Does not change API keys or request shape only how the user picks dates.
class DateRangePickerField extends StatefulWidget {
const DateRangePickerField({
super.key,
required this.startController,
required this.endController,
this.hintText,
this.labelText,
this.txtwidth,
this.txtheight = 35,
this.firstDate,
this.lastDate,
this.onRangeSelected,
this.rangeValidator,
this.showLabelAboveField = false,
this.labelStyle,
this.useFormField = true,
});
final TextEditingController startController;
final TextEditingController endController;
/// Shown when no range is selected.
final String? hintText;
/// Optional label above the field (e.g. `Date range *`).
final String? labelText;
final double? txtwidth;
final double? txtheight;
final DateTime? firstDate;
final DateTime? lastDate;
/// Called after user confirms a range (same moment controllers are updated).
final void Function(DateTimeRange range)? onRangeSelected;
/// When non-null and [useFormField] is true, wrapped in [FormField] for [FormState.validate].
final String? Function(
TextEditingController start,
TextEditingController end,
)? rangeValidator;
final bool showLabelAboveField;
final TextStyle? labelStyle;
/// If false, no [FormField] use when this widget is not under a [Form].
final bool useFormField;
static final DateFormat storageFormat = DateFormat('dd-MM-yyyy');
static final DateFormat displayFormat = DateFormat('dd MMM yyyy');
/// Matches previous end-date-only validation: if start is set, end is required and not before start.
static String? defaultFilterValidator(
TextEditingController start,
TextEditingController end,
) {
final s = start.text.trim();
final e = end.text.trim();
if (s.isNotEmpty) {
try {
storageFormat.parse(s);
if (e.isEmpty) return 'End Date is required';
final sd = storageFormat.parse(s);
final ed = storageFormat.parse(e);
if (ed.isBefore(sd)) {
return 'End Date cannot be earlier than Start Date';
}
} catch (_) {
return 'Invalid date';
}
}
return null;
}
@override
State<DateRangePickerField> createState() => _DateRangePickerFieldState();
}
class _DateRangePickerFieldState extends State<DateRangePickerField> {
void _onTextChanged() {
if (mounted) setState(() {});
}
@override
void initState() {
super.initState();
widget.startController.addListener(_onTextChanged);
widget.endController.addListener(_onTextChanged);
}
@override
void dispose() {
widget.startController.removeListener(_onTextChanged);
widget.endController.removeListener(_onTextChanged);
super.dispose();
}
String _displayText() {
final s = widget.startController.text.trim();
final e = widget.endController.text.trim();
if (s.isEmpty && e.isEmpty) {
return widget.hintText ?? 'Select date range';
}
if (s.isNotEmpty && e.isNotEmpty) {
try {
final sd = DateRangePickerField.storageFormat.parse(s);
final ed = DateRangePickerField.storageFormat.parse(e);
return '${DateRangePickerField.displayFormat.format(sd)} ${DateRangePickerField.displayFormat.format(ed)}';
} catch (_) {
return '$s $e';
}
}
if (s.isNotEmpty) return s;
return e;
}
Future<void> _pick(BuildContext context, FormFieldState<String>? field) async {
final first = widget.firstDate ?? DateTime(2000);
final last = widget.lastDate ?? DateTime(2100);
DateTimeRange? initial;
try {
final s = widget.startController.text.trim();
final e = widget.endController.text.trim();
if (s.isNotEmpty && e.isNotEmpty) {
initial = DateTimeRange(
start: DateRangePickerField.storageFormat.parse(s),
end: DateRangePickerField.storageFormat.parse(e),
);
} else if (s.isNotEmpty) {
final d = DateRangePickerField.storageFormat.parse(s);
initial = DateTimeRange(start: d, end: d);
}
} catch (_) {}
final picked = await showCompactDateRangePicker(
context: context,
firstDate: first,
lastDate: last,
initialRange: initial,
);
if (picked != null) {
widget.startController.text =
DateRangePickerField.storageFormat.format(picked.start);
widget.endController.text =
DateRangePickerField.storageFormat.format(picked.end);
field?.didChange('range');
widget.onRangeSelected?.call(picked);
setState(() {});
}
}
static const _border = Color(0xFFE2E8F0);
static const _error = Color(0xFFD83731);
Widget _tappableField({
required bool hasError,
required VoidCallback onTap,
}) {
final empty = widget.startController.text.trim().isEmpty &&
widget.endController.text.trim().isEmpty;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Container(
width: widget.txtwidth ?? MediaQuery.sizeOf(context).width,
height: widget.txtheight,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: hasError ? _error : _border,
width: 1,
),
),
child: Row(
children: [
Expanded(
child: Text(
_displayText(),
style: TextStyle(
fontSize: 11,
color: hasError
? _error
: empty
? const Color(0xFF64748B)
: Colors.black,
),
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.date_range, size: 16, color: Color(0xFF5A6C7D)),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final labelStyle = widget.labelStyle ??
const TextStyle(fontSize: 12, fontWeight: FontWeight.w600);
Widget content({required bool hasError, FormFieldState<String>? field}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (widget.showLabelAboveField && widget.labelText != null) ...[
Text(widget.labelText!, style: labelStyle),
const SizedBox(height: 5),
],
_tappableField(
hasError: hasError,
onTap: () => _pick(context, field),
),
if (hasError && field != null && (field.errorText ?? '').isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 5, left: 5),
child: Text(
field.errorText ?? '',
style: const TextStyle(color: _error, fontSize: 10),
),
),
],
);
}
if (!widget.useFormField || widget.rangeValidator == null) {
return content(hasError: false, field: null);
}
return FormField<String>(
initialValue: '',
validator: (_) => widget.rangeValidator!(
widget.startController,
widget.endController,
),
builder: (field) {
final hasError = field.hasError;
return content(hasError: hasError, field: field);
},
);
}
}

View File

@ -8,6 +8,7 @@ import '../layouts/responsive_layout.dart';
import '../providers/manager_provider.dart';
import '../providers/userRoleProvider.dart';
import '../themes/indicators/date_field_theme.dart';
import '../themes/indicators/date_range_picker_field.dart';
import '../themes/indicators/input_field_decoration.dart';
// class DateFilterRow extends StatelessWidget {
@ -276,9 +277,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
List<Widget> getRowChildren(bool isMobile, double spacing) {
final dateFields = [
buildStartDate(context),
SizedBox(width: spacing),
buildEndDate(context),
buildDateRangeFilter(context),
SizedBox(width: spacing),
if (!widget.compactDateFiltersOnly) ...[
@ -371,78 +370,18 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
);
}
Widget buildStartDate(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text('Start Date', style: _textStyle),
// SizedBox(height: 5),
SizedBox(
height: 35,
child: ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.1,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
// validator: (value) => Validators.requiredField(value, "date"),
controller: widget.startController,
onDateSelected: (date) {
print("SPicked Date: $date");
widget.startController.text = DateFormat(
'dd-MM-yyyy',
).format(date);
// controllers['date']?.text = date as String;
// Future.microtask(() => widget.onFilter());
},
),
),
],
);
}
Widget buildEndDate(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text('End Date', style: _textStyle),
// SizedBox(height: 5),
SizedBox(
height: 35,
child: ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.1,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
validator: (value) {
final fromText = widget.startController.text ?? '';
if (fromText.isNotEmpty) {
final startDate = DateFormat('dd-MM-yyyy').parse(fromText);
if (value == null || value.isEmpty) {
return "End Date is required";
}
final endDate = DateFormat('dd-MM-yyyy').parse(value);
if (endDate.isBefore(startDate)) {
return "End Date cannot be earlier than Start Date";
}
}
return null; // no error
},
controller: widget.endController,
lastDate: DateTime.now(),
onDateSelected: (edate) {
print("EPicked Date: $edate");
widget.endController.text = DateFormat('dd-MM-yyyy').format(edate);
Future.microtask(() => widget.onFilter());
// controllers['date']?.text = date as String;
},
),
),
],
Widget buildDateRangeFilter(BuildContext context) {
final w = ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.15;
return DateRangePickerField(
startController: widget.startController,
endController: widget.endController,
hintText: 'Select date range',
txtwidth: w,
lastDate: DateTime.now(),
rangeValidator: DateRangePickerField.defaultFilterValidator,
onRangeSelected: (_) => Future.microtask(() => widget.onFilter()),
);
}

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"