FEAT_PAYOUT REWORKS

This commit is contained in:
sanjeev.p 2026-04-03 15:04:34 +05:30
parent d94a16cc4a
commit 9a6685382a
39 changed files with 15203 additions and 590 deletions

File diff suppressed because one or more lines are too long

View File

@ -7,9 +7,11 @@ import 'package:nhance_partner/presentation/screens/Masters/PaymentMode/paymentL
import 'package:nhance_partner/presentation/screens/UserManagement/Profile/profile_web.dart';
import 'package:nhance_partner/presentation/screens/dashboard/dashboard_nw.dart';
import 'package:nhance_partner/presentation/screens/payout/invoice_list.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../data/services/auth_service.dart';
import '../../presentation/providers/manager_provider.dart';
import '../../presentation/providers/userRoleProvider.dart';
import '../../presentation/screens/Enquiry/enquiry/tabs.dart';
import '../../presentation/screens/Enquiry/policy_claims_endros/claims.dart';
import '../../presentation/screens/Enquiry/policy_claims_endros/endorsomentUpdateValidation.dart';
@ -33,6 +35,8 @@ import '../../presentation/screens/handler/enquiryListOld.dart';
import '../../presentation/screens/home/home_screen.dart';
import '../../presentation/screens/login/login_screen.dart';
import '../../presentation/screens/payout/payout_screen.dart';
import '../../presentation/screens/payout/payout_details.dart';
import '../../presentation/screens/payout/payout_list.dart';
import '../../presentation/screens/splash/splash_screen.dart';
import '../../presentation/screens/staff/Enquiry/enquiry_inline_Edit_17Nov.dart';
import '../../presentation/screens/staff/Enquiry/enquiry_inline_list.dart';
@ -42,7 +46,11 @@ import '../../presentation/screens/staff/policy/policy.dart';
import '../../presentation/screens/staff/policy/policy_list.dart';
import '../../presentation/screens/staff/policy/policy_validation.dart';
import '../../presentation/screens/staff/quotations/quotation.dart';
import '../../presentation/screens/Grid/gridList.dart';
import '../../presentation/screens/Grid/gridUpload.dart';
import '../../presentation/screens/Grid/gridView.dart';
import '../config/navigation_service.dart';
import '../../presentation/screens/dashboard/partner_portal_dashboard.dart';
final GoRouter appRouter = GoRouter(
navigatorKey: navigatorKey,
@ -57,13 +65,17 @@ final GoRouter appRouter = GoRouter(
path: AppRoutes.login,
builder: (context, state) => const LoginScreen(),
),
// GoRoute(
// path: AppRoutes.home,
// builder: (context, state) => const HomeScreen(),
// ),
GoRoute(
path: AppRoutes.partnerPortalDashboard,
builder: (context, state) => const PartnerPortalDashboard(),
),
GoRoute(
path: AppRoutes.dashboard,
// builder: (context, state) => const DashboardScreen(),
builder: (context, state) => const Dashboard(),
),
@ -79,6 +91,17 @@ final GoRouter appRouter = GoRouter(
path: AppRoutes.invoiceList,
builder: (context, state) => const InvoiceList(),
),
GoRoute(
path: AppRoutes.payoutList,
builder: (context, state) => const PayoutList(),
),
GoRoute(
path: AppRoutes.payoutDetails,
builder: (context, state) {
final editItem = state.extra as Map<String, dynamic>?;
return PayOutDetails(editItem: editItem);
},
),
GoRoute(
path: AppRoutes.profile,
@ -236,7 +259,6 @@ final GoRouter appRouter = GoRouter(
path: AppRoutes.endorsomentValidation,
name: "endorsomentValidation",
pageBuilder: (context, state) {
final extra = state.extra as Map<String, dynamic>;
return CustomTransitionPage(
@ -293,7 +315,7 @@ final GoRouter appRouter = GoRouter(
builder: (context, state) => PolicyStaffEnqList(),
),
GoRoute(
GoRoute(
path: AppRoutes.enquiryForStaff,
builder: (context, state) => const EnquiryListStaffInline(),
// builder: (context, state) => const EnquiryListStaffInlineTST(),
@ -331,6 +353,18 @@ final GoRouter appRouter = GoRouter(
path: AppRoutes.paymentModeLst,
builder: (context, state) => const PaymentLsit(),
),
GoRoute(
path: AppRoutes.gridList,
builder: (context, state) => const GridListScreen(),
),
GoRoute(
path: AppRoutes.gridUpload,
builder: (context, state) => const GridUploadScreen(),
),
GoRoute(
path: AppRoutes.gridView,
builder: (context, state) => const GridViewScreen(),
),
],
redirect: (context, state) async {
final loggedIn = await AuthService.isLoggedIn();
@ -351,9 +385,30 @@ final GoRouter appRouter = GoRouter(
}
if (loggedIn && goingToLogin) {
// Logged in but going to login send to "next" if present
// Logged in but opening login continue to intended route or dashboard
final next = state.uri.queryParameters['next'];
return next ?? AppRoutes.login;
final prefs = await SharedPreferences.getInstance();
final role = prefs.getString('userRole');
if (next != null && next.isNotEmpty) {
if (role == 'agent' && next == AppRoutes.dashboard) {
return AppRoutes.partnerPortalDashboard;
}
return next;
}
if (role == 'staff') return AppRoutes.enquiryForStaff;
//if (role == 'Accounts') return AppRoutes.invoiceList;
if (role == 'Accounts') return AppRoutes.payoutList;
if (role == 'agent') return AppRoutes.partnerPortalDashboard;
return AppRoutes.dashboard;
}
// Hard guard: agent must never land on manager dashboard route.
if (loggedIn && currentLocation == AppRoutes.dashboard) {
final prefs = await SharedPreferences.getInstance();
final role = prefs.getString('userRole');
if (role == 'agent') return AppRoutes.partnerPortalDashboard;
}
// if (!loggedIn && !goingToLogin) return AppRoutes.login;

View File

@ -1,7 +1,23 @@
class AppRoutes {
// ---------------------------------------------------------------------------
// Post-login routing (JWT role_id userRoleProvider first screen)
// ---------------------------------------------------------------------------
// | role_id | userRoleProvider | First route | Screen / notes |
// |---------|------------------|--------------------------|-----------------------|
// | 1 | manager | /dashboard | Manager Dashboard |
// | 2 | handler | /enquiryHandlerLst | Handler enquiry list |
// | 3 | staff | /enquiryForStaff | Staff inline enquiry |
// | 4 | Accounts | /enquiryLst | Enquiry page |
// | (else) | agent | /partnerPortalDashboard | Partner portal |
// Partner Login as Partner forces role `agent` partner route above. |
// ---------------------------------------------------------------------------
static const String splash = '/';
static const String home = '/home';
static const String dashboard = '/dashboard';
/// Partner (agent) home shows `PartnerPortalDashboard`.
static const String partnerPortalDashboard = '/partnerPortalDashboard';
static const String login = '/login';
static const String profile = '/profile';
static const String agentLst = '/agentLst';
@ -34,4 +50,10 @@ class AppRoutes {
static const String paymentModeLst = '/paymentMode';
static const String endorsementTypeLst = '/endorsementTypeLst';
static const String vehicleTypeLst = '/vehicleTypeLst';
static const String payoutList = '/payoutList';
static const String payoutDetails = '/payoutDetails';
static const String payoutGrid = '/payoutGrid';
static const String gridList = '/gridList';
static const String gridUpload = '/gridUpload';
static const String gridView = '/gridView';
}

View File

@ -57,9 +57,9 @@ class ApiService {
// }
Future<Map<String, dynamic>> _makeGetRequest(
Uri url,
Map<String, String> headers,
) async {
Uri url,
Map<String, String> headers,
) async {
final response = await http.get(url, headers: headers);
return _handleResponse(response);
}
@ -431,9 +431,9 @@ class ApiService {
String path,
String id,
String month,
dynamic managerId,
{DateTime? toDate, DateTime? fromDate}
) async {
dynamic managerId,
{DateTime? toDate, DateTime? fromDate}
) async {
print('Sq getxl 1 - $path');
print('Sq toDate - $toDate');
print('Sq fromDate - $fromDate');
@ -467,18 +467,18 @@ class ApiService {
if (path == 'Insurer') {
pathVal =
'dashboard/downloadExcelInsurer?manager_id=$managerId&month=$month&insurer_id=$id';
'dashboard/downloadExcelInsurer?manager_id=$managerId&month=$month&insurer_id=$id';
} else if (path == 'Broker') {
pathVal =
'dashboard/downloadExcelBroker?manager_id=$managerId&month=$month&broker_id=$id';
'dashboard/downloadExcelBroker?manager_id=$managerId&month=$month&broker_id=$id';
} else if (path == 'Product') {
pathVal =
'dashboard/downloadExcelProduct?manager_id=$managerId&month=$month&vehicle_type=$id';
'dashboard/downloadExcelProduct?manager_id=$managerId&month=$month&vehicle_type=$id';
} else if (path == 'PerformingTop50') {
pathVal = 'dashboard/downloadExcelPerformingAgentsTop50?manager_id=$managerId';
} else if (path == 'PerformingAgentsByID') {
pathVal =
'dashboard/downloadExcelPerformingAgentsByID?manager_id=$managerId&agent_id=$id';
'dashboard/downloadExcelPerformingAgentsByID?manager_id=$managerId&agent_id=$id';
} else if (path == 'NonPerformingBelow50K') {
pathVal = 'dashboard/downloadExcelLowPremium?manager_id=$managerId';
} else if (path == 'staff_pending_summary') {
@ -487,7 +487,7 @@ class ApiService {
pathVal = 'dashboard/downloadExcelStaffPendingSummaryByID?manager_id=$managerId&staff_id=$id&status=$month';
} else {
//NoBusiness
pathVal =
pathVal =
'dashboard/downloadExcelWithoutPolicies?manager_id=$managerId';
}
@ -728,13 +728,13 @@ class ApiService {
if (masterName == 'Broker') {
url = Uri.parse('${Env.apiUrl}master/updateBrokerStatus/$id');
}
}
else if (masterName == 'EndorsementType') {
url = Uri.parse('${Env.apiUrl}master/updateEndorsementStatus/$id');
}
}
else if (masterName == 'VehicleType') {
url = Uri.parse('${Env.apiUrl}master/updateVehicleTypeStatus/$id');
}
}
else {
url = Uri.parse('${Env.apiUrl}master/updatePaymentModeStatus/$id');
}
@ -767,7 +767,7 @@ class ApiService {
final url;
url = Uri.parse('${Env.apiUrl}dashboard/managerDashboard?manager_id=$id&staff_id=$userId');
url = Uri.parse('${Env.apiUrl}dashboard/managerDashboard?manager_id=$id&staff_id=$userId');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
@ -837,7 +837,7 @@ class ApiService {
if (fromDate == null || toDate == null) {
url = Uri.parse(
'${Env.apiUrl}dashboard/partnerDashboard?manager_id=$id',
);
);
}
/// CASE 2: both dates present filtered API
else {
@ -846,9 +846,9 @@ class ApiService {
url = Uri.parse(
'${Env.apiUrl}dashboard/partnerDashboard'
'?manager_id=$id'
'&from_date=$from'
'&to_date=$to',
'?manager_id=$id'
'&from_date=$from'
'&to_date=$to',
);
}
@ -879,17 +879,19 @@ class ApiService {
// -------------------------------- AGENT ----------------------------------------------
Future<Map<String, dynamic>> fetchAgentIncentiveList(agentId) async {
Future<Map<String, dynamic>> fetchAgentIncentiveList(
dynamic agentId, {
String type = 'incentive',
}) async {
// print(_token);
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse(
'${Env.apiUrl}agent/agentIncentiveFileList?agent_id=${agentId}',
'${Env.apiUrl}agent/agentIncentiveFileList?agent_id=$agentId&type=$type',
);
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
@ -898,6 +900,111 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> fetchGridFileList() async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}grid/fileList');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
// ----------------------------------- PAYOUT GRID ----------------------------------------------
Future<Map<String, dynamic>> loadPayoutGrid({String? role, String? fileId}) async {
if (_token == null) {
await _initializeToken();
}
final roleTrimmed = role?.trim();
final fileIdTrimmed = fileId?.trim();
final endpoint = Uri.parse('${Env.apiUrl}grid');
// final endpoint = Uri.parse('http://localhost/nhance_partner_be/grid');
final queryParameters = <String, String>{};
if (roleTrimmed != null && roleTrimmed.isNotEmpty) {
queryParameters['role'] = roleTrimmed;
}
if (fileIdTrimmed != null && fileIdTrimmed.isNotEmpty) {
queryParameters['file_id'] = fileIdTrimmed;
}
final url = endpoint.replace(queryParameters: queryParameters);
final headers = {
'Authorization': 'Bearer ${_token ?? ''}',
'app-signature': Env.App_Signature,
};
return await _makeGetRequest(url, headers);
}
Future<void> downloadGridExcel({
String? role,
String? fileId,
String? insurer,
String? vehicleType,
String? segment,
String? rto,
String? planType,
String? search,
String? loggedId,
}) async {
if (_token == null) {
await _initializeToken();
}
final endpoint = Uri.parse('${Env.apiUrl}grid/download');
// final endpoint = Uri.parse('http://localhost/nhance_partner_be/grid/download');
final queryParameters = <String, String>{};
void addQuery(String key, String? value) {
final v = value?.trim();
if (v != null && v.isNotEmpty) {
queryParameters[key] = v;
}
}
addQuery('role', role);
addQuery('file_id', fileId);
addQuery('insurer', insurer);
addQuery('vehicle_type', vehicleType);
addQuery('segment', segment);
addQuery('rto', rto);
addQuery('plan_type', planType);
addQuery('search', search);
addQuery('logged_id', loggedId);
final url = endpoint.replace(queryParameters: queryParameters);
final headers = {
'Authorization': 'Bearer ${_token ?? ''}',
'app-signature': Env.App_Signature,
};
final response = await _makeGethttpRequest(url, headers);
if (response.statusCode != 200) {
throw Exception('Failed to download grid excel');
}
final contentType = response.headers['content-type'] ?? '';
if (contentType.contains('application/json')) {
throw Exception('No export data available for selected filters');
}
final blob = html.Blob([response.bodyBytes]);
final blobUrl = html.Url.createObjectUrlFromBlob(blob);
final fileName = extractFileName(
response.headers['content-disposition'],
'grid_export.xlsx',
);
html.AnchorElement(href: blobUrl)
..setAttribute('download', fileName)
..click();
html.Url.revokeObjectUrl(blobUrl);
}
Future<Map<String, dynamic>> fetchManagerIncentiveList(mangerId) async {
// print(_token);
if (_token == null) {
@ -1246,6 +1353,70 @@ class ApiService {
return response;
}
//
/// GET /partner/{id}/details
/// Returns: profile info, policy count, premium totals, commission, clients
Future<Map<String, dynamic>> getPartnerDetails(dynamic id) async {
if (_token == null) await _initializeToken();
final url = Uri.parse('${Env.apiUrl}partner/$id/details');
final headers = {
'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
/// GET /partner/{id}/policies
/// Returns: list of policies with policy_no, holder_name, product, premium, status
Future<Map<String, dynamic>> getPartnerPolicies(dynamic id) async {
if (_token == null) await _initializeToken();
final url = Uri.parse('${Env.apiUrl}partner/$id/policies');
final headers = {
'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
/// GET /partner/{id}/renewals?days=N
/// Returns: list of policies expiring within [days] days
/// [days] default is 20 pass 10, 20, 30 or 45 from the UI dropdown
Future<Map<String, dynamic>> getPartnerRenewals(
dynamic id, {
int days = 20,
}) async {
if (_token == null) await _initializeToken();
final url = Uri.parse('${Env.apiUrl}partner/$id/renewals?days=$days');
final headers = {
'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
/// GET /partner/{id}/earnings
/// Returns: list of monthly earning records with
/// month_key, month_label, premium, policies, paid/status
Future<Map<String, dynamic>> getPartnerEarnings(dynamic id) async {
if (_token == null) await _initializeToken();
final url = Uri.parse('${Env.apiUrl}partner/$id/earnings');
final headers = {
'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
//
Future<Map<String, dynamic>> findEnqQuotePolicyView(id) async {
print('findEnqQuotePolicyVie2w - $id');
if (_token == null) {
@ -1415,6 +1586,8 @@ class ApiService {
String? selectedStaffId,
String ? selectedStatus,
String ? selectedInsurer,
/// Accounts + policy list: filter by partner (agent id).
String? selectedAgentId,
}) async {
print(
"fetchPolicyDataOnlyListAPI - mangerID- $managerId - id - $id - role -$role >> selectedInsurer -$selectedInsurer ",
@ -1441,12 +1614,15 @@ class ApiService {
query = 'agent_id=$id';
}
final url = Uri.parse(
'${Env.apiUrl}enquiry/enquiryList?$query&only_policy_data=true&from_date=${fromDate ?? ''}&to_date=${toDate ?? ''}&staff_id=$selectedStaffId',
);
// final url = Uri.parse(
// 'http://localhost/nhance_partner_be/enquiry/enquiryList?$query&only_policy_data=true&from_date=${fromDate ?? ''}&to_date=${toDate ?? ''}&staff_id=$selectedStaffId',
// );
var listPath =
'${Env.apiUrl}enquiry/enquiryList?$query&only_policy_data=true&from_date=${fromDate ?? ''}&to_date=${toDate ?? ''}&staff_id=${selectedStaffId ?? ''}';
if (role == 'Accounts' &&
selectedAgentId != null &&
selectedAgentId.toString().trim().isNotEmpty) {
listPath +=
'&agent_id=${Uri.encodeComponent(selectedAgentId.toString().trim())}';
}
final url = Uri.parse(listPath);
// if (role == 'manager') {
// url = Uri.parse(
@ -1565,16 +1741,16 @@ class ApiService {
// ----------------------------------- ENDORSEMENT -------------------------------------------------
Future<Map<String, dynamic>> fetchEndorsementList(
managerId,
int id,
role, {
String? fromDate,
String? toDate,
String? endorsementType,
String? insurerId,
String? status,
String? verification,
}) async {
managerId,
int id,
role, {
String? fromDate,
String? toDate,
String? endorsementType,
String? insurerId,
String? status,
String? verification,
}) async {
print('fetchEndorsementList in');
if (_token == null) {
await _initializeToken();
@ -1604,7 +1780,7 @@ class ApiService {
print('fetchEndorsementList api');
final url = Uri.parse('${Env.apiUrl}endorsement/endorsementList')
.replace(queryParameters: queryParams);
.replace(queryParameters: queryParams);
final headers = {
'Authorization': 'Bearer $_token',
@ -1760,7 +1936,7 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> fetchAgentUnusedCommissionList(id,broker_id) async {
Future<Map<String, dynamic>> fetchAgentUnusedCommissionList(id) async {
print('fetchAGENTNameDropDown');
if (_token == null) {
await _initializeToken();
@ -1768,9 +1944,13 @@ class ApiService {
dynamic url;
print('fetchAGENTNameDropDown 1');
url = Uri.parse(
'${Env.apiUrl}invoice/getAgentUnusedCommissionList?manager_id=$id&broker_id=$broker_id',
);
// url = Uri.parse(
// '${Env.apiUrl}invoice/getAgentUnusedCommissionList?manager_id=$id',
// );
// url = Uri.parse(
// 'http://localhost/nhance_partner_be/invoice/getAgentUnusedCommissionList?manager_id=$id',
// );
print('fetchAGENTNameDropDown 2');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
@ -1870,7 +2050,8 @@ class ApiService {
// --------------------------------- PayOut Module----------------------------------------------
Future<Map<String, dynamic>> getCommissionRateList(data) async {
print("getCommissionRateList------- $data}");
final url = Uri.parse('${Env.apiUrl}/invoice/commission-rate-list');
// final url = Uri.parse('${Env.apiUrl}/invoice/commission-rate-list');
// final url = Uri.parse('http://localhost/nhance_partner_be/invoice/commission-rate-list');
print("getCommissionRateList 1");
// final token = await getToken(); // Fetch token
@ -1896,7 +2077,8 @@ class ApiService {
}
Future<Map<String, dynamic>> getCreateOrUpdate(data) async {
final url = Uri.parse('${Env.apiUrl}invoice/create-or-update');
final url = Uri.parse('${Env.apiUrl}invoice/create-or-update');
// final url = Uri.parse('http://localhost/nhance_partner_be/invoice/create-or-update');
// final token = await getToken(); // Fetch token
@ -1933,6 +2115,23 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> getPayoutList() async {
// print(_token);
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}invoice/list');
//final url = Uri.parse('http://localhost/nhance_partner_be/invoice/list');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
Future<Map<String, dynamic>> deleteInvoice(ID) async {
// print(_token);
if (_token == null) {
@ -1981,6 +2180,231 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> addInvoicePayment(
Map<String, dynamic> data,
) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}invoice/add-payment');
// final url = Uri.parse('http://localhost/nhance_partner_be/invoice/add-payment');
final headers = {
'Authorization': 'Bearer $_token',
'Content-Type': 'application/json',
'app-signature': Env.App_Signature,
};
final response = await _makePostRequestJson(url, data, headers);
return response;
}
Future<Map<String, dynamic>> getInvoiceUtrDetails(dynamic invoiceId) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}invoice/utrDetails?invoice_id=$invoiceId');
//final url = Uri.parse('http://localhost/nhance_partner_be/invoice/utrDetails?invoice_id=$invoiceId');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
Future<Map<String, dynamic>> updateInvoiceUtrDetails(
Map<String, dynamic> data,
) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}invoice/updateUtrDetails');
// final url = Uri.parse('http://localhost/nhance_partner_be/invoice/updateUtrDetails');
final headers = {
'Authorization': 'Bearer $_token',
'Content-Type': 'application/json',
'app-signature': Env.App_Signature,
};
final response = await _makePostRequestJson(url, data, headers);
return response;
}
Future<Map<String, dynamic>> addInvoicePaymentHistory(dynamic invoiceId) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse(
'${Env.apiUrl}invoice/add-payment-history?invoice_id=$invoiceId',
);
// final url = Uri.parse(
// 'http://localhost/nhance_partner_be/invoice/add-payment-history?invoice_id=$invoiceId',
// );
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
Future<Map<String, dynamic>> listInvoiceUtrDetails(dynamic invoiceId) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse(
'${Env.apiUrl}invoice/listUtrDetails?invoice_id=$invoiceId',
);
// final url = Uri.parse(
// 'http://localhost/nhance_partner_be/invoice/listUtrDetails?invoice_id=$invoiceId',
// );
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
Future<Map<String, dynamic>> bulkUploadInvoiceCommission({
required PlatformFile file,
required Map<String, dynamic> data,
}) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}invoice/bulk-upload-commission');
try {
final request = http.MultipartRequest('POST', url);
request.headers.addAll({
'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature,
});
if (file.bytes == null) throw Exception('Could not read file bytes');
request.files.add(
http.MultipartFile.fromBytes(
'file_name',
file.bytes!,
filename: file.name,
),
);
data.forEach((key, value) {
if (value != null) {
request.fields[key] = value.toString();
}
});
final streamedResponse = await request.send();
final responseBody = await streamedResponse.stream.bytesToString();
if (streamedResponse.statusCode == 401 ||
streamedResponse.statusCode == 403) {
await clearLocalStorageAndRedirect();
return {'status': 'error', 'message': 'Session expired'};
}
if (streamedResponse.statusCode != 200) {
return {
'status': 'error',
'message': 'Server Error: ${streamedResponse.statusCode}',
};
}
final decoded = jsonDecode(responseBody);
return decoded is Map<String, dynamic>
? decoded
: {'status': 'error', 'message': 'Unexpected response format'};
} catch (e) {
return {'status': 'error', 'message': e.toString()};
}
}
Future<Map<String, dynamic>> bulkUploadInvoiceCommissionProceed(
Map<String, dynamic> data,
) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}invoice/bulk-upload-commission/proceed');
final headers = {
'Authorization': 'Bearer $_token',
'Content-Type': 'application/json',
'app-signature': Env.App_Signature,
};
final response = await _makePostRequestJson(url, data, headers);
return response;
}
Future<Map<String, dynamic>> uploadPolicyCommissionExcel({
required dynamic id,
required PlatformFile file,
bool proceedPartnerMismatch = false,
}) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}policy/uploadCommissionExcel');
try {
final request = http.MultipartRequest('POST', url);
request.headers.addAll({
'Authorization': 'Bearer $_token',
'app-signature': Env.App_Signature,
});
if (file.bytes == null) throw Exception('Could not read file bytes');
request.files.add(
http.MultipartFile.fromBytes(
'file',
file.bytes!,
filename: file.name,
),
);
request.fields['id'] = id.toString();
if (proceedPartnerMismatch) {
request.fields['proceed_partner_mismatch'] = '1';
}
final streamedResponse = await request.send();
final responseBody = await streamedResponse.stream.bytesToString();
if (streamedResponse.statusCode == 401 ||
streamedResponse.statusCode == 403) {
await clearLocalStorageAndRedirect();
return {'status': 'error', 'message': 'Session expired'};
}
if (streamedResponse.statusCode != 200) {
return {
'status': 'error',
'message': 'Server Error: ${streamedResponse.statusCode}',
};
}
final decoded = jsonDecode(responseBody);
return decoded is Map<String, dynamic>
? decoded
: {'status': 'error', 'message': 'Unexpected response format'};
} catch (e) {
return {'status': 'error', 'message': e.toString()};
}
}
Future<Map<String, dynamic>> findPolicyApi(ID) async {
// print(_token);
if (_token == null) {
@ -2059,6 +2483,24 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> updatePolicyCommissionApi(data) async {
final url = Uri.parse('${Env.apiUrl}policy/updatePolicyCommission');
//final url = Uri.parse('http://localhost/nhance_partner_be/policy/updatePolicyCommission');
if (_token == null) {
throw Exception('Token not found. Please log in.');
}
final headers = {
'Authorization': 'Bearer $_token',
'Content-Type': 'application/json',
'app-signature': Env.App_Signature,
};
final response = await _makePostRequestJson(url, data, headers);
return response;
}
Future<Map<String, dynamic>> fetchPolicyMasterDropDown(String val) async {
// print(_token);
if (_token == null) {
@ -2088,7 +2530,6 @@ class ApiService {
final url = Uri.parse(
'${Env.apiUrl}reports/endorsement-excel?manager_id=$managerId',
);
// final url = Uri.parse('http://localhost/nhance_partner_be/reports/endorsement-excel?manager_id=$managerId');
print('getPdfDownload endorsement-excel - $url');
await _initializeToken();
@ -2153,6 +2594,25 @@ class ApiService {
}
}
/// Payout sample: `from_date` / `to_date` as `yyyy-MM-dd`, `agent_code` as partner code (may be empty).
Future<void> downloadPendingCommissionExcel({
required String fromDate,
required String toDate,
required String agentCode,
}) async {
final query = Uri(
queryParameters: {
'from_date': fromDate,
'to_date': toDate,
'agent_code': agentCode,
},
).query;
await getPdfDownload(
'policy/downloadPendingCommissionExcel?$query',
'pending_commission',
);
}
Future<void> generatePolicyExcel(managerId, fromDate, toDate, searchValue,flag,insurer) async {
final url = Uri.parse(
'${Env.apiUrl}reports/policy-excel?manager_id=$managerId&from_date=${fromDate ?? ''}&to_date=${toDate ?? ''}&search=${searchValue ?? ''}&show_policy_report=${flag ?? ''}&insurer_id=${insurer ?? ''}',
@ -2275,7 +2735,6 @@ class ApiService {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}endorsement/deleteEndorsement?id=$id');
// final url = Uri.parse('http://localhost/nhance_partner_be/endorsement/deleteEndorsement?id=$id');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
@ -2295,7 +2754,6 @@ class ApiService {
}
final url = Uri.parse('${Env.apiUrl}endorsement/uploadEndorsementFile');
// final url = Uri.parse('http://localhost/nhance_partner_be/endorsement/uploadEndorsementFile');
try {
final request = http.MultipartRequest('POST', url);

View File

@ -63,34 +63,55 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
// Set active menu based on current route
void _setActiveMenu() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final currentRoute = GoRouterState.of(context).uri.path;
final currentRoute = GoRouterState.of(context).uri.path.toLowerCase();
setState(() {
if (currentRoute.contains('dashboard')) {
_activeMenu = 'Dashboard';
} else if (currentRoute.contains('enquiry')) {
_activeMenu = 'Enquiry';
} else if (currentRoute.contains('agent') ||
currentRoute.contains('staff') ||
currentRoute.contains('salesExecutive')) {
_activeMenu = 'User';
} else if (currentRoute.contains('claim') ||
currentRoute.contains('endosement') ||
currentRoute.contains('policy') ||
currentRoute.contains('attendance')) {
_activeMenu = 'Reports';
} else if (currentRoute.contains('broker') ||
currentRoute.contains('payment')) {
_activeMenu = 'Masters';
} else if (currentRoute.contains('invoice') || // ADD THIS
currentRoute.contains('payout')) {
_activeMenu = 'Invoice';
} else if (currentRoute.contains('Endorsement')) {
_activeMenu = 'Endorsement';
}
_activeMenu = _menuKeyForRoute(currentRoute);
});
});
}
String? _menuKeyForRoute(String currentRoute) {
final route = currentRoute.toLowerCase();
if (route.contains('dashboard') ||
route.contains('partnerportaldashboard')) {
return 'Dashboard';
}
if (route.contains('enquiry')) {
return 'Enquiry';
}
if (route.contains('endorsement')) {
return 'Endorsement';
}
if (route.contains('agent') ||
route.contains('staff') ||
route.contains('salesexecutive') ||
route.contains('pos')) {
return 'User';
}
if (route.contains('claim') ||
route.contains('policy') ||
route.contains('attendance') ||
route.contains('monthlycommission') ||
route.contains('gridlist') ||
route.contains('gridview')) {
return 'Reports';
}
if (route.contains('broker') ||
route.contains('payment') ||
route.contains('vehicletype') ||
route.contains('endorsementtype')) {
return 'Masters';
}
if (route.contains('payoutgrid')) {
return 'PayoutGrid';
}
if (route.contains('invoice') ||
(route.contains('payout') && !route.contains('payoutgrid'))) {
return 'Payout';
}
return null;
}
Future<void> _initializeToken() async {
_token = await AuthService.getToken();
if (_token != null) {
@ -116,7 +137,7 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
_queueTimer = Timer.periodic(
const Duration(seconds: 30),
(_) => getStaffLevelCount(
(_) => getStaffLevelCount(
profileData?['manager_id'],
profileData?['id'],
),
@ -249,6 +270,23 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
),
],
if (key == 'Reports') ...[
if (role == 'manager' ||
role == 'Accounts' ||
role == 'agent') ...[
_buildPopupItem(
label: "Grid List",
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'Reports');
context.go(
role == 'agent'
? AppRoutes.gridView
: AppRoutes.gridList,
);
},
),
const SizedBox(height: 2),
],
if (role == 'manager') ...[
_buildPopupItem(
label: "Attendance",
@ -342,6 +380,13 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
@override
Widget build(BuildContext context) {
final roleId = ref.watch(userRoleProvider);
final currentRouteLower =
GoRouterState.of(context).uri.path.toLowerCase();
var activeMenu =
_menuKeyForRoute(currentRouteLower) ?? _activeMenu;
if (currentRouteLower.contains('payoutgrid') && roleId != 'agent') {
activeMenu = 'Reports';
}
return AppBar(
// backgroundColor: const Color(0xFFD6F6F4),
@ -366,12 +411,15 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
_buildMenuItem(
icon: Icons.dashboard,
label: "Dashboard",
isActive: _activeMenu == 'Dashboard',
isActive: activeMenu == 'Dashboard',
onTap: () async {
_hidePopup();
setState(() => _activeMenu = 'Dashboard');
await _clearDashboardFilters();
context.go(AppRoutes.dashboard);
final dashboardRoute = roleId == 'agent'
? AppRoutes.partnerPortalDashboard
: AppRoutes.dashboard;
context.go(dashboardRoute);
},
),
@ -386,7 +434,7 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
_buildMenuItem(
icon: Icons.list_alt_rounded,
label: "Enquiry",
isActive: _activeMenu == 'Enquiry',
isActive: activeMenu == 'Enquiry',
onTap: () async {
_hidePopup();
setState(() => _activeMenu = 'Enquiry');
@ -402,7 +450,7 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
const SizedBox(width: 8),
_buildMenuItem(
isActive: _activeMenu == 'Endorsement',
isActive: activeMenu == 'Endorsement',
icon: Icons.checklist_outlined,
label: "Endorsement",
onTap: () async {
@ -413,8 +461,6 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
},
),
const SizedBox(width: 8),
// User (Manager Only) - with hover popup
@ -423,7 +469,7 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
icon: Icons.person_add_alt,
label: "User",
popupKey: 'User',
isActive: _activeMenu == 'User',
isActive: activeMenu == 'User',
),
const SizedBox(width: 8),
],
@ -434,7 +480,7 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
icon: Icons.settings_suggest_outlined,
label: "Masters",
popupKey: 'Masters',
isActive: _activeMenu == 'Masters',
isActive: activeMenu == 'Masters',
),
const SizedBox(width: 8),
],
@ -445,27 +491,26 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
icon: Icons.receipt_long,
label: "Reports",
popupKey: 'Reports',
isActive: _activeMenu == 'Reports',
isActive: activeMenu == 'Reports',
),
const SizedBox(width: 8),
],
// Pay Out button for Accounts
// Payout (Accounts only) single top-level item, no submenu
if (role == 'Accounts') ...[
_buildMenuItem(
isActive: _activeMenu == 'Invoice',
icon: Icons.checklist_outlined,
label: "Invoice",
onTap: () async {
icon: Icons.payments_outlined,
label: "Payout",
isActive: activeMenu == 'Payout',
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'Invoice');
await _clearDashboardFilters();
context.go(AppRoutes.invoiceList);
setState(() => _activeMenu = 'Payout');
context.go(AppRoutes.payoutList);
},
),
],
const Spacer(),
// Raise Enquiry button for agents
@ -666,11 +711,7 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
return MouseRegion(
onEnter: (_) {
final box = iconContext.findRenderObject() as RenderBox;
_showQueuePopup(
context,
box.localToGlobal(Offset.zero),
box.size,
);
_showQueuePopup(context, box.localToGlobal(Offset.zero), box.size);
},
onExit: (_) => _hidePopup(),
child: Stack(
@ -689,28 +730,30 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
// 🔴 Badge
// if (queueCount > 0)
Positioned(
right: -6,
top: -6,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints:
const BoxConstraints(minWidth: 18, minHeight: 18),
child: Text(
queueCount.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
Positioned(
right: -6,
top: -6,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 18,
minHeight: 18,
),
child: Text(
queueCount.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
@ -765,17 +808,17 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style:
GoogleFonts.inter(fontSize: 12, color: Colors.grey.shade700)),
style:
GoogleFonts.inter(fontSize: 12, color: Colors.grey.shade700)),
Text(value.toString(),
style: GoogleFonts.inter(
fontSize: 12, fontWeight: FontWeight.w600)),
style: GoogleFonts.inter(
fontSize: 12, fontWeight: FontWeight.w600)),
],
),
);
}
// Simple menu item widget
Widget _buildMenuItem({

View File

@ -157,7 +157,7 @@ class PolicyTabState extends ConsumerState<PolicyTab> {
controllers['policyNo']?.text =
record['policy_number']?.toString() ?? '';
controllers['paymentMode']?.text =
record['payment_mode']?.toString() ?? '';
record['payment_mode_value']?.toString() ?? '';
controllers['premAmount']?.text =
record['premium_amount']?.toString() ?? '';
controllers['planType']?.text =

View File

@ -19,6 +19,10 @@ import '../../../themes/indicators/search_field_theme.dart';
import '../../../themes/indicators/text_field_theme.dart';
import '../../../providers/manager_provider.dart';
final decimalFormatter = [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
];
class UpdateEndorsementDialog extends StatefulWidget {
final Map<String, dynamic> item;
@ -179,6 +183,7 @@ class _UpdateEndorsementDialogState extends State<UpdateEndorsementDialog> {
try {
final Uri uri = Uri.parse('${Env.apiUrl}endorsement/updateEndorsement');
// final Uri uri = Uri.parse('http://localhost/nhance_partner_be/endorsement/updateEndorsement');
if (_token == null) throw Exception('Token not found. Please log in.');
@ -739,9 +744,12 @@ class _UpdateEndorsementDialogState extends State<UpdateEndorsementDialog> {
backgroundColor: Color(0xFFEDF6F5),
controller: controllers['endorsement_premium']!,
validator: (value) => Validators.requiredField(value, "endorsement_premium"),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
keyboardType: const TextInputType.numberWithOptions(decimal: true),
/*
* Allow decimal amount (e.g. 6000.00).
* Integer-only formatter was removing "." and corrupting value.
*/
inputFormatters: decimalFormatter,
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.26,

View File

@ -199,8 +199,16 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
if (response['status'] == 'success') {
final data = response['data'];
final fromDate = response['from_date'] ?? '';
final toDate = response['to_date'] ?? '';
/*
* Some APIs do not return from_date/to_date in the payload.
* Keep UI date fields stable by falling back to the request dates.
*/
final fromDate = (response['from_date'] ?? '').toString().trim().isNotEmpty
? response['from_date'].toString()
: (fromDt?.toString() ?? '');
final toDate = (response['to_date'] ?? '').toString().trim().isNotEmpty
? response['to_date'].toString()
: (toDt?.toString() ?? '');
print('FromDate : $fromDate');
print('ToDate : $toDate');

View File

@ -6,6 +6,7 @@ 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:intl/intl.dart';
import '../../../../core/config/env.dart';
import '../../../../core/services/api_service.dart';
import '../../../../data/services/auth_service.dart';
@ -1709,26 +1710,40 @@ class _CreateEndorsementDialogState
policyStartController,
isStartDateFocused,
(val) => setState(() => isStartDateFocused = val),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
}
Widget buildPolicyEndDate(context) {
// Parse start date as minDate, fallback to DateTime(2000)
DateTime minDate = DateTime(2000);
if (policyStartController.text.isNotEmpty) {
try {
minDate = DateFormat('dd-MM-yyyy').parse(policyStartController.text);
} catch (_) {}
}
return buildDateField(
context,
"Policy End Date",
policyEndController,
isEndDateFocused,
(val) => setState(() => isEndDateFocused = val),
(val) => setState(() => isEndDateFocused = val),
firstDate: minDate, // 👈 min = policy start date
lastDate: DateTime(2100), // 👈 allow future
);
}
Widget buildDateField(
BuildContext context,
String label,
TextEditingController controller,
bool isFocused,
Function(bool) onFocusChanged,
) {
BuildContext context,
String label,
TextEditingController controller,
bool isFocused,
Function(bool) onFocusChanged, {
DateTime? firstDate, // 👈 ADD
DateTime? lastDate, // 👈 ADD
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -1764,9 +1779,31 @@ class _CreateEndorsementDialogState
DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2000),
lastDate: DateTime.now(),
initialDate: DateTime.now(),
firstDate: firstDate ?? DateTime(2000),
lastDate: lastDate ?? DateTime(2100),
// Clamp initialDate so it's always inside firstDate..lastDate
initialDate: () {
final fd = firstDate ?? DateTime(2000);
final ld = lastDate ?? DateTime(2100);
DateTime initial = DateTime.now();
// Try to parse existing controller value
if (controller.text.isNotEmpty) {
try {
initial = DateFormat('dd-MM-yyyy').parse(controller.text);
} catch (_) {}
}
// Clamp: if initial is before firstDate, use firstDate
if (initial.isBefore(fd)) return fd;
// Clamp: if initial is after lastDate, use lastDate
if (initial.isAfter(ld)) return ld;
return initial;
}(),
);
onFocusChanged(false);

View File

@ -83,6 +83,7 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
String? selectedFileNames;
String? selectedPaymentModeId;
String? selectedPaymentModeValue;
bool isPolicyFromFocused = false;
bool isEndorsementTypeFocused = false;
@ -322,6 +323,7 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
void setEditValues() {
final item = widget.item;
print("IITTTEM - $item");
if (item == null) return;
final policyFromValue = item['policy_from']?.toString() ?? '';
@ -335,7 +337,10 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
// pendingDaysController.text = item['pending_days']?.toString() ?? '';
premiumController.text = item['endorsement_premium']?.toString() ?? '';
commissionController.text = item['commission_amount']?.toString() ?? '';
selectedPaymentModeId = item['payment_mode_id']?.toString();
selectedPaymentModeId =
item['payment_mode_id']?.toString() ?? item['payment_mode']?.toString();
selectedPaymentModeValue =
item['payment_mode_value']?.toString() ?? item['value']?.toString();
insuredNameController.text = item['insured_name'] ?? '';
regNoController.text = item['reg_no'] ?? '';
@ -394,12 +399,6 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
setState(() {
getPaymentModeData = List<Map<String, dynamic>>.from(response['data']);
filteredPaymentModeData = List.from(getPaymentModeData);
// Re-apply selectedPaymentModeId AFTER list is loaded
final savedId = widget.item?['payment_mode_id']?.toString();
if (savedId != null) {
selectedPaymentModeId = savedId;
}
});
}
} catch (e) {
@ -677,6 +676,7 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
});
final Uri uri = Uri.parse('${Env.apiUrl}endorsement/updateEndorsement');
// final Uri uri = Uri.parse('http://localhost/nhance_partner_be/endorsement/updateEndorsement');
if (_token == null) {
throw Exception('Token not found. Please log in.');
@ -749,13 +749,32 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
print("Response status: ${response.statusCode}");
print("Response body: ${response.body}");
/*
* Treat API success only when payload status is "success".
* Some endpoints can return 200 with a failure body, which causes false UI success.
*/
if (response.statusCode == 200 || response.statusCode == 201) {
print("✅ Partner submitted successfully!");
print("Response: ${response.body}");
ToastHelper.showSuccessToast(context, 'Saved Successfully');
context.pop(true);
final dynamic responseBody = jsonDecode(response.body);
final bool isApiSuccess =
(responseBody?['status']?.toString().toLowerCase() == 'success');
if (!isApiSuccess) {
final String backendMessage = responseBody?['data']?.toString() ??
responseBody?['message']?.toString() ??
'Failed To Save';
ToastHelper.showErrorToast(context, backendMessage);
return;
}
final bool isClosedStatus = _isClosedStatusFromResponse(response.body);
final String successMessage = isClosedStatus
? 'Data accuracy confirmed. Status changed to Closed.'
: 'Saved Successfully';
ToastHelper.showSuccessToast(context, successMessage);
context.pop(true);
widget.onSubmit("success");
} else if (response.statusCode == 403) {
await apiService.clearLocalStorageAndRedirect();
@ -779,6 +798,41 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
}
}
bool _isClosedStatusFromResponse(String responseBody) {
try {
final dynamic decoded = jsonDecode(responseBody);
final String statusText =
(decoded?['data']?['status']?.toString().toLowerCase() ?? '');
final String messageText =
(decoded?['message']?.toString().toLowerCase() ?? '');
return statusText == 'closed' || messageText.contains('closed');
} catch (_) {
return responseBody.toLowerCase().contains('closed');
}
}
Future<bool> _confirmDataAccuracyChange(BuildContext context) async {
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('Confirm Data Accuracy'),
content: const Text('The status will be automatically updated to "Closed."'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Confirm'),
),
],
),
) ??
false;
}
String get pdfDocumentTitle {
if (existingEndorsementFileName != null && existingEndorsementFileName!.isNotEmpty) {
return 'Endorsement Document';
@ -1580,7 +1634,7 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
child: ElevatedButton(
onPressed: (!isVerifiedAllowed || isSubmitLoading)
? null
: () {
: () async {
if (!_formKeyEndrosment.currentState!.validate()) return;
if (showFinancialFields && !hasCommission) {
@ -1591,8 +1645,18 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
return;
}
final bool isAccountsRole = roleId == 'Accounts';
if (isAccountsRole) {
final bool confirmed =
await _confirmDataAccuracyChange(context);
if (!confirmed) return;
}
final dataSet = endrosmentDetails();
createUserData(dataSet);
if (isAccountsRole) {
dataSet['status'] = 'Closed';
}
await createUserData(dataSet);
},
style: ElevatedButton.styleFrom(
@ -2296,16 +2360,28 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
policyStartController,
isStartDateFocused,
(val) => setState(() => isStartDateFocused = val),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
}
Widget buildPolicyEndDate(context) {
// Parse start date as minDate, fallback to DateTime(2000)
DateTime minDate = DateTime(2000);
if (policyStartController.text.isNotEmpty) {
try {
minDate = DateFormat('dd-MM-yyyy').parse(policyStartController.text);
} catch (_) {}
}
return buildDateField(
context,
"Policy End Date",
policyEndController,
isEndDateFocused,
(val) => setState(() => isEndDateFocused = val),
firstDate: minDate, // 👈 min = policy start date
lastDate: DateTime(2100),
);
}
@ -2315,6 +2391,10 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
TextEditingController controller,
bool isFocused,
Function(bool) onFocusChanged,
{
DateTime? firstDate, // 👈 ADD
DateTime? lastDate, // 👈 ADD
}
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -2346,12 +2426,33 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
child: GestureDetector(
onTap: () async {
onFocusChanged(true);
DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2000),
lastDate: DateTime.now(),
initialDate: DateTime.now(),
firstDate: firstDate ?? DateTime(2000),
lastDate: lastDate ?? DateTime(2100),
// Clamp initialDate so it's always inside firstDate..lastDate
initialDate: () {
final fd = firstDate ?? DateTime(2000);
final ld = lastDate ?? DateTime(2100);
DateTime initial = DateTime.now();
// Try to parse existing controller value
if (controller.text.isNotEmpty) {
try {
initial = DateFormat('dd-MM-yyyy').parse(controller.text);
} catch (_) {}
}
// Clamp: if initial is before firstDate, use firstDate
if (initial.isBefore(fd)) return fd;
// Clamp: if initial is after lastDate, use lastDate
if (initial.isAfter(ld)) return ld;
return initial;
}(),
);
onFocusChanged(false);
@ -2498,8 +2599,12 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
label: "Endorsement Premium",
controller: premiumController,
isRequired: showFinancialFields,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
keyboardType: const TextInputType.numberWithOptions(decimal: true),
/*
* Keep decimal values (e.g. 6000.00).
* digitsOnly strips "." and turns 6000.00 into 600000.
*/
inputFormatters: decimalFormatter,
isFocused: isPremiumFocused,
onFocusChanged: (val) {
setState(() => isPremiumFocused = val);
@ -2512,8 +2617,12 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
label: "Commission Amount",
controller: commissionController,
isRequired: showFinancialFields,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
keyboardType: const TextInputType.numberWithOptions(decimal: true),
/*
* Keep decimal values (e.g. 2900.50).
* digitsOnly strips "." and corrupts monetary amounts.
*/
inputFormatters: decimalFormatter,
isFocused: isCommissionFocused,
onFocusChanged: (val) {
setState(() => isCommissionFocused = val);
@ -2716,7 +2825,7 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
// 🔥 Clear old values
premiumController.clear();
commissionController.clear();
selectedPaymentModeId = null;
// selectedPaymentModeId = null; // why this command
});
},
child: Container(
@ -2768,7 +2877,7 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
// 🔥 Clear old values
premiumController.clear();
commissionController.clear();
selectedPaymentModeId = null;
// selectedPaymentModeId = null;
});
},
child: Container(
@ -3136,11 +3245,10 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
isPolicyNumberReadOnly = true;
}
// 🔥 CLEAR FIELDS WHEN SWITCHING
// Clear only fields that are not relevant to Internal.
if (isInternal) {
selectedInsurer = null;
selectedBroker = null;
selectedPaymentModeId = null;
insuredNameController.clear();
regNoController.clear();
@ -3228,6 +3336,7 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
}
Widget buildPaymentModeDropdown() {
print("selectedPaymentModeId======>$selectedPaymentModeId");
Map<String, dynamic>? selectedItem =
getPaymentModeData
.where(
@ -3241,6 +3350,21 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
)
: null;
// Fallback: if id does not match, resolve by saved text value.
if (selectedItem == null &&
selectedPaymentModeValue != null &&
selectedPaymentModeValue!.trim().isNotEmpty) {
final normalizedSelected = selectedPaymentModeValue!.trim().toLowerCase();
final matches = getPaymentModeData.where((item) {
final value = item['value']?.toString().trim().toLowerCase() ?? '';
return value == normalizedSelected;
});
if (matches.isNotEmpty) {
selectedItem = matches.first;
selectedPaymentModeId = selectedItem['id']?.toString();
}
}
return buildCommonDropdown(
label: "Payment Mode",
hintText: "Select Payment Mode",
@ -3249,7 +3373,12 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
selectedItem: selectedItem,
/// 🔥 IMPORTANT FIX HERE
itemAsString: (val) => val['value'] ?? "",
itemAsString: (val) =>
val['value']?.toString() ??
val['payment_mode_value']?.toString() ??
val['payment_mode']?.toString() ??
val['name']?.toString() ??
"",
isFocused: isPaymentModeFocused,
onFocusChanged: (val) {
@ -3259,6 +3388,11 @@ class _endorsomentValidationState extends ConsumerState<endorsomentValidation> {
if (val != null) {
setState(() {
selectedPaymentModeId = val['id'].toString();
selectedPaymentModeValue = (val['value'] ??
val['payment_mode_value'] ??
val['payment_mode'] ??
val['name'])
?.toString();
});
}
},

View File

@ -968,8 +968,17 @@ class endosementState extends ConsumerState<Endorsement> {
Expanded(
flex: 3,
child:
Text(item['policy_number'] ?? '-', style: _dataBold)),
child: Tooltip(
message: _policyNumberHoverText(
item['policy_number'],
item['policy_from'],
),
child: Text(
item['policy_number'] ?? '-',
style: _dataBold,
overflow: TextOverflow.ellipsis,
),
)),
Expanded(
flex: 2,
@ -1107,19 +1116,30 @@ class endosementState extends ConsumerState<Endorsement> {
),
// --- DOWNLOAD ---
_actionIconButton(
context: context,
icon: Icons.download_rounded,
tooltip: 'Download',
iconColor: Colors.blue.shade600,
hoverColor: Colors.blue.shade50, // red tint on hover for delete
onTap: () => apiService.downloadFile(
apiUrl:
'endorsement/downloadEndorsementCompletionFile?id=$id',
apiId: id,
localFile: null,
fileName: fileName,
),
// --- DOWNLOAD BUTTON ---
Builder(
builder: (context) {
final String? uploadedFile = item['endorsement_completion_file'];
final String? uploadedFileName = (uploadedFile != null && uploadedFile.isNotEmpty)
? uploadedFile.split('/').last
: null;
if (uploadedFileName == null) return const SizedBox.shrink();
return _actionIconButton(
context: context,
icon: Icons.download_rounded,
tooltip: 'Download Endorsement Document',
iconColor: Colors.blue.shade600,
hoverColor: Colors.blue.shade50,
onTap: () => apiService.downloadFile(
apiUrl: 'endorsement/downloadEndorsementCompletionFile?id=${item['id']}&type=completion',
apiId: item['id'].toString(),
localFile: null,
fileName: uploadedFileName,
),
);
},
),
// --- DELETE (Accounts only) ---
@ -2200,6 +2220,22 @@ class endosementState extends ConsumerState<Endorsement> {
);
}
/*
* Hover text format:
* "INHY/00009887 (internal)" or "0123489945666 (external)".
*/
String _policyNumberHoverText(dynamic policyNumber, dynamic policyFrom) {
final String number = (policyNumber?.toString().trim().isNotEmpty ?? false)
? policyNumber.toString().trim()
: '-';
final String source = policyFrom?.toString().trim().toLowerCase() ?? '';
if (source == 'internal' || source == 'external') {
return '$number ($source)';
}
return number;
}
int _calculatePendingDays(String? createdAt) {
if (createdAt == null || createdAt.isEmpty) return 0;
try {
@ -2212,13 +2248,41 @@ class endosementState extends ConsumerState<Endorsement> {
}
String formatDateForList(String? dateStr) {
if (dateStr == null || dateStr.isEmpty || dateStr == '-') return '-';
/*
* Normalize all non-usable date values for UI.
* Required output: "-" for Internal/Invalid/null/empty/zero-range dates.
*/
if (dateStr == null) return '-';
final String normalized = dateStr.trim();
if (normalized.isEmpty || normalized == '-') return '-';
if (normalized.toLowerCase() == 'internal') return '-';
if (normalized.toLowerCase() == 'invalid date') return '-';
try {
// Catch invalid dates like "0000-00-00" or "30-11--0001"
if (dateStr.contains('0000') || dateStr.startsWith('30-11--')) return '-';
/*
* Catch zero/invalid ranges:
* - 0000-00-00
* - dd-00-yyyy / dd-mm-0000
* - malformed historical invalid strings.
*/
if (normalized.contains('0000') ||
normalized.contains('-00-') ||
normalized.startsWith('00-') ||
normalized.endsWith('-00') ||
normalized.startsWith('30-11--')) {
return '-';
}
DateTime parsed = DateTime.parse(dateStr);
DateTime parsed;
/*
* API commonly sends date as dd-MM-yyyy (e.g. 05-12-2025),
* while some flows may send ISO (yyyy-MM-dd). Support both.
*/
if (RegExp(r'^\d{2}-\d{2}-\d{4}$').hasMatch(normalized)) {
parsed = DateFormat('dd-MM-yyyy').parseStrict(normalized);
} else {
parsed = DateTime.parse(normalized);
}
// Validate year is reasonable
if (parsed.year < 2000 || parsed.year > 2100) return '-';
@ -2275,12 +2339,12 @@ class endosementState extends ConsumerState<Endorsement> {
mainAxisSize: MainAxisSize.min,
children: [
// UPLOAD LABEL
Text('Upload Document', style: GoogleFonts.poppins(fontSize: 11, fontWeight: FontWeight.w500)),
// Text('Upload Endorsement Document', style: GoogleFonts.poppins(fontSize: 11, fontWeight: FontWeight.w500)),
const SizedBox(height: 6),
// UPLOAD FIELD
ThemedUploadField(
hintText: selectedFileNames ?? "Upload Document",
hintText: selectedFileNames ?? "Upload Endorsement Document",
padHorizontal: 4,
padVertical: 5,
fontSZ: 11,
@ -2360,7 +2424,7 @@ class endosementState extends ConsumerState<Endorsement> {
Icon(Icons.info_outline, color: Colors.orange.shade400, size: 14),
const SizedBox(width: 6),
Text(
'No file uploaded yet',
'No Endorsement file uploaded yet',
style: GoogleFonts.poppins(fontSize: 11, color: Colors.orange.shade700),
),
],

View File

@ -0,0 +1,267 @@
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/core/routing/routes.dart';
import 'package:nhance_partner/core/services/api_service.dart';
import 'package:nhance_partner/data/utils/Pagination.dart';
import 'package:nhance_partner/presentation/layouts/main_layout.dart';
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
import 'gridUpload.dart';
import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart';
class GridListScreen extends ConsumerStatefulWidget {
const GridListScreen({super.key});
@override
ConsumerState<GridListScreen> createState() => _GridListScreenState();
}
class _GridListScreenState extends ConsumerState<GridListScreen> {
final ApiService _apiService = ApiService();
final TextEditingController _searchController = TextEditingController();
int _currentPage = 1;
int _itemsPerPage = 10;
bool _isLoading = false;
List<Map<String, dynamic>> _allRows = [];
List<Map<String, dynamic>> _filteredRows = [];
bool get _showUploadButton {
final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase();
return role == 'manager' || role == 'accounts';
}
@override
void initState() {
super.initState();
Future.microtask(_loadGridList);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadGridList() async {
setState(() => _isLoading = true);
try {
final response = await _apiService.fetchGridFileList();
if ((response['status'] ?? '').toString().toLowerCase() == 'success') {
final rows = List<Map<String, dynamic>>.from(response['data'] ?? const []);
setState(() {
_allRows = rows;
_filteredRows = List<Map<String, dynamic>>.from(rows);
_currentPage = 1;
});
} else {
setState(() {
_allRows = [];
_filteredRows = [];
});
}
} catch (_) {
setState(() {
_allRows = [];
_filteredRows = [];
});
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
void _filterRows(String query) {
final q = query.trim().toLowerCase();
setState(() {
_currentPage = 1;
if (q.isEmpty) {
_filteredRows = List<Map<String, dynamic>>.from(_allRows);
return;
}
_filteredRows = _allRows.where((item) {
return (item['id'] ?? '').toString().toLowerCase().contains(q) ||
(item['incentive_file_name'] ?? '').toString().toLowerCase().contains(q) ||
(item['vaild_from'] ?? item['valid_from'] ?? '')
.toString()
.toLowerCase()
.contains(q) ||
(item['created_by_name'] ?? '').toString().toLowerCase().contains(q) ||
(item['created_date'] ?? '').toString().toLowerCase().contains(q) ||
(item['created_at'] ?? '').toString().toLowerCase().contains(q);
}).toList();
});
}
List<Map<String, dynamic>> get _pageRows {
final list = List<Map<String, dynamic>>.from(_filteredRows);
list.sort((a, b) {
final left = int.tryParse(a['id']?.toString() ?? '') ?? 0;
final right = int.tryParse(b['id']?.toString() ?? '') ?? 0;
return right.compareTo(left);
});
if (list.isEmpty) return [];
final maxPage = (list.length / _itemsPerPage).ceil();
final safePage = _currentPage.clamp(1, maxPage);
final start = (safePage - 1) * _itemsPerPage;
final end = (start + _itemsPerPage).clamp(0, list.length);
return list.sublist(start, end);
}
@override
Widget build(BuildContext context) {
return MainLayout(
title: 'Grid List',
body: SelectionArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: [
Row(
children: [
Text('Grid List', style: _titleStyle),
const Spacer(),
ThemedSearchField(
hintText: 'Search',
backgroundColor: Colors.white,
txtHeight: 34,
txtwidth: MediaQuery.of(context).size.width * 0.18,
controller: _searchController,
onChanged: _filterRows,
),
const SizedBox(width: 10),
if (_showUploadButton)
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2E7D6E),
foregroundColor: Colors.white,
),
onPressed: () async {
final shouldRefresh = await showDialog<bool>(
context: context,
builder: (_) => const UploadGridModal(),
);
if (shouldRefresh == true && mounted) {
await _loadGridList();
}
},
icon: const Icon(Icons.add, size: 18),
label: const Text('Upload Grid'),
),
],
),
const SizedBox(height: 10),
_buildHeader(),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _pageRows.isEmpty
? const Center(child: Text('No available data'))
: ListView.builder(
itemCount: _pageRows.length,
itemBuilder: (context, index) {
final row = _pageRows[index];
final sno = ((_currentPage - 1) * _itemsPerPage) + index + 1;
return _buildRow(context, row, sno);
},
),
),
PaginationControls(
currentPage: _currentPage,
itemsPerPage: _itemsPerPage,
totalItems: _filteredRows.length,
onPageChanged: (page) => setState(() => _currentPage = page),
onItemsPerPageChanged: (items) => setState(() {
_itemsPerPage = items;
_currentPage = 1;
}),
),
],
),
),
),
);
}
Widget _buildHeader() {
return Container(
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
child: Row(
children: [
Expanded(flex: 2, child: Text('Valid From', style: _headerStyle)),
Expanded(flex: 4, child: Text('File Name', style: _headerStyle)),
Expanded(flex: 2, child: Text('Created By', style: _headerStyle)),
Expanded(flex: 2, child: Text('Created Date', style: _headerStyle)),
Expanded(flex: 1, child: Text('Action', style: _headerStyle)),
],
),
);
}
Widget _buildRow(BuildContext context, Map<String, dynamic> row, int sno) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(bottom: BorderSide(color: Colors.blueGrey, width: 0.15)),
),
child: Row(
children: [
Expanded(
flex: 2,
child: Text(
(row['vaild_from'] ?? row['valid_from'] ?? '-').toString(),
style: _dataStyle,
),
),
Expanded(
flex: 4,
child: Text(
(row['incentive_file_name'] ?? '-').toString(),
style: _dataStyle,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
Expanded(
flex: 2,
child: Text((row['created_by_name'] ?? '-').toString(), style: _dataStyle),
),
Expanded(
flex: 2,
child: Text((row['created_date'] ?? '-').toString(), style: _dataStyle),
),
Expanded(
flex: 1,
child: IconButton(
tooltip: 'View',
onPressed: () =>
context.go('${AppRoutes.gridView}?file_id=${row['id']}'),
icon: const Icon(Icons.visibility_outlined, size: 18),
),
),
],
),
);
}
}
class GridList extends GridListScreen {
const GridList({super.key});
}
final _titleStyle = GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w600);
final _dataStyle = GoogleFonts.inter(fontSize: 12, fontWeight: FontWeight.w400);
final _headerStyle = GoogleFonts.poppins(
fontSize: 11.2,
fontWeight: FontWeight.w500,
color: const Color(0xFF1E293B),
);

View File

@ -0,0 +1,884 @@
// import 'dart:io' as html;
// import 'dart:nativewrappers/_internal/vm/lib/typed_data_patch.dart';
import 'dart:convert';
import 'dart:typed_data'; // Import for Uint8List
import 'package:dropdown_search/dropdown_search.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.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.dart';
import 'package:toastification/toastification.dart';
import 'package:universal_html/html.dart' as html;
import 'package:nhance_partner/core/config/env.dart';
import 'package:nhance_partner/core/services/api_service.dart';
import 'package:nhance_partner/data/services/auth_service.dart';
import 'package:nhance_partner/data/utils/validators.dart';
import 'package:nhance_partner/presentation/layouts/main_layout.dart';
import 'package:nhance_partner/presentation/providers/manager_provider.dart';
import 'package:nhance_partner/presentation/themes/indicators/customizd_file_upload.dart';
import 'package:nhance_partner/presentation/themes/indicators/input_field_decoration.dart';
import 'package:nhance_partner/presentation/themes/indicators/month_field_theme.dart';
import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart';
// import '../../../themes/indicators/upload_doc_theme.dart';
/// function to open the modal
void showUploadGridModal(BuildContext context) {
showDialog(
context: context,
builder: (context) => const UploadGridModal(),
);
}
class UploadGridModal extends ConsumerStatefulWidget {
const UploadGridModal({super.key});
@override
ConsumerState<UploadGridModal> createState() =>
UploadGridModalState();
}
class UploadGridModalState extends ConsumerState<UploadGridModal> {
late ApiService apiService;
bool isLoading = false;
final _formKey = GlobalKey<FormState>();
final GlobalKey<DropdownSearchState<Map<String, dynamic>>> dropDownKey =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
List<Map<String, dynamic>> getAgentData = [];
List<Map<String, dynamic>> filteredData = [];
List<Map<String, dynamic>> getAgentGridFileData = [];
List<Map<String, dynamic>> filteredGridData = [];
Map<String, TextEditingController> controllers = {};
// html.File? passportFile;
List<PlatformFile> selectedFiles = [];
String? selectedFileNames;
List<String> tabHeader = ['name', 'agentId', 'date', 'search'];
String? _token;
dynamic userId;
dynamic managerId;
dynamic agentId;
DateTime? _validFrom;
String _formatDateForApi(DateTime date) =>
date.toIso8601String().split('T').first;
String _formatDateForUi(DateTime date) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
Future<void> _pickValidFrom() async {
final today = DateTime.now();
final minDate = DateTime(today.year, today.month, today.day);
final safeInitial = (_validFrom != null && _validFrom!.isAfter(minDate))
? _validFrom!
: minDate;
final picked = await showDatePicker(
context: context,
firstDate: minDate,
lastDate: DateTime(2100),
initialDate: safeInitial,
);
if (picked == null) return;
setState(() => _validFrom = picked);
// Backend expects `incentive_month` but payout-grid upload uses a full date.
controllers['date']?.text = _formatDateForApi(picked);
}
Map<String, dynamic> dataDetails() {
final selectedDate = controllers["date"]?.text;
final data = {
"agent_id": agentId,
"incentive_month": selectedDate,
"created_by": userId,
"file_type": "grid",
};
return data;
}
@override
void initState() {
super.initState();
apiService = ApiService();
for (String field in tabHeader) {
controllers[field] = TextEditingController();
}
Future.microtask(_loadInitialModalData);
_initializeToken();
// getAgentList();
}
Future<void> _loadInitialModalData() async {
managerId = ref.read(managerIdProvider);
userId = ref.read(userIdProvider);
agentId = null;
if (managerId != null) {
await getAgentList(managerId);
}
// Always fetch history list when modal opens.
// if (agentId != null) {
// await getAgentIncenctiveFileList(agentId);
// }
}
Future<void> _initializeToken() async {
_token = await AuthService.getToken();
print("APISERTOKEN - $_token");
}
@override
void dispose() {
for (var controller in controllers.values) {
controller.dispose();
}
controllers['date']?.dispose();
controllers['code']?.dispose();
super.dispose();
}
Future<void> refresh() async {
setState(() {
// reset file fields
selectedFiles = [];
// selectedFileNames = "";
selectedFileNames = null;
// getAgentData = [];
// filteredData = [];
getAgentGridFileData = [];
filteredGridData = [];
// clear all text controllers
for (var controller in controllers.values) {
controller.clear();
}
_validFrom = null;
dropDownKey.currentState?.clear(); // 👈 clear selected agent
controllers['agentId']?.clear(); // clear text field value too
controllers['uploadFile']?.clear();
});
// Re-fetch history after clearing UI fields.
// if (agentId != null) {
// await getAgentIncenctiveFileList(agentId);
// }
}
void filterData(String query) {
print("FilterDAta - $query");
setState(() {
filteredGridData = getAgentGridFileData.where((item) {
return (item['incentive_month'] ?? '-').toLowerCase().contains(
query.toLowerCase(),
);
}).toList();
});
}
Future<void> getAgentList(int id) async {
print('getClaimList called');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchAgentUserList(managerId);
if (response['status'] == 'success') {
print('getAgentListData - ${response['data']}');
setState(() {
getAgentData = List<Map<String, dynamic>>.from(response['data']);
print('API Data - $getAgentData');
filteredData = List.from(getAgentData);
// print('originalData - $filteredData');
});
} else {
getAgentData = [];
filteredData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
Future<void> getAgentIncenctiveFileList(agentId) async {
print('getClaimList agentId - $agentId');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchAgentIncentiveList(agentId, type: 'grid');
if (response['status'] == 'success') {
print('AgentIncentive - ${response['data']}');
setState(() {
getAgentGridFileData = List<Map<String, dynamic>>.from(
response['data'],
);
print('API AgentIncentive - $getAgentGridFileData');
filteredGridData = List.from(getAgentGridFileData);
// print('originalData - $filteredGridData');
});
} else {
getAgentGridFileData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
Future<void> handleSave() async {
final dateText = controllers['date']?.text.trim() ?? '';
if (dateText.isEmpty) {
ToastHelper.showErrorToast(context, 'Select valid from date');
return;
}
if (!_formKey.currentState!.validate()) return;
print("Handl1");
final dataSet = dataDetails();
print("dataSetAgent - $dataSet");
print("Handl13f");
if (selectedFiles.isEmpty) {
print("❌ No file selected!");
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a file before submitting"),
),
);
return;
}
final invalidFiles = selectedFiles.where((file) {
final name = file.name.toLowerCase();
return !(name.endsWith('.xls') || name.endsWith('.xlsx'));
}).toList();
if (invalidFiles.isNotEmpty) {
ToastHelper.showErrorToast(
context,
'Upload only Excel files (.xls, .xlsx)',
);
return;
}
setState(() {
uploadIncentiveData(dataSet);
});
print("Handl2");
}
Future<void> uploadIncentiveData(Map<String, dynamic> dataSet) async {
final uri = Uri.parse('${Env.apiUrl}grid/upload');
if (_token == null) {
throw Exception('Token not found. Please log in.');
}
// Use MultipartRequest (POST only)
final request = http.MultipartRequest('POST', uri);
request.headers['Authorization'] = 'Bearer $_token';
request.headers['app-signature'] = Env.App_Signature;
print("USerDAta - $dataSet");
// dataSet.forEach((key, value) {
// request.fields[key] = value.toString();
// print("✅ Encoded travel_details2: ${request.fields[key]}");
// });
dataSet.forEach((key, value) {
if (key != 'incentive_file_name') {
request.fields[key] = value.toString();
print("✅ Encoded $key: ${request.fields[key]}");
}
});
if (selectedFiles.isNotEmpty) {
for (final file in selectedFiles) {
try {
if (file.bytes != null) {
final multipartFile = http.MultipartFile.fromBytes(
'incentive_file_name',
file.bytes!,
filename: file.name,
);
request.files.add(multipartFile);
} else if (file.path != null) {
final multipartFile = await http.MultipartFile.fromPath(
'incentive_file_name',
file.path!,
filename: file.name,
);
request.files.add(multipartFile);
}
print("📎 File attached: ${file.name}");
} catch (e) {
print("❌ Failed to attach file ${file.name}: $e");
}
}
}
// Attach file if selected
// if (passportFile != null) {
// try {
// final reader = html.FileReader();
// reader.readAsArrayBuffer(passportFile! as html.Blob);
// await reader.onLoad.first;
//
// final data = reader.result as Uint8List;
//
// final multipartFile = http.MultipartFile.fromBytes(
// 'incentive_file_name',
// data,
// filename: passportFile!.name,
// );
//
// request.files.add(multipartFile);
// print("📎 File attached: ${passportFile!.name}");
// } catch (e) {
// print("❌ Failed to read file: $e");
// }
// } else {
// print("⚠️ No passport file selected.");
// }
print("🚀 Sending request with fields: ${request.fields}");
try {
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
print("Response status: ${response.statusCode}");
print("Response body: ${response.body}");
final responseBody = json.decode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
// dispose();
print("✅ Partner submitted successfully!");
print("📨 Response: ${response.body}");
final data = dataDetails();
final agentId = null;
final status = responseBody['status'];
final message = responseBody['data'] ?? '';
if (status == 'success') {
ToastHelper.showSuccessToast(context, 'File uploaded successfully!');
// await getAgentIncenctiveFileList(agentId);
// Close the modal and signal the parent to refresh.
Navigator.pop(context, true);
} else {
ToastHelper.showErrorToast(
context,
message.isNotEmpty ? message : 'Something went wrong!',
);
}
// context.go(AppRoutes.agentLst);
} else if (response.statusCode == 403) {
await apiService.clearLocalStorageAndRedirect();
} else {
print("❌ Submission failed. Status: ${response.statusCode}");
print("📨 Body: ${response.body}");
String errorMessage = 'There was a problem in creating user. Please try again.';
try {
if (responseBody['data'] is String &&
(responseBody['data'] as String).isNotEmpty) {
final parsedData = json.decode(responseBody['data']);
if (parsedData is Map<String, dynamic> &&
parsedData['message'] != null) {
errorMessage = parsedData['message'].toString();
}
} else if (responseBody['message'] != null) {
errorMessage = responseBody['message'].toString();
}
} catch (_) {
// keep fallback message
}
ToastHelper.showErrorToast(context, errorMessage);
}
} catch (e) {
print("🔥 Error submitting user: $e");
}
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final isCompact = screenWidth < 1000;
final dialogWidth = isCompact ? screenWidth * 0.95 : screenWidth * 0.55;
final fieldWidth = isCompact ? dialogWidth * 0.9 : screenWidth * 0.27;
return SelectionArea(
child:AlertDialog(
backgroundColor: Colors.white,
content: Container(
width: dialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
"Grid Files",
style: GoogleFonts.poppins(
fontSize: 14,
color: Color(0xFF50A398),
fontWeight: FontWeight.w500,
),
),
Spacer(),
Tooltip(
message: 'Refresh',
child: IconButton(
icon: const Icon(
Icons.refresh,
size: 18,
color: Color(0xFF2E7D6E),
// color: Color(0xFF425B5B),
),
onPressed: () {
refresh();
},
splashRadius: 28,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
),
),
Tooltip(
message: 'Close',
child: IconButton(
icon: const Icon(
Icons.close,
size: 18,
// color: Color(0xFF425B5B),
color: Color(0xFF2E7D6E),
),
onPressed: () {
Navigator.pop(context);
},
splashRadius: 28,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Form(
key: _formKey,
child: Row(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Valid From',
style: _labelStyle,
),
SizedBox(height: 5),
SizedBox(
width: MediaQuery.of(context).size.width * 0.27,
height: 40,
child: InkWell(
onTap: _pickValidFrom,
borderRadius: BorderRadius.circular(6),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFE2E8F0)),
color: Colors.white,
),
child: Row(
children: [
Expanded(
child: Text(
_validFrom == null
? 'Select Valid From'
: _formatDateForUi(_validFrom!),
style: GoogleFonts.inter(
fontSize: 12,
color: _validFrom == null
? const Color(0xFFCBD5E1)
: Colors.black,
),
),
),
const Icon(
Icons.calendar_today_outlined,
size: 16,
color: Color(0xFF94A3B8),
),
],
),
),
),
),
],
),
SizedBox(width: 15),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('Upload Files', style: _labelStyle),
],
),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ThemedUploadField(
key: ValueKey(selectedFileNames),
allowMultiple: true,
// hintText: "Upload Document",
txtheight: 40,
// validator: (value) => Validators.requiredField(
// value,
// "Upload Document",
// ),
// validator: (value) {
// if (passportFile == null) {
// return "Please upload a file";
// }
// return null;
// },
// backgroundColor: Color(0xFFECECEC),
backgroundColor: Colors.white,
borderColor: Color(0xFFE2E8F0),
allowedExtensions: const ['xls', 'xlsx'],
hintText:
(selectedFileNames == null ||
selectedFileNames!.isEmpty)
? "Upload Document"
: selectedFileNames,
txtwidth: isCompact
? fieldWidth * 0.71
: MediaQuery.of(context).size.width * 0.19,
onFilesSelected: (fileNames, files) {
print("Files picked: ${fileNames.join(', ')}");
// print("Size: ${file.size}");
// print(
// "Path: ${file.path}",
// ); // works on mobile/desktop
// print("Bytes: ${file.bytes}");
setState(() {
selectedFiles = files;
selectedFileNames = fileNames.join(', ');
});
},
),
SizedBox(width: 15),
Container(
width: isCompact
? fieldWidth * 0.25
: MediaQuery.of(context).size.width * 0.068,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFF425B5B),
backgroundColor: Color(0xFF2E7D6E),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
6,
), // 👈 reduce radius (default ~20)
),
),
onPressed: () {
handleSave();
// Navigator.pop(context);
},
child: Text(
"Submit",
style: GoogleFonts.poppins(fontSize: 12),
),
),
),
],
),
],
),
],
),
),
],
),
const SizedBox(height: 15),
// fILE LIST section
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text('Files', style: _labelHeaderStyle),
// ThemedSearchField(
// hintText: 'Search',
// backgroundColor: Colors.white,
// // backgroundColor: Color(0xFFECECEC),
// onChanged: filterData,
// // onChanged: null,
// controller: controllers['search']!,
// txtwidth: MediaQuery.of(context).size.width * 0.15,
// txtHeight: 30,
// ),
// ],
// ),
// const SizedBox(height: 10),
//
// Expanded(
// child: isLoading
// ? const Center(child: CircularProgressIndicator())
// : filteredGridData.isEmpty
// ? const Center(child: Text("No files found"))
// : LayoutBuilder(
// builder: (context, constraints) {
// final fileAreaWidth = constraints.maxWidth;
// final crossAxisCount =
// fileAreaWidth < 520 ? 1 : 2;
// const spacing = 10.0;
// final tileWidth = (fileAreaWidth -
// spacing * (crossAxisCount - 1) -
// 16) /
// crossAxisCount;
// const targetTileHeight = 70.0;
// final aspectRatio = (tileWidth / targetTileHeight)
// .clamp(3.2, 12.0);
//
// return GridView.builder(
// padding: const EdgeInsets.all(8),
// gridDelegate:
// SliverGridDelegateWithFixedCrossAxisCount(
// crossAxisCount: crossAxisCount,
// crossAxisSpacing: spacing,
// mainAxisSpacing: spacing,
// childAspectRatio: aspectRatio,
// ),
// itemCount: filteredGridData.length,
// itemBuilder: (context, index) {
// final file = filteredGridData[index];
// return IncentiveFileRow(
// fileName:
// file['incentive_file_name'] ?? "Unknown",
// date: file['incentive_month'] ?? "-",
// onUpload: () {
// print("Upload ${file['id']}");
// final selectedId = file['id'];
// final path =
// 'agent/downloadAgentIncentiveFile?type=grid&id=$selectedId';
// apiService.getPdfDownload(path, selectedId);
// },
// onDelete: () async {
// print("Delete ${file['id']}");
// final selectedId = file['id'];
// final response = await apiService
// .deleteAgentIncentiveFile(selectedId);
// print("res - $response");
//
// if (response['status'] == 'success') {
// print("DELTED");
//
// final data =
// dataDetails(); // Map<String, dynamic>
// final agentId = null;
//
// // getAgentIncenctiveFileList(agentId);
// } else {
// // Fluttertoast.showToast(
// // msg: "Something went wrong",
// // toastLength: Toast.LENGTH_SHORT,
// // gravity: ToastGravity.BOTTOM,
// // );
// }
// },
// );
// },
// );
// },
// ),
// ),
],
),
),
),);
}
}
final _labelStyle = GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 12,
);
final _labelHeaderStyle = GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
);
class IncentiveFileRow extends StatelessWidget {
final String fileName;
final String date;
final VoidCallback? onUpload;
final VoidCallback? onDelete;
const IncentiveFileRow({
super.key,
required this.fileName,
required this.date,
this.onUpload,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE3E3E3)),
borderRadius: BorderRadius.circular(6),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(6.0),
decoration: BoxDecoration(
color: const Color(0xFFF4F6F8),
borderRadius: BorderRadius.circular(6.0),
border: Border.all(color: const Color(0xFFE3E3E3)),
),
child: const Icon(
Icons.file_present,
color: Color(0xFF838587),
size: 18,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
fileName,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w400,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 2),
Text(
date,
style: GoogleFonts.poppins(
color: const Color(0xFF6E6E6E),
fontSize: 10,
),
),
],
),
),
const SizedBox(width: 8),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onUpload,
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(
Icons.file_download_outlined,
color: Color(0xFF6E6E6E),
size: 20,
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onDelete,
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(
Icons.delete_outlined,
color: Color(0xFF6E6E6E),
size: 20,
),
),
),
),
],
),
);
}
}
class GridUploadScreen extends StatelessWidget {
const GridUploadScreen({super.key});
@override
Widget build(BuildContext context) {
return MainLayout(
title: 'Grid Upload',
body: Center(
child: ElevatedButton.icon(
onPressed: () => showUploadGridModal(context),
icon: const Icon(Icons.upload_file_outlined),
label: const Text('Open Grid Upload'),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:nhance_partner/data/utils/toastNotification.dart';
import 'package:nhance_partner/presentation/themes/indicators/text_field_theme.dart';
import 'package:toastification/toastification.dart';
@ -20,7 +19,6 @@ import '../../../../data/services/auth_service.dart';
import '../../../../data/utils/validators.dart';
import '../../../providers/manager_provider.dart';
import '../../../themes/indicators/customizd_file_upload.dart';
import '../../../themes/indicators/date_field_theme.dart';
import '../../../themes/indicators/input_field_decoration.dart';
import '../../../themes/indicators/month_field_theme.dart';
import '../../../themes/indicators/search_field_theme.dart';
@ -56,7 +54,7 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
Map<String, TextEditingController> controllers = {};
// html.File? passportFile;
PlatformFile? passportFile;
List<PlatformFile> selectedFiles = [];
String? selectedFileNames;
List<String> tabHeader = ['name', 'agentId', 'date', 'search'];
@ -65,10 +63,12 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
dynamic managerId;
dynamic agentId;
Map<String, dynamic> dataDetails() {
final selectedDate = controllers["date"]?.text;
final data = {
"agent_id": agentId,
"incentive_month": controllers["date"]?.text,
"incentive_month": selectedDate,
"created_by": userId,
"file_type": "incentive",
};
return data;
}
@ -110,7 +110,7 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
Future<void> refresh() async {
setState(() {
// reset file fields
passportFile = null;
selectedFiles = [];
// selectedFileNames = "";
selectedFileNames = null;
@ -179,7 +179,7 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
});
try {
final response = await apiService.fetchAgentIncentiveList(agentId);
final response = await apiService.fetchAgentIncentiveList(agentId, type: 'incentive');
if (response['status'] == 'success') {
print('AgentIncentive - ${response['data']}');
@ -214,7 +214,7 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
print("dataSetAgent - $dataSet");
print("Handl13f");
if (passportFile == null) {
if (selectedFiles.isEmpty) {
print("❌ No file selected!");
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
@ -257,26 +257,28 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
}
});
if (passportFile != null) {
try {
if (passportFile!.bytes != null) {
final multipartFile = http.MultipartFile.fromBytes(
'incentive_file_name',
passportFile!.bytes!,
filename: passportFile!.name,
);
request.files.add(multipartFile);
} else if (passportFile!.path != null) {
final multipartFile = await http.MultipartFile.fromPath(
'certificate_file_name',
passportFile!.path!,
filename: passportFile!.name,
);
request.files.add(multipartFile);
if (selectedFiles.isNotEmpty) {
for (final file in selectedFiles) {
try {
if (file.bytes != null) {
final multipartFile = http.MultipartFile.fromBytes(
'incentive_file_name',
file.bytes!,
filename: file.name,
);
request.files.add(multipartFile);
} else if (file.path != null) {
final multipartFile = await http.MultipartFile.fromPath(
'incentive_file_name',
file.path!,
filename: file.name,
);
request.files.add(multipartFile);
}
print("📎 File attached: ${file.name}");
} catch (e) {
print("❌ Failed to attach file ${file.name}: $e");
}
print("📎 File attached: ${passportFile!.name}");
} catch (e) {
print("❌ Failed to attach file: $e");
}
}
@ -341,28 +343,23 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
} else {
print("❌ Submission failed. Status: ${response.statusCode}");
print("📨 Body: ${response.body}");
String errorMessage = 'There was a problem in creating user. Please try again.';
try {
if (responseBody['data'] is String &&
(responseBody['data'] as String).isNotEmpty) {
final parsedData = json.decode(responseBody['data']);
if (parsedData is Map<String, dynamic> &&
parsedData['message'] != null) {
errorMessage = parsedData['message'].toString();
}
} else if (responseBody['message'] != null) {
errorMessage = responseBody['message'].toString();
}
} catch (_) {
// keep fallback message
}
// Sort by created_on (latest first)
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Partner Creation Failed"),
content: Text(
"There was a problem in creating user. Please try again.",
),
actions: [
TextButton(
child: Text("OK"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
ToastHelper.showErrorToast(context, errorMessage);
}
} catch (e) {
print("🔥 Error submitting user: $e");
@ -371,12 +368,19 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final isCompact = screenWidth < 1000;
final dialogWidth = isCompact ? screenWidth * 0.95 : screenWidth * 0.55;
final dialogHeight = isCompact ? screenHeight * 0.82 : screenHeight * 0.7;
final fieldWidth = isCompact ? dialogWidth * 0.9 : screenWidth * 0.27;
return SelectionArea(
child:AlertDialog(
backgroundColor: Colors.white,
content: Container(
width: MediaQuery.of(context).size.width * 0.55,
height: MediaQuery.of(context).size.height * 0.7,
width: dialogWidth,
height: dialogHeight,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
@ -431,7 +435,141 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
],
),
const SizedBox(height: 5),
Row(
isCompact
? Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Partner Name', style: _labelStyle),
SizedBox(height: 5),
Container(
width: fieldWidth,
height: 40,
child: DropdownSearch<Map<String, dynamic>>(
key: dropDownKey,
selectedItem: null,
items: (filter, infiniteScrollProps) {
return filteredData; // pass the whole object
},
itemAsString: (agent) =>
agent['name'].toString(), // what to show
compareFn: (item, selectedItem) =>
item['id'] ==
selectedItem['id'], // compare by id
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: "Select Partner",
).copyWith(
hintStyle: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
filled: true,
fillColor: Colors.white,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFE2E8F0),
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFE2E8F0),
width: 1.5,
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
),
),
popupProps: PopupProps.menu(
fit: FlexFit.loose,
menuProps:
MenuProps(backgroundColor: Colors.white),
showSearchBox: true,
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
hintText: "Search partner...",
hintStyle: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFEDF6F5),
width: 1,
),
),
),
),
itemBuilder:
(
context,
item,
isDisabled,
isSelected,
) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
child: Text(
item['name'].toString(),
style: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
),
);
},
),
onChanged: (agent) {
if (agent != null) {
print("Selected Partner Name: ${agent['name']}");
print("Agent Code: ${agent['agent_code']}");
print("Agent Id: ${agent['id']}");
getAgentIncenctiveFileList(agent['id']);
controllers['agentId']?.text =
agent['agent_code'];
agentId = agent['id'];
}
},
),
),
],
),
SizedBox(height: 10),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Partner ID', style: _labelStyle),
SizedBox(height: 5),
ThemedFormField(
controller: controllers['agentId']!,
txtwidth: fieldWidth,
txtheight: 40,
borderColor: Color(0xFFE2E8F0),
highlightColor: Color(0xFF50A398),
readOnly: true,
),
],
),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
@ -442,7 +580,7 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
SizedBox(height: 5),
Container(
width: MediaQuery.of(context).size.width * 0.27,
width: fieldWidth,
height: 40,
child: DropdownSearch<Map<String, dynamic>>(
key: dropDownKey,
@ -559,7 +697,7 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
SizedBox(height: 5),
ThemedFormField(
controller: controllers['agentId']!,
txtwidth: MediaQuery.of(context).size.width * 0.27,
txtwidth: fieldWidth,
txtheight: 40,
borderColor: Color(0xFFE2E8F0),
highlightColor: Color(0xFF50A398),
@ -587,14 +725,15 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Month', style: _labelStyle),
Text(
'Month',
style: _labelStyle,
),
SizedBox(height: 5),
ThemedMonthField(
hintText: "Select Month",
txtwidth: MediaQuery.of(context).size.width * 0.27,
txtheight: 40,
// backgroundColor: const Color(0xFFECECEC),
validator: (value) =>
Validators.requiredField(value, "date"),
borderColor: Color(0xFFE2E8F0),
@ -602,7 +741,6 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
controller: controllers['date']!,
onDateSelected: (date) {
print("Picked Date: $date");
// controllers['date']?.text = date as String;
},
),
],
@ -612,7 +750,11 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Upload File', style: _labelStyle),
Row(
children: [
Text('Upload Files', style: _labelStyle),
],
),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -620,6 +762,7 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
children: [
ThemedUploadField(
key: ValueKey(selectedFileNames),
allowMultiple: true,
// hintText: "Upload Document",
txtheight: 40,
// validator: (value) => Validators.requiredField(
@ -635,30 +778,33 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
// backgroundColor: Color(0xFFECECEC),
backgroundColor: Colors.white,
borderColor: Color(0xFFE2E8F0),
allowedExtensions: null,
hintText:
(selectedFileNames == null ||
selectedFileNames!.isEmpty)
? "Upload Document"
: selectedFileNames,
txtwidth:
MediaQuery.of(context).size.width * 0.19,
onFileSelected: (fileName, file) {
print("File picked: ${fileName}");
txtwidth: isCompact
? fieldWidth * 0.71
: MediaQuery.of(context).size.width * 0.19,
onFilesSelected: (fileNames, files) {
print("Files picked: ${fileNames.join(', ')}");
// print("Size: ${file.size}");
// print(
// "Path: ${file.path}",
// ); // works on mobile/desktop
// print("Bytes: ${file.bytes}");
setState(() {
passportFile = file;
selectedFileNames = fileName;
selectedFiles = files;
selectedFileNames = fileNames.join(', ');
});
},
),
SizedBox(width: 15),
Container(
width:
MediaQuery.of(context).size.width * 0.068,
width: isCompact
? fieldWidth * 0.25
: MediaQuery.of(context).size.width * 0.068,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFF425B5B),
@ -713,68 +859,68 @@ class UploadIncentiveModalState extends ConsumerState<UploadIncentiveModal> {
child: isLoading
? const Center(child: CircularProgressIndicator())
: filteredIncentiveData.isEmpty
? const Center(child: Text("No incentive files found"))
: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // two columns
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 6.5, // adjust height of rows
? const Center(child: Text("No files found"))
: LayoutBuilder(
builder: (context, constraints) {
final fileAreaWidth = constraints.maxWidth;
final crossAxisCount =
fileAreaWidth < 520 ? 1 : 2;
const spacing = 10.0;
final tileWidth = (fileAreaWidth -
spacing * (crossAxisCount - 1) -
16) /
crossAxisCount;
const targetTileHeight = 70.0;
final aspectRatio = (tileWidth / targetTileHeight)
.clamp(3.2, 12.0);
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
childAspectRatio: aspectRatio,
),
itemCount: filteredIncentiveData.length,
itemBuilder: (context, index) {
final file = filteredIncentiveData[index];
itemCount: filteredIncentiveData.length,
itemBuilder: (context, index) {
final file = filteredIncentiveData[index];
return IncentiveFileRow(
fileName:
file['incentive_file_name'] ?? "Unknown",
date: file['incentive_month'] ?? "-",
onUpload: () {
print("Upload ${file['id']}");
final selectedId = file['id'];
final path =
'agent/downloadAgentIncentiveFile?id=$selectedId';
apiService.getPdfDownload(path, selectedId);
},
onDelete: () async {
print("Delete ${file['id']}");
final selectedId = file['id'];
final response = await apiService
.deleteAgentIncentiveFile(selectedId);
print("res - $response");
// final sortedData = [...filteredIncentiveData]
// ..sort((a, b) {
// final dateA =
// DateTime.tryParse(a['created_on'] ?? '') ??
// DateTime(1970);
// final dateB =
// DateTime.tryParse(b['created_on'] ?? '') ??
// DateTime(1970);
// return dateB.compareTo(dateA); // latest first
// });
// final file = sortedData[index];
return IncentiveFileRow(
fileName: file['incentive_file_name'] ?? "Unknown",
date: file['incentive_month'] ?? "-",
onUpload: () {
print("Upload ${file['id']}");
final selectedId = file['id'];
final path =
'agent/downloadAgentIncentiveFile?id=$selectedId';
apiService.getPdfDownload(path, selectedId);
},
onDelete: () async {
print("Delete ${file['id']}");
final selectedId = file['id'];
final response = await apiService
.deleteAgentIncentiveFile(selectedId);
print("res - $response");
if (response['status'] == 'success') {
print("DELTED");
if (response['status'] == 'success') {
print("DELTED");
final data =
dataDetails(); // Map<String, dynamic>
final agentId = data["agent_id"];
final data =
dataDetails(); // this is a Map<String, dynamic>
final agentId = data["agent_id"];
getAgentIncenctiveFileList(agentId);
// Fluttertoast.showToast(
// msg: response['data'] ?? "Success",
// toastLength: Toast.LENGTH_SHORT,
// gravity: ToastGravity.BOTTOM,
// );
} else {
// Fluttertoast.showToast(
// msg: "Something went wrong",
// toastLength: Toast.LENGTH_SHORT,
// gravity: ToastGravity.BOTTOM,
// );
}
getAgentIncenctiveFileList(agentId);
} else {
// Fluttertoast.showToast(
// msg: "Something went wrong",
// toastLength: Toast.LENGTH_SHORT,
// gravity: ToastGravity.BOTTOM,
// );
}
},
);
},
);
},
@ -814,17 +960,14 @@ class IncentiveFileRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(6.0),
// margin: const EdgeInsets.only(bottom: 5), // spacing between rows
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
// color: Colors.amber.shade50,
border: Border.all(color: const Color(0xFFE3E3E3)),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(6.0),
@ -840,47 +983,60 @@ class IncentiveFileRow extends StatelessWidget {
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
fileName,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w400,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
softWrap: true,
maxLines: 1,
),
),
Text(
date,
style: GoogleFonts.poppins(
color: Color(0xFF6E6E6E),
fontSize: 10,
const SizedBox(height: 2),
Text(
date,
style: GoogleFonts.poppins(
color: const Color(0xFF6E6E6E),
fontSize: 10,
),
),
),
],
),
const Spacer(),
GestureDetector(
onTap: onUpload,
child: const Icon(
Icons.file_download_outlined,
color: Color(0xFF6E6E6E),
size: 18,
],
),
),
SizedBox(width: 10),
GestureDetector(
onTap: onDelete,
child: const Icon(
Icons.delete_outlined,
color: Color(0xFF6E6E6E),
size: 18,
const SizedBox(width: 8),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onUpload,
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(
Icons.file_download_outlined,
color: Color(0xFF6E6E6E),
size: 20,
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onDelete,
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(
Icons.delete_outlined,
color: Color(0xFF6E6E6E),
size: 20,
),
),
),
),
],

View File

@ -108,6 +108,9 @@ class AgentListState extends ConsumerState<AgentList> {
(item['agent_code'] ?? '-').toLowerCase().contains(
query.toLowerCase(),
) ||
(item['sales_executive_name'] ?? '-').toLowerCase().contains(
query.toLowerCase(),
) ||
(item['retention_rate'] ?? '-').toLowerCase().contains(
query.toLowerCase(),
) ||
@ -423,6 +426,10 @@ class AgentListState extends ConsumerState<AgentList> {
flex: 2,
child: Text('Phone Number', style: _headerStyle),
),
Expanded(
flex: 2,
child: Text('Sales Executive Name', style: _headerStyle),
),
Expanded(
flex: 1,
child: Text(
@ -547,6 +554,10 @@ class AgentListState extends ConsumerState<AgentList> {
flex: 2,
child: Text(item['mobile'] ?? '-', style: _dataBold),
),
Expanded(
flex: 2,
child: Text(item['sales_executive_name'] ?? '-', style: _dataBold),
),
Expanded(
flex: 1,
child: Text(item['retention_rate'] ?? '-', style: _dataBold),

View File

@ -97,7 +97,7 @@ class ProfileState extends ConsumerState<Profile> {
if (roleId == 1) {
response = await apiService.fetchManagerIncentiveList(id);
} else {
response = await apiService.fetchAgentIncentiveList(id);
response = await apiService.fetchAgentIncentiveList(id, type: 'incentive');
}
if (response['status'] == 'success') {
@ -349,7 +349,7 @@ class ProfileState extends ConsumerState<Profile> {
)
: filteredIncentiveData.isEmpty
? const Center(
child: Text("No incentive files found"),
child: Text("No files found"),
)
: SizedBox(
height: ResponsiveLayout.isMobile(context)
@ -449,7 +449,7 @@ class ProfileState extends ConsumerState<Profile> {
: null,
child: isLoading
? const Center(
child: Text("No incentive files found"),
child: Text("No files found"),
)
: Column(
children: [
@ -614,7 +614,7 @@ class ProfileState extends ConsumerState<Profile> {
)
: filteredIncentiveData.isEmpty
? const Center(
child: Text("No incentive files found"),
child: Text("No files found"),
)
: SizedBox(
height: ResponsiveLayout.isMobile(context)

View File

@ -116,7 +116,7 @@ class ProfilePopUpState extends ConsumerState<ProfilePopUp> {
if (roleId == 1) {
response = await apiService.fetchManagerIncentiveList(id);
} else {
response = await apiService.fetchAgentIncentiveList(id);
response = await apiService.fetchAgentIncentiveList(id, type: 'incentive');
}
if (response['status'] == 'success') {

View File

@ -67,21 +67,6 @@ class StaffState extends ConsumerState<Staff> {
dynamic userId;
dynamic managerId;
Map<String, dynamic> dataDetails() {
final data = {
"name": controllers["name"]?.text,
"email": controllers["email"]?.text,
"mobile": controllers["mobile"]?.text,
// "emp_id": controllers["code"]?.text,
"is_active": isActive,
"role_id": selectedRole,
// "handler_id": selectedHandler,
"handler_id": selectedHandlerIds,
"manager_id": userId,
};
return data;
}
@override
void initState() {
super.initState();
@ -104,6 +89,24 @@ class StaffState extends ConsumerState<Staff> {
});
}
Map<String, dynamic> dataDetails() {
final data = {
"name": controllers["name"]?.text,
"email": controllers["email"]?.text,
"mobile": controllers["mobile"]?.text,
// "emp_id": controllers["code"]?.text,
"is_active": isActive,
"role_id": selectedRole,
// "handler_id": selectedHandler,
"handler_id": selectedHandlerIds,
// Always send mapped manager id:
// 1) Secondary Manager -> mapped Manager ID
// 2) All other roles -> mapped Manager ID
"manager_id": managerId,
};
return data;
}
Future<void> _initializeToken() async {
_token = await AuthService.getToken();
print("APISERTOKEN - $_token");
@ -172,6 +175,16 @@ class StaffState extends ConsumerState<Staff> {
return;
}
// Prevent submit without mapped manager id.
if (managerId == null || managerId.toString().trim().isEmpty) {
print('Manager mapping missing. managerId: $managerId, userId: $userId');
ToastHelper.showSuccessToast(
context,
'Manager is missing. Please map a manager and try again.',
);
return;
}
setState(() {
if (_formKey.currentState!.validate()) {
dataDetails();

View File

@ -0,0 +1,788 @@
//
// partner_portal_dashboard.dart Nhance Partner Portal
//
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/services/api_service.dart';
import '../../providers/manager_provider.dart';
import '../../providers/userRoleProvider.dart';
import '../../layouts/main_layout.dart';
String _currentFinYear() {
final now = DateTime.now();
final s = now.month >= 4 ? now.year : now.year - 1;
return '$s-${s + 1}';
}
List<String> _allFinYears() {
final now = DateTime.now();
final cur = now.month >= 4 ? now.year : now.year - 1;
return [for (int y = 2022; y <= cur; y++) '$y-${y + 1}'];
}
String _fyShort(String fy) { final p = fy.split('-'); return 'FY ${p[0].substring(2)}${p[1].substring(2)}'; }
String _fyFull(String fy) { final p = fy.split('-'); return 'FY ${p[0]}${p[1]}'; }
String _fyRange(String fy) { final p = fy.split('-'); return 'Apr ${p[0]} Mar ${p[1]}'; }
DateTime _fyStart(String fy) => DateTime(int.parse(fy.split('-').first), 4, 1);
DateTime _fyEnd(String fy) => DateTime(int.parse(fy.split('-').last), 3, 31);
class PartnerPortalDashboard extends ConsumerStatefulWidget {
const PartnerPortalDashboard({super.key});
@override
ConsumerState<PartnerPortalDashboard> createState() => _State();
}
class _State extends ConsumerState<PartnerPortalDashboard> {
late ApiService api;
bool loading = false;
bool _isAgentRole = false;
int? _agentId;
Map<String, dynamic> partnerDetails = {};
List<Map<String, dynamic>> allPolicies = [];
List<Map<String, dynamic>> allRenewals = [];
List<Map<String, dynamic>> filteredRenewals = [];
int selectedRenewalDays = 10, renewalPage = 0;
static const kPerPage = 4;
List<Map<String, dynamic>> allEarnings = [];
String selectedFY = _currentFinYear();
@override
void initState() {
super.initState();
api = ApiService();
Future.microtask(() {
final role = (ref.read(userRoleProvider) ?? '').toString().trim().toLowerCase();
_isAgentRole = role == 'agent' || role == 'a';
_agentId = ref.read(userIdProvider);
/*
* Partner dashboard is only for Agent logins.
* Use userIdProvider as agent id (not managerIdProvider).
*/
if (_isAgentRole && _agentId != null) {
_loadAll(_agentId);
}
});
}
Future<void> _loadAll(dynamic id) async {
setState(() => loading = true);
try {
await Future.wait([
_fetchDetails(id), _fetchPolicies(id),
_fetchRenewals(id, days: selectedRenewalDays), _fetchEarnings(id),
]);
} finally { setState(() => loading = false); }
}
Future<void> _fetchDetails(dynamic id) async {
final r = await api.getPartnerDetails(id);
if (r['status'] == 'success') setState(() => partnerDetails = Map.from(r['data'] ?? {}));
}
Future<void> _fetchPolicies(dynamic id) async {
final r = await api.getPartnerPolicies(id);
if (r['status'] == 'success') setState(() => allPolicies = List<Map<String, dynamic>>.from((r['data'] ?? []).map((e) => Map<String, dynamic>.from(e))));
}
Future<void> _fetchRenewals(dynamic id, {int days = 10}) async {
final r = await api.getPartnerRenewals(id, days: days);
if (r['status'] == 'success') {
final raw = List<Map<String, dynamic>>.from((r['data'] ?? []).map((e) => Map<String, dynamic>.from(e)));
setState(() {
selectedRenewalDays = days;
allRenewals = raw;
filteredRenewals = raw;
renewalPage = 0;
});
}
}
Future<void> _fetchEarnings(dynamic id) async {
final r = await api.getPartnerEarnings(id);
if (r['status'] == 'success') setState(() => allEarnings = List<Map<String, dynamic>>.from((r['data'] ?? []).map((e) => Map<String, dynamic>.from(e))));
}
void _applyRenewalFilter(int days) {
setState(() {
selectedRenewalDays = days;
filteredRenewals = allRenewals.where((r) => (r['days_left'] ?? 999) <= days).toList();
renewalPage = 0;
});
}
List<Map<String, dynamic>> get _fyEarnings {
final s = _fyStart(selectedFY), e = _fyEnd(selectedFY);
return allEarnings.where((row) {
try {
final raw = row['month_key']?.toString() ?? '';
final dt = raw.length == 7 ? DateTime.parse('$raw-01') : DateTime.tryParse(raw);
if (dt == null) return true;
return !dt.isBefore(s) && !dt.isAfter(e);
} catch (_) { return true; }
}).toList();
}
Map<String, dynamic> get _fyAgg {
double pre = 0, com = 0, tds = 0, net = 0; int pol = 0; bool allPaid = true;
for (final e in _fyEarnings) {
pre += _d(e['premium']); com += _d(e['commission']);
tds += _d(e['tds']); net += _d(e['net_payout']);
pol += int.tryParse(e['policies']?.toString() ?? '0') ?? 0;
if ((e['paid']?.toString()) != '1' && e['paid'] != true) allPaid = false;
}
if (com == 0 && pre > 0) { com = pre * 0.15; tds = com * 0.10; net = com - tds; }
return { 'premium': pre, 'commission': com, 'tds': tds, 'netPayout': net, 'policies': pol, 'paid': allPaid && _fyEarnings.isNotEmpty };
}
double _d(dynamic v) => double.tryParse(v?.toString() ?? '0') ?? 0;
List<Map<String, dynamic>> get _pageRenewals {
final s = renewalPage * kPerPage;
final e = (s + kPerPage).clamp(0, filteredRenewals.length);
return s >= filteredRenewals.length ? [] : filteredRenewals.sublist(s, e);
}
int get _totalPages => (filteredRenewals.length / kPerPage).ceil();
String _inr(dynamic v) {
final n = _d(v);
if (n >= 10000000) return 'Rs.${(n/10000000).toStringAsFixed(1)}Cr';
if (n >= 100000) return 'Rs.${(n/100000).toStringAsFixed(1)}L';
if (n >= 1000) return 'Rs.${(n/1000).toStringAsFixed(1)}K';
return 'Rs.${n.toStringAsFixed(0)}';
}
String _initials(String name) {
final p = name.trim().split(' ');
return p.length >= 2 ? '${p[0][0]}${p[1][0]}'.toUpperCase() : name.substring(0, name.length.clamp(0, 2)).toUpperCase();
}
@override
Widget build(BuildContext context) {
if (!_isAgentRole) {
return MainLayout(
title: 'Partner Dashboard',
body: const SizedBox.shrink(),
);
}
return MainLayout(
title: 'Partner Dashboard',
body: SafeArea(
child: loading
? const Center(child: CircularProgressIndicator(color: Color(0xFF0ABFA3)))
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_label('Partner Details'),
_PartnerProfileCard(details: partnerDetails, initials: _initials(partnerDetails['agent_name']?.toString() ?? 'PD')),
const SizedBox(height: 14),
_label('Overview'),
_OverviewBar(details: partnerDetails, inr: _inr),
const SizedBox(height: 14),
// _label('Policy List & Renewal Alerts'),
_PolicyRenewalRow(
policies: allPolicies,
currentPageRenewals: _pageRenewals, totalRenewalPages: _totalPages,
renewalPage: renewalPage, selectedRenewalDays: selectedRenewalDays,
filteredRenewalsCount: filteredRenewals.length,
onRenewalDays: (d) {
final id = _agentId ?? ref.read(userIdProvider);
if (id != null) {
_fetchRenewals(id, days: d);
} else {
_applyRenewalFilter(d);
}
},
onRenewalPageChanged: (p) => setState(() => renewalPage = p),
inr: _inr,
),
const SizedBox(height: 14),
_label('Earning Details'),
_EarningDetailsSection(
allFinYears: _allFinYears(), selectedFinYear: selectedFY,
onFinYearChanged: (fy) => setState(() => selectedFY = fy),
aggregate: _fyAgg, earningsForFY: _fyEarnings, inr: _inr,
),
const SizedBox(height: 20),
]),
),
),
);
}
Widget _label(String text) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(children: [
Text(text.toUpperCase(), style: GoogleFonts.inter(fontSize: 10.5, fontWeight: FontWeight.w700, color: const Color(0xFF8AABB5), letterSpacing: .8)),
const SizedBox(width: 10),
const Expanded(child: Divider(color: Color(0xFFD9F0ED), thickness: 1)),
]),
);
}
//
// PARTNER PROFILE CARD
// Fields: agent_name, agent_code, mobile, email, manager_name, status
// All come from partnerDetails() partner_agent table
//
class _PartnerProfileCard extends StatelessWidget {
final Map<String, dynamic> details; final String initials;
const _PartnerProfileCard({required this.details, required this.initials});
@override
Widget build(BuildContext context) {
final name = details['agent_name'] ?? '--';
final code = details['agent_code'] ?? '--';
final mobile = details['mobile'] ?? '--';
final email = details['email'] ?? '--';
final manager = details['manager_name'] ?? '--';
final status = details['status'] ?? 'Active';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(12)),
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
Container(width: 48, height: 48,
decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xFF0ABFA3), border: Border.all(color: const Color(0x19000000), width: 2)),
alignment: Alignment.center,
child: Text(initials, style: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white))),
const SizedBox(width: 14),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(name.toString(), style: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))),
const SizedBox(height: 5),
Wrap(spacing: 16, runSpacing: 4, children: [
_m('Code', code.toString()),
_m('Mobile', mobile.toString()),
_m('Email', email.toString()),
_m('Manager', manager.toString()),
]),
])),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(color: const Color(0x380ABFA3), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0x4D0ABFA3))),
child: Text('$status', style: GoogleFonts.inter(fontSize: 11, fontWeight: FontWeight.w600, color: const Color(0xFF0B8D77)))),
]),
);
}
Widget _m(String l, String v) => RichText(text: TextSpan(
text: '$l: ', style: GoogleFonts.inter(fontSize: 11.5, color: const Color(0xFF8AABB5)),
children: [TextSpan(text: v, style: GoogleFonts.inter(fontSize: 11.5, fontWeight: FontWeight.w500, color: const Color(0xFF0F2D3D)))]));
}
//
// OVERVIEW BAR
// mapped_policies / active_policies COUNT from partner_policy WHERE agent_id=$id
// total_premium / commission_earned SUM from partner_policy
// enquiry_* COUNT from partner_enquiry WHERE agent_id=$id, is_active=1
// endorsement_* COUNT from partner_endorsement_request WHERE agent_id=$id, is_active=1
// Clients removed. Mapped Policies shows Active dot only.
//
class _OverviewBar extends StatelessWidget {
final Map<String, dynamic> details; final String Function(dynamic) inr;
const _OverviewBar({required this.details, required this.inr});
@override
Widget build(BuildContext context) {
final mp = details['mapped_policies'] ?? '0';
final ip = details['issued_policies'] ?? '0'; // policy_number IS NOT NULL
final pp2 = details['pending_policies'] ?? '0'; // policy_number IS NULL
final tp = details['total_premium'] ?? '0';
final com = details['commission_earned'] ?? '0';
final cr = details['commission_rate'] ?? '15';
final eq = details['enquiry_total'] ?? '0';
final eqd = details['enquiry_completed'] ?? '0';
final eqp = details['enquiry_pending'] ?? '0';
final en = details['endorsement_total'] ?? '0';
final end = details['endorsement_done'] ?? '0';
final enp = details['endorsement_pending'] ?? '0';
return Container(
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(10),
boxShadow: const [BoxShadow(color: Color(0x06000000), blurRadius: 4, offset: Offset(0, 2))]),
child: LayoutBuilder(builder: (ctx, box) {
final wide = box.maxWidth > 700;
if (wide) return IntrinsicHeight(child: Row(children: [
_sc('Mapped Policies', mp.toString(), const Color(0xFF0ABFA3), [_dot(const Color(0xFF10B981), '$ip Issued'), _dot(const Color(0xFFF97316), '$pp2 Pending')], false),
_sc('Total Premium', inr(tp), const Color(0xFF10B981), [_dot(Colors.grey, 'FY ${_currentFinYear()}')]),
_sc('Delegation', inr(com), const Color(0xFFF97316), [_dot(Colors.grey, '$cr% rate')]),
_fc('Enquiry', eqd.toString(), eq.toString(), const Color(0xFF3B82F6), [_dot(const Color(0xFF10B981), '$eqd Done'), _dot(const Color(0xFFF97316), '$eqp Pending')]),
_fc('Endorsement', end.toString(), en.toString(), const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done'), _dot(const Color(0xFFF97316), '$enp Pending')]),
]));
return Wrap(children: [
_wc('Mapped Policies', mp.toString(), const Color(0xFF0ABFA3), [_dot(const Color(0xFF10B981), '$ip Issued'), _dot(const Color(0xFFF97316), '$pp2 Pending')]),
_wc('Total Premium', inr(tp), const Color(0xFF10B981), [_dot(Colors.grey, 'FY ${_currentFinYear()}')]),
_wc('Delegation', inr(com), const Color(0xFFF97316), [_dot(Colors.grey, '$cr% rate')]),
_wc('Enquiry', '$eqd / $eq', const Color(0xFF3B82F6), [_dot(const Color(0xFF10B981), '$eqd Done')]),
_wc('Endorsement', '$end / $en', const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done')]),
]);
}),
);
}
Widget _sc(String l, String v, Color c, List<Widget> d, [bool lb=true]) => Expanded(child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: lb ? const BoxDecoration(border: Border(left: BorderSide(color: Color(0xFFD9F0ED)))) : null,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(l,style:_labelSmall), const SizedBox(height:4), Text(v,style:_bigNum.copyWith(color:c)), const SizedBox(height:4), Wrap(spacing:8,runSpacing:2,children:d)])));
Widget _fc(String l, String dn, String tot, Color c, List<Widget> d) => Expanded(child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: const BoxDecoration(border: Border(left: BorderSide(color: Color(0xFFD9F0ED)))),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(l,style:_labelSmall), const SizedBox(height:4),
Row(crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [Text(dn,style:_bigNum.copyWith(color:c)), Text(' / ',style:GoogleFonts.inter(fontSize:14,fontWeight:FontWeight.w300,color:const Color(0xFFCBD5E1))), Text(tot,style:GoogleFonts.inter(fontSize:14,fontWeight:FontWeight.w600,color:const Color(0xFF94A3B8)))]),
const SizedBox(height:4), Wrap(spacing:8,runSpacing:2,children:d)])));
Widget _wc(String l, String v, Color c, List<Widget> d) => SizedBox(width:160, child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFD9F0ED)))),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(l,style:_labelSmall), const SizedBox(height:4), Text(v,style:_bigNum.copyWith(color:c)), const SizedBox(height:4), Wrap(spacing:6,children:d)])));
Widget _dot(Color c, String l) => Row(mainAxisSize: MainAxisSize.min, children: [Container(width:7,height:7,decoration:BoxDecoration(shape:BoxShape.circle,color:c)), const SizedBox(width:4), Text(l,style:_subInfo)]);
}
//
// POLICY LIST + RENEWAL ALERTS ROW
//
class _PolicyRenewalRow extends StatelessWidget {
final List<Map<String, dynamic>> policies, currentPageRenewals;
final int totalRenewalPages, renewalPage, selectedRenewalDays, filteredRenewalsCount;
final ValueChanged<int> onRenewalDays, onRenewalPageChanged;
final String Function(dynamic) inr;
const _PolicyRenewalRow({required this.policies, required this.currentPageRenewals,
required this.totalRenewalPages, required this.renewalPage,
required this.selectedRenewalDays, required this.filteredRenewalsCount,
required this.onRenewalDays, required this.onRenewalPageChanged, required this.inr});
@override
Widget build(BuildContext context) => _RenewalAlertsCard(
currentPageRenewals: currentPageRenewals,
totalPages: totalRenewalPages,
currentPage: renewalPage,
selectedDays: selectedRenewalDays,
totalCount: filteredRenewalsCount,
onDays: onRenewalDays,
onPageChanged: onRenewalPageChanged,
inr: inr,
);
}
// Policy list card
// holder_name = partner_policy.insured_name (aliased in API)
// product = partner_policy.product column; fallback to vehicle_type
class _PolicyListCard extends StatelessWidget {
final List<Map<String, dynamic>> policies; final String Function(dynamic) inr;
const _PolicyListCard({required this.policies, required this.inr});
@override
Widget build(BuildContext context) => _DashCard(
header: Row(children: [Text('Policy List', style: _chartHeader), const SizedBox(width:8), _CountBadge(policies.length)]),
body: policies.isEmpty
? const _EmptyState(message:'No policies found for this partner')
: Column(children: [
_TableHeader(headers: const ['Policy No.', 'Holder', 'Product', 'Premium']),
ConstrainedBox(constraints: const BoxConstraints(maxHeight: 340),
child: ListView.separated(
shrinkWrap: true, itemCount: policies.length,
separatorBuilder: (_,__) => const Divider(height:1, color:Color(0xFFD9F0ED)),
itemBuilder: (_,i) {
final p = policies[i];
final productLabel = (p['product']?.toString().isNotEmpty == true)
? p['product'].toString()
: (p['vehicle_type']?.toString() ?? '--');
return Container(
padding: const EdgeInsets.symmetric(vertical:8, horizontal:12),
child: Row(children: [
Expanded(flex:2, child: Text(p['policy_no']?.toString() ?? '--', style:_tableTeal, overflow:TextOverflow.ellipsis)),
Expanded(flex:2, child: Text(p['holder_name']?.toString() ?? '--', style:_tableData, overflow:TextOverflow.ellipsis)),
Expanded(
flex: 2,
child: Text(
productLabel,
style: _tableData,
overflow: TextOverflow.ellipsis,
),
),
Expanded(flex:2, child: Text(inr(p['premium'] ?? 0), style:_tableDataBold)),
]),
);
},
)),
]),
);
}
class _RenewalAlertsCard extends StatelessWidget {
final List<Map<String, dynamic>> currentPageRenewals;
final int totalPages, currentPage, selectedDays, totalCount;
final ValueChanged<int> onDays, onPageChanged;
final String Function(dynamic) inr;
const _RenewalAlertsCard({required this.currentPageRenewals, required this.totalPages, required this.currentPage, required this.selectedDays, required this.totalCount, required this.onDays, required this.onPageChanged, required this.inr});
@override
Widget build(BuildContext context) => _DashCard(
header: Row(children: [
Container(width:30,height:30,decoration:BoxDecoration(color:const Color(0xFFFFEDED),borderRadius:BorderRadius.circular(8)),child:const Icon(Icons.notifications_active_outlined,size:16,color:Color(0xFFEF4444))),
const SizedBox(width:8),
Column(crossAxisAlignment:CrossAxisAlignment.start, children:[Text('Renewal Alerts',style:_chartHeader), Text('At-risk policies',style:GoogleFonts.inter(fontSize:9.5,color:const Color(0xFF8AABB5)))]),
const Spacer(),
Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: const Color(0xFFF2F4F6),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Wrap(
spacing: 2,
runSpacing: 2,
children: [10, 20, 30, 40, 50].map((d) {
final act = selectedDays == d;
return GestureDetector(
onTap: () => onDays(d),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: act ? const Color(0xFF000666) : Colors.transparent,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: act ? const Color(0xFF000666) : Colors.transparent,
),
),
child: Text(
'$d Days',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: .4,
color: act ? Colors.white : const Color(0xFF454652),
),
),
),
);
}).toList(),
),
),
]),
body: totalCount == 0
? const _EmptyState(message:'No renewals due within selected period')
: Padding(
padding: const EdgeInsets.all(10),
child: LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
final crossAxisCount = w >= 1100 ? 4 : w >= 850 ? 3 : w >= 560 ? 2 : 1;
return Column(children: [
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
mainAxisExtent: 90,
),
itemCount: currentPageRenewals.length,
itemBuilder: (_, i) => _RenewalAlertCard(renewal: currentPageRenewals[i], inr: inr),
),
if (totalPages > 1) ...[
const SizedBox(height: 10),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Text('${currentPage+1} / $totalPages', style: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF8AABB5))),
const SizedBox(width: 8),
_PA(icon: Icons.chevron_left, enabled: currentPage > 0, onTap: () => onPageChanged(currentPage - 1)),
const SizedBox(width: 4),
_PA(icon: Icons.chevron_right, enabled: currentPage < totalPages - 1, onTap: () => onPageChanged(currentPage + 1)),
]),
],
]);
},
),
),
);
}
// renewal card holder_name=insured_name, premium=premium_amount (both aliased in API)
class _RenewalAlertCard extends StatelessWidget {
final Map<String, dynamic> renewal; final String Function(dynamic) inr;
const _RenewalAlertCard({required this.renewal, required this.inr});
@override
Widget build(BuildContext context) {
final d = int.tryParse(renewal['days_left']?.toString()??'0')??0;
Color dc, bb, bf;
if (d<=10){dc=const Color(0xFFEF4444);bb=const Color(0xFFFFEDED);bf=const Color(0xFFEF4444);}
else if(d<=20){dc=const Color(0xFFF97316);bb=const Color(0xFFFEF0E7);bf=const Color(0xFFF97316);}
else{dc=const Color(0xFF94A3B8);bb=const Color(0xFFF1F5F9);bf=const Color(0xFF64748B);}
return Container(
padding:const EdgeInsets.symmetric(horizontal:10,vertical:7),
decoration:BoxDecoration(color:Colors.white,border:Border.all(color:const Color(0xFFE2E8F0)),borderRadius:BorderRadius.circular(10),
boxShadow:const[BoxShadow(color:Color(0x06000000),blurRadius:4,offset:Offset(0,2))]),
child:Column(mainAxisSize: MainAxisSize.min,crossAxisAlignment:CrossAxisAlignment.start,children:[
Row(children:[Container(width:8,height:8,decoration:BoxDecoration(shape:BoxShape.circle,color:dc)),const Spacer(),
Container(padding:const EdgeInsets.symmetric(horizontal:7,vertical:2),decoration:BoxDecoration(color:bb,borderRadius:BorderRadius.circular(4)),
child:Text('DUE IN $d DAYS',style:GoogleFonts.inter(fontSize:9,fontWeight:FontWeight.w700,color:bf,letterSpacing:.3)))]),
const SizedBox(height:4),
Text(renewal['holder_name']?.toString()??'--',style:GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w600,color:const Color(0xFF0F2D3D)),maxLines:1,overflow:TextOverflow.ellipsis),
const SizedBox(height:1),
Row(mainAxisAlignment:MainAxisAlignment.spaceBetween,children:[
Flexible(child:Text('Policy: ${renewal['policy_no']?.toString()??'--'}',style:GoogleFonts.inter(fontSize:9.5,color:const Color(0xFF8AABB5)),overflow:TextOverflow.ellipsis)),
Text(inr(renewal['premium']??0),style:GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w700,color:const Color(0xFF0F2D3D))),
]),
]));
}
}
typedef _PA = _PaginationArrow;
class _PaginationArrow extends StatelessWidget {
final IconData icon; final bool enabled; final VoidCallback onTap;
const _PaginationArrow({required this.icon, required this.enabled, required this.onTap});
@override
Widget build(BuildContext context) => GestureDetector(onTap:enabled?onTap:null,child:Container(width:28,height:28,
decoration:BoxDecoration(color:enabled?const Color(0xFF0F2D3D):const Color(0xFFF1F5F9),borderRadius:BorderRadius.circular(6)),
child:Icon(icon,size:18,color:enabled?Colors.white:const Color(0xFFCBD5E1))));
}
//
// EARNING DETAILS SECTION
//
class _EarningDetailsSection extends StatelessWidget {
final List<String> allFinYears;
final String selectedFinYear;
final ValueChanged<String> onFinYearChanged;
final Map<String, dynamic> aggregate;
final List<Map<String, dynamic>> earningsForFY;
final String Function(dynamic) inr;
const _EarningDetailsSection({required this.allFinYears, required this.selectedFinYear, required this.onFinYearChanged, required this.aggregate, required this.earningsForFY, required this.inr});
Widget _hero() {
final revenue = inr(aggregate['premium'] ?? 0);
final netPayout = inr(aggregate['netPayout'] ?? 0);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 4, offset: Offset(0, 2))]),
child: LayoutBuilder(builder: (ctx, box) {
final wide = box.maxWidth > 500;
final left = Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
// Text('PERFORMANCE DASHBOARD', style: GoogleFonts.inter(fontSize: 9.5, fontWeight: FontWeight.w700, color: const Color(0xFF8AABB5), letterSpacing: 1.2)),
// const SizedBox(height: 6),
Text('Monthly Revenue', style: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))),
const SizedBox(height: 14),
Row(crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [
Text(revenue, style: GoogleFonts.manrope(fontSize: 28, fontWeight: FontWeight.w700, color: const Color(0xFF000666))),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(color: const Color(0xFFE6FAF7), borderRadius: BorderRadius.circular(6)),
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.trending_up, size: 12, color: Color(0xFF0ABFA3)),
const SizedBox(width: 4),
Text('+12.4%', style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.w700, color: const Color(0xFF0ABFA3))),
]),
),
]),
const SizedBox(height: 6),
Text('Accumulated earnings for ${_fyFull(selectedFinYear)}.', style: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF8AABB5))),
]);
final payout = Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
decoration: BoxDecoration(color: const Color(0xFFF8FFFE), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFD9F0ED))),
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.savings_outlined, size: 28, color: Color(0xFF0ABFA3)),
const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('TOTAL PAYOUT TO DATE', style: GoogleFonts.inter(fontSize: 8.5, fontWeight: FontWeight.w700, color: const Color(0xFF8AABB5), letterSpacing: .8)),
const SizedBox(height: 4),
Text(netPayout, style: GoogleFonts.manrope(fontSize: 20, fontWeight: FontWeight.w700, color: const Color(0xFF000666))),
]),
]),
);
return wide
? Row(crossAxisAlignment: CrossAxisAlignment.center, children: [Expanded(child: left), const SizedBox(width: 20), payout])
: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [left, const SizedBox(height: 16), payout]);
}),
);
}
Widget _fySection() {
final agg = aggregate;
final stats = [
_SI('Financial Year', _fyFull(selectedFinYear), Icons.calendar_today_outlined, const Color(0xFF000666)),
_SI('Premium Collected', inr(agg['premium'] ?? 0), Icons.payments_outlined, const Color(0xFF006B5C)),
_SI('Policies', '${agg['policies'] ?? 0}', Icons.policy_outlined, const Color(0xFF1A237E)),
_SI('Delegation (15%)', inr(agg['commission'] ?? 0), Icons.percent_outlined, const Color(0xFF059669)),
_SI('TDS (10%)', inr(agg['tds'] ?? 0), Icons.account_balance_outlined, const Color(0xFFD97706)),
_SI('Net Payout', inr(agg['netPayout'] ?? 0), Icons.savings_outlined, const Color(0xFF059669)),
];
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(12)),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
const Icon(Icons.currency_rupee_rounded, size: 16, color: Color(0xFF006B5C)),
const SizedBox(width: 6),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('Earning Details', style: GoogleFonts.inter(fontSize: 13, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))),
Text('${_fyFull(selectedFinYear)} · ${_fyRange(selectedFinYear)}', style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5))),
]),
const Spacer(),
_FYTabBar(allFinYears: allFinYears, selected: selectedFinYear, onChanged: onFinYearChanged),
]),
const SizedBox(height: 14),
LayoutBuilder(builder: (ctx, box) {
final cols = box.maxWidth > 700 ? 6 : box.maxWidth > 450 ? 3 : 2;
final w = (box.maxWidth - (cols - 1) * 10) / cols;
return Wrap(spacing: 10, runSpacing: 10, children: stats.map((s) => SizedBox(width: w, child: _StatCard(item: s))).toList());
}),
]),
);
}
Widget _txnTable() => Container(
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(10),
boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 4, offset: Offset(0, 2))]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFD9F0ED)))),
child: Row(children: [
Text('Recent Transactions', style: GoogleFonts.inter(fontSize: 13, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))),
const SizedBox(width: 8), _CountBadge(earningsForFY.length), const Spacer(),
Text('Showing for ${_fyFull(selectedFinYear)}', style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5))),
]),
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), color: const Color(0xFFF2F4F6),
child: Row(children: ['Month','Premium','Policies','Delegation','Net Payout']
.map((h) => Expanded(flex: 2, child: Text(h, style: _tableHeadStyle))).toList()),
),
earningsForFY.isEmpty
? const SizedBox(
height: 180,
child: Center(
child: _EmptyState(
message: 'No transactions for this financial year',
),
),
)
: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 320),
child: ListView.separated(
shrinkWrap: true, physics: const ClampingScrollPhysics(),
itemCount: earningsForFY.length,
separatorBuilder: (_, __) => const Divider(height: 1, color: Color(0xFFD9F0ED)),
itemBuilder: (_, i) {
final e = earningsForFY[i];
final label = e['month_label']?.toString() ?? e['month_key']?.toString() ?? '--';
double pre = double.tryParse(e['premium']?.toString() ?? '0') ?? 0;
double com = double.tryParse(e['commission']?.toString() ?? '0') ?? (pre * 0.15);
double tds = double.tryParse(e['tds']?.toString() ?? '0') ?? (com * 0.10);
double net = double.tryParse(e['net_payout']?.toString() ?? '0') ?? (com - tds);
return Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
child: Row(children: [
Expanded(flex:2, child: Text(label, style:_tableTeal, overflow:TextOverflow.ellipsis)),
Expanded(flex:2, child: Text(inr(pre), style:_tableDataBold.copyWith(color:const Color(0xFF000666)))),
Expanded(flex:2, child: Text('${e['policies']??0}', style:_tableDataBold)),
Expanded(flex:2, child: Text(inr(com), style:_tableDataBold.copyWith(color:const Color(0xFF059669)))),
Expanded(flex:2, child: Text(inr(net), style:_tableDataBold.copyWith(color:const Color(0xFF059669)))),
]),
);
},
),
),
]),
);
@override
Widget build(BuildContext context) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_hero(), const SizedBox(height: 14), _fySection(), const SizedBox(height: 14), _txnTable(),
]);
}
typedef _SI = _StatItem;
class _StatItem { final String label, value; final IconData icon; final Color color; const _StatItem(this.label, this.value, this.icon, this.color); }
class _StatCard extends StatelessWidget {
final _StatItem item; const _StatCard({required this.item});
@override
Widget build(BuildContext context) => Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: const [BoxShadow(color: Color(0x07000000), blurRadius: 4, offset: Offset(0, 2))]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Flexible(child: Text(item.label.toUpperCase(), style: GoogleFonts.inter(fontSize: 8.5, fontWeight: FontWeight.w800, color: const Color(0xFF8AABB5), letterSpacing: .6), overflow: TextOverflow.ellipsis)),
Icon(item.icon, size: 16, color: item.color),
]),
const SizedBox(height: 8),
Text(item.value, style: GoogleFonts.manrope(fontSize: 18, fontWeight: FontWeight.w700, color: item.color)),
]),
);
}
class _FYTabBar extends StatelessWidget {
final List<String> allFinYears; final String selected; final ValueChanged<String> onChanged;
const _FYTabBar({required this.allFinYears, required this.selected, required this.onChanged});
@override
Widget build(BuildContext context) => Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(color: const Color(0xFFF2F4F6), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE2E8F0))),
child: Wrap(spacing: 2, runSpacing: 2, children: allFinYears.map((fy) {
final act = fy == selected;
return GestureDetector(onTap: () => onChanged(fy), child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(color: act ? const Color(0xFF000666) : Colors.transparent, borderRadius: BorderRadius.circular(6), border: Border.all(color: act ? const Color(0xFF000666) : Colors.transparent)),
child: Text(_fyShort(fy), style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: .4, color: act ? Colors.white : const Color(0xFF454652))),
));
}).toList()),
);
}
//
// SHARED SMALL WIDGETS
//
class _DashCard extends StatelessWidget {
final Widget header, body; const _DashCard({required this.header, required this.body});
@override
Widget build(BuildContext context) => Container(
decoration:BoxDecoration(color:Colors.white,border:Border.all(color:const Color(0xFFD9F0ED)),borderRadius:BorderRadius.circular(10),
boxShadow:const[BoxShadow(color:Color(0x08000000),blurRadius:4,offset:Offset(0,2))]),
child:Column(crossAxisAlignment:CrossAxisAlignment.stretch,children:[
Container(padding:const EdgeInsets.symmetric(horizontal:14,vertical:11),decoration:const BoxDecoration(border:Border(bottom:BorderSide(color:Color(0xFFD9F0ED)))),child:header),
body]));
}
class _TableHeader extends StatelessWidget {
final List<String> headers; const _TableHeader({required this.headers});
@override
Widget build(BuildContext context) => Container(
padding:const EdgeInsets.symmetric(vertical:8,horizontal:12), color:const Color(0xFFE3F9F8),
child:Row(children:headers.map((h)=>Expanded(flex:2,child:Text(h,style:_tableHeadStyle))).toList()));
}
class _CountBadge extends StatelessWidget {
final int count; const _CountBadge(this.count);
@override
Widget build(BuildContext context) => Container(
padding:const EdgeInsets.symmetric(horizontal:8,vertical:2),
decoration:BoxDecoration(color:const Color(0xFFE6FAF7),borderRadius:BorderRadius.circular(12)),
child:Text('$count',style:GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w600,color:const Color(0xFF0ABFA3))));
}
class _EmptyState extends StatelessWidget {
final String message; const _EmptyState({required this.message});
@override
Widget build(BuildContext context) => Padding(padding:const EdgeInsets.symmetric(vertical:32),
child:Column(children:[Text('No records found',style:GoogleFonts.inter(fontSize:13,fontWeight:FontWeight.w500,color:const Color(0xFF8AABB5))),
const SizedBox(height:3),Text(message,style:GoogleFonts.inter(fontSize:11.5,color:const Color(0xFFB0C8D0)))]));
}
class _ProductBadge extends StatelessWidget {
final String product; const _ProductBadge(this.product);
@override
Widget build(BuildContext context) => Container(
padding:const EdgeInsets.symmetric(horizontal:8,vertical:2),
decoration:BoxDecoration(color:const Color(0xFFE6FAF7),borderRadius:BorderRadius.circular(20)),
child:Text(product,style:GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w600,color:const Color(0xFF089886)),overflow:TextOverflow.ellipsis));
}
// Text Styles
final _chartHeader = GoogleFonts.inter(fontSize:12,color:Colors.black,fontWeight:FontWeight.w600);
final _labelSmall = GoogleFonts.inter(fontSize:10,fontWeight:FontWeight.w600,color:const Color(0xFF8AABB5),letterSpacing:.7);
final _bigNum = GoogleFonts.inter(fontSize:21,fontWeight:FontWeight.w700,color:const Color(0xFF0ABFA3));
final _subInfo = GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w400,color:const Color(0xFF4A6B78));
final _tableHeadStyle = GoogleFonts.inter(fontSize:10.5,fontWeight:FontWeight.w600,color:const Color(0xFF8AABB5),letterSpacing:.5);
final _tableData = GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w400,color:const Color(0xFF1A3340));
final _tableDataBold = GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w600,color:const Color(0xFF1A3340));
final _tableTeal = GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w600,color:const Color(0xFF089886));

View File

@ -448,8 +448,16 @@ class EnquiryHandlerState extends ConsumerState<EnquiryListHandler> {
if (response['status'] == 'success') {
final data = response['data'];
final fromDate = response['from_date'] ?? '';
final toDate = response['to_date'] ?? '';
/*
* Some APIs do not return from_date/to_date in the payload.
* Keep UI date fields stable by falling back to the request dates.
*/
final fromDate = (response['from_date'] ?? '').toString().trim().isNotEmpty
? response['from_date'].toString()
: (fromDt?.toString() ?? '');
final toDate = (response['to_date'] ?? '').toString().trim().isNotEmpty
? response['to_date'].toString()
: (toDt?.toString() ?? '');
print('FromDate : $fromDate');
print('ToDate : $toDate');

View File

@ -220,7 +220,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
if (role == 'staff') {
context.go(AppRoutes.enquiryForStaff);
} else if (role == 'Accounts') {
context.go(AppRoutes.invoiceList);
context.go(AppRoutes.payoutList);
} else if (role == 'agent') {
context.go(AppRoutes.partnerPortalDashboard);
} else {
context.go(AppRoutes.dashboard);
}
@ -482,7 +484,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
ToastHelper.showSuccessToast(context, 'OTP verified successfully');
await AuthService.saveToken(token);
await AuthService.saveToken(token);
await _saveUserRole(token);
await _saveIdsFromToken(token);
@ -491,7 +493,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
if (userRole == 'staff') {
context.go(AppRoutes.enquiryForStaff);
} else if (userRole == 'Accounts') {
context.go(AppRoutes.invoiceList);
context.go(AppRoutes.payoutList);
} else if (userRole == 'agent') {
context.go(AppRoutes.partnerPortalDashboard);
} else {
context.go(AppRoutes.dashboard);
}
@ -754,36 +758,48 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
const SizedBox(height: 40),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Welcome Back",
style: GoogleFonts.poppins(
fontSize: 50,
fontWeight: FontWeight.w600,
color: Color(0xFF425C5C),
child: LayoutBuilder(
builder: (context, leftPaneConstraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: leftPaneConstraints.maxHeight,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Welcome Back",
style: GoogleFonts.poppins(
fontSize: 50,
fontWeight: FontWeight.w600,
color: Color(0xFF425C5C),
),
),
const SizedBox(height: 20),
Image.asset(
"assets/login/login-content.png",
// height:
// MediaQuery.of(context).size.height *
// 0.5, // 40% of screen height
height: leftPaneConstraints.maxHeight * 0.55,
fit: BoxFit.contain,
),
const SizedBox(height: 10),
Text(
'${'"'}Manage your policies, track quotations, and grow your business all in one place.${'"'}',
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 14,
color: Color(0xFF000000),
fontWeight: FontWeight.w500,
),
),
],
),
),
),
const SizedBox(height: 20),
Image.asset(
"assets/login/login-content.png",
height:
MediaQuery.of(context).size.height *
0.5, // 40% of screen height
fit: BoxFit.contain,
),
const SizedBox(height: 10),
Text(
'${'"'}Manage your policies, track quotations, and grow your business all in one place.${'"'}',
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 14,
color: Color(0xFF000000),
fontWeight: FontWeight.w500,
),
),
],
);
},
),
),
],

View File

@ -12,6 +12,7 @@ import '../../themes/indicators/date_field_theme.dart';
import '../../themes/indicators/input_field_decoration.dart';
class DateFilterRowPayout extends ConsumerStatefulWidget {
final bool showBrokerFilter;
final TextEditingController startController;
final String? selectedBroker;
final List<dynamic>? selectedParnter;
@ -29,6 +30,7 @@ class DateFilterRowPayout extends ConsumerStatefulWidget {
const DateFilterRowPayout({
super.key,
this.showBrokerFilter = true,
required this.startController,
required this.endController,
required this.onFilter,
@ -93,14 +95,23 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
// getPartnerDetails(userID);
// }
if (managerId != null && widget.selectedBroker != null) {
if (widget.showBrokerFilter &&
managerId != null &&
widget.selectedBroker != null) {
print('managerId - $managerId');
print('SelectedBroker - $widget.selectedBroker');
getAgentList(managerId,widget.selectedBroker);
getAgentList(managerId);
}
if (!widget.showBrokerFilter && managerId != null) {
// Broker filter hidden: load partner list directly by manager.
getAgentList(managerId);
}
});
getBroker();
if (widget.showBrokerFilter) {
getBroker();
}
}
Future<void> getBroker() async {
@ -134,7 +145,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
}
}
Future<void> getAgentList(id,broker_id) async {
Future<void> getAgentList(id) async {
print('getAgentListData called');
setState(() {
@ -142,7 +153,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
});
try {
final response = await apiService.fetchAgentUnusedCommissionList(id,broker_id);
final response = await apiService.fetchAgentUnusedCommissionList(id);
print('getAgentListData called response');
print('get Agent- ${response['data']}');
if (response['status'] == 'success') {
@ -218,8 +229,10 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
SizedBox(width: spacing),
buildEndDate(context),
SizedBox(width: spacing),
buildBroker(context),
SizedBox(width: spacing),
if (widget.showBrokerFilter) ...[
buildBroker(context),
SizedBox(width: spacing),
],
buildPartner(context),
SizedBox(width: spacing),
];
@ -531,7 +544,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
});
// 3. Fetch new agents based on this broker
getAgentList(managerId!, selectedId);
getAgentList(managerId!);
}
},
@ -548,7 +561,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Referer", style: _textStyle),
Text("Partner", style: _textStyle),
SizedBox(height: 5),
SizedBox(
height: 35,
@ -599,7 +612,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: "Select Referer",
label: "Select Partner",
).copyWith(
hintStyle: GoogleFonts.inter(
fontSize: 12,
@ -640,7 +653,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRowPayout> {
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: "Search Referer...",
hintText: "Search Partner...",
hintStyle: GoogleFonts.inter(
fontSize: 11,
color: Colors.black,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,12 @@ class policylistState extends ConsumerState<policylist> {
dynamic SelectedStatus;
dynamic SelectedInsurer;
dynamic SelectedStaffId;
/// Partner (agent) id for Accounts policy list filter.
String? SelectedPartnerId;
/// Partner `agent_code` for payout sample Excel when Accounts.
String selectedPartnerAgentCode = '';
/// From staff dropdown (manager/handler) for payout sample when set.
String selectedStaffAgentCode = '';
// List<Map<String, dynamic>> dataVal = [];
List<Map<String, dynamic>> getStaffData = [];
List<Map<String, dynamic>> originalData = [];
@ -146,8 +152,10 @@ class policylistState extends ConsumerState<policylist> {
fromDate: fromDt,
toDate: toDt,
selectedStaffId: SelectedStaffId ?? '',
selectedStatus:selectedStatus,
selectedInsurer:selectedInsurer
selectedStatus: selectedStatus,
selectedInsurer: selectedInsurer,
selectedAgentId:
role == 'Accounts' ? (SelectedPartnerId ?? '') : null,
);
if (response['status'] == 'success') {
@ -245,6 +253,9 @@ class policylistState extends ConsumerState<policylist> {
void refrshfilterDateRange() {
setState(() {
SelectedStaffId = null;
SelectedPartnerId = null;
selectedPartnerAgentCode = '';
selectedStaffAgentCode = '';
controllers['startDate']!.clear();
controllers['endDate']!.clear();
SelectedStatus = (roleId == 'Accounts') ? 'Not Verified' : 'All'; //
@ -341,6 +352,35 @@ class policylistState extends ConsumerState<policylist> {
// }
}
Future<void> downloadPendingCommissionPayoutSample() async {
final fromText = controllers['startDate']?.text.trim() ?? '';
final toText = controllers['endDate']?.text.trim() ?? '';
if (fromText.isEmpty || toText.isEmpty) {
ToastHelper.showErrorToast(context, 'Please select start and end dates');
return;
}
try {
final fromDt = DateFormat('dd-MM-yyyy').parse(fromText);
final toDt = DateFormat('dd-MM-yyyy').parse(toText);
final fromIso = DateFormat('yyyy-MM-dd').format(fromDt);
final toIso = DateFormat('yyyy-MM-dd').format(toDt);
await apiService.downloadPendingCommissionExcel(
fromDate: fromIso,
toDate: toIso,
agentCode: selectedPartnerAgentCode.isNotEmpty
? selectedPartnerAgentCode
: selectedStaffAgentCode,
);
} catch (e) {
if (mounted) {
ToastHelper.showErrorToast(
context,
'Could not download file. Check dates or try again.',
);
}
}
}
@override
Widget build(BuildContext context) {
return MainLayout(
@ -440,6 +480,22 @@ class policylistState extends ConsumerState<policylist> {
print('Selected Filterd STAFF Id - $val');
SelectedStaffId = val;
},
onStaffAgentCodeChanged: (code) {
setState(() {
selectedStaffAgentCode = code ?? '';
});
},
selectedPartnerId: SelectedPartnerId,
onPartnerChanged: (id) {
setState(() {
SelectedPartnerId = id;
});
},
onPartnerAgentCodeChanged: (code) {
setState(() {
selectedPartnerAgentCode = code ?? '';
});
},
selectedStaffId: SelectedStaffId,
startController: controllers['startDate']!,
endController: controllers['endDate']!,
@ -485,6 +541,22 @@ class policylistState extends ConsumerState<policylist> {
print('Selected Filterd STAFF Id - $val');
SelectedStaffId = val;
},
onStaffAgentCodeChanged: (code) {
setState(() {
selectedStaffAgentCode = code ?? '';
});
},
selectedPartnerId: SelectedPartnerId,
onPartnerChanged: (id) {
setState(() {
SelectedPartnerId = id;
});
},
onPartnerAgentCodeChanged: (code) {
setState(() {
selectedPartnerAgentCode = code ?? '';
});
},
selectedStaffId: SelectedStaffId,
startController: controllers['startDate']!,
endController: controllers['endDate']!,
@ -589,6 +661,26 @@ class policylistState extends ConsumerState<policylist> {
),
),
),
SizedBox(width: 8),
Tooltip(
message:
'Download payout sample Excel (fill Payout Amount and re-upload)',
child: InkWell(
onTap: () => downloadPendingCommissionPayoutSample(),
child: Container(
padding: const EdgeInsets.all(7.0),
decoration: BoxDecoration(
color: const Color(0xFF1565C0),
borderRadius: BorderRadius.circular(8.0),
),
child: Icon(
Icons.description_outlined,
size: 15,
color: Colors.white,
),
),
),
),
SizedBox(width: 10),
],
),

View File

@ -45,6 +45,7 @@ class _policyValidationState extends ConsumerState<policyValidation> {
bool isLoadingBroker = false;
bool isLoadingPaymentMode = false;
bool isLoadingInsurancePlan = false;
bool isCommissionManuallyEditable = false;
dynamic managerId;
dynamic userId;
@ -1425,6 +1426,62 @@ class _policyValidationState extends ConsumerState<policyValidation> {
print('_fetchCommision OUT');
}
bool get _showChangeCommissionAction {
final String status =
(widget.item?['is_data_accuracy_checked']?.toString() ?? '').trim();
return status == '0' || status == '1';
}
bool get _isDataAccuracyChecked {
final String status =
(widget.item?['is_data_accuracy_checked']?.toString() ?? '').trim();
return status == '1';
}
Future<void> _updateCommissionOnly() async {
final dynamic policyId = widget.item?['policy_id'] ?? widget.item?['id'];
final String commissionText =
controllers['commission_amount']?.text.trim() ?? '';
if (policyId == null) {
ToastHelper.showWarningToast(context, 'Missing policy id');
return;
}
final double? commission = double.tryParse(commissionText);
if (commission == null) {
ToastHelper.showWarningToast(context, 'Please enter valid commission amount');
return;
}
setState(() => isLoading = true);
try {
final payload = {
'id': policyId,
'commission_amount': commission.toStringAsFixed(2),
'updated_by': userId,
};
final result = await apiService.updatePolicyCommissionApi(payload);
if (result['status'] == 'success') {
if (!mounted) return;
setState(() => isCommissionManuallyEditable = false);
ToastHelper.showSuccessToast(context, 'Commission updated successfully');
} else {
ToastHelper.showErrorToast(
context,
result['data']?.toString() ??
result['message']?.toString() ??
'Failed to update commission',
);
}
} catch (e) {
ToastHelper.showErrorToast(context, 'Exception: $e');
} finally {
if (mounted) setState(() => isLoading = false);
}
}
Widget buildVehicleType(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -2939,6 +2996,8 @@ class _policyValidationState extends ConsumerState<policyValidation> {
'Commission Amount *',
controllers['commission_amount']!,
required: false,
readOnly:
!isCommissionManuallyEditable,
keyboardType:
TextInputType.numberWithOptions(
decimal: true,
@ -2953,10 +3012,39 @@ class _policyValidationState extends ConsumerState<policyValidation> {
mainAxisAlignment: MainAxisAlignment
.end, // Pushes children to the right
children: [
if (_showChangeCommissionAction)
TextButton(
onPressed: isLoading
? null
: () async {
/*
* First click: allow manual edit.
* Next click while editable: submit to updatePolicyCommission API.
*/
if (!isCommissionManuallyEditable) {
setState(() {
isCommissionManuallyEditable =
true;
});
return;
}
await _updateCommissionOnly();
},
child: Text(
isCommissionManuallyEditable
? 'Update Commission'
: 'Change Commission',
style: TextStyle(
color: isLoading
? Colors.grey
: const Color(0xFF2E7D6E),
fontWeight: FontWeight.bold,
),
),
),
TextButton(
onPressed:
(widget.item?['is_data_accuracy_checked'] ==
'1' ||
(_isDataAccuracyChecked ||
isLoading)
? null // Disable if already checked or currently loading
: _fetchCommision,
@ -2966,8 +3054,7 @@ class _policyValidationState extends ConsumerState<policyValidation> {
: "Calculate",
style: TextStyle(
color:
(widget.item?['is_data_accuracy_checked'] ==
'1' ||
(_isDataAccuracyChecked ||
isLoading)
? Colors.grey
: const Color(

View File

@ -14,12 +14,15 @@ class ThemedUploadField extends StatefulWidget {
final Color? borderColor;
final Color? errorBorderColor;
final Function(String fileName, PlatformFile file)? onFileSelected;
final Function(List<String> fileNames, List<PlatformFile> files)?
onFilesSelected;
FormFieldValidator<String>? validator;
final List<String>? allowedExtensions;
final bool isTxtBtnCase;
final String? txtName;
final double? padVertical;
final double? padHorizontal;
final bool allowMultiple;
ThemedUploadField({
super.key,
@ -32,12 +35,14 @@ class ThemedUploadField extends StatefulWidget {
this.borderColor,
this.errorBorderColor,
this.onFileSelected,
this.onFilesSelected,
this.validator,
this.allowedExtensions,
this.isTxtBtnCase = false,
this.txtName,
this.padVertical,
this.padHorizontal,
this.allowMultiple = false,
});
@override
@ -47,6 +52,7 @@ class ThemedUploadField extends StatefulWidget {
class _ThemedUploadFieldState extends State<ThemedUploadField> {
final fileService = FileUploadService();
String? selectedFileName;
String? selectedMultipleFileNames;
String? errorMessage;
// @override
@ -62,16 +68,27 @@ class _ThemedUploadFieldState extends State<ThemedUploadField> {
// }
Future<void> _pickFile() async {
final error = await fileService.pickSingleFile(
// maxFileSizeInMB: 3,
allowedExtensions: widget.allowedExtensions,
);
final error = widget.allowMultiple
? await fileService.pickMultipleFiles(
allowedExtensions: widget.allowedExtensions,
)
: await fileService.pickSingleFile(
allowedExtensions: widget.allowedExtensions,
);
if (error != null) {
setState(() => errorMessage = error);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error)));
} else if (fileService.singleFile != null) {
} else if (widget.allowMultiple && fileService.multipleFiles.isNotEmpty) {
final names = fileService.multipleFiles.map((file) => file.name).toList();
setState(() {
selectedMultipleFileNames = names.join(', ');
errorMessage = null;
});
widget.onFilesSelected?.call(names, fileService.multipleFiles);
} else if (!widget.allowMultiple && fileService.singleFile != null) {
setState(() {
selectedFileName = fileService.singleFile!.name;
errorMessage = null;
@ -122,7 +139,9 @@ class _ThemedUploadFieldState extends State<ThemedUploadField> {
if (!widget.isTxtBtnCase) ...[
Expanded(
child: Text(
selectedFileName ??
(widget.allowMultiple
? selectedMultipleFileNames
: selectedFileName) ??
widget.hintText ??
"Upload Document",
style: TextStyle(

View File

@ -16,6 +16,7 @@ class ThemedDateField extends StatefulWidget {
this.controller,
this.validator,
this.lastDate, // new
this.firstDate,
});
final String? hintText;
@ -28,6 +29,7 @@ class ThemedDateField extends StatefulWidget {
final Function(DateTime)? onDateSelected;
final String? Function(String? value)? validator; // new
final DateTime? lastDate;
final DateTime? firstDate;
@override
State<ThemedDateField> createState() => _ThemedDateFieldState();
@ -48,7 +50,7 @@ class _ThemedDateFieldState extends State<ThemedDateField> {
final pickedDate = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: DateTime(2000),
firstDate: widget.firstDate ?? DateTime(2000),
// lastDate: DateTime(2100),
lastDate: widget.lastDate ?? DateTime(2100),
initialEntryMode: DatePickerEntryMode.calendarOnly,

View File

@ -18,6 +18,13 @@ import 'dart:io' show File; // Only used in mobile/desktop
import 'package:path_provider/path_provider.dart';
import 'package:open_file/open_file.dart';
/// Optional per-field Excel cell formatter. Use when raw map values differ from
/// on-screen display (dates, status labels, etc.). Key must match [keys] entries.
typedef ExportValueFormatter = String Function(
dynamic value,
Map<String, dynamic> row,
);
class ExportBtn extends HookWidget {
final String sheetName;
final String fileName;
@ -26,6 +33,8 @@ class ExportBtn extends HookWidget {
// final List<String> headers; // dynamic headers
final List<String>? keys; // dynamic headers
final bool? txt;
/// When set, these keys are exported as the returned string (not raw int/double).
final Map<String, ExportValueFormatter>? valueFormatters;
const ExportBtn({
super.key,
@ -37,6 +46,7 @@ class ExportBtn extends HookWidget {
// required this.headers,
required this.displayHeaders,
this.keys,
this.valueFormatters,
});
@override
@ -54,6 +64,7 @@ class ExportBtn extends HookWidget {
displayHeaders: displayHeaders!,
keys: keys!,
fileName: fileName,
valueFormatters: valueFormatters,
);
ToastHelper.showSuccessToast(context, 'Export finished');
} catch (e) {
@ -117,6 +128,7 @@ class ExcelExporter {
required List<String> displayHeaders,
required List<String> keys,
required String fileName,
Map<String, ExportValueFormatter>? valueFormatters,
}) async {
if (displayHeaders.length != keys.length) {
throw ArgumentError('displayHeaders and keys must have same length');
@ -169,6 +181,7 @@ class ExcelExporter {
// Reverse the data list so latest entries come first
final reversedData = data.reversed.toList();
final formatters = valueFormatters;
for (var i = 0; i < reversedData.length; i++) {
final rowMap = reversedData[i];
@ -180,6 +193,11 @@ class ExcelExporter {
if (key == 'sno' || key.toLowerCase() == 's.no' || key.toLowerCase() == 'sno.') {
value = i + 1;
} else if (formatters != null && formatters.containsKey(key)) {
final formatted = formatters[key]!(rowMap[key], rowMap);
final text = formatted.trim().isEmpty ? '-' : formatted;
row.add(TextCellValue(text));
continue;
} else if (key.toLowerCase() == 'is_active') {
final rawVal = rowMap[key];
value = (rawVal == 1 || rawVal == '1') ? 'Active' : 'Inactive';

View File

@ -6,6 +6,7 @@ class FileUploadService {
factory FileUploadService() => _instance;
PlatformFile? singleFile;
List<PlatformFile> multipleFiles = [];
/// Default allowed extensions
static const defaultAllowedExtensions = ['pdf', 'png', 'jpg', 'jpeg'];
@ -40,7 +41,35 @@ class FileUploadService {
return null; // success
}
Future<String?> pickMultipleFiles({
int maxFileSizeInMB = 5,
List<String>? allowedExtensions,
}) async {
final extensions = allowedExtensions ?? defaultAllowedExtensions;
final result = await FilePicker.platform.pickFiles(
allowMultiple: true,
withData: true,
type: FileType.custom,
allowedExtensions: extensions,
);
if (result != null && result.files.isNotEmpty) {
for (final file in result.files) {
final ext = file.extension?.toLowerCase() ?? '';
if (!extensions.contains(ext)) {
return "Unsupported format: ${file.name}";
}
}
multipleFiles = result.files;
}
return null;
}
void clearSingle() {
singleFile = null;
}
void clearMultiple() {
multipleFiles = [];
}
}

View File

@ -43,6 +43,12 @@ class DateFilterRow extends ConsumerStatefulWidget {
final ValueChanged<String?>? onStatusChanged;
final ValueChanged<String?>? onInsurerChanged;
final ValueChanged<String?>? onFilterStaff;
/// Staff row may include `agent_code` (e.g. payout sample Excel for manager/handler).
final ValueChanged<String?>? onStaffAgentCodeChanged;
/// Accounts + Policy: selected partner `id` from [agentListForEnquiryCreationDropdown].
final String? selectedPartnerId;
final ValueChanged<String?>? onPartnerChanged;
final ValueChanged<String?>? onPartnerAgentCodeChanged;
final String? dataFrom;
final VoidCallback onFilter;
final VoidCallback onRefresh;
@ -60,6 +66,10 @@ class DateFilterRow extends ConsumerStatefulWidget {
required this.onStatusChanged,
this.onInsurerChanged,
this.onFilterStaff,
this.onStaffAgentCodeChanged,
this.selectedPartnerId,
this.onPartnerChanged,
this.onPartnerAgentCodeChanged,
required this.selectedStaffId,
this.selectedStatusVal,
this.selectedInsurerVal,
@ -83,6 +93,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
Map<String, dynamic>? selectedInsurerData;
List<Map<String, dynamic>> filteredInsurerData = [];
List<Map<String, dynamic>> getInsurerDetailsData = [];
List<Map<String, dynamic>> filteredPartnerData = [];
String? selectedInsurer;
String? selectedStaff;
@ -92,6 +103,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
bool isLoadingA = false;
bool isLoading = false;
bool _partnerFetchAttempted = false;
@override
void initState() {
@ -110,6 +122,11 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
}
print('***Insurer***');
getInsurerDetails();
if (widget.role == 'Accounts' &&
widget.dataFrom == 'Policy' &&
managerId != null) {
getPartnerListForAccountsPolicy();
}
});
// Future.microtask(() {
@ -190,8 +207,45 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
}
}
Future<void> getPartnerListForAccountsPolicy() async {
if (widget.role != 'Accounts' || widget.dataFrom != 'Policy') return;
if (_partnerFetchAttempted) return;
_partnerFetchAttempted = true;
final mid = managerId ?? ref.read(managerIdProvider);
if (mid == null) return;
final managerIdInt = mid is int ? mid : int.tryParse(mid.toString());
if (managerIdInt == null) return;
try {
final response = await apiService.fetchAgentUserList(managerIdInt);
if (response['status'] == 'success' && response['data'] != null) {
setState(() {
filteredPartnerData = List<Map<String, dynamic>>.from(
response['data'],
);
});
} else {
setState(() => filteredPartnerData = []);
}
} catch (e) {
print('getPartnerListForAccountsPolicy: $e');
setState(() => filteredPartnerData = []);
}
}
@override
Widget build(BuildContext context) {
if (widget.role == 'Accounts' &&
widget.dataFrom == 'Policy' &&
filteredPartnerData.isEmpty &&
!_partnerFetchAttempted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
getPartnerListForAccountsPolicy();
}
});
}
final spacing = 5.0;
// final rowChildren = [
@ -230,7 +284,9 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
buildSelectStaffMem(context),
SizedBox(width: spacing),
],
if (widget.role == 'Accounts' && widget.dataFrom == 'Policy') ...[
if (widget.role == 'Accounts' && widget.dataFrom == 'Policy') ...[
buildSelectPartnerAccountsPolicy(context),
SizedBox(width: spacing),
buildPolicyReportFlagSearch(context),
SizedBox(width: spacing),
buildSelectInsurer(context),
@ -669,6 +725,148 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
);
}
/// Searchable partner (agent) list for **Accounts** on **Policy** `agent/agentList` via [ApiService.fetchAgentUserList].
Widget buildSelectPartnerAccountsPolicy(BuildContext context) {
if (widget.role != 'Accounts' || widget.dataFrom != 'Policy') {
return const SizedBox.shrink();
}
Map<String, dynamic>? selectedPartner;
final pid = widget.selectedPartnerId;
if (pid != null &&
pid.toString().isNotEmpty &&
filteredPartnerData.isNotEmpty) {
try {
selectedPartner = filteredPartnerData.firstWhere(
(e) => e['id'].toString() == pid.toString(),
);
} catch (_) {
selectedPartner = null;
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 35,
child: Container(
width: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.14,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: DropdownSearch<Map<String, dynamic>>(
selectedItem: selectedPartner,
items: (filter, infiniteScrollProps) async {
if (filter.isEmpty) return filteredPartnerData;
final q = filter.toLowerCase();
return filteredPartnerData.where((item) {
final name = item['name']?.toString().toLowerCase() ?? '';
final code =
item['agent_code']?.toString().toLowerCase() ?? '';
return name.contains(q) || code.contains(q);
}).toList();
},
itemAsString: (val) => val['name'].toString(),
compareFn: (item, selectedItem) =>
item['id'].toString() == selectedItem['id'].toString(),
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem != null ? selectedItem['name'].toString() : '',
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: 'Partner Name',
).copyWith(
filled: true,
fillColor: Colors.white,
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 1,
),
),
),
popupProps: PopupProps.menu(
fit: FlexFit.loose,
constraints: const BoxConstraints(maxHeight: 280),
menuProps: const MenuProps(backgroundColor: Colors.white),
showSearchBox: true,
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: 'Partner Name',
hintStyle: GoogleFonts.inter(
fontSize: 10,
color: Colors.grey,
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white, width: 1.5),
),
),
),
itemBuilder: (context, item, isDisabled, isSelected) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
(item['name'] ?? '').toString().toUpperCase(),
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
Text(
(item['agent_code'] ?? '').toString().toUpperCase(),
style: GoogleFonts.inter(
fontSize: 11,
color: Colors.grey,
),
),
],
),
);
},
),
onChanged: (val) {
if (val != null) {
widget.onPartnerChanged?.call(val['id']?.toString());
widget.onPartnerAgentCodeChanged?.call(
val['agent_code']?.toString(),
);
Future.microtask(() => widget.onFilter());
} else {
widget.onPartnerChanged?.call(null);
widget.onPartnerAgentCodeChanged?.call(null);
Future.microtask(() => widget.onFilter());
}
},
),
),
),
],
);
}
Widget buildSelectStaffMem(BuildContext context) {
// Map<String, dynamic>? selectedVehicle = filteredStaffData.firstWhere(
// (item) => item['id'].toString() == selectedStaff,
@ -819,9 +1017,15 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
if (widget.onFilterStaff != null)
widget.onFilterStaff!(val['id']);
widget.onStaffAgentCodeChanged?.call(
val['agent_code']?.toString(),
);
Future.microtask(() => widget.onFilter());
// controllers['agentId']?.text = val['agent_code'];
// agentId = agent['id'];
} else {
widget.onStaffAgentCodeChanged?.call(null);
}
},
),

View File

@ -2,8 +2,9 @@ import 'package:flutter/material.dart';
class DrawerContentWrapper extends StatelessWidget {
final Widget child;
final bool isActive;
const DrawerContentWrapper({super.key, required this.child});
const DrawerContentWrapper({super.key, required this.child,this.isActive = false});
@override
Widget build(BuildContext context) {
@ -15,7 +16,7 @@ class DrawerContentWrapper extends StatelessWidget {
width: 55,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: const Color(0xFFB2D8D3),
color: isActive ? const Color(0xFF0ABFA3) : const Color(0xFFB2D8D3),
),
child: Center(child: child),
),

View File

@ -17,7 +17,13 @@ class DrawerMenu extends ConsumerStatefulWidget {
class DrawerMenuState extends ConsumerState<DrawerMenu> {
OverlayEntry? _overlayEntry;
void _showPopup(BuildContext context, Offset offset, Size size, String key) {
void _showPopup(
BuildContext context,
Offset offset,
Size size,
String key,
dynamic roleId,
) {
_hidePopup(); // remove previous popup
_overlayEntry = OverlayEntry(
@ -52,6 +58,19 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
),
],
if (key == 'Reports') ...[
if (roleId == 'manager' ||
roleId == 'Accounts' ||
roleId == 'agent') ...[
_buildPopupItem(
label: "Grid List",
onTap: () => context.go(
roleId == 'agent'
? AppRoutes.gridView
: AppRoutes.gridList,
),
),
const SizedBox(height: 5),
],
_buildPopupItem(
label: "Claims",
onTap: () => context.go(AppRoutes.claimlist),
@ -94,6 +113,7 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
@override
Widget build(BuildContext context) {
final roleId = ref.watch(userRoleProvider);
final currentPath = GoRouterState.of(context).matchedLocation;
print('RoleId - $roleId');
return Container(
@ -113,6 +133,9 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
width: 25,
),
label: "Dashboard",
isActive:
currentPath == AppRoutes.dashboard ||
currentPath == AppRoutes.partnerPortalDashboard,
onTap: () async {
_hidePopup();
@ -120,7 +143,10 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
await prefs.remove('dashboardKeyProvider');
await prefs.remove('dashboardStatusProvider');
await prefs.remove('dashboardStaffIdProvider');
context.go(AppRoutes.dashboard);
final dashboardRoute = roleId == 'agent'
? AppRoutes.partnerPortalDashboard
: AppRoutes.dashboard;
context.go(dashboardRoute);
},
),
@ -169,6 +195,7 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
popupKey: 'Reports',
),
if (roleId == 'manager')
_buildMenuItem(
context: context,
@ -187,6 +214,7 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
required BuildContext context,
required Widget icon,
required String label,
bool isActive = false,
required VoidCallback onTap,
}) {
return Column(
@ -195,10 +223,10 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
onTap: onTap,
child: MouseRegion(
onEnter: (_) => _hidePopup(), // hide any popup when hovering
child: DrawerContentWrapper(child: icon),
child: DrawerContentWrapper(child: icon, isActive: isActive),
),
),
DrawerLabel(label),
DrawerLabel(label, isActive: isActive),
],
);
}
@ -218,7 +246,8 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
final renderBox = itemContext.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
_showPopup(context, offset, size, popupKey);
final roleId = ref.read(userRoleProvider);
_showPopup(context, offset, size, popupKey, roleId);
},
child: DrawerContentWrapper(child: icon),
);
@ -233,7 +262,8 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
// Drawer label widget
class DrawerLabel extends StatelessWidget {
final String text;
const DrawerLabel(this.text, {super.key});
final bool isActive;
const DrawerLabel(this.text, {super.key, this.isActive = false});
@override
Widget build(BuildContext context) {
@ -245,7 +275,11 @@ class DrawerLabel extends StatelessWidget {
children: parts.map((line) {
return Text(
line,
style: GoogleFonts.inter(color: Colors.white, fontSize: 11.5),
style: GoogleFonts.inter(
color: isActive ? const Color(0xFF0ABFA3) : Colors.white,
fontSize: 11.5,
fontWeight: isActive ? FontWeight.w700 : FontWeight.w400,
),
textAlign: TextAlign.center,
);
}).toList(),

View File

@ -17,7 +17,13 @@ class DrawerMenu extends ConsumerStatefulWidget {
class DrawerMenuState extends ConsumerState<DrawerMenu> {
OverlayEntry? _overlayEntry;
void _showPopup(BuildContext context, Offset offset, Size size, String key) {
void _showPopup(
BuildContext context,
Offset offset,
Size size,
String key,
dynamic roleId,
) {
_hidePopup(); // clear old one before creating new
_overlayEntry = OverlayEntry(
@ -56,6 +62,19 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
),
],
if (key == 'Reports') ...[
if (roleId == 'manager' ||
roleId == 'Accounts' ||
roleId == 'agent') ...[
_buildPopupItem(
label: "Grid List",
onTap: () => context.go(
roleId == 'agent'
? AppRoutes.gridView
: AppRoutes.gridList,
),
),
const SizedBox(height: 5),
],
_buildPopupItem(
label: "Claims",
onTap: () => context.go(AppRoutes.claimlist),
@ -256,7 +275,7 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
final key = 'User';
_showPopup(context, offset, size, key);
_showPopup(context, offset, size, key, roleId);
},
child: DrawerContentWrapper(
@ -279,7 +298,7 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
final key = "Reports";
_showPopup(context, offset, size, key);
_showPopup(context, offset, size, key, roleId);
},
child: DrawerContentWrapper(
@ -294,6 +313,7 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
),
DrawerLabel("Reports"),
],
],
),
);

View File

@ -64,7 +64,10 @@ class _MobileBottomMenuState extends ConsumerState<MobileBottomMenu> {
switch (index) {
case 0:
context.go(AppRoutes.dashboard);
final dashboardRoute = roleId == 'agent'
? AppRoutes.partnerPortalDashboard
: AppRoutes.dashboard;
context.go(dashboardRoute);
break;
case 1:

View File

@ -14,9 +14,9 @@
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<!-- <base href="$FLUTTER_BASE_HREF"> -->
<!-- <base href="/partner/"> Live build: also check env.dart (line 8) -->
<base href="/nhance/partner/app/"> <!-- Testing build: also check env.dart (line 9) -->
<!-- <base href="$FLUTTER_BASE_HREF"> -->
<!-- <base href="/partner/"> <!- Live build: also check env.dart (line 8) -->
<base href="/nhance/partner/app/">
<!-- <base href="{Env.baseHref}">-->
<meta charset="UTF-8">