fixed the dependent flow.

wala_plus
FaizHashmi 3 hours ago
parent 2cc22bdc21
commit aff3bc2fc0

@ -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);
}
}

@ -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<GenericResponseModel?> processContactMazayaAccess({required String accessAction, int? contactRelationshipId, int? contactPersonId, int? personId}) async {
Future<GenericResponseModel?> 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<String, dynamic> 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) {

@ -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<String, dynamic> 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<String, dynamic> 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;
}
}
}

@ -111,6 +111,11 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
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<String> parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList();
@ -176,7 +181,7 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
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: <String>[last4]);
}
@ -210,6 +215,11 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
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<DependentDetailsScreen> {
),
),
// 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) ...<Widget>[const SizedBox(width: 8), const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 24)],
],
),
),
@ -405,7 +414,7 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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),

@ -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<FamilyMembers> {
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<FamilyMembers> {
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<FamilyMembers> {
getEmployeeContacts();
}
} catch (ex) {
log("erros: ${ex.toString()}");
Utils.hideLoading(context);
Utils.handleException(ex, context, null);
}
@ -374,9 +386,29 @@ class _FamilyMembersState extends State<FamilyMembers> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(item.cONTACTNAME ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B))),
Row(
children: <Widget>[
Flexible(
child: Text(
item.cONTACTNAME ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B)),
),
),
if (_isNhicVerified(item)) ...<Widget>[const SizedBox(width: 8), const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 20)],
],
),
const SizedBox(height: 3),
Row(children: <Widget>[Flexible(child: (item.rELATIONSHIP ?? "").toText12(maxLine: 1, color: const Color(0xFF168A89)))]),
Row(
children: <Widget>[
Flexible(child: (item.rELATIONSHIP ?? "").toText12(maxLine: 1, color: const Color(0xFF168A89))),
if (_isNhicVerified(item)) ...<Widget>[
" • ".toText12(color: const Color(0xFF94A3B8)),
LocaleKeys.verified.tr().toText12(weight: FontWeight.w500, color: const Color(0xFF059669)),
],
],
),
],
),
),

@ -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),

Loading…
Cancel
Save