insurer master ans claims

This commit is contained in:
Surendiran 2026-06-11 09:24:04 +05:30
parent 78d4d786a7
commit 6c65a71d1f
15 changed files with 1074 additions and 152 deletions

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@ class Env {
);
static const String apiUrl = String.fromEnvironment(
'API_URL',
// defaultValue: 'https://partner.nhanceindia.in/partner_api/api/', /* Live build (enable index.html line 18) */
defaultValue: 'https://venbait.in/nhance/partner/dev/api/', /* Test build (enable index.html line 19) */
defaultValue: 'https://partner.nhanceindia.in/partner_api/api/', /* Live build (enable index.html line 18) */
// defaultValue: 'https://venbait.in/nhance/partner/dev/api/', /* Test build (enable index.html line 19) */
// defaultValue: 'http://localhost/nhance_partner_be/', /* localhost build (enable index.html line 19) */
);
// static const String baseUrl = String.fromEnvironment(

View File

@ -18,6 +18,7 @@ import '../../presentation/screens/Enquiry/policy_claims_endros/endorsomentUpdat
import '../../presentation/screens/Enquiry/policy_claims_endros/endrosment.dart';
import '../../presentation/screens/Masters/Brokers/brokerList.dart';
import '../../presentation/screens/Masters/EndorsementType/endorsementList.dart';
import '../../presentation/screens/Masters/Insurers/insurerList.dart';
import '../../presentation/screens/Masters/VehicleType/vehicleList.dart';
import '../../presentation/screens/StaffAttendance/attendanceAllDetails.dart';
import '../../presentation/screens/StaffAttendance/individual_Attendance.dart';
@ -381,6 +382,10 @@ final GoRouter appRouter = GoRouter(
path: AppRoutes.paymentModeLst,
builder: (context, state) => const PaymentLsit(),
),
GoRoute(
path: AppRoutes.insurerLst,
builder: (context, state) => const InsurerList(),
),
GoRoute(
path: AppRoutes.gridList,
builder: (context, state) => const GridListScreen(),

View File

@ -55,6 +55,7 @@ class AppRoutes {
static const String paymentModeLst = '/paymentMode';
static const String endorsementTypeLst = '/endorsementTypeLst';
static const String vehicleTypeLst = '/vehicleTypeLst';
static const String insurerLst = '/insurerLst';
static const String payoutList = '/payoutList';
static const String payoutDetails = '/payoutDetails';
static const String payoutDetailsView = '/payoutDetailsView';

View File

@ -734,7 +734,10 @@ class ApiService {
}
else if (masterName == 'VehicleType') {
url = Uri.parse('${Env.apiUrl}master/updateVehicleTypeStatus/$id');
}
}
else if (masterName == 'Insurer') {
url = Uri.parse('${Env.apiUrl}master/updateInsurerStatus/$id');
}
else {
url = Uri.parse('${Env.apiUrl}master/updatePaymentModeStatus/$id');
}
@ -2233,18 +2236,28 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> fetchAgentUnusedCommissionList(id) async {
Future<Map<String, dynamic>> fetchAgentUnusedCommissionList(
id, {
String? fromDate,
String? toDate,
}) async {
print('fetchAGENTNameDropDown');
if (_token == null) {
await _initializeToken();
}
final queryParameters = <String, String>{
'manager_id': id.toString(),
if (fromDate != null && fromDate.trim().isNotEmpty)
'from_date': fromDate.trim(),
if (toDate != null && toDate.trim().isNotEmpty) 'to_date': toDate.trim(),
};
dynamic url;
print('fetchAGENTNameDropDown 1');
url = Uri.parse(
'${Env.apiUrl}invoice/getAgentUnusedCommissionList?manager_id=$id',
);
'${Env.apiUrl}invoice/getAgentUnusedCommissionList',
).replace(queryParameters: queryParameters);
print('fetchAGENTNameDropDown 2');
final headers = {
@ -2438,24 +2451,33 @@ class ApiService {
Future<Map<String, dynamic>> getPayoutList({
String? fromDate,
String? toDate,
String? agentName,
String? agentCode,
String? policyNo,
}) async {
// print(_token);
if (_token == null) {
await _initializeToken();
}
final Uri url;
if (fromDate != null &&
fromDate.isNotEmpty &&
toDate != null &&
toDate.isNotEmpty) {
url = Uri.parse('${Env.apiUrl}invoice/list').replace(
queryParameters: {'from_date': fromDate, 'to_date': toDate},
);
} else {
url = Uri.parse('${Env.apiUrl}invoice/list');
final query = <String, String>{};
void addParam(String key, String? value) {
final trimmed = value?.trim();
if (trimmed != null && trimmed.isNotEmpty) {
query[key] = trimmed;
}
}
addParam('from_date', fromDate);
addParam('to_date', toDate);
addParam('agent_name', agentName);
addParam('agent_code', agentCode);
addParam('policy_no', policyNo);
final url = Uri.parse('${Env.apiUrl}invoice/list').replace(
queryParameters: query.isEmpty ? null : query,
);
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,

View File

@ -99,7 +99,8 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
if (route.contains('broker') ||
route.contains('payment') ||
route.contains('vehicletype') ||
route.contains('endorsementtype')) {
route.contains('endorsementtype') ||
route.contains('insurer')) {
return 'Masters';
}
if (route.contains('payoutgrid')) {
@ -361,6 +362,15 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
context.go(AppRoutes.paymentModeLst);
},
),
SizedBox(height: 2),
_buildPopupItem(
label: "Insurer",
onTap: () {
setState(() => _activeMenu = 'Masters');
_hidePopup();
context.go(AppRoutes.insurerLst);
},
),
],
if (key == 'Payout' && role == 'Accounts') ...[

View File

@ -0,0 +1,277 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:nhance_partner/data/utils/toastNotification.dart';
import 'package:nhance_partner/presentation/themes/indicators/text_field_theme_inline_editor.dart';
import '../../../../core/config/env.dart';
import '../../../../core/services/api_service.dart';
import '../../../../data/services/auth_service.dart';
import '../../../../data/utils/validators.dart';
class Insurer extends ConsumerStatefulWidget {
final String? id;
final Map<String, dynamic>? data;
final VoidCallback onSubmit;
const Insurer({
super.key,
this.id,
this.data,
required this.onSubmit,
});
@override
ConsumerState<Insurer> createState() => InsurerState();
}
class InsurerState extends ConsumerState<Insurer> {
final _formKey = GlobalKey<FormState>();
late ApiService apiService;
List<String> tabHeader = ['name', 'code'];
String? _token;
dynamic selectedId;
Map<String, TextEditingController> controllers = {};
bool get _isEdit => selectedId != null;
Map<String, dynamic> dataDetails() {
final data = <String, dynamic>{
'name': controllers['name']?.text.trim(),
'short_name': controllers['code']?.text.trim(),
};
if (_isEdit) {
data['id'] = selectedId.toString();
}
return data;
}
@override
void initState() {
super.initState();
apiService = ApiService();
for (final field in tabHeader) {
controllers[field] = TextEditingController();
}
_initializeToken();
updateData();
}
void updateData() {
if (widget.data == null) return;
final data = widget.data!;
selectedId = data['id'];
controllers['name']?.text = data['name']?.toString() ?? '';
controllers['code']?.text = data['short_name']?.toString() ?? '';
}
Future<void> _initializeToken() async {
_token = await AuthService.getToken();
}
Future<void> handleSave() async {
final nameEmpty =
controllers['name'] == null || controllers['name']!.text.trim().isEmpty;
final codeEmpty =
controllers['code'] == null || controllers['code']!.text.trim().isEmpty;
if (nameEmpty && codeEmpty) {
ToastHelper.showWarningToast(
context,
'Insurer and Short Name are required',
);
return;
}
if (nameEmpty) {
ToastHelper.showWarningToast(context, 'Insurer is required');
return;
}
if (codeEmpty) {
ToastHelper.showWarningToast(context, 'Short Name is required');
return;
}
if (!_formKey.currentState!.validate()) return;
await createUserData(dataDetails());
}
void refresh() {
widget.onSubmit();
setState(() {
selectedId = null;
for (final controller in controllers.values) {
controller.clear();
}
});
}
Future<void> createUserData(Map<String, dynamic> data) async {
if (_token == null) {
throw Exception('Token not found. Please log in.');
}
try {
final response = await http.post(
Uri.parse('${Env.apiUrl}master/saveInsurer'),
headers: {
'Authorization': 'Bearer $_token',
'Content-Type': 'application/json',
'app-signature': Env.App_Signature,
},
body: jsonEncode(data),
);
if (response.statusCode == 200) {
final responseBody = jsonDecode(response.body);
final message = responseBody['message']?.toString() ??
(_isEdit
? 'Insurer updated successfully.'
: 'Insurer created successfully.');
if (responseBody['status'] == 200 ||
responseBody['status'] == 'success') {
refresh();
if (mounted) {
ToastHelper.showSuccessToast(context, message);
}
} else if (mounted) {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(_isEdit ? 'Insurer Update Failed' : 'Insurer Save Failed'),
content: Text(message),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
}
} else if (response.statusCode == 403) {
await apiService.clearLocalStorageAndRedirect();
}
} catch (e) {
if (mounted) {
ToastHelper.showWarningToast(context, 'Error saving insurer: $e');
}
}
}
@override
void dispose() {
for (final controller in controllers.values) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return SelectionArea(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
buildFormFields(),
const SizedBox(width: 5),
InkWell(
onTap: handleSave,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: const Color(0xFF2E7D6E),
),
child: Text(
'Save',
style: GoogleFonts.inter(color: Colors.white, fontSize: 10),
),
),
),
const SizedBox(width: 5),
InkWell(
onTap: refresh,
child: Container(
padding: const EdgeInsets.all(6.0),
decoration: BoxDecoration(
color: const Color(0xFF2E7D6E),
borderRadius: BorderRadius.circular(5.0),
),
child: const Icon(Icons.refresh, size: 13, color: Colors.white),
),
),
],
),
);
}
Widget buildFormFields() {
return Form(
key: _formKey,
child: Row(
children: [
buildName(),
const SizedBox(width: 10),
buildShortName(),
],
),
);
}
Widget buildName() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Insurer *', style: _textStyle),
const SizedBox(width: 10),
ThemedFormInlineField(
controller: controllers['name']!,
validator: (value) => Validators.requiredField(value, 'name'),
txtwidth: MediaQuery.of(context).size.width * 0.15,
borderColor: const Color(0xFFE2E8F0),
highlightColor: const Color(0xFF50A398),
isdense: true,
errFieldHgt: 0,
),
],
);
}
Widget buildShortName() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Short Name *', style: _textStyle),
const SizedBox(width: 10),
ThemedFormInlineField(
controller: controllers['code']!,
validator: (value) => Validators.requiredField(value, 'code'),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\-_ ]')),
],
txtwidth: MediaQuery.of(context).size.width * 0.15,
borderColor: const Color(0xFFE2E8F0),
highlightColor: const Color(0xFF50A398),
isdense: true,
errFieldHgt: 0,
),
],
);
}
static final _textStyle = GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
);
}

View File

@ -0,0 +1,322 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
import 'package:nhance_partner/presentation/screens/Masters/Insurers/insurer.dart';
import '../../../../core/routing/routes.dart';
import '../../../../core/services/api_service.dart';
import '../../../../data/utils/Pagination.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';
class InsurerList extends ConsumerStatefulWidget {
const InsurerList({super.key});
@override
ConsumerState<InsurerList> createState() => InsurerListState();
}
class InsurerListState extends ConsumerState<InsurerList> {
int currentPage = 1;
int itemsPerPage = 10;
late ApiService apiService;
dynamic managerId;
dynamic role;
dynamic prefid;
List<Map<String, dynamic>> getInsurerData = [];
List<Map<String, dynamic>> filteredData = [];
bool isLoading = false;
Map<String, dynamic>? selectedInsurer;
dynamic selectedId;
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
apiService = ApiService();
Future.microtask(() {
prefid = ref.read(managerIdProvider);
role = ref.read(userRoleProvider);
if (prefid != null && role != null) {
getInsurers();
}
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
List<dynamic> get _paginatedData {
final sortedData = [...filteredData]
..sort((a, b) {
final idA = int.tryParse(a['id']?.toString() ?? '') ?? 0;
final idB = int.tryParse(b['id']?.toString() ?? '') ?? 0;
return idB.compareTo(idA);
});
if (sortedData.isEmpty) return [];
final maxPage = (sortedData.length / itemsPerPage).ceil();
final safePage = currentPage.clamp(1, maxPage);
final startIndex = (safePage - 1) * itemsPerPage;
final endIndex = (startIndex + itemsPerPage).clamp(0, sortedData.length);
return sortedData.sublist(startIndex, endIndex);
}
void handleEdit(Map<String, dynamic> item) {
setState(() {
selectedId = item['id'];
selectedInsurer = item;
});
}
void filterData(String query) {
setState(() {
final q = query.toLowerCase().trim();
if (q.isEmpty) {
filteredData = List.from(getInsurerData);
return;
}
filteredData = getInsurerData.where((item) {
final name = (item['name'] ?? '').toString().toLowerCase();
final code = (item['short_name'] ?? '').toString().toLowerCase();
return name.contains(q) || code.contains(q);
}).toList();
});
}
Future<void> getInsurers() async {
setState(() => isLoading = true);
try {
final response = await apiService.fetchMasterDropDown('Insurers');
if (response['status'] == 200 || response['status'] == 'success') {
setState(() {
getInsurerData = List<Map<String, dynamic>>.from(response['data']);
filteredData = List.from(getInsurerData);
});
} else {
getInsurerData = [];
filteredData = [];
}
} catch (_) {
getInsurerData = [];
filteredData = [];
} finally {
if (mounted) setState(() => isLoading = false);
}
}
@override
Widget build(BuildContext context) {
managerId = ref.watch(managerIdProvider);
return MainLayout(
title: 'Insurer',
body: SelectionArea(
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => context.go(AppRoutes.dashboard),
child: Text(
'Insurer',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 5),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Insurer(
key: ValueKey(selectedId),
data: selectedInsurer,
id: selectedId,
onSubmit: () {
getInsurers();
setState(() {
selectedInsurer = null;
selectedId = null;
});
},
),
const Spacer(),
ThemedSearchField(
hintText: 'Search',
backgroundColor: const Color(0xFFFFFFFF),
txtHeight: 30,
onChanged: filterData,
controller: _searchController,
txtwidth: MediaQuery.of(context).size.width * 0.15,
),
const SizedBox(width: 10),
ExportBtn(
sheetName: 'Insurer',
fileName: 'insurer_list',
txt: !ResponsiveLayout.isMobile(context),
data: filteredData,
displayHeaders: [
'S.No.',
'Insurer',
'Short Name',
],
keys: ['sno', 'name', 'short_name'],
),
],
),
const SizedBox(height: 10),
Container(
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: Row(
children: [
SizedBox(width: 100, child: Text('S.No.', style: _headerStyle)),
Expanded(flex: 2, child: Text('Insurer', style: _headerStyle)),
Expanded(flex: 1, child: Text('Short Name', style: _headerStyle)),
Expanded(flex: 1, child: Text('Action', style: _headerStyle)),
],
),
),
Expanded(
child: ColoredBox(
color: Colors.white,
child: _buildDataTable(),
),
),
],
),
),
),
PaginationControls(
currentPage: currentPage,
itemsPerPage: itemsPerPage,
totalItems: filteredData.length,
onPageChanged: (page) => setState(() => currentPage = page),
onItemsPerPageChanged: (items) {
setState(() {
itemsPerPage = items;
currentPage = 1;
});
},
),
],
),
),
),
);
}
Widget _buildDataTable() {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (filteredData.isEmpty) {
return const SizedBox(
height: 50,
child: Center(child: Text('No available data')),
);
}
final sortedData = [..._paginatedData];
return ListView.builder(
itemCount: sortedData.length,
itemBuilder: (context, index) {
final startIndex = ((currentPage - 1) * itemsPerPage);
final item = sortedData[index];
final sno = startIndex + index + 1;
return _buildDataRow(item, sno);
},
);
}
Widget _buildDataRow(Map<String, dynamic> item, int sno) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 16),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(color: Colors.blueGrey, width: 0.15),
),
),
child: Row(
children: [
SizedBox(width: 100, child: Text('$sno', style: _dataBold)),
Expanded(
flex: 2,
child: Text(item['name'] ?? '-', style: _dataBold),
),
Expanded(
flex: 1,
child: Text(item['short_name'] ?? '-', style: _dataBold),
),
Expanded(
flex: 1,
child: Tooltip(
message: 'Edit',
child: IconButton(
icon: Image.asset(
'assets/miscellaneous/Edit.png',
height: 12,
width: 15,
),
onPressed: () => handleEdit(item),
splashRadius: 28,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
),
),
),
],
),
);
}
static final _dataBold = GoogleFonts.inter(
fontSize: 11.5,
fontWeight: FontWeight.w400,
color: const Color(0xFF000000),
);
static final _headerStyle = GoogleFonts.poppins(
fontSize: 11.2,
fontWeight: FontWeight.w500,
color: const Color(0xFF1E293B),
);
}

View File

@ -37,6 +37,12 @@ class DateFilterRowPayout extends ConsumerStatefulWidget {
/// When `null`, legacy: date range and partner row together.
final bool? policyTableHasRows;
/// Optional per-column table search fields (shown between date range and partner).
final Widget? tableSearchFields;
/// When true, [getAgentUnusedCommissionList] receives [startController]/[endController] dates.
final bool passDateRangeToAgentList;
const DateFilterRowPayout({
super.key,
this.showBrokerFilter = true,
@ -55,6 +61,8 @@ class DateFilterRowPayout extends ConsumerStatefulWidget {
this.isMobile = false,
this.dataFrom,
this.policyTableHasRows,
this.tableSearchFields,
this.passDateRangeToAgentList = false,
});
@override
@ -115,7 +123,9 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
if (!widget.showBrokerFilter && managerId != null) {
// Broker filter hidden: load partner list directly by manager.
getAgentList(managerId);
if (!_shouldDeferAgentListUntilDateRange()) {
getAgentList(managerId);
}
}
});
@ -155,7 +165,35 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
}
}
bool _hasSelectedDateRange() {
return widget.startController.text.trim().isNotEmpty &&
widget.endController.text.trim().isNotEmpty;
}
bool _shouldDeferAgentListUntilDateRange() {
return widget.passDateRangeToAgentList && !_hasSelectedDateRange();
}
String? _agentListFromDate() {
if (!widget.passDateRangeToAgentList) return null;
final value = widget.startController.text.trim();
return value.isEmpty ? null : value;
}
String? _agentListToDate() {
if (!widget.passDateRangeToAgentList) return null;
final value = widget.endController.text.trim();
return value.isEmpty ? null : value;
}
Future<void> getAgentList(id) async {
if (_shouldDeferAgentListUntilDateRange()) {
setState(() {
getPartnerData = [];
filteredPartnerData = [];
});
return;
}
print('getAgentListData called');
setState(() {
@ -163,7 +201,11 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
});
try {
final response = await apiService.fetchAgentUnusedCommissionList(id);
final response = await apiService.fetchAgentUnusedCommissionList(
id,
fromDate: _agentListFromDate(),
toDate: _agentListToDate(),
);
print('getAgentListData called response');
print('get Agent- ${response['data']}');
if (response['status'] == 'success') {
@ -276,7 +318,15 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
child: Tooltip(
message: 'Refresh',
child: IconButton(
onPressed: widget.onRefresh,
onPressed: () {
if (widget.passDateRangeToAgentList) {
setState(() {
getPartnerData = [];
filteredPartnerData = [];
});
}
widget.onRefresh();
},
icon: const Icon(
Icons.refresh,
size: 18,
@ -304,6 +354,10 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
if (isMobile) {
return [
buildDateRangeFilter(context),
if (widget.tableSearchFields != null) ...[
const SizedBox(height: 10),
widget.tableSearchFields!,
],
const SizedBox(height: 10),
Wrap(
spacing: spacing,
@ -315,6 +369,10 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
}
return [
buildDateRangeFilter(context),
if (widget.tableSearchFields != null) ...[
SizedBox(width: spacing),
widget.tableSearchFields!,
],
SizedBox(width: spacing),
Spacer(),
...partnerActionsRow,
@ -349,7 +407,12 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
txtheight: 32,
lastDate: DateTime.now(),
rangeValidator: DateRangePickerField.defaultFilterValidator,
onRangeSelected: (_) => Future.microtask(() => widget.onFilter()),
onRangeSelected: (_) => Future.microtask(() {
if (widget.passDateRangeToAgentList && managerId != null) {
getAgentList(managerId);
}
widget.onFilter();
}),
);
}

View File

@ -13,6 +13,7 @@ import '../../layouts/responsive_layout.dart';
import '../../providers/manager_provider.dart';
import '../../providers/userRoleProvider.dart';
import '../../themes/indicators/export_btn.dart';
import '../../themes/indicators/search_field_theme.dart';
import 'custom_dateRange.dart';
class PayOutDetails extends ConsumerStatefulWidget {
@ -69,6 +70,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
'invoiceDate',
'invoiceStatus',
'utrNumber',
'tableSearch',
];
// --------------------------
@ -218,43 +220,38 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
super.dispose();
}
void filterPolicyData(String query) {
final lowerQuery = query.toLowerCase();
String _partnerDisplayText(Map<String, dynamic> item) {
if (item['agent_code'] != null && item['agent_name'] != null) {
return '${item['agent_code']} - ${item['agent_name']}';
}
return (item['agent_name'] ?? '').toString();
}
print('PD => filterPolicyData - $lowerQuery');
bool _matchesTableSearch(Map<String, dynamic> item, String query) {
if (query.isEmpty) return true;
final searchableFields = [
(item['policy_no'] ?? '').toString(),
_formatDate(item['issued_date']?.toString() ?? ''),
_partnerDisplayText(item),
(item['customer_name'] ?? '').toString(),
_amountText(item['premium_amount']),
(item['premium_amount'] ?? '').toString(),
];
return searchableFields.any(
(field) => field.toLowerCase().contains(query),
);
}
void applyTableFilters() {
final query = controllers['tableSearch']?.text.trim().toLowerCase() ?? '';
print('PD => filteredPolicies1 - $filteredPolicies');
setState(() {
currentPage = 1;
if (query.trim().isEmpty) {
print('PD => filteredPolicies2');
filteredPolicies = List.from(masterPolicies);
return;
}
print('PD => filteredPolicies3');
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);
}).toList();
filteredPolicies = masterPolicies
.where((item) => _matchesTableSearch(item, query))
.toList();
});
}
@ -269,6 +266,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
selectedPolicies = {};
totalPolicies = '';
totalCommission = '';
masterPolicies = [];
filteredPolicies = [];
currentPage = 1;
_disposeCommissionControllers();
@ -407,21 +405,17 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
if (response['status'] == 'success') {
print('PD =>API Data response - $response');
setState(() {
filteredPolicies = List<Map<String, dynamic>>.from(response['data']);
_rebuildCommissionControllers(filteredPolicies);
masterPolicies = List<Map<String, dynamic>>.from(response['data']);
filteredPolicies = List.from(masterPolicies);
_rebuildCommissionControllers(masterPolicies);
hasFetchedTableData = true;
currentPage = 1;
print('PD =>API Data - $filteredPolicies');
// filteredPolicies = allPolicies.where((p) {
// if (p["agentId"] != selectedAgentId) return false;
// if (policyTillDate != null &&
// DateTime.parse(p["date"]).isAfter(policyTillDate!))
// return false;
// return true;
// }).toList();
});
applyTableFilters();
} else {
setState(() {
masterPolicies = [];
filteredPolicies = [];
_rebuildCommissionControllers(filteredPolicies);
hasFetchedTableData = true;
@ -431,6 +425,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
} catch (e) {
print('PD =>Exception occurred: $e');
setState(() {
masterPolicies = [];
filteredPolicies = [];
_rebuildCommissionControllers(filteredPolicies);
hasFetchedTableData = false;
@ -900,9 +895,13 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
role: roleId,
id: userId,
showBrokerFilter: false,
passDateRangeToAgentList: true,
policyTableHasRows: filteredPolicies.isNotEmpty,
selectedParnter: selectedAgentId,
selectedBroker: selectedBrokerID,
tableSearchFields: hasFetchedTableData
? _buildPolicyTableSearchFields(context)
: null,
onPartnerChanges: (val) {
setState(() {
@ -1174,6 +1173,24 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
);
}
Widget _buildPolicyTableSearchFields(BuildContext context) {
final width = ResponsiveLayout.isMobile(context)
? MediaQuery.of(context).size.width * 0.55
: MediaQuery.of(context).size.width * 0.2;
return SizedBox(
width: width,
child: ThemedSearchField(
hintText: 'Search',
backgroundColor: Colors.white,
txtHeight: 30,
controller: controllers['tableSearch']!,
txtwidth: width,
onChanged: (_) => applyTableFilters(),
),
);
}
Widget _buildDataTable(BuildContext context) {
if (filteredPolicies.isEmpty) {
return const SizedBox(
@ -1205,6 +1222,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
Widget _buildDataRow(Map<String, dynamic> item, int sno) {
final int id = int.tryParse(item["policy_id"]?.toString() ?? "0") ?? 0;
final bool isSelected = selectedPolicies.contains(id);
final partner = _partnerDisplayText(item);
return InkWell(
onTap: () {
setState(() {
@ -1274,9 +1292,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
Expanded(
flex: _flexAgent,
child: Text(
(item["agent_code"] != null && item["agent_name"] != null)
? '${item["agent_code"]} - ${item["agent_name"]}'
: (item["agent_name"] ?? '-'),
partner.isEmpty ? '-' : partner,
textAlign: TextAlign.left,
style: _tableDataStyle,
overflow: TextOverflow.ellipsis,
@ -1345,9 +1361,9 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
'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(),
'partner': _partnerDisplayText(p).isEmpty
? '-'
: _partnerDisplayText(p),
'customer': p['customer_name'],
'premium': _amountText(p['premium_amount']),
'payout': _amountText(p['commission_amount']),

View File

@ -24,6 +24,8 @@ class PayoutList extends ConsumerStatefulWidget {
}
class _PayoutListState extends ConsumerState<PayoutList> {
static const double _payoutFilterFieldHeight = 35;
int currentPage = 1;
int itemsPerPage = 10;
bool isLoading = false;
@ -35,7 +37,13 @@ class _PayoutListState extends ConsumerState<PayoutList> {
late ApiService apiService;
final _formKey = GlobalKey<FormState>();
Map<String, TextEditingController> controllers = {};
List<String> tabHeader = ['startDate', 'endDate'];
List<String> tabHeader = [
'startDate',
'endDate',
'agentName',
'agentCode',
'policyNo',
];
dynamic SelectedStatus;
dynamic SelectedStaffId;
DateTime _paymentDate = DateTime.now();
@ -75,6 +83,9 @@ class _PayoutListState extends ConsumerState<PayoutList> {
final response = await apiService.getPayoutList(
fromDate: from.isNotEmpty ? from : null,
toDate: to.isNotEmpty ? to : null,
agentName: controllers['agentName']?.text,
agentCode: controllers['agentCode']?.text,
policyNo: controllers['policyNo']?.text,
);
if (response['status'] == 'success') {
@ -157,13 +168,11 @@ class _PayoutListState extends ConsumerState<PayoutList> {
void refrshfilterDateRange() {
setState(() {
// SelectedStaffId = null;
controllers['startDate']!.clear();
controllers['endDate']!.clear();
controllers['startDate']?.text = '';
controllers['endDate']?.text = '';
// Reset the FormField validation
controllers['agentName']!.clear();
controllers['agentCode']!.clear();
controllers['policyNo']!.clear();
_formKey.currentState?.reset();
});
getPayoutList();
@ -174,13 +183,16 @@ class _PayoutListState extends ConsumerState<PayoutList> {
final q = query.toLowerCase();
setState(() {
filteredData = getPayoutData.where((item) {
// Search matches visible table columns only (broker / referer / updated hidden).
// Search matches visible table columns only.
final invoiceDateSearch = _formatDateSafe(item['invoice_date'])
.toLowerCase();
final statusSearch =
_exportPayoutStatusLabel(item['payout_status']).toLowerCase();
return invoiceDateSearch.contains(q) ||
(item['agent_name'] ?? '-').toString().toLowerCase().contains(q) ||
(item['agent_code'] ?? '-').toString().toLowerCase().contains(q) ||
(item['policy_no'] ?? '-').toString().toLowerCase().contains(q) ||
(item['invoice_no'] ?? '-').toLowerCase().contains(q) ||
(item['invoice_amount_indian_format'] ?? '-')
.toLowerCase()
@ -654,9 +666,122 @@ class _PayoutListState extends ConsumerState<PayoutList> {
);
}
Widget _payoutFilterField({
required String hint,
required TextEditingController controller,
required double width,
}) {
return Container(
width: width,
height: _payoutFilterFieldHeight,
padding: const EdgeInsets.symmetric(horizontal: 15),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: TextField(
controller: controller,
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black),
decoration: InputDecoration(
isDense: true,
hintText: hint,
hintStyle: GoogleFonts.poppins(
fontSize: 11,
color: const Color(0xFF64748B),
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
);
}
Widget _payoutFilterActions() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Tooltip(
message: 'Filter',
child: IconButton(
icon: const Icon(
Icons.search_rounded,
size: 18,
color: Color(0xFF94A3B8),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
filterDateRange();
}
},
),
),
Tooltip(
message: 'Refresh',
child: IconButton(
onPressed: refrshfilterDateRange,
icon: const Icon(
Icons.refresh,
size: 18,
color: Color(0xFF94A3B8),
),
),
),
],
);
}
Widget _payoutTextFilters(BuildContext context) {
final fieldWidth = ResponsiveLayout.isMobile(context) ? 120.0 : 140.0;
final isMobile = ResponsiveLayout.isMobile(context);
final fields = [
_payoutFilterField(
hint: 'Agent Name',
controller: controllers['agentName']!,
width: fieldWidth,
),
_payoutFilterField(
hint: 'Agent Code',
controller: controllers['agentCode']!,
width: fieldWidth,
),
_payoutFilterField(
hint: 'Policy No',
controller: controllers['policyNo']!,
width: fieldWidth,
),
_payoutFilterActions(),
];
if (isMobile) {
return Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: fields,
);
}
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var i = 0; i < fields.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
fields[i],
],
],
);
}
Widget _payoutInvoiceDateFilter(BuildContext context) {
return DateFilterRow(
final isMobile = ResponsiveLayout.isMobile(context);
final dateFilter = DateFilterRow(
compactDateFiltersOnly: true,
hideFilterActions: true,
dataFrom: 'Payout',
role: roleId,
id: userId,
@ -665,10 +790,31 @@ class _PayoutListState extends ConsumerState<PayoutList> {
endController: controllers['endDate']!,
onStatusChanged: (_) {},
formKey: _formKey,
isMobile: ResponsiveLayout.isMobile(context),
isMobile: isMobile,
onFilter: filterDateRange,
onRefresh: refrshfilterDateRange,
);
if (isMobile) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
dateFilter,
const SizedBox(height: 8),
_payoutTextFilters(context),
],
);
}
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
dateFilter,
const SizedBox(width: 8),
_payoutTextFilters(context),
],
);
}
Widget _buildContent(BuildContext context) {

View File

@ -169,6 +169,8 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
// int selectedIndex = 1;
int selectedIndex = 0;
int autoRefreshIndex = 0;
int _staffListRequestSeq = 0;
int _staffListInFlight = 0;
final List<String> enquiryTabs = ["In Progress", "Completed"];
@ -452,7 +454,6 @@ if (managerId != null &&
}
void filterDateRange() {
// Call your API
_searchStaffController.clear();
getStaffList(
managerId,
@ -462,6 +463,42 @@ if (managerId != null &&
);
}
bool _isCompletedEnquiryStatus(dynamic status) {
return status?.toString().trim().toLowerCase() == 'completed';
}
void _splitEnquiryLists(List<Map<String, dynamic>> data) {
inProgressData = data
.where((item) => !_isCompletedEnquiryStatus(item['enquiry_status']))
.toList();
completedData = data
.where((item) => _isCompletedEnquiryStatus(item['enquiry_status']))
.toList();
}
void _syncFilteredDataForCurrentTab({bool resetCompletedPage = false}) {
filteredData = selectedIndex == 0
? List<Map<String, dynamic>>.from(inProgressData)
: List<Map<String, dynamic>>.from(completedData);
if (selectedIndex != 1) return;
if (resetCompletedPage || filteredData.isEmpty) {
completedCurrentPage = 1;
} else {
final maxPage = (filteredData.length / completedItemsPerPage)
.ceil()
.clamp(1, 999999);
if (completedCurrentPage > maxPage) {
completedCurrentPage = maxPage;
}
}
totalCompletedPages = filteredData.isEmpty
? 1
: (filteredData.length / completedItemsPerPage).ceil().clamp(1, 999999);
}
@override
void dispose() {
refreshSub.close();
@ -520,31 +557,19 @@ if (managerId != null &&
List<Map<String, dynamic>> get paginatedCompletedData {
final dataToUse = filteredData;
if (dataToUse.isEmpty) return [];
if (dataToUse.isEmpty || selectedIndex != 1) return dataToUse;
if (selectedIndex == 1) {
totalCompletedPages = (dataToUse.length / completedItemsPerPage)
.ceil()
.clamp(1, double.infinity)
.toInt();
final maxPage = (dataToUse.length / completedItemsPerPage)
.ceil()
.clamp(1, 999999);
final safePage = completedCurrentPage.clamp(1, maxPage);
final startIndex = (safePage - 1) * completedItemsPerPage;
final endIndex = (startIndex + completedItemsPerPage).clamp(
0,
dataToUse.length,
);
final startIndex = (completedCurrentPage - 1) * completedItemsPerPage;
// 🚨 Guard: page out of range after search
if (startIndex >= dataToUse.length) {
completedCurrentPage = 1;
return dataToUse.take(completedItemsPerPage).toList();
}
final endIndex = (startIndex + completedItemsPerPage).clamp(
0,
dataToUse.length,
);
return dataToUse.sublist(startIndex, endIndex);
}
return dataToUse;
return dataToUse.sublist(startIndex, endIndex);
}
Future<void> autoRefrshfilterDateRange() async {
@ -706,6 +731,10 @@ if (managerId != null &&
}
}
void _syncStaffListLoader() {
isLoadingStaffList = isActionable && _staffListInFlight > 0;
}
Future<void> getStaffList(int managerId,
role, {
String fromDate = '',
@ -714,8 +743,10 @@ if (managerId != null &&
String SelectedStaffId = '',
}) async {
print('A613 => Fns called => $managerId | $role');
final requestId = ++_staffListRequestSeq;
setState(() {
isLoadingStaffList = true;
_staffListInFlight++;
_syncStaffListLoader();
});
print('A713 => Fns called => $managerId | $role');
@ -818,6 +849,11 @@ if (managerId != null &&
print('ToDate : $toDate');
print('A1213 => Fns called => $managerId | $role');
if (requestId != _staffListRequestSeq) {
print('Ignoring stale enquiry list response: $requestId');
return;
}
setState(() {
_resetKeyEnable = false;
@ -827,36 +863,30 @@ if (managerId != null &&
if (data is List) {
getStaffData = List<Map<String, dynamic>>.from(data);
originalData = List<Map<String, dynamic>>.from(data);
inProgressData = originalData
.where((item) => item['enquiry_status'] != 'Completed')
.toList();
print('inProgressData - $inProgressData');
completedData = originalData
.where((item) => item['enquiry_status'] == 'Completed')
.toList();
print('completedData - $completedData');
filteredData = (selectedIndex == 0)
? List.from(inProgressData)
: List.from(completedData);
_splitEnquiryLists(originalData);
if (SelectedStatus == 'Completed') {
selectedIndex = 1;
}
_syncFilteredDataForCurrentTab(resetCompletedPage: true);
print('filteredData count: ${filteredData.length}');
print('inProgressData count: ${inProgressData.length}');
print('completedData count: ${completedData.length}');
} else if (data is Map) {
getStaffData = [Map<String, dynamic>.from(data)];
originalData = List.from(getStaffData);
inProgressData = [];
completedData = [];
filteredData = [];
} else {
getStaffData = [];
originalData = [];
inProgressData = [];
completedData = [];
filteredData = [];
}
originalData = getStaffData;
filteredData = List.from(originalData);
});
// FIX CHANGE 2: Clear dashboardKey AFTER first successful load
@ -868,15 +898,22 @@ if (managerId != null &&
print('✅ dashboardKey cleared after first successful load');
}
} else {
getStaffData = [];
originalData = [];
} else if (requestId == _staffListRequestSeq) {
setState(() {
getStaffData = [];
originalData = [];
inProgressData = [];
completedData = [];
filteredData = [];
});
}
} catch (e) {
print('Exception occurred: $e');
} finally {
if (!mounted) return;
setState(() {
isLoadingStaffList = false;
if (_staffListInFlight > 0) _staffListInFlight--;
_syncStaffListLoader();
});
}
}
@ -1913,7 +1950,9 @@ if (managerId != null &&
Widget _buildContent(BuildContext context) {
final isDesktop = !ResponsiveLayout.isMobile(context);
return Column(
return Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 10),
@ -2005,6 +2044,7 @@ if (managerId != null &&
isActionable = newValue;
userPreference =
newValue; // Save this as the new "normal"
_syncStaffListLoader();
});
final prefs =
await SharedPreferences.getInstance();
@ -2104,6 +2144,7 @@ if (managerId != null &&
// CASE 2: Search cleared -> Restore the status from memory
isActionable = userPreference;
}
_syncStaffListLoader();
});
},
),
@ -2167,6 +2208,19 @@ if (managerId != null &&
child: _buildDataTable(context),
),
],
),
if (isLoadingStaffList)
Positioned.fill(
child: Container(
color: Colors.white.withOpacity(0.55),
child: const Center(
child: CircularProgressIndicator(
color: Color(0xFF2E7D6E),
),
),
),
),
],
);
}
@ -2175,6 +2229,7 @@ if (managerId != null &&
'To be assigned': true,
'Assigned': true,
'In progress': true,
'Completed': true,
};
Map<String, String> statusGroupMap = {
@ -2211,17 +2266,17 @@ if (managerId != null &&
grouped["To be assigned"] = [];
grouped["Assigned"] = [];
grouped["In progress"] = [];
} else {
grouped["Completed"] = [];
}
for (var item in filteredDataByTab) {
final status = (item['enquiry_status'] ?? '').toString();
if (grouped.containsKey(status)) {
grouped[status]!.add(item);
for (final item in filteredDataByTab) {
final status = (item['enquiry_status'] ?? '').toString();
if (grouped.containsKey(status)) {
grouped[status]!.add(item);
}
}
return grouped;
}
grouped["Completed"] = List<Map<String, dynamic>>.from(filteredDataByTab);
return grouped;
}
@ -2369,7 +2424,8 @@ if (managerId != null &&
// Replace your _buildDataTableContent() method with this optimized version
Widget _buildDataTableContent() {
if (filteredData.isEmpty) {
final visibleRows = filteredDataByTab;
if (visibleRows.isEmpty) {
return const SizedBox(
height: 50,
child: Center(child: Text('No available data')),
@ -3485,16 +3541,8 @@ if (managerId != null &&
child: GestureDetector(
onTap: () {
setState(() {
print('TAPPED TAB');
selectedIndex = index;
filteredData = (index == 0) ? inProgressData : completedData;
if (index == 1) {
completedCurrentPage = 1;
// final newValue = false;
// isActionable = newValue;
}
_syncFilteredDataForCurrentTab(resetCompletedPage: index == 1);
_searchStaffController.clear();
});
},

View File

@ -25,6 +25,7 @@ class ThemedSearchField extends HookWidget {
this.maxLength, // new
this.inputFormatters,
this.onChanged,
this.showSearchIcon = true,
});
final String hintText;
@ -44,6 +45,7 @@ class ThemedSearchField extends HookWidget {
final TextInputType? keyboardType;
final int? maxLength;
final List<TextInputFormatter>? inputFormatters;
final bool showSearchIcon;
@override
Widget build(BuildContext context) {
@ -86,11 +88,16 @@ class ThemedSearchField extends HookWidget {
// maxWidth: 25 + 16 + 10,
// maxHeight: 25 + (8 * 2),
// ),
suffixIcon: Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 10),
child: Icon(Icons.search, color: const Color(0xFF94A3B8), size: 18),
// child: Icon(Icons.search, color: const Color(0xFF686868), size: 20),
),
suffixIcon: showSearchIcon
? Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 10),
child: Icon(
Icons.search,
color: const Color(0xFF94A3B8),
size: 18,
),
)
: null,
);
return Container(

View File

@ -58,6 +58,8 @@ class DateFilterRow extends ConsumerStatefulWidget {
/// When true, only start/end date fields and filter/refresh actions are shown
/// (no status, staff, insurer, or partner controls).
final bool compactDateFiltersOnly;
/// When true, filter/refresh buttons are omitted (caller renders them elsewhere).
final bool hideFilterActions;
final role;
final id;
@ -82,6 +84,7 @@ class DateFilterRow extends ConsumerStatefulWidget {
required this.id,
this.isMobile = false,
this.compactDateFiltersOnly = false,
this.hideFilterActions = false,
this.dataFrom,
});
@ -344,7 +347,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
];
if (isMobile) {
// For mobile: buttons below fields
if (widget.hideFilterActions) return dateFields;
return [
...dateFields,
SizedBox(height: 10),
@ -352,6 +355,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
];
} else {
// For desktop: buttons inline with fields
if (widget.hideFilterActions) return dateFields;
return [...dateFields, ...buttons];
}
}

View File

@ -16,8 +16,9 @@
-->
<!-- <base href="$FLUTTER_BASE_HREF"> -->
<base href="/partner/">
<!-- <base href="/nhance/partner/app/">-->
<!-- <base href="{Env.baseHref}">-->
<!-- below one is dev-->
<!-- <base href="/nhance/partner/app/">-->
<!-- <base href="{Env.baseHref}">-->
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">