FEAT_PAYOUT REWORKS
This commit is contained in:
parent
d94a16cc4a
commit
9a6685382a
File diff suppressed because one or more lines are too long
@ -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;
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
267
lib/presentation/screens/Grid/gridList.dart
Normal file
267
lib/presentation/screens/Grid/gridList.dart
Normal 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),
|
||||
);
|
||||
884
lib/presentation/screens/Grid/gridUpload.dart
Normal file
884
lib/presentation/screens/Grid/gridUpload.dart
Normal 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'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
5410
lib/presentation/screens/Grid/gridView.dart
Normal file
5410
lib/presentation/screens/Grid/gridView.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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();
|
||||
|
||||
788
lib/presentation/screens/dashboard/partner_portal_dashboard.dart
Normal file
788
lib/presentation/screens/dashboard/partner_portal_dashboard.dart
Normal 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));
|
||||
@ -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');
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -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
1962
lib/presentation/screens/payout/payout_details.dart
Normal file
1962
lib/presentation/screens/payout/payout_details.dart
Normal file
File diff suppressed because it is too large
Load Diff
1852
lib/presentation/screens/payout/payout_list.dart
Normal file
1852
lib/presentation/screens/payout/payout_list.dart
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@ -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),
|
||||
),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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"),
|
||||
],
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user