fixed the dependent flow.

wala_plus_with_dialog_removal
FaizHashmi 2 weeks ago
parent 2cc22bdc21
commit aff3bc2fc0

@ -37,16 +37,24 @@ APIException _throwAPIException(Response response) {
case 200: case 200:
APIError? apiError; APIError? apiError;
if (response.body != null && response.body.isNotEmpty) { if (response.body != null && response.body.isNotEmpty) {
var jsonError = jsonDecode(response.body); try {
print(jsonError); var jsonError = jsonDecode(response.body);
apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); 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); return APIException(APIException.BAD_REQUEST, error: apiError);
case 400: case 400:
APIError? apiError; APIError? apiError;
if (response.body != null && response.body.isNotEmpty) { if (response.body != null && response.body.isNotEmpty) {
var jsonError = jsonDecode(response.body); try {
apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); 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); return APIException(APIException.BAD_REQUEST, error: apiError);
case 401: case 401:
@ -121,6 +129,7 @@ class ApiClient {
if (ex is APIException) { if (ex is APIException) {
rethrow; rethrow;
} else { } else {
log("BAD_RESPONSE_FORMAT for $url -> body: ${response.body}");
throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex); 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/api/api_client.dart';
import 'package:mohem_flutter_app/app_state/app_state.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/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/generic_response_model.dart';
import 'package:mohem_flutter_app/models/get_approves_list_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'; 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/classes/utils.dart';
import 'package:mohem_flutter_app/models/start_phone_approval_process_model.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 { class ProfileApiClient {
static final ProfileApiClient _instance = ProfileApiClient._internal(); 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 url = "${ApiConsts.erpRest}PROCESS_CONTACT_MAZAYA_ACCESS";
String action = accessAction.trim().toLowerCase() == "revoke" ? "Revoke" : "Grant"; 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}; Map<String, dynamic> postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999, "P_ACCESS_ACTION": action};
if (contactRelationshipId != null) { if (contactRelationshipId != null) {
postParams["P_CONTACT_RELATIONSHIP_ID"] = contactRelationshipId; postParams["P_CONTACT_RELATIONSHIP_ID"] = contactRelationshipId;
@ -518,6 +551,19 @@ class ProfileApiClient {
if (personId != null) { if (personId != null) {
postParams["P_PERSON_ID"] = personId; 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); postParams.addAll(AppState().postParamsJson);
return await ApiClient().postJsonForObject( return await ApiClient().postJsonForObject(
(json) { (json) {

@ -13,18 +13,30 @@ class GetEmployeeContactsList {
dynamic mazayaAccess; dynamic mazayaAccess;
String? nATIONALIDENTIFIER; String? nATIONALIDENTIFIER;
GetEmployeeContactsList( // Real ERP response fields - needed to call the grant-access API later (as P_BIRTH_DATE /
{this.cONTACTNAME, // P_NHIC_VERIFIED_FLAG request params). PatientIdentificationID/PatientOutSA are NOT
this.cONTACTPERSONID, // response fields - they're constructed at call time (from nATIONALIDENTIFIER, and a
this.cONTACTRELATIONSHIPID, // hardcoded 0, respectively), so they don't belong on this model.
this.cONTACTTYPE, // Not a plain string per ERP - keep untyped so whatever shape comes back (epoch, nested
this.dATEOFBIRTH, // date object, etc.) survives round-trip, instead of assuming a format.
this.pERSONID, dynamic pBIRTHDATE;
this.pRIMARYCONTACTFLAG, String? pNHICVERIFIEDFLAG;
this.rELATIONSHIP,
this.mAZAYAACCESSFLG, GetEmployeeContactsList({
this.mazayaAccess, this.cONTACTNAME,
this.nATIONALIDENTIFIER}); 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) { GetEmployeeContactsList.fromJson(Map<String, dynamic> json) {
cONTACTNAME = json['CONTACT_NAME']; cONTACTNAME = json['CONTACT_NAME'];
@ -37,7 +49,8 @@ class GetEmployeeContactsList {
rELATIONSHIP = json['RELATIONSHIP']; rELATIONSHIP = json['RELATIONSHIP'];
mAZAYAACCESSFLG = json['MAZAYA_ACCESS_FLG']?.toString() ?? json['MAZAYA_ACCESS']?.toString(); mAZAYAACCESSFLG = json['MAZAYA_ACCESS_FLG']?.toString() ?? json['MAZAYA_ACCESS']?.toString();
mazayaAccess = mAZAYAACCESSFLG; mazayaAccess = mAZAYAACCESSFLG;
nATIONALIDENTIFIER = json['NATIONAL_IDENTIFIER']?.toString() ?? nATIONALIDENTIFIER =
json['NATIONAL_IDENTIFIER']?.toString() ??
json['NATIONAL_ID']?.toString() ?? json['NATIONAL_ID']?.toString() ??
json['CIVIL_ID']?.toString() ?? json['CIVIL_ID']?.toString() ??
json['IQAMA_NUMBER']?.toString() ?? json['IQAMA_NUMBER']?.toString() ??
@ -47,9 +60,11 @@ class GetEmployeeContactsList {
json['IDENTIFICATION_ID']?.toString() ?? json['IDENTIFICATION_ID']?.toString() ??
json['NATIONALID']?.toString(); json['NATIONALID']?.toString();
if (nATIONALIDENTIFIER == null || nATIONALIDENTIFIER!.trim().isEmpty) { 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"); 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() { Map<String, dynamic> toJson() {
@ -65,6 +80,8 @@ class GetEmployeeContactsList {
data['MAZAYA_ACCESS_FLG'] = mAZAYAACCESSFLG; data['MAZAYA_ACCESS_FLG'] = mAZAYAACCESSFLG;
data['MAZAYA_ACCESS'] = mazayaAccess; data['MAZAYA_ACCESS'] = mazayaAccess;
data['NATIONAL_IDENTIFIER'] = nATIONALIDENTIFIER; data['NATIONAL_IDENTIFIER'] = nATIONALIDENTIFIER;
data['P_BIRTH_DATE'] = pBIRTHDATE;
data['P_NHIC_VERIFIED_FLAG'] = pNHICVERIFIEDFLAG;
return data; return data;
} }
} }

@ -111,6 +111,11 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
return flag?.trim().toUpperCase() == 'Y'; return flag?.trim().toUpperCase() == 'Y';
} }
bool get _isNhicVerified {
if (_contact == null) return false;
return _contact!.pNHICVERIFIEDFLAG?.trim().toUpperCase() == 'Y';
}
String _getInitials(String? name) { String _getInitials(String? name) {
if (name == null || name.trim().isEmpty) return "D"; if (name == null || name.trim().isEmpty) return "D";
List<String> parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList(); 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) { String _formatNationalId(String? nationalId) {
if (nationalId == null || nationalId.trim().isEmpty) return "-"; 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; String last4 = clean.length >= 4 ? clean.substring(clean.length - 4) : clean;
return LocaleKeys.idEnding.tr(args: <String>[last4]); return LocaleKeys.idEnding.tr(args: <String>[last4]);
} }
@ -210,6 +215,11 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
contactRelationshipId: _contact!.cONTACTRELATIONSHIPID, contactRelationshipId: _contact!.cONTACTRELATIONSHIPID,
contactPersonId: _contact!.cONTACTPERSONID, contactPersonId: _contact!.cONTACTPERSONID,
personId: _contact!.pERSONID, 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(() { setState(() {
@ -380,9 +390,8 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
), ),
), ),
// Verified badge on right // Verified badge on right - only when NHIC has actually verified this contact
const SizedBox(width: 8), if (_isNhicVerified) ...<Widget>[const SizedBox(width: 8), const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 24)],
const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 24),
], ],
), ),
), ),
@ -405,7 +414,7 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
LocaleKeys.keyDetails.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)), 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), const SizedBox(height: 14),

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
@ -63,6 +65,10 @@ class _FamilyMembersState extends State<FamilyMembers> {
return flag?.trim().toUpperCase() == 'Y'; return flag?.trim().toUpperCase() == 'Y';
} }
bool _isNhicVerified(GetEmployeeContactsList contact) {
return contact.pNHICVERIFIEDFLAG?.trim().toUpperCase() == 'Y';
}
void _confirmToggleMazayaAccess(GetEmployeeContactsList contact) { void _confirmToggleMazayaAccess(GetEmployeeContactsList contact) {
bool isAllowed = _isAccessAllowed(contact); bool isAllowed = _isAccessAllowed(contact);
String dependentName = contact.cONTACTNAME ?? ""; String dependentName = contact.cONTACTNAME ?? "";
@ -92,6 +98,11 @@ class _FamilyMembersState extends State<FamilyMembers> {
contactRelationshipId: contact.cONTACTRELATIONSHIPID, contactRelationshipId: contact.cONTACTRELATIONSHIPID,
contactPersonId: contact.cONTACTPERSONID, contactPersonId: contact.cONTACTPERSONID,
personId: contact.pERSONID, 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 // Keep this exact `contact` instance in sync too - it's what gets handed to
@ -115,6 +126,7 @@ class _FamilyMembersState extends State<FamilyMembers> {
getEmployeeContacts(); getEmployeeContacts();
} }
} catch (ex) { } catch (ex) {
log("erros: ${ex.toString()}");
Utils.hideLoading(context); Utils.hideLoading(context);
Utils.handleException(ex, context, null); Utils.handleException(ex, context, null);
} }
@ -374,9 +386,29 @@ class _FamilyMembersState extends State<FamilyMembers> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ 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), 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(), constraints: const BoxConstraints(),
onPressed: () => onCloseTap ?? Navigator.pop(context), onPressed: () => onCloseTap ?? Navigator.pop(context),
// onPressed: () => Navigator.pop(context), // onPressed: () => Navigator.pop(context),
) ),
], ],
), ),
message.toText16(color: MyColors.lightGrayColor), message.toText16(color: MyColors.lightGrayColor),

Loading…
Cancel
Save