import 'dart:developer'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.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/colors.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/dashboard/menu_entries.dart'; import 'package:mohem_flutter_app/models/get_employee_contacts.model.dart'; import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart'; import 'package:mohem_flutter_app/ui/profile/add_update_family_member.dart'; import 'package:mohem_flutter_app/ui/profile/dependent_details_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:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; class FamilyMembers extends StatefulWidget { const FamilyMembers({Key? key}) : super(key: key); @override _FamilyMembersState createState() => _FamilyMembersState(); } class _FamilyMembersState extends State { List getEmployeeContactsList = []; bool fromMazaya = false; bool _argsLoaded = false; bool _isLoading = false; final TextEditingController _searchController = TextEditingController(); String _searchQuery = ""; @override void initState() { super.initState(); getEmployeeContacts(); } @override void didChangeDependencies() { super.didChangeDependencies(); if (!_argsLoaded) { Object? args = ModalRoute.of(context)?.settings.arguments; if (args is Map && args['fromMazaya'] == true) { fromMazaya = true; } _argsLoaded = true; } } @override void dispose() { _searchController.dispose(); super.dispose(); } bool _isAccessAllowed(GetEmployeeContactsList contact) { String? flag = contact.mAZAYAACCESSFLG ?? contact.mazayaAccess?.toString(); 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 ?? ""; 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), // Same gradient as the "Add another user" button on this screen (DefaultButton's default). confirmButtonColors: const [MyColors.gradiantEndColor, MyColors.gradiantStartColor], confirmText: LocaleKeys.yes.tr(), cancelText: LocaleKeys.no.tr(), onConfirm: () => _toggleMazayaAccess(contact), ); } Future _toggleMazayaAccess(GetEmployeeContactsList contact) async { bool current = _isAccessAllowed(contact); String action = current ? "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, ); // Keep this exact `contact` instance in sync too - it's what gets handed to // DependentDetailsScreen below, and that's a different object than whatever the fresh // list refetch returns, so without this it would show the old (pre-toggle) access state // even though the screen's own "just granted/revoked" badge is correct. contact.mAZAYAACCESSFLG = action == "Grant" ? "Y" : "N"; contact.mazayaAccess = contact.mAZAYAACCESSFLG; List rawList = await ProfileApiClient().getEmployeeContacts(); getEmployeeContactsList = rawList; Utils.hideLoading(context); if (mounted) { setState(() {}); // Same destination as tapping the card itself, so toggling from either entry point // lands on the exact same screen (grant/revoke badge, button, and share-link section // all reflect the new state there) - no toast on top of it. await Navigator.push(context, MaterialPageRoute(builder: (BuildContext cxt) => DependentDetailsScreen(contact: contact, recentAction: action == "Grant"))); getEmployeeContacts(); } } catch (ex) { log("erros: ${ex.toString()}"); Utils.hideLoading(context); Utils.handleException(ex, context, null); } } 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(); } List get _filteredList { if (_searchQuery.trim().isEmpty) return getEmployeeContactsList; String q = _searchQuery.trim().toLowerCase(); return getEmployeeContactsList.where((GetEmployeeContactsList item) { String name = (item.cONTACTNAME ?? "").toLowerCase(); String rel = (item.rELATIONSHIP ?? "").toLowerCase(); String personId = (item.pERSONID?.toString() ?? "").toLowerCase(); String contactPersonId = (item.cONTACTPERSONID?.toString() ?? "").toLowerCase(); String contactType = (item.cONTACTTYPE ?? "").toLowerCase(); return name.contains(q) || rel.contains(q) || personId.contains(q) || contactPersonId.contains(q) || contactType.contains(q); }).toList(); } void getEmployeeContacts() async { setState(() { _isLoading = true; }); try { List rawList = await ProfileApiClient().getEmployeeContacts(); getEmployeeContactsList = rawList; } catch (ex) { Utils.handleException(ex, context, null); } finally { if (mounted) { setState(() { _isLoading = false; }); } } } GetMenuEntriesList _getContactMenuEntries(BuildContext context) { List menuData = context.watch().getMenuEntriesList ?? []; List arr = menuData.where((GetMenuEntriesList e) => e.requestType == 'CONTACT').toList(); return arr.isNotEmpty ? arr.first : GetMenuEntriesList(); } @override Widget build(BuildContext context) { bool isAr = AppState().isArabic(context); GetMenuEntriesList menuEntries = _getContactMenuEntries(context); bool canAddFamilyMember = menuEntries.addButton == 'Y' || menuEntries.updateButton == 'Y'; bool canUpdateFamilyMember = menuEntries.updateButton == 'Y'; bool canDeleteFamilyMember = menuEntries.deleteButton == 'Y'; List displayList = _filteredList; 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), (fromMazaya ? LocaleKeys.myDependents.tr() : LocaleKeys.profile_familyDetails.tr()).toText20(isBold: true, 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: [ // Search Bar Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), child: Container( height: 48, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE2E8F0))), padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( children: [ const Icon(Icons.search, color: Color(0xFF64748B), size: 20), const SizedBox(width: 10), Expanded( child: TextField( controller: _searchController, onChanged: (String val) { setState(() { _searchQuery = val; }); }, decoration: InputDecoration( border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero, hintText: fromMazaya ? LocaleKeys.searchDependents.tr() : LocaleKeys.searchFamilyMembers.tr(), hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 14), ), ), ), if (_searchQuery.isNotEmpty) InkWell( onTap: () { _searchController.clear(); setState(() { _searchQuery = ""; }); }, child: const Padding(padding: EdgeInsets.symmetric(horizontal: 4), child: Icon(Icons.close, color: Color(0xFF94A3B8), size: 18)), ), ], ), ), ), // Dependents list Expanded( child: _isLoading ? _buildDependentsShimmer() : displayList.isEmpty ? Utils.getNoDataWidget(context) : ListView.separated( padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), separatorBuilder: (BuildContext cxt, int index) => const SizedBox(height: 12), itemCount: displayList.length, itemBuilder: (BuildContext context, int index) { return _buildDependentCard(displayList[index], isAr, canUpdateFamilyMember, canDeleteFamilyMember); }, ), ), // Bottom action button: + Add another user DefaultButton( fromMazaya ? LocaleKeys.addAnotherUser.tr() : LocaleKeys.addNewFamilyMember.tr(), (canAddFamilyMember || fromMazaya) ? () async { await Navigator.pushNamed(context, AppRoutes.addDependentVerify, arguments: {'fromMazaya': fromMazaya}); getEmployeeContacts(); } : null, iconData: Icons.add, isTextExpanded: false, height: 48, borderRadius: 12, ).insideContainer, ], ), ), ); } // Same Shimmer.fromColors skeleton-card convention used elsewhere in the app (e.g. // dashboard_screen_for_dependet.dart's retailer/category shimmers), shaped like the // real _buildDependentCard layout so the loading state doesn't jump around once it loads. Widget _buildDependentsShimmer() { return Shimmer.fromColors( baseColor: const Color(0xFFE2E8F0), highlightColor: const Color(0xFFF8FAFC), child: ListView.separated( padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), separatorBuilder: (BuildContext _, int __) => const SizedBox(height: 12), itemCount: 6, itemBuilder: (BuildContext _, int __) { return Container( padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container(width: 48, height: 48, decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle)), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container(height: 18, width: 140, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), const SizedBox(height: 6), Container(height: 14, width: 90, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), ], ), ), ], ), const SizedBox(height: 14), Container(height: 1, color: Colors.white), const SizedBox(height: 12), Center(child: Container(height: 14, width: 150, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4)))), ], ), ); }, ), ); } Widget _buildDependentCard(GetEmployeeContactsList item, bool isAr, bool canUpdate, bool canDelete) { bool isAllowed = _isAccessAllowed(item); return InkWell( borderRadius: BorderRadius.circular(16), onTap: () async { await Navigator.push(context, MaterialPageRoute(builder: (BuildContext cxt) => DependentDetailsScreen(contact: item))); getEmployeeContacts(); }, child: Container( padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFF1F5F9)), boxShadow: const [BoxShadow(color: Color(0x0A000000), blurRadius: 8, offset: Offset(0, 3))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Top row: Avatar, Info, and Status Badge Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Initials Circle Container( width: 48, height: 48, decoration: const BoxDecoration(color: Color(0xFFE6F5F3), shape: BoxShape.circle), child: Center(child: Text(_getInitials(item.cONTACTNAME), style: const TextStyle(color: Color(0xFF168A89), fontWeight: FontWeight.w700, fontSize: 15))), ), const SizedBox(width: 12), // Name & Details Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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))), if (_isNhicVerified(item)) ...[ " • ".toText12(color: const Color(0xFF94A3B8)), LocaleKeys.verified.tr().toText12(weight: FontWeight.w500, color: const Color(0xFF059669)), ], ], ), ], ), ), // Trailing arrow (shown in Mazaya flow): the whole card opens // DependentDetailsScreen on tap, so this just signals that. // Same "arrow_next.svg" + RotatedBox-for-RTL convention used in menus_widget.dart // and services_widget.dart on the dashboard. if (fromMazaya) ...[ const SizedBox(width: 8), RotatedBox(quarterTurns: isAr ? 2 : 4, child: SvgPicture.asset("assets/images/arrow_next.svg", color: const Color(0xFF94A3B8), width: 14)), ], ], ), const SizedBox(height: 14), const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)), const SizedBox(height: 12), // Bottom Action Row if (fromMazaya) ...[ // Mazaya Access Buttons: Revoke or Allow Center( child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () => _confirmToggleMazayaAccess(item), child: Padding( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(isAllowed ? Icons.close : Icons.check, size: isAllowed ? 16 : 18, color: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89)), const SizedBox(width: 6), Text( isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr(), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89), decoration: TextDecoration.underline, decorationColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89), ), ), ], ), ), ), ), ] else ...[ // Profile Flow: Update and Remove action buttons Row( children: [ Expanded( child: InkWell( borderRadius: BorderRadius.circular(10), onTap: canUpdate ? () { int? relationId = item.cONTACTRELATIONSHIPID; if (relationId != null) { showUpdateAlertDialog(context, relationId, 2, "UPDATE"); } } : null, child: Container( height: 38, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: canUpdate ? const Color(0xFFCBD5E1) : const Color(0xFFE2E8F0)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.edit_outlined, size: 15, color: canUpdate ? const Color(0xFF475569) : const Color(0xFF94A3B8)), const SizedBox(width: 6), LocaleKeys.update.tr().toText12(color: canUpdate ? const Color(0xFF475569) : const Color(0xFF94A3B8)), ], ), ), ), ), const SizedBox(width: 10), Expanded( child: InkWell( borderRadius: BorderRadius.circular(10), onTap: canDelete ? () { int? relationId = item.cONTACTRELATIONSHIPID; if (relationId != null) { showRemoveAlertDialog(context, relationId); } } : null, child: Container( height: 38, decoration: BoxDecoration( color: canDelete ? const Color(0xFFFFF5F5) : Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: canDelete ? const Color(0xFFFFCDD2) : const Color(0xFFE2E8F0)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.delete_outline_rounded, size: 15, color: canDelete ? const Color(0xFFED1C24) : const Color(0xFF94A3B8)), const SizedBox(width: 6), LocaleKeys.remove.tr().toText12(color: canDelete ? const Color(0xFFED1C24) : const Color(0xFF94A3B8)), ], ), ), ), ), ], ), ], ], ), ), ); } void showUpdateAlertDialog(BuildContext context, int relationId, int flag, String actionType) { Widget cancelButton = TextButton(child: LocaleKeys.cancel.tr().toText14(), onPressed: () => Navigator.pop(context)); Widget continueButton = TextButton( child: LocaleKeys.ok.tr().toText14(), onPressed: () { Navigator.pop(context); Navigator.pushNamed(context, AppRoutes.addUpdateFamilyMember, arguments: FamilyMemberScreenParams(relationId, flag: 2, actionType: "UPDATE")).then((_) => getEmployeeContacts()); }, ); AlertDialog alert = AlertDialog(title: LocaleKeys.confirm.tr().toText16(isBold: true), content: LocaleKeys.updateMember.tr().toText14(), actions: [cancelButton, continueButton]); showDialog(context: context, builder: (BuildContext context) => alert); } void showRemoveAlertDialog(BuildContext context, int relationId) { Widget cancelButton = TextButton(child: LocaleKeys.cancel.tr().toText14(), onPressed: () => Navigator.pop(context)); Widget continueButton = TextButton( child: LocaleKeys.ok.tr().toText14(), onPressed: () { Navigator.pop(context); Navigator.pushNamed(context, AppRoutes.deleteFamilyMember, arguments: relationId).then((_) => getEmployeeContacts()); }, ); AlertDialog alert = AlertDialog(title: LocaleKeys.confirm.tr().toText16(isBold: true), content: LocaleKeys.removeThisMember.tr().toText14(), actions: [cancelButton, continueButton]); showDialog(context: context, builder: (BuildContext context) => alert); } }