import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mohem_flutter_app/api/profile_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/get_employee_contacts.model.dart'; import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; class _ShieldIconPainter extends CustomPainter { final Color color; final bool isGranted; _ShieldIconPainter({required this.color, required this.isGranted}); @override void paint(Canvas canvas, Size size) { Paint paint = Paint() ..color = color ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round; double w = size.width; double h = size.height; Path path = Path(); path.moveTo(w * 0.5, h * 0.1); path.cubicTo(w * 0.72, h * 0.1, w * 0.88, h * 0.18, w * 0.88, h * 0.38); path.cubicTo(w * 0.88, h * 0.65, w * 0.68, h * 0.84, w * 0.5, h * 0.92); path.cubicTo(w * 0.32, h * 0.84, w * 0.12, h * 0.65, w * 0.12, h * 0.38); path.cubicTo(w * 0.12, h * 0.18, w * 0.28, h * 0.1, w * 0.5, h * 0.1); path.close(); canvas.drawPath(path, paint); if (isGranted) { Path checkPath = Path(); checkPath.moveTo(w * 0.34, h * 0.49); checkPath.lineTo(w * 0.46, h * 0.61); checkPath.lineTo(w * 0.66, h * 0.39); canvas.drawPath(checkPath, paint); } else { canvas.drawLine(Offset(w * 0.38, h * 0.38), Offset(w * 0.62, h * 0.62), paint); canvas.drawLine(Offset(w * 0.62, h * 0.38), Offset(w * 0.38, h * 0.62), paint); } } @override bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) => color != oldDelegate.color || isGranted != oldDelegate.isGranted; } class DependentDetailsScreen extends StatefulWidget { final GetEmployeeContactsList? contact; // Null = just viewing (arrived by tapping the card, no top badge). True/false = this screen // is being opened right after a successful grant/revoke performed elsewhere (e.g. the // dependents list's inline "Allow/Revoke Mazaya Access" link), so the matching badge shows // on first build. Toggling from within this screen itself (_performToggleAccess) sets the // equivalent local state directly instead of needing this. final bool? recentAction; const DependentDetailsScreen({Key? key, this.contact, this.recentAction}) : super(key: key); @override _DependentDetailsScreenState createState() => _DependentDetailsScreenState(); } class _DependentDetailsScreenState extends State { GetEmployeeContactsList? _contact; bool _argsLoaded = false; // null = just viewing (arrived by tapping the card, no badge) - true = just granted here, // false = just revoked here. Set once per toggle in _performToggleAccess; a tap-only visit // never sets it, so the top badge only ever reflects an action just taken on this screen. bool? _recentAction; @override void didChangeDependencies() { super.didChangeDependencies(); if (!_argsLoaded) { if (widget.contact != null) { _contact = widget.contact; } else { Object? args = ModalRoute.of(context)?.settings.arguments; if (args is GetEmployeeContactsList) { _contact = args; } else if (args is Map && args['contact'] is GetEmployeeContactsList) { _contact = args['contact'] as GetEmployeeContactsList; } } _recentAction = widget.recentAction; _argsLoaded = true; } } bool get _isAccessAllowed { if (_contact == null) return false; String? flag = _contact!.mAZAYAACCESSFLG ?? _contact!.mazayaAccess?.toString(); 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(); if (parts.isEmpty) return "D"; if (parts.length == 1) { return parts.first.length >= 2 ? parts.first.substring(0, 2).toUpperCase() : parts.first.toUpperCase(); } return "${parts.first[0]}${parts.last[0]}".toUpperCase(); } String _getFirstName(String? name) { if (name == null || name.trim().isEmpty) return ""; List parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList(); if (parts.isEmpty) return ""; return parts.first; } DateTime? _parseBirthDate(String? dateStr) { if (dateStr == null || dateStr.trim().isEmpty) return null; String s = dateStr.trim(); RegExp dateRegex = RegExp(r'/?Date\((-?\d+)(?:[+-]\d+)?\)/?'); Match? match = dateRegex.firstMatch(s); if (match != null) { try { int millis = int.parse(match.group(1)!); DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true); return DateTime(dtUtc.year, dtUtc.month, dtUtc.day); } catch (_) {} } if (RegExp(r'^-?\d{10,14}$').hasMatch(s)) { try { int millis = int.parse(s); DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true); return DateTime(dtUtc.year, dtUtc.month, dtUtc.day); } catch (_) {} } DateTime? dt = DateTime.tryParse(s); if (dt != null) return DateTime(dt.year, dt.month, dt.day); List formats = ['yyyy-MM-dd', 'dd/MM/yyyy', 'MM/dd/yyyy', 'dd-MM-yyyy', 'yyyy/MM/dd', 'dd-MMM-yyyy', 'dd MMM yyyy']; for (String fmt in formats) { try { DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(s); return DateTime(parsed.year, parsed.month, parsed.day); } catch (_) {} } return null; } String _formatDob(DateTime? dob, String? raw) { if (dob != null) { return DateFormat('dd MMM yyyy', 'en_US').format(dob); } if (raw != null && raw.trim().isNotEmpty) { return raw.split(' ').first; } return "-"; } String _formatNationalId(String? nationalId) { if (nationalId == null || nationalId.trim().isEmpty) return "-"; String clean = nationalId.replaceAll('-', '').trim(); String last4 = clean.length >= 4 ? clean.substring(clean.length - 4) : clean; return LocaleKeys.idEnding.tr(args: [last4]); } void _onToggleAccess() { if (_contact == null) return; bool isAllowed = _isAccessAllowed; String dependentName = _contact!.cONTACTNAME ?? ""; ConfirmBottomSheet.show( context, title: isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr(), message: isAllowed ? LocaleKeys.confirmRevokeMazayaAccess.tr(args: [dependentName]) : LocaleKeys.confirmGrantMazayaAccess.tr(args: [dependentName]), icon: isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline, iconColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89), confirmText: LocaleKeys.yes.tr(), cancelText: LocaleKeys.no.tr(), onConfirm: _performToggleAccess, ); } Future _performToggleAccess() async { if (_contact == null) return; bool isAllowed = _isAccessAllowed; String action = isAllowed ? "Revoke" : "Grant"; try { Utils.showLoading(context); await ProfileApiClient().processContactMazayaAccess( accessAction: action, 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(() { _contact!.mAZAYAACCESSFLG = action == "Grant" ? "Y" : "N"; _contact!.mazayaAccess = _contact!.mAZAYAACCESSFLG; // Top badge reflects whichever action was just taken on this screen, not just "was // granted at some point" - a later revoke flips it so a stale grant badge never lingers. _recentAction = action == "Grant"; }); Utils.hideLoading(context); // Stay on this same screen either way - it already reflects the new access state via // setState above (status badge, top banner, action button, and the share-link section // all update in place), so tapping a dependent card and successfully toggling access // both land on the exact same screen instead of a separate "success" screen. The top // badge itself is the confirmation now, so no toast on top of it. } catch (ex) { Utils.hideLoading(context); Utils.handleException(ex, context, null); } } // Full (unmasked) DOB for the share message - distinct from _formatDob/_formatNationalId // above, which are for the masked on-screen Key Details display. String _formatDobForShare() { DateTime? dob = _parseBirthDate(_contact?.dATEOFBIRTH); if (dob != null) return DateFormat('dd-MM-yyyy').format(dob); String raw = _contact?.dATEOFBIRTH?.trim() ?? ""; if (raw.isEmpty) return ""; if (raw.contains('T')) return raw.split('T').first; if (raw.contains(' ')) return raw.split(' ').first; return raw; } // Same message built for both "Share Download Link" and "Copy Link" - they must always // carry identical content, so this is the single source of truth for it. String _buildMazayaInviteMessage(String linkToShare) { String? nationalId = _contact?.nATIONALIDENTIFIER?.trim(); String nationalIdLine = (nationalId == null || nationalId.isEmpty) ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId"; return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDobForShare()}"; } // Shares the invite text together with the language-appropriate steps PDF from assets. // Falls back to sharing just the text if the PDF can't be loaded/copied for any reason. Future _shareMazayaInvite(bool isAr, String linkToShare) async { String message = _buildMazayaInviteMessage(linkToShare); try { ByteData bytes = await rootBundle.load(MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr)); Directory tempDir = await getTemporaryDirectory(); String localFileName = isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf"; File pdfFile = File('${tempDir.path}/$localFileName'); await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes)); await SharePlus.instance.share(ShareParams(text: message, files: [XFile(pdfFile.path)])); } catch (_) { try { await SharePlus.instance.share(ShareParams(text: message)); } catch (_) {} } } @override Widget build(BuildContext context) { DateTime? dob = _parseBirthDate(_contact?.dATEOFBIRTH); String initials = _getInitials(_contact?.cONTACTNAME); String fullName = _contact?.cONTACTNAME ?? ""; String firstName = _getFirstName(_contact?.cONTACTNAME); String relation = (_contact?.rELATIONSHIP != null && _contact!.rELATIONSHIP!.trim().isNotEmpty) ? _contact!.rELATIONSHIP!.trim() : (_contact?.cONTACTTYPE?.trim() ?? ""); String formattedId = _formatNationalId(_contact?.nATIONALIDENTIFIER); String formattedDob = _formatDob(dob, _contact?.dATEOFBIRTH); bool isAllowed = _isAccessAllowed; bool isAr = AppState().isArabic(context); String linkToShare = MazayaAccessResultScreen.getDownloadLink(); return Scaffold( backgroundColor: const Color(0xFFF8FAFC), appBar: PreferredSize( preferredSize: const Size.fromHeight(56), child: SafeArea( child: Container( height: 56, color: const Color(0xFFF8FAFC), padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ InkWell( borderRadius: BorderRadius.circular(20), onTap: () => Navigator.pop(context), child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))), ), const SizedBox(width: 8), LocaleKeys.dependentDetails.tr().toText20(color: const Color(0xFF1E293B)), const Spacer(), InkWell( borderRadius: BorderRadius.circular(20), onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)), child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))), ), ], ), ), ), ), body: SafeArea( child: Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 12, 20, 30), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Shown only right after a grant/revoke action taken on this screen - not // when just viewing a dependent by tapping into this screen (_recentAction // is null in that case). if (_recentAction != null) ...[ Center( child: Container( width: 72, height: 72, decoration: BoxDecoration(color: _recentAction! ? const Color(0xFFE6F7F2) : const Color(0xFFFEECEB), shape: BoxShape.circle), child: Center( child: CustomPaint( size: const Size(36, 36), painter: _ShieldIconPainter(color: _recentAction! ? const Color(0xFF168A89) : const Color(0xFFED1C24), isGranted: _recentAction!), ), ), ), ), const SizedBox(height: 10), Center( child: (_recentAction! ? LocaleKeys.mazayaAccessGranted.tr() : LocaleKeys.mazayaAccessRevoked.tr()).toText16( weight: FontWeight.w500, color: _recentAction! ? const Color(0xFF168A89) : const Color(0xFFED1C24), ), ), const SizedBox(height: 16), ], // Profile Card (Mint Green) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: const Color(0xFFE8F7F2), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFBCE6D7), width: 1.2)), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Avatar circle with initials Container( width: 52, height: 52, decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), child: Center(child: initials.toText17(color: const Color(0xFF168A89))), ), const SizedBox(width: 14), // Dependent Name & Relation Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ fullName.toText16(maxlines: 2, weight: FontWeight.w500, color: const Color(0xFF1E293B)), if (relation.isNotEmpty) ...[const SizedBox(height: 3), relation.toText12(maxLine: 1, weight: FontWeight.w500, color: const Color(0xFF168A89))], ], ), ), // 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)], ], ), ), const SizedBox(height: 16), // Key details Card Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFF1F5F9)), boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(0, 2))], ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.keyDetails.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)), if (_isNhicVerified) LocaleKeys.verified.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF059669)), ], ), const SizedBox(height: 14), const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)), const SizedBox(height: 14), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.nationalId.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), formattedId.toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)), ], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.dateOfBirth.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), formattedDob.toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)), ], ), ], ), ), const SizedBox(height: 24), // Services & actions section header LocaleKeys.servicesAndActions.tr().toText17(weight: FontWeight.w500, color: const Color(0xFF1E293B)), const SizedBox(height: 4), LocaleKeys.startDependentFlowsDesc.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), const SizedBox(height: 12), // Mazaya access Card Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFF1F5F9)), boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row: Mazaya access + Badge Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.mazayaAccess.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), decoration: BoxDecoration(color: isAllowed ? const Color(0xFFECFDF5) : const Color(0xFFFEE2E2), borderRadius: BorderRadius.circular(12)), child: (isAllowed ? LocaleKeys.active.tr() : LocaleKeys.accessOff.tr()).toText12( weight: FontWeight.w500, color: isAllowed ? const Color(0xFF059669) : const Color(0xFFEF4444), ), ), ], ), const SizedBox(height: 6), LocaleKeys.allowDependentMazayaOffersDesc.tr(args: [firstName.isNotEmpty ? firstName : fullName]).toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), const SizedBox(height: 16), const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)), const SizedBox(height: 14), // Login method row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.loginMethod.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), LocaleKeys.nationalIdAndDob.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)), ], ), const SizedBox(height: 12), // Verification row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.verification.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)), LocaleKeys.sponsorOtp.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)), ], ), const SizedBox(height: 20), // Action Button: Allow Mazaya Access (Teal) or Revoke Mazaya Access (Red Outline) InkWell( borderRadius: BorderRadius.circular(12), onTap: _onToggleAccess, child: Container( height: 48, width: double.infinity, decoration: BoxDecoration( color: isAllowed ? const Color(0xFFFEF2F2) : const Color(0xFF22B0A6), borderRadius: BorderRadius.circular(12), border: isAllowed ? Border.all(color: const Color(0xFFFECACA), width: 1.2) : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox(width: 18, height: 18, child: CustomPaint(painter: _ShieldIconPainter(color: isAllowed ? const Color(0xFFDC2626) : Colors.white, isGranted: !isAllowed))), const SizedBox(width: 8), (isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr()).toText16( weight: FontWeight.w500, color: isAllowed ? const Color(0xFFDC2626) : Colors.white, ), ], ), ), ), if (isAllowed) ...[ const SizedBox(height: 12), Center(child: LocaleKeys.revokingPreventsAccessDesc.tr().toText11(weight: FontWeight.w400, color: const Color(0xFF64748B), isCenter: true)), ], ], ), ), ], ), ), ), // Share download link - same bottom-sticky button convention used elsewhere in // the app (e.g. "Join Marathon"), shown once access is granted. if (isAllowed) DefaultButton(LocaleKeys.shareDownloadLink.tr(), () => _shareMazayaInvite(isAr, linkToShare)).insideContainer, ], ), ), ); } }