Merge branch 'master' of bitbucket.org:jubilian/nhance_partner
This commit is contained in:
commit
cb9c84bf0b
File diff suppressed because one or more lines are too long
@ -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');
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -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,
|
||||
|
||||
446
lib/presentation/themes/indicators/date_range_picker_dialog.dart
Normal file
446
lib/presentation/themes/indicators/date_range_picker_dialog.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
255
lib/presentation/themes/indicators/date_range_picker_field.dart
Normal file
255
lib/presentation/themes/indicators/date_range_picker_field.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
22
pubspec.lock
22
pubspec.lock
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user