You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mohemm-flutter-app/lib/ui/profile/family_members.dart

561 lines
25 KiB
Dart

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<FamilyMembers> {
List<GetEmployeeContactsList> getEmployeeContactsList = <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<void> _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<GetEmployeeContactsList> 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<dynamic>(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<String> 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<GetEmployeeContactsList> 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<GetEmployeeContactsList> rawList = await ProfileApiClient().getEmployeeContacts();
getEmployeeContactsList = rawList;
} catch (ex) {
Utils.handleException(ex, context, null);
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
GetMenuEntriesList _getContactMenuEntries(BuildContext context) {
List<GetMenuEntriesList> menuData = context.watch<DashboardProviderModel>().getMenuEntriesList ?? <GetMenuEntriesList>[];
List<GetMenuEntriesList> 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<GetEmployeeContactsList> 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: <Widget>[
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: <Widget>[
// 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: <Widget>[
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: <String, dynamic>{'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: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
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: <Widget>[
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<dynamic>(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>[BoxShadow(color: Color(0x0A000000), blurRadius: 8, offset: Offset(0, 3))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Top row: Avatar, Info, and Status Badge
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// 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: <Widget>[
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))),
if (_isNhicVerified(item)) ...<Widget>[
" • ".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) ...<Widget>[
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) ...<Widget>[
// 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: <Widget>[
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 ...<Widget>[
// Profile Flow: Update and Remove action buttons
Row(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[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: <Widget>[cancelButton, continueButton]);
showDialog(context: context, builder: (BuildContext context) => alert);
}
}