From 66148a91dc1f4195b2623e0897d7a69c2c70b2c6 Mon Sep 17 00:00:00 2001 From: SurendarSuri30 Date: Wed, 5 Aug 2026 09:49:23 +0530 Subject: [PATCH] add dependent flow --- android/app/build.gradle | 9 +- lib/pages/postEnrollment/policies.dart | 1109 +++++++++++++++-- .../postEnrollment/service/api_service.dart | 63 + pubspec.yaml | 4 +- 4 files changed, 1094 insertions(+), 91 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ebec86e..f75395b 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -19,12 +19,12 @@ if (project.hasProperty('google-services.json')) { def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { - flutterVersionCode = '70' + flutterVersionCode = '71' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '2.0.31' + flutterVersionName = '2.0.32' } def keystoreProperties = new Properties() @@ -35,7 +35,8 @@ if (keystorePropertiesFile.exists()) { android { namespace "nh.ind.nhance.benefits" - compileSdk flutter.compileSdkVersion + // Google Play requires target API 36 (Android 16) as of Aug 31, 2026 + compileSdk 36 ndkVersion flutter.ndkVersion compileOptions { @@ -66,7 +67,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion + targetSdkVersion 36 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/lib/pages/postEnrollment/policies.dart b/lib/pages/postEnrollment/policies.dart index 51f8116..11c272c 100755 --- a/lib/pages/postEnrollment/policies.dart +++ b/lib/pages/postEnrollment/policies.dart @@ -8,7 +8,6 @@ import 'package:jwt_decode/jwt_decode.dart'; import 'package:nhance_app_pwa/customAppBar/customAppBar.dart'; import 'package:nhance_app_pwa/pages/postEnrollment/service/api_service.dart'; import 'package:nhance_app_pwa/pages/postEnrollment/service/svg_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../customAppBar/customFooter.dart'; import '../helpers/custom_download_snackbar.dart'; @@ -57,6 +56,19 @@ class _policiesState extends State { bool ECardHide = true; bool _isExpanded = false; bool _isDownloadingEcard = false; + bool _isLoadingEmployeePolicy = false; + bool _employeePolicyFetched = false; + bool _addButtonShow = false; + List _mappedFamilyFloaters = []; + List _allowedRelationships = []; + dynamic _basicCoverSi; + Map? _employeePolicyData; + bool _argsInitialized = false; + + final TextEditingController _relationShipController = TextEditingController(); + final TextEditingController _memberNameController = TextEditingController(); + final TextEditingController _dobController = TextEditingController(); + final TextEditingController _effectiveDateController = TextEditingController(); void _onTabChanged(int index) { setState(() { @@ -85,6 +97,10 @@ class _policiesState extends State { @override void dispose() { + _relationShipController.dispose(); + _memberNameController.dispose(); + _dobController.dispose(); + _effectiveDateController.dispose(); super.dispose(); } @@ -92,18 +108,782 @@ class _policiesState extends State { logDebug('_loadToken'); final String? token = await TokenService.getPostToken(); _token = token; - final SharedPreferences prefs = await SharedPreferences.getInstance(); - if (token != null && token.isNotEmpty) { - // Decode the JWT token received from the API response - mobileNo = session.mobileNo; - client_branch_id = session.empClientBranchId; - empCodeString = session.empCodeString; - empName = session.gpaEmpName; - logDebug(empCodeString); // Check if emp_code is correct - empPrimaryId = session.empPrimaryId; - client_id = session.client_id; + if (token != null && token.isNotEmpty) { + mobileNo = session.mobileNo; + client_branch_id = session.empClientBranchId; + empCodeString = session.empCodeString; + empName = session.gpaEmpName; + logDebug(empCodeString); + empPrimaryId = session.empPrimaryId; + client_id = session.client_id; + await _fetchEmployeePolicyIfNeeded(); + } + } + + bool _isGmcOrOpdPolicy(String? policyType) { + final type = (policyType ?? '').trim().toUpperCase(); + return type == 'GMC' || type == 'OPD' || type == 'GMC - OPD'; + } + + void _applyEmployeePolicyList(List? policyList) { + employeeDetails.clear(); + if (policyList == null) return; + for (final employee in policyList) { + if (employee is! Map) continue; + employeeDetails.add({ + 'id': employee['id']?.toString() ?? + employee['employee_id']?.toString() ?? + '', + 'name': employee['name']?.toString() ?? '', + 'relationship': employee['relationship']?.toString() ?? '', + 'dob': employee['dob']?.toString() ?? '', + 'gender': employee['gender']?.toString() ?? '', + if (employee['emp_status'] != null) + 'emp_status': employee['emp_status'].toString(), + if (employee['status'] != null) 'status': employee['status'].toString(), + if (employee['basic_cover_si'] != null) + 'basic_cover_si': employee['basic_cover_si'].toString(), + if (employee['dependent_effective_date'] != null) + 'dependent_effective_date': + employee['dependent_effective_date'].toString(), + if (employee['date_coverage'] != null) + 'date_coverage': employee['date_coverage'].toString(), + }); + } + sortEmployeeDetails(employeeDetails); + } + + Future _fetchEmployeePolicyIfNeeded({bool force = false}) async { + final args = widget.arguments; + if (args == null || !_isGmcOrOpdPolicy(args['policy_type']?.toString())) { + return; + } + if (!force && (_isLoadingEmployeePolicy || _employeePolicyFetched)) return; + + final clientPolicyId = args['client_policy_id']?.toString(); + if (empPrimaryId == null || + empCodeString == null || + client_id == null || + client_branch_id == null || + clientPolicyId == null || + clientPolicyId.isEmpty) { + logDebug('getEmployeePolicy skipped: missing required params'); + return; + } + + if (!mounted) return; + setState(() { + _isLoadingEmployeePolicy = true; + if (force) _employeePolicyFetched = false; + }); + + try { + final response = await apiService.getEmployeePolicy( + id: empPrimaryId.toString(), + empCode: empCodeString.toString(), + clientId: client_id.toString(), + clientBranchId: client_branch_id.toString(), + clientPolicyId: clientPolicyId, + ); + + if (!mounted) return; + + if (response['status'] == 'success') { + final data = response['data']; + setState(() { + EmployeePolicy = response['EmployeePolicy'] ?? []; + _addButtonShow = response['add_button_show'] == true; + if (data is Map) { + _employeePolicyData = Map.from(data); + _mappedFamilyFloaters = data['mapped_family_floaters'] is List + ? List.from(data['mapped_family_floaters']) + : []; + _allowedRelationships = data['allowed_relationships'] is List + ? List.from(data['allowed_relationships']) + : (data['relationship'] is List + ? List.from(data['relationship']) + : []); + _basicCoverSi = _resolveBasicCoverSi(data); + } + _applyEmployeePolicyList( + EmployeePolicy is List ? EmployeePolicy as List : null, + ); + _employeePolicyFetched = true; + _isLoadingEmployeePolicy = false; + }); + logDebug('getEmployeePolicy success; add_button_show=$_addButtonShow'); + } else { + setState(() { + _isLoadingEmployeePolicy = false; + _employeePolicyFetched = true; + }); + final message = response['message']?.toString() ?? + response['data']?.toString() ?? + 'Failed to load employee policy'; + ToastHelper.showErrorToast(context, message); + logDebug('getEmployeePolicy failed: $message'); + } + } catch (e) { + logDebug('getEmployeePolicy exception: $e'); + if (mounted) { + setState(() { + _isLoadingEmployeePolicy = false; + _employeePolicyFetched = true; + }); + ToastHelper.showErrorToast( + context, + 'Unable to load dependent details. Please try again.', + ); + } + } + } + + dynamic _resolveBasicCoverSi(Map data) { + if (data['family_floaters_of_dependent_and_si_value'] != null) { + return data['family_floaters_of_dependent_and_si_value']; + } + if (data['si_value'] != null) return data['si_value']; + if (data['sum_insured'] != null) return data['sum_insured']; + if (data['Policy_Terms'] is Map && + data['Policy_Terms']['sum_insured'] != null) { + return data['Policy_Terms']['sum_insured']; + } + final floaters = data['mapped_family_floaters']; + if (floaters is List) { + for (final floater in floaters) { + if (floater is Map && + floater['is_value_exist'] == true && + floater['data'] is Map && + floater['data']['basic_cover_si'] != null) { + return floater['data']['basic_cover_si']; + } + } + } + return argumentsData != null ? argumentsData['si_value'] : null; + } + + bool _isSpouseRelationship(String? relationship) => + (relationship ?? '').trim().toLowerCase() == 'spouse'; + + bool _isChildRelationship(String? relationship) { + final value = (relationship ?? '').trim().toLowerCase(); + return value == 'son' || value == 'daughter' || value == 'child'; + } + + bool _isWithinLast30Days(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final selected = DateTime(date.year, date.month, date.day); + final diff = today.difference(selected).inDays; + return !selected.isAfter(today) && diff <= 30; + } + + String _thirtyDayRuleMessage(String? relationship) { + if (_isSpouseRelationship(relationship)) { + return 'A dependent can only be added within 30 days of the Marriage Date'; + } + return 'A dependent can only be added within 30 days of the Birth Date'; + } + + /// Relationship options from empty family slots only (`is_value_exist == false`). + List> _availableSlotRelationships() { + final result = >[]; + final seen = {}; + + for (final floater in _mappedFamilyFloaters) { + if (floater is! Map || floater['is_value_exist'] != false) continue; + final data = floater['data']; + if (data is! Map) continue; + + final List relationships = []; + if (data['allowed_relationships'] is List) { + for (final item in data['allowed_relationships'] as List) { + final name = item?.toString().trim() ?? ''; + if (name.isNotEmpty) relationships.add(name); + } + } else { + final formType = (data['form_type'] ?? '').toString().toLowerCase(); + if (formType == 'spouse') { + relationships.add('Spouse'); + } else if (formType == 'child') { + relationships.addAll(['Son', 'Daughter']); + } + } + + for (final relationship in relationships) { + final key = relationship.toLowerCase(); + if (seen.contains(key)) continue; + seen.add(key); + result.add({ + 'relationship': relationship, + 'age_validation': + data['age_validation'] ?? {'min': '0', 'max': '99'}, + 'form_type': data['form_type'], + 'family_floater_key': data['family_floater_key'], + 'client_policy_id': data['client_policy_id'], + 'basic_cover_si': data['basic_cover_si'] ?? _basicCoverSi, + }); + } + } + return result; + } + + String _formatDateForApi(DateTime date) => + DateFormat('dd/MM/yyyy').format(date); + + DateTime? _parseApiDate(String raw) { + if (raw.trim().isEmpty) return null; + try { + if (raw.contains('/')) { + return DateFormat('dd/MM/yyyy').parseStrict(raw); + } + if (raw.contains('-') && raw.length == 10 && raw[4] == '-') { + return DateTime.parse(raw); + } + if (raw.contains('-')) { + return DateFormat('dd-MM-yyyy').parseStrict(raw); + } + return DateFormat('d MMM yyyy').parseStrict(raw); + } catch (_) { + return null; + } + } + + String _normalizeDobForDisplay(String raw) { + final parsed = _parseApiDate(raw); + if (parsed == null) return raw; + return _formatDateForApi(parsed); + } + + Future _pickDate({ + required TextEditingController controller, + Map? relationshipData, + bool isMarriageDate = false, + bool restrictToLastThirtyDays = false, + String? thirtyDayToastMessage, + }) async { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + DateTime initial = today; + DateTime first = DateTime(1900); + DateTime last = today; + + if (isMarriageDate || restrictToLastThirtyDays) { + // Spouse marriage date / Child DOB: only allow last 30 days. + first = today.subtract(const Duration(days: 30)); + last = today; + initial = today; + } else if (relationshipData != null) { + final age = relationshipData['age_validation']; + if (age is Map) { + final min = int.tryParse(age['min']?.toString() ?? '') ?? 0; + final max = int.tryParse(age['max']?.toString() ?? '') ?? 100; + last = DateTime(now.year - min, now.month, now.day); + first = DateTime(now.year - max, now.month, now.day); + initial = last; + } + } + + if (controller.text.isNotEmpty) { + final existing = _parseApiDate(controller.text); + if (existing != null) initial = existing; + } + + final picked = await showDatePicker( + context: context, + initialDate: initial.isBefore(first) + ? first + : (initial.isAfter(last) ? last : initial), + firstDate: first, + lastDate: last.isBefore(first) ? first : last, + ); + if (picked == null) return; + + if ((isMarriageDate || restrictToLastThirtyDays) && + !_isWithinLast30Days(picked)) { + ToastHelper.showWarningToast( + context, + thirtyDayToastMessage ?? + (isMarriageDate + ? 'A dependent can only be added within 30 days of the Marriage Date' + : 'A dependent can only be added within 30 days of the Birth Date'), + ); + return; + } + + controller.text = _formatDateForApi(picked); + } + + void _onAddFamilyMember() { + _openDependentForm(action: 'Add'); + } + + void _onEditFamilyMember(Map detail) { + _openDependentForm(action: 'Edit', detail: detail); + } + + String _resolveCoverageDateForDetail(Map detail) { + final directCoverageDate = detail['date_coverage']?.toString().trim() ?? ''; + if (directCoverageDate.isNotEmpty) return directCoverageDate; + + final directDependentDate = + detail['dependent_effective_date']?.toString().trim() ?? ''; + if (directDependentDate.isNotEmpty) return directDependentDate; + + final detailId = detail['id']?.toString().trim() ?? ''; + final detailRelationship = + (detail['relationship']?.toString().trim().toLowerCase()) ?? ''; + + for (final floater in _mappedFamilyFloaters) { + if (floater is! Map || floater['is_value_exist'] != true) continue; + final data = floater['data']; + if (data is! Map) continue; + + final floaterId = data['employee_id']?.toString().trim() ?? ''; + final floaterRelationship = + (data['relationship']?.toString().trim().toLowerCase()) ?? ''; + + final idMatches = detailId.isNotEmpty && floaterId == detailId; + final relationMatches = + detailRelationship.isNotEmpty && floaterRelationship == detailRelationship; + if (!idMatches && !relationMatches) continue; + + final coverage = data['date_coverage']?.toString().trim() ?? ''; + if (coverage.isNotEmpty) return coverage; + + final dependentDate = + data['dependent_effective_date']?.toString().trim() ?? ''; + if (dependentDate.isNotEmpty) return dependentDate; + } + + return ''; + } + + Future _onDeleteFamilyMember(Map detail) async { + final id = detail['id'] ?? ''; + final relationship = (detail['relationship'] ?? '').toLowerCase(); + if (id.isEmpty || relationship == 'self') { + ToastHelper.showWarningToast(context, 'Cannot delete this member'); + return; + } + + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text( + 'Delete Dependent', + style: GoogleFonts.poppins(fontWeight: FontWeight.w600), + ), + content: Text( + 'Are you sure you want to delete ${detail['name']}?', + style: GoogleFonts.poppins(fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancel'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE26728), + ), + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Delete', style: TextStyle(color: Colors.white)), + ), + ], + ), + ); + + if (confirmed != true) return; + + try { + final response = await apiService.deleteDependence(id); + if (!mounted) return; + if (response['status'] == 'success') { + ToastHelper.showSuccessToast( + context, + response['message']?.toString() ?? 'Dependent deleted successfully', + ); + await _fetchEmployeePolicyIfNeeded(force: true); + } else { + ToastHelper.showErrorToast( + context, + response['message']?.toString() ?? + response['data']?.toString() ?? + 'Failed to delete dependent', + ); + } + } catch (e) { + logDebug('deleteDependence exception: $e'); + if (mounted) { + ToastHelper.showErrorToast( + context, + 'Unable to delete dependent. Please try again.', + ); + } + } + } + + void _openDependentForm({ + required String action, + Map? detail, + }) { + final relationshipObjects = action == 'Add' + ? _availableSlotRelationships() + : >[]; + + String? dropdownValue; + Map? selectedRelationship; + + _relationShipController.clear(); + _memberNameController.clear(); + _dobController.clear(); + _effectiveDateController.clear(); + + if (action == 'Edit' && detail != null) { + dropdownValue = detail['relationship']; + _relationShipController.text = detail['relationship'] ?? ''; + _memberNameController.text = detail['name'] ?? ''; + _dobController.text = _normalizeDobForDisplay(detail['dob'] ?? ''); + if (_isSpouseRelationship(dropdownValue)) { + final coverageDate = _resolveCoverageDateForDetail(detail); + _effectiveDateController.text = _normalizeDobForDisplay( + coverageDate, + ); + } + + selectedRelationship = { + 'relationship': dropdownValue, + 'age_validation': {'min': '0', 'max': '99'}, + 'basic_cover_si': detail['basic_cover_si'] ?? _basicCoverSi, + }; + relationshipObjects.add(selectedRelationship); + } + + if (relationshipObjects.isEmpty && action == 'Add') { + ToastHelper.showWarningToast( + context, + 'No family slots available to add', + ); + return; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + bool isSaving = false; + return StatefulBuilder( + builder: (context, setDialogState) { + final isSpouse = _isSpouseRelationship(dropdownValue); + final isChild = _isChildRelationship(dropdownValue); + + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: SizedBox( + width: Responsive.isDesktop(context) ? 500 : 350, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + action == 'Edit' + ? 'Edit Family Member' + : 'Add Family Member', + style: GoogleFonts.poppins( + fontSize: 18, + fontWeight: FontWeight.bold, + color: const Color(0xFFE26728), + ), + ), + const SizedBox(height: 20), + DropdownButtonFormField( + value: dropdownValue, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Relationship', + ), + items: relationshipObjects.map((item) { + final value = item['relationship']?.toString() ?? ''; + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: action == 'Edit' + ? null + : (value) { + setDialogState(() { + dropdownValue = value; + _relationShipController.text = value ?? ''; + selectedRelationship = + relationshipObjects.firstWhere( + (item) => item['relationship'] == value, + ); + _dobController.clear(); + _effectiveDateController.clear(); + }); + }, + ), + const SizedBox(height: 15), + TextFormField( + controller: _memberNameController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Member Name As Per Govt Id Proof', + ), + ), + const SizedBox(height: 15), + TextFormField( + controller: _dobController, + readOnly: true, + onTap: isSaving || dropdownValue == null + ? null + : () async { + await _pickDate( + controller: _dobController, + relationshipData: selectedRelationship, + restrictToLastThirtyDays: isChild, + ); + setDialogState(() {}); + }, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'Date of Birth', + suffixIcon: IconButton( + icon: const Icon(Icons.calendar_today), + onPressed: isSaving || dropdownValue == null + ? null + : () async { + await _pickDate( + controller: _dobController, + relationshipData: selectedRelationship, + restrictToLastThirtyDays: isChild, + ); + setDialogState(() {}); + }, + ), + ), + ), + if (isSpouse) ...[ + const SizedBox(height: 15), + TextFormField( + controller: _effectiveDateController, + readOnly: true, + onTap: isSaving + ? null + : () async { + await _pickDate( + controller: _effectiveDateController, + isMarriageDate: true, + thirtyDayToastMessage: + _thirtyDayRuleMessage(dropdownValue), + ); + setDialogState(() {}); + }, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'Date of Wedding', + suffixIcon: IconButton( + icon: const Icon(Icons.calendar_today), + onPressed: isSaving + ? null + : () async { + await _pickDate( + controller: _effectiveDateController, + isMarriageDate: true, + thirtyDayToastMessage: + _thirtyDayRuleMessage( + dropdownValue), + ); + setDialogState(() {}); + }, + ), + ), + ), + ], + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: isSaving + ? null + : () => Navigator.pop(dialogContext), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 10), + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE26728), + ), + onPressed: isSaving + ? null + : () async { + if (dropdownValue == null || + _memberNameController.text + .trim() + .isEmpty || + _dobController.text.trim().isEmpty || + (isSpouse && + _effectiveDateController.text + .trim() + .isEmpty)) { + ToastHelper.showWarningToast( + context, + 'All fields are required', + ); + return; + } + + final dob = _parseApiDate( + _dobController.text.trim()); + if (isChild && + (dob == null || + !_isWithinLast30Days(dob))) { + ToastHelper.showWarningToast( + context, + _thirtyDayRuleMessage(dropdownValue), + ); + return; + } + + if (isSpouse) { + final marriageDate = _parseApiDate( + _effectiveDateController.text.trim(), + ); + if (marriageDate == null || + !_isWithinLast30Days( + marriageDate)) { + ToastHelper.showWarningToast( + context, + _thirtyDayRuleMessage( + dropdownValue), + ); + return; + } + } + + setDialogState(() => isSaving = true); + final saved = await _saveDependent( + action: action, + relationship: dropdownValue!, + detail: detail, + selectedSlot: selectedRelationship, + ); + if (!dialogContext.mounted) return; + setDialogState(() => isSaving = false); + if (saved) { + Navigator.pop(dialogContext); + } + }, + child: isSaving + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + 'Save', + style: TextStyle(color: Colors.white), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + } + + Future _saveDependent({ + required String action, + required String relationship, + Map? detail, + Map? selectedSlot, + }) async { + final clientPolicyId = selectedSlot?['client_policy_id']?.toString() ?? + _employeePolicyData?['ClientPolicyId']?.toString() ?? + widget.arguments?['client_policy_id']?.toString() ?? + ''; + if (empCodeString == null || + client_id == null || + client_branch_id == null || + clientPolicyId.isEmpty) { + ToastHelper.showErrorToast(context, 'Missing required policy details'); + return false; + } + + final si = selectedSlot?['basic_cover_si'] ?? + detail?['basic_cover_si'] ?? + _basicCoverSi ?? + argumentsData?['si_value'] ?? + 0; + + final dependentEffectiveDate = _isSpouseRelationship(relationship) + ? _effectiveDateController.text.trim() + : _dobController.text.trim(); + + final body = { + 'emp_code': empCodeString, + 'client_id': int.tryParse(client_id.toString()) ?? client_id, + 'client_branch_id': + int.tryParse(client_branch_id.toString()) ?? client_branch_id, + 'client_policy_id': int.tryParse(clientPolicyId) ?? clientPolicyId, + 'name': _memberNameController.text.trim(), + 'relationship': relationship, + 'dob': _dobController.text.trim(), + 'basic_cover_si': int.tryParse(si.toString()) ?? si, + 'dependent_effective_date': dependentEffectiveDate, + if (action == 'Edit' && (detail?['id']?.isNotEmpty ?? false)) + 'id': int.tryParse(detail!['id']!) ?? detail['id'], + }; + + try { + final response = await apiService.addEmployeeAndDependence([body]); + if (!mounted) return false; + + if (response['status'] == 'success') { + ToastHelper.showSuccessToast( + context, + response['message']?.toString() ?? + 'Dependent submitted for approval', + ); + await _fetchEmployeePolicyIfNeeded(force: true); + return true; + } + + ToastHelper.showErrorToast( + context, + response['message']?.toString() ?? + response['data']?.toString() ?? + 'Failed to save dependent', + ); + return false; + } catch (e) { + logDebug('addEmployeeAndDependence exception: $e'); + if (mounted) { + ToastHelper.showErrorToast( + context, + 'Unable to save dependent. Please try again.', + ); + } + return false; + } } - } Future getEcardDownload(id) async { if (_isDownloadingEcard) return; @@ -224,6 +1004,10 @@ class _policiesState extends State { String formatDob(String dob) { try { + if (dob.contains('/')) { + final date = DateFormat('dd/MM/yyyy').parseStrict(dob); + return DateFormat('d MMM yyyy').format(date); + } final date = DateTime.parse(dob); // input format: yyyy-MM-dd return DateFormat("d MMM yyyy").format(date); // output: 18 Jul 1988 } catch (e) { @@ -259,8 +1043,8 @@ class _policiesState extends State { return DateTime(1900); // fallback } - List> sortEmployeeDetails( - List> list) { + List> sortEmployeeDetails( + List> list) { int priority(String r) { final rel = r.toLowerCase(); @@ -276,30 +1060,17 @@ class _policiesState extends State { return 99; } - DateTime parseDob(String dob) { - final parts = dob.split(' '); - final day = int.parse(parts[0]); - final monthMap = { - 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, - 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, - 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12, - }; - final month = monthMap[parts[1]]!; - final year = int.parse(parts[2]); - return DateTime(year, month, day); - } - list.sort((a, b) { - final p1 = priority(a['relationship']); - final p2 = priority(b['relationship']); + final p1 = priority(a['relationship'] ?? ''); + final p2 = priority(b['relationship'] ?? ''); // main relationship order if (p1 != p2) return p1.compareTo(p2); // ONLY children → oldest first if (p1 == 3) { - final d1 = parseDobSafe(a['dob']); - final d2 = parseDobSafe(b['dob']); + final d1 = parseDobSafe(a['dob'] ?? ''); + final d2 = parseDobSafe(b['dob'] ?? ''); return d1.compareTo(d2); // older first } @@ -339,36 +1110,35 @@ class _policiesState extends State { @override Widget build(BuildContext context) { - // dynamic arguments = ModalRoute.of(context)!.settings.arguments; final arguments = widget.arguments; logDebug('arguments'); logDebug(arguments); if (arguments != null && arguments is Map) { argumentsData = arguments; - if (argumentsData.containsKey('EmployeePolicy') && - argumentsData['EmployeePolicy'] is List) { - // Clear the employeeDetails list to remove previous data - employeeDetails.clear(); - // Iterate over each employee in the 'EmployeePolicy' list - argumentsData['EmployeePolicy'].forEach((employee) { - // Extract name and relationship and add to employeeDetails array - String name = employee['name']; - String relationship = employee['relationship']; - String dob = employee['dob']; - String gender = employee['gender']; - String id = employee['id']; - employeeDetails.add({'id':id,'name': name, 'relationship': relationship,'dob':dob,'gender':gender}); - }); - } - setState(() { - if(argumentsData['policy_type'] == 'GMC' || argumentsData['policy_type'] == 'GMC - Parents' || argumentsData['policy_type'] == 'GMC - Topup' || argumentsData['policy_type'] == 'GMC - Topup(Parents)'){ - ECardHide = true; - } else { - ECardHide = false; - } - }); + if (!_argsInitialized) { + _argsInitialized = true; + final policyType = argumentsData['policy_type']?.toString(); + ECardHide = policyType == 'GMC' || + policyType == 'GMC - Parents' || + policyType == 'GMC - Topup' || + policyType == 'GMC - Topup(Parents)'; - sortEmployeeDetails(employeeDetails); + // Seed from route args until getEmployeePolicy returns (GMC/OPD), + // or keep args as source of truth for other policy types. + if (!_employeePolicyFetched && + argumentsData.containsKey('EmployeePolicy') && + argumentsData['EmployeePolicy'] is List) { + _applyEmployeePolicyList(argumentsData['EmployeePolicy'] as List); + } + + if (_isGmcOrOpdPolicy(policyType) && + !_employeePolicyFetched && + !_isLoadingEmployeePolicy) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _fetchEmployeePolicyIfNeeded(); + }); + } + } logDebug('employeeDetails'); logDebug(employeeDetails); } @@ -538,8 +1308,40 @@ class _policiesState extends State { : EdgeInsets.only( top: 15, bottom: 15, left: 20, right: 20), child: Column( - children: employeeDetails.map((detail) { + children: [ + if (_isLoadingEmployeePolicy) + const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFFE26728), + ), + ), + ), + ), + ...employeeDetails.map((detail) { int index = employeeDetails.indexOf(detail); + final isPending = + (detail['emp_status'] ?? '') + .toLowerCase() == + 'pending_approval'; + final isSelf = + (detail['relationship'] ?? '') + .toLowerCase() == + 'self'; + final canManageDependent = _employeePolicyFetched && + _isGmcOrOpdPolicy( + argumentsData != null + ? argumentsData['policy_type'] + ?.toString() + : null, + ) && + !isSelf && + isPending; return Container( decoration: BoxDecoration( border: Border( @@ -594,15 +1396,39 @@ class _policiesState extends State { const SizedBox(width: 6), // Name text - Text( - detail['name']!, - textAlign: TextAlign.start, - style: GoogleFonts.poppins( - fontSize: Responsive.isDesktop(context) ? 16 : 14, - color: const Color(0xFF000000), - fontWeight: FontWeight.w400, + Flexible( + child: Text( + detail['name']!, + textAlign: TextAlign.start, + style: GoogleFonts.poppins( + fontSize: Responsive.isDesktop(context) ? 16 : 14, + color: const Color(0xFF000000), + fontWeight: FontWeight.w400, + ), ), ), + if (isPending) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: const Color(0xFFFFF1DD), + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + 'Pending', + style: GoogleFonts.poppins( + fontSize: 10, + color: const Color(0xFFE26728), + fontWeight: FontWeight.w500, + ), + ), + ), + ], ], ), @@ -639,33 +1465,91 @@ class _policiesState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - if(ECardHide)...[ - Container( - alignment: Alignment.centerRight, - child: MouseRegion( - cursor: SystemMouseCursors.click, // Web pointer - child: GestureDetector( - onTap: _isDownloadingEcard - ? null - : () { - getEcardDownload(detail['id']); - }, - child: Tooltip( - message: 'E-Card', - waitDuration: const Duration(milliseconds: 300), - showDuration: const Duration(seconds: 2), - child: SvgPicture.string( - SvgService.getSvg('ECard'), - width: 25, - height: 25, + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + if (canManageDependent) ...[ + MouseRegion( + cursor: SystemMouseCursors + .click, + child: GestureDetector( + onTap: () => + _onEditFamilyMember( + detail), + child: const Tooltip( + message: 'Edit', + child: Icon( + Icons.edit, + size: 20, + color: Color( + 0xFFE26728), + ), ), ), ), - ), - ), + const SizedBox(width: 10), + MouseRegion( + cursor: SystemMouseCursors + .click, + child: GestureDetector( + onTap: () => + _onDeleteFamilyMember( + detail), + child: const Tooltip( + message: 'Delete', + child: Icon( + Icons.delete, + size: 20, + color: Color( + 0xFFE26728), + ), + ), + ), + ), + ], + if (ECardHide && + !isPending) ...[ + if (canManageDependent) + const SizedBox(width: 10), + MouseRegion( + cursor: SystemMouseCursors + .click, + child: GestureDetector( + onTap: + _isDownloadingEcard + ? null + : () { + getEcardDownload( + detail[ + 'id']); + }, + child: Tooltip( + message: 'E-Card', + waitDuration: + const Duration( + milliseconds: + 300), + showDuration: + const Duration( + seconds: 2), + child: + SvgPicture.string( + SvgService.getSvg( + 'ECard'), + width: 25, + height: 25, + ), + ), + ), + ), + ], + ], + ), + if (canManageDependent || + (ECardHide && !isPending)) const SizedBox(height: 4), - ], Text( detail['relationship']!, @@ -686,6 +1570,61 @@ class _policiesState extends State { ], )); }).toList(), + if (_addButtonShow && + _isGmcOrOpdPolicy( + argumentsData != null + ? argumentsData['policy_type'] + ?.toString() + : null)) ...[ + if (employeeDetails.isNotEmpty) + const SizedBox(height: 12), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: _onAddFamilyMember, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + vertical: Responsive.isDesktop(context) + ? 14 + : 12, + horizontal: 12, + ), + decoration: BoxDecoration( + border: Border.all( + color: const Color(0xFF000000), + width: 1, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Icon( + Icons.person_add, + color: Color(0xFF000000), + size: 22, + ), + const SizedBox(width: 10), + Text( + 'Add Family Member', + style: GoogleFonts.poppins( + fontSize: + Responsive.isDesktop(context) + ? 16 + : 14, + fontWeight: FontWeight.w500, + color: const Color(0xFF000000), + ), + ), + ], + ), + ), + ), + ), + ], + ], ), ) ])), diff --git a/lib/pages/postEnrollment/service/api_service.dart b/lib/pages/postEnrollment/service/api_service.dart index 0767450..4696c2a 100755 --- a/lib/pages/postEnrollment/service/api_service.dart +++ b/lib/pages/postEnrollment/service/api_service.dart @@ -101,6 +101,62 @@ class ApiService { return response; } + /// Dependent-add UI: policy details, family slots, and add_button_show. + Future> getEmployeePolicy({ + required String id, + required String empCode, + required String clientId, + required String clientBranchId, + required String clientPolicyId, + }) async { + logDebug(_postToken); + if (_postToken == null) { + await _initializeToken(); + } + final url = Uri.parse( + '${Environment.apiUrl}getEmployeeByPolicy?id=$id&emp_code=$empCode&client_id=$clientId&client_branch_id=$clientBranchId&client_policy_id=$clientPolicyId'); + final headers = { + 'Authorization': 'Bearer $_postToken' ?? '', + }; + final response = await _makeGetRequest(url, headers); + return response; + } + + /// Add or update dependent (creates with pending_approval). + Future> addEmployeeAndDependence( + List> dependents) async { + logDebug(_postToken); + if (_postToken == null) { + await _initializeToken(); + } + final url = Uri.parse('${Environment.apiUrl}addEmployeeAndDependence'); + final headers = { + 'Authorization': 'Bearer $_postToken' ?? '', + 'Content-Type': 'application/json', + }; + final response = await _makePostRequestWithoutFormData( + url, + jsonEncode(dependents), + headers, + ); + return response; + } + + /// Soft-delete a dependent (status → deleted, is_active → 0). + Future> deleteDependence(String dependentId) async { + logDebug(_postToken); + if (_postToken == null) { + await _initializeToken(); + } + final url = + Uri.parse('${Environment.apiUrl}deleteDependence?id=$dependentId'); + final headers = { + 'Authorization': 'Bearer $_postToken' ?? '', + }; + final response = await _makeGetRequest(url, headers); + return response; + } + Future> getFAQsApiData() async { logDebug(_postToken); if (_postToken == null) { @@ -458,6 +514,13 @@ class ApiService { ToastHelper.showWarningToast(context, message); return {}; } else { + // Return API error payload when available so callers can show messages. + try { + final decoded = jsonDecode(response.body); + if (decoded is Map) { + return decoded; + } + } catch (_) {} throw Exception('Failed to load data'); } } diff --git a/pubspec.yaml b/pubspec.yaml index 2fb09ba..2cb42c7 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,8 +17,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. #version: 1.2.42+99 -version: 1.0.43+50 -#version: 2.0.31+70 +version: 1.0.44+51 +#version: 2.0.32+71 environment: sdk: '>=3.3.3 <4.0.0'