From aff3bc2fc0a7c4f266ea9c3749b6d9d574783f5c Mon Sep 17 00:00:00 2001 From: FaizHashmi Date: Tue, 13 Oct 2026 18:21:32 +0300 Subject: [PATCH] fixed the dependent flow. --- lib/api/api_client.dart | 19 ++++++-- lib/api/profile_api_client.dart | 48 +++++++++++++++++++- lib/models/get_employee_contacts.model.dart | 47 +++++++++++++------ lib/ui/profile/dependent_details_screen.dart | 19 ++++++-- lib/ui/profile/family_members.dart | 36 ++++++++++++++- lib/widgets/dialogs/confirm_dialog.dart | 2 +- 6 files changed, 142 insertions(+), 29 deletions(-) diff --git a/lib/api/api_client.dart b/lib/api/api_client.dart index 0ef23ed..7ac6c8d 100644 --- a/lib/api/api_client.dart +++ b/lib/api/api_client.dart @@ -37,16 +37,24 @@ APIException _throwAPIException(Response response) { case 200: APIError? apiError; if (response.body != null && response.body.isNotEmpty) { - var jsonError = jsonDecode(response.body); - print(jsonError); - apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); + try { + var jsonError = jsonDecode(response.body); + print(jsonError); + apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); + } catch (_) { + log("Non-JSON 200 error body: ${response.body}"); + } } return APIException(APIException.BAD_REQUEST, error: apiError); case 400: APIError? apiError; if (response.body != null && response.body.isNotEmpty) { - var jsonError = jsonDecode(response.body); - apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); + try { + var jsonError = jsonDecode(response.body); + apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); + } catch (_) { + log("Non-JSON 400 error body: ${response.body}"); + } } return APIException(APIException.BAD_REQUEST, error: apiError); case 401: @@ -121,6 +129,7 @@ class ApiClient { if (ex is APIException) { rethrow; } else { + log("BAD_RESPONSE_FORMAT for $url -> body: ${response.body}"); throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex); } } diff --git a/lib/api/profile_api_client.dart b/lib/api/profile_api_client.dart index ef2c189..23197a5 100644 --- a/lib/api/profile_api_client.dart +++ b/lib/api/profile_api_client.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:mohem_flutter_app/api/api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/classes/date_uitl.dart'; import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:mohem_flutter_app/models/get_approves_list_model.dart'; import 'package:mohem_flutter_app/models/get_contact_dff_structure_list.dart'; @@ -29,6 +30,25 @@ import 'package:mohem_flutter_app/models/profile/patient_info_model.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/models/start_phone_approval_process_model.dart'; +// The ERP WCF service's DataContractJsonSerializer rejects any DateTime-typed field unless +// it's wrapped as "/Date(ms)/" - confirmed by its own error on this exact call: +// "DateTime content '6/23/1989 12:00:00 AM' does not start with '/Date(' and end with ')/' +// as required for JSON." GET_EMPLOYEE_CONTACTS hands DATE_OF_BIRTH back as a plain +// "M/d/yyyy h:mm:ss a" string, so it has to be converted before going back out as P_BIRTH_DATE. +String? _toWcfJsonDate(rawDate) { + if (rawDate == null) return null; + String raw = rawDate.toString().trim(); + if (raw.isEmpty) return null; + if (raw.contains('/Date(')) return raw; + try { + DateTime parsed = DateUtil.convertSimpleStringDateToDate(raw); + int utcMillis = DateTime.utc(parsed.year, parsed.month, parsed.day).millisecondsSinceEpoch; + return "/Date($utcMillis)/"; + } catch (_) { + return null; + } +} + class ProfileApiClient { static final ProfileApiClient _instance = ProfileApiClient._internal(); @@ -505,9 +525,22 @@ class ProfileApiClient { ); } - Future processContactMazayaAccess({required String accessAction, int? contactRelationshipId, int? contactPersonId, int? personId}) async { + Future processContactMazayaAccess({ + required String accessAction, + int? contactRelationshipId, + int? contactPersonId, + int? personId, + // Same shape as the NHIC-style patient-info call: PatientIdentificationID is the + // dependent's national ID (GetEmployeeContactsList.nATIONALIDENTIFIER), PatientOutSA is + // always 0, and the other two are passed straight through from that same contact's + // P_BIRTH_DATE / P_NHIC_VERIFIED_FLAG response fields. + String? patientIdentificationId, + patientBirthDate, + String? nhicVerifiedFlag, + }) async { String url = "${ApiConsts.erpRest}PROCESS_CONTACT_MAZAYA_ACCESS"; String action = accessAction.trim().toLowerCase() == "revoke" ? "Revoke" : "Grant"; + bool isGrant = action == "Grant"; Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999, "P_ACCESS_ACTION": action}; if (contactRelationshipId != null) { postParams["P_CONTACT_RELATIONSHIP_ID"] = contactRelationshipId; @@ -518,6 +551,19 @@ class ProfileApiClient { if (personId != null) { postParams["P_PERSON_ID"] = personId; } + // Only the Grant call carries these - Revoke's payload stays exactly as it was before + // this feature. P_BIRTH_DATE/P_NHIC_VERIFIED_FLAG are always sent (even as null, if the + // source contact from GET_EMPLOYEE_CONTACTS didn't have a value) once we're granting. + if (isGrant) { + postParams["PatientOutSA"] = 0; + postParams["P_BIRTH_DATE"] = _toWcfJsonDate(patientBirthDate); + postParams["P_NHIC_VERIFIED_FLAG"] = nhicVerifiedFlag?.trim(); + if (patientIdentificationId != null && patientIdentificationId.trim().isNotEmpty) { + // National ID comes formatted with dashes (e.g. "2-5616-1545-7") - the backend wants + // the plain digit string (e.g. "2561615457"). + postParams["PatientIdentificationID"] = patientIdentificationId.replaceAll('-', '').trim(); + } + } postParams.addAll(AppState().postParamsJson); return await ApiClient().postJsonForObject( (json) { diff --git a/lib/models/get_employee_contacts.model.dart b/lib/models/get_employee_contacts.model.dart index 8e6ff24..070e802 100644 --- a/lib/models/get_employee_contacts.model.dart +++ b/lib/models/get_employee_contacts.model.dart @@ -13,18 +13,30 @@ class GetEmployeeContactsList { dynamic mazayaAccess; String? nATIONALIDENTIFIER; - GetEmployeeContactsList( - {this.cONTACTNAME, - this.cONTACTPERSONID, - this.cONTACTRELATIONSHIPID, - this.cONTACTTYPE, - this.dATEOFBIRTH, - this.pERSONID, - this.pRIMARYCONTACTFLAG, - this.rELATIONSHIP, - this.mAZAYAACCESSFLG, - this.mazayaAccess, - this.nATIONALIDENTIFIER}); + // Real ERP response fields - needed to call the grant-access API later (as P_BIRTH_DATE / + // P_NHIC_VERIFIED_FLAG request params). PatientIdentificationID/PatientOutSA are NOT + // response fields - they're constructed at call time (from nATIONALIDENTIFIER, and a + // hardcoded 0, respectively), so they don't belong on this model. + // Not a plain string per ERP - keep untyped so whatever shape comes back (epoch, nested + // date object, etc.) survives round-trip, instead of assuming a format. + dynamic pBIRTHDATE; + String? pNHICVERIFIEDFLAG; + + GetEmployeeContactsList({ + this.cONTACTNAME, + this.cONTACTPERSONID, + this.cONTACTRELATIONSHIPID, + this.cONTACTTYPE, + this.dATEOFBIRTH, + this.pERSONID, + this.pRIMARYCONTACTFLAG, + this.rELATIONSHIP, + this.mAZAYAACCESSFLG, + this.mazayaAccess, + this.nATIONALIDENTIFIER, + this.pBIRTHDATE, + this.pNHICVERIFIEDFLAG, + }); GetEmployeeContactsList.fromJson(Map json) { cONTACTNAME = json['CONTACT_NAME']; @@ -37,7 +49,8 @@ class GetEmployeeContactsList { rELATIONSHIP = json['RELATIONSHIP']; mAZAYAACCESSFLG = json['MAZAYA_ACCESS_FLG']?.toString() ?? json['MAZAYA_ACCESS']?.toString(); mazayaAccess = mAZAYAACCESSFLG; - nATIONALIDENTIFIER = json['NATIONAL_IDENTIFIER']?.toString() ?? + nATIONALIDENTIFIER = + json['NATIONAL_IDENTIFIER']?.toString() ?? json['NATIONAL_ID']?.toString() ?? json['CIVIL_ID']?.toString() ?? json['IQAMA_NUMBER']?.toString() ?? @@ -47,9 +60,11 @@ class GetEmployeeContactsList { json['IDENTIFICATION_ID']?.toString() ?? json['NATIONALID']?.toString(); if (nATIONALIDENTIFIER == null || nATIONALIDENTIFIER!.trim().isEmpty) { - // TODO: remove once the real key for a contact's national ID is confirmed from a live response. debugPrint("GetEmployeeContactsList: no national id match found, raw contact json: $json"); } + + pBIRTHDATE = json['P_BIRTH_DATE']; + pNHICVERIFIEDFLAG = json['P_NHIC_VERIFIED_FLAG']?.toString(); } Map toJson() { @@ -65,6 +80,8 @@ class GetEmployeeContactsList { data['MAZAYA_ACCESS_FLG'] = mAZAYAACCESSFLG; data['MAZAYA_ACCESS'] = mazayaAccess; data['NATIONAL_IDENTIFIER'] = nATIONALIDENTIFIER; + data['P_BIRTH_DATE'] = pBIRTHDATE; + data['P_NHIC_VERIFIED_FLAG'] = pNHICVERIFIEDFLAG; return data; } -} \ No newline at end of file +} diff --git a/lib/ui/profile/dependent_details_screen.dart b/lib/ui/profile/dependent_details_screen.dart index 7ec051d..066c091 100644 --- a/lib/ui/profile/dependent_details_screen.dart +++ b/lib/ui/profile/dependent_details_screen.dart @@ -111,6 +111,11 @@ class _DependentDetailsScreenState extends State { return flag?.trim().toUpperCase() == 'Y'; } + bool get _isNhicVerified { + if (_contact == null) return false; + return _contact!.pNHICVERIFIEDFLAG?.trim().toUpperCase() == 'Y'; + } + String _getInitials(String? name) { if (name == null || name.trim().isEmpty) return "D"; List parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList(); @@ -176,7 +181,7 @@ class _DependentDetailsScreenState extends State { String _formatNationalId(String? nationalId) { if (nationalId == null || nationalId.trim().isEmpty) return "-"; - String clean = nationalId.trim(); + String clean = nationalId.replaceAll('-', '').trim(); String last4 = clean.length >= 4 ? clean.substring(clean.length - 4) : clean; return LocaleKeys.idEnding.tr(args: [last4]); } @@ -210,6 +215,11 @@ class _DependentDetailsScreenState extends State { contactRelationshipId: _contact!.cONTACTRELATIONSHIPID, contactPersonId: _contact!.cONTACTPERSONID, personId: _contact!.pERSONID, + patientIdentificationId: _contact!.nATIONALIDENTIFIER, + // DATE_OF_BIRTH, not the separate P_BIRTH_DATE response field - that one stays null + // for every contact until NHIC verification happens, so it's never a usable source here. + patientBirthDate: _contact!.dATEOFBIRTH, + nhicVerifiedFlag: _contact!.pNHICVERIFIEDFLAG, ); setState(() { @@ -380,9 +390,8 @@ class _DependentDetailsScreenState extends State { ), ), - // Verified badge on right - const SizedBox(width: 8), - const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 24), + // Verified badge on right - only when NHIC has actually verified this contact + if (_isNhicVerified) ...[const SizedBox(width: 8), const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 24)], ], ), ), @@ -405,7 +414,7 @@ class _DependentDetailsScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.keyDetails.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)), - LocaleKeys.verified.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF059669)), + if (_isNhicVerified) LocaleKeys.verified.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF059669)), ], ), const SizedBox(height: 14), diff --git a/lib/ui/profile/family_members.dart b/lib/ui/profile/family_members.dart index 6cae37d..0634498 100644 --- a/lib/ui/profile/family_members.dart +++ b/lib/ui/profile/family_members.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -63,6 +65,10 @@ class _FamilyMembersState extends State { return flag?.trim().toUpperCase() == 'Y'; } + bool _isNhicVerified(GetEmployeeContactsList contact) { + return contact.pNHICVERIFIEDFLAG?.trim().toUpperCase() == 'Y'; + } + void _confirmToggleMazayaAccess(GetEmployeeContactsList contact) { bool isAllowed = _isAccessAllowed(contact); String dependentName = contact.cONTACTNAME ?? ""; @@ -92,6 +98,11 @@ class _FamilyMembersState extends State { contactRelationshipId: contact.cONTACTRELATIONSHIPID, contactPersonId: contact.cONTACTPERSONID, personId: contact.pERSONID, + patientIdentificationId: contact.nATIONALIDENTIFIER, + // DATE_OF_BIRTH, not the separate P_BIRTH_DATE response field - that one stays null + // for every contact until NHIC verification happens, so it's never a usable source here. + patientBirthDate: contact.dATEOFBIRTH, + nhicVerifiedFlag: contact.pNHICVERIFIEDFLAG, ); // Keep this exact `contact` instance in sync too - it's what gets handed to @@ -115,6 +126,7 @@ class _FamilyMembersState extends State { getEmployeeContacts(); } } catch (ex) { + log("erros: ${ex.toString()}"); Utils.hideLoading(context); Utils.handleException(ex, context, null); } @@ -374,9 +386,29 @@ class _FamilyMembersState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(item.cONTACTNAME ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B))), + Row( + children: [ + Flexible( + child: Text( + item.cONTACTNAME ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B)), + ), + ), + if (_isNhicVerified(item)) ...[const SizedBox(width: 8), const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 20)], + ], + ), const SizedBox(height: 3), - Row(children: [Flexible(child: (item.rELATIONSHIP ?? "").toText12(maxLine: 1, color: const Color(0xFF168A89)))]), + Row( + children: [ + Flexible(child: (item.rELATIONSHIP ?? "").toText12(maxLine: 1, color: const Color(0xFF168A89))), + if (_isNhicVerified(item)) ...[ + " • ".toText12(color: const Color(0xFF94A3B8)), + LocaleKeys.verified.tr().toText12(weight: FontWeight.w500, color: const Color(0xFF059669)), + ], + ], + ), ], ), ), diff --git a/lib/widgets/dialogs/confirm_dialog.dart b/lib/widgets/dialogs/confirm_dialog.dart index 560bd26..30a9bef 100644 --- a/lib/widgets/dialogs/confirm_dialog.dart +++ b/lib/widgets/dialogs/confirm_dialog.dart @@ -44,7 +44,7 @@ class ConfirmDialog extends StatelessWidget { constraints: const BoxConstraints(), onPressed: () => onCloseTap ?? Navigator.pop(context), // onPressed: () => Navigator.pop(context), - ) + ), ], ), message.toText16(color: MyColors.lightGrayColor),