Merge branch 'wala_plus' of http://34.17.182.140/Haroon6138/mohemm-flutter-app into wala_plus

* 'wala_plus' of http://34.17.182.140/Haroon6138/mohemm-flutter-app:
  UI Fixes
wala_plus
Sultan khan 3 weeks ago
commit 87bcdf72cb

@ -149,7 +149,7 @@ class Utils {
} }
String errorMessage; String errorMessage;
if (exception.error.errorType != null && exception.error.errorType == 4) { if (exception is APIException && exception.error?.errorType == 4) {
Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.appUpdateScreen, (_) => false, arguments: exception.error?.errorMessage); Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.appUpdateScreen, (_) => false, arguments: exception.error?.errorMessage);
} else { } else {
if (exception is APIException) { if (exception is APIException) {

@ -1,6 +1,10 @@
import 'dart:io';
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/services.dart';
import 'package:mohem_flutter_app/api/profile_api_client.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/classes/utils.dart';
import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/config/routes.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart';
@ -8,6 +12,9 @@ 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/models/get_employee_contacts.model.dart';
import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.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/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 { class _ShieldIconPainter extends CustomPainter {
final Color color; final Color color;
@ -17,12 +24,13 @@ class _ShieldIconPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
Paint paint = Paint() Paint paint =
..color = color Paint()
..style = PaintingStyle.stroke ..color = color
..strokeWidth = 2.0 ..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round ..strokeWidth = 2.0
..strokeJoin = StrokeJoin.round; ..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
double w = size.width; double w = size.width;
double h = size.height; double h = size.height;
@ -44,28 +52,25 @@ class _ShieldIconPainter extends CustomPainter {
checkPath.lineTo(w * 0.66, h * 0.39); checkPath.lineTo(w * 0.66, h * 0.39);
canvas.drawPath(checkPath, paint); canvas.drawPath(checkPath, paint);
} else { } else {
canvas.drawLine( canvas.drawLine(Offset(w * 0.38, h * 0.38), Offset(w * 0.62, h * 0.62), paint);
Offset(w * 0.38, h * 0.38), canvas.drawLine(Offset(w * 0.62, h * 0.38), Offset(w * 0.38, h * 0.62), paint);
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 @override
bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) => bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) => color != oldDelegate.color || isGranted != oldDelegate.isGranted;
color != oldDelegate.color || isGranted != oldDelegate.isGranted;
} }
class DependentDetailsScreen extends StatefulWidget { class DependentDetailsScreen extends StatefulWidget {
final GetEmployeeContactsList? contact; final GetEmployeeContactsList? contact;
const DependentDetailsScreen({Key? key, this.contact}) : super(key: key); // True when this screen is being opened right after a successful grant (e.g. from the
// dependents list's inline "Allow Mazaya Access" link), so the success badge shows on
// first build. Granting from within this screen itself (_performToggleAccess) sets the
// equivalent local state directly instead of needing this.
final bool justGranted;
const DependentDetailsScreen({Key? key, this.contact, this.justGranted = false}) : super(key: key);
@override @override
_DependentDetailsScreenState createState() => _DependentDetailsScreenState(); _DependentDetailsScreenState createState() => _DependentDetailsScreenState();
@ -74,6 +79,7 @@ class DependentDetailsScreen extends StatefulWidget {
class _DependentDetailsScreenState extends State<DependentDetailsScreen> { class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
GetEmployeeContactsList? _contact; GetEmployeeContactsList? _contact;
bool _argsLoaded = false; bool _argsLoaded = false;
bool _justGranted = false;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@ -89,6 +95,7 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
_contact = args['contact'] as GetEmployeeContactsList; _contact = args['contact'] as GetEmployeeContactsList;
} }
} }
_justGranted = widget.justGranted;
_argsLoaded = true; _argsLoaded = true;
} }
} }
@ -141,15 +148,7 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
DateTime? dt = DateTime.tryParse(s); DateTime? dt = DateTime.tryParse(s);
if (dt != null) return DateTime(dt.year, dt.month, dt.day); if (dt != null) return DateTime(dt.year, dt.month, dt.day);
List<String> formats = <String>[ List<String> formats = <String>['yyyy-MM-dd', 'dd/MM/yyyy', 'MM/dd/yyyy', 'dd-MM-yyyy', 'yyyy/MM/dd', 'dd-MMM-yyyy', 'dd MMM yyyy'];
'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) { for (String fmt in formats) {
try { try {
DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(s); DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(s);
@ -185,14 +184,9 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
ConfirmBottomSheet.show( ConfirmBottomSheet.show(
context, context,
title: isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr(), title: isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr(),
message: isAllowed message: isAllowed ? LocaleKeys.confirmRevokeMazayaAccess.tr(args: <String>[dependentName]) : LocaleKeys.confirmGrantMazayaAccess.tr(args: <String>[dependentName]),
? LocaleKeys.confirmRevokeMazayaAccess.tr(args: <String>[dependentName])
: LocaleKeys.confirmGrantMazayaAccess.tr(args: <String>[dependentName]),
icon: isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline, icon: isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline,
iconColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89), iconColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89),
confirmButtonColors: isAllowed
? const <Color>[Color(0xFFEF4444), Color(0xFFDC2626)]
: const <Color>[Color(0xFF22B0A6), Color(0xFF168A89)],
confirmText: LocaleKeys.yes.tr(), confirmText: LocaleKeys.yes.tr(),
cancelText: LocaleKeys.no.tr(), cancelText: LocaleKeys.no.tr(),
onConfirm: _performToggleAccess, onConfirm: _performToggleAccess,
@ -216,27 +210,19 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
setState(() { setState(() {
_contact!.mAZAYAACCESSFLG = action == "Grant" ? "Y" : "N"; _contact!.mAZAYAACCESSFLG = action == "Grant" ? "Y" : "N";
_contact!.mazayaAccess = _contact!.mAZAYAACCESSFLG; _contact!.mazayaAccess = _contact!.mAZAYAACCESSFLG;
// Tick badge reflects the most recent action, not just "was granted at some point" -
// a later revoke clears it so a stale grant doesn't linger in state.
_justGranted = action == "Grant";
}); });
Utils.hideLoading(context); Utils.hideLoading(context);
// Stay on this same screen either way - it already reflects the new access state via
// setState above (status badge, action button, and the share-link section all update
// in place), so tapping a dependent card and successfully granting access both land on
// the exact same screen instead of a separate "success" screen.
if (mounted) { if (mounted) {
if (action == "Grant") { Utils.showToast(action == "Grant" ? LocaleKeys.mazayaAccessGranted.tr() : LocaleKeys.revokeMazayaAccess.tr());
Navigator.push(
context,
MaterialPageRoute<dynamic>(
builder: (BuildContext cxt) => MazayaAccessResultScreen(
isGranted: true,
dependentName: _contact!.cONTACTNAME ?? "",
dependentInitials: _getInitials(_contact!.cONTACTNAME),
nationalId: _contact!.nATIONALIDENTIFIER,
dateOfBirth: _contact!.dATEOFBIRTH,
),
),
);
} else {
Utils.showToast(LocaleKeys.revokeMazayaAccess.tr());
}
} }
} catch (ex) { } catch (ex) {
Utils.hideLoading(context); Utils.hideLoading(context);
@ -244,18 +230,58 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
} }
} }
// 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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
DateTime? dob = _parseBirthDate(_contact?.dATEOFBIRTH); DateTime? dob = _parseBirthDate(_contact?.dATEOFBIRTH);
String initials = _getInitials(_contact?.cONTACTNAME); String initials = _getInitials(_contact?.cONTACTNAME);
String fullName = _contact?.cONTACTNAME ?? ""; String fullName = _contact?.cONTACTNAME ?? "";
String firstName = _getFirstName(_contact?.cONTACTNAME); String firstName = _getFirstName(_contact?.cONTACTNAME);
String relation = (_contact?.rELATIONSHIP != null && _contact!.rELATIONSHIP!.trim().isNotEmpty) String relation = (_contact?.rELATIONSHIP != null && _contact!.rELATIONSHIP!.trim().isNotEmpty) ? _contact!.rELATIONSHIP!.trim() : (_contact?.cONTACTTYPE?.trim() ?? "");
? _contact!.rELATIONSHIP!.trim()
: (_contact?.cONTACTTYPE?.trim() ?? "");
String formattedId = _formatNationalId(_contact?.nATIONALIDENTIFIER); String formattedId = _formatNationalId(_contact?.nATIONALIDENTIFIER);
String formattedDob = _formatDob(dob, _contact?.dATEOFBIRTH); String formattedDob = _formatDob(dob, _contact?.dATEOFBIRTH);
bool isAllowed = _isAccessAllowed; bool isAllowed = _isAccessAllowed;
bool isAr = AppState().isArabic(context);
String linkToShare = MazayaAccessResultScreen.getDownloadLink();
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8FAFC), backgroundColor: const Color(0xFFF8FAFC),
@ -271,31 +297,15 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
InkWell( InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.pop(context), onTap: () => Navigator.pop(context),
child: const Padding( child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))),
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.arrow_back_ios_new_rounded,
size: 20,
color: Color(0xFF1E293B),
),
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
LocaleKeys.dependentDetails.tr().toText20( LocaleKeys.dependentDetails.tr().toText20(color: const Color(0xFF1E293B)),
color: const Color(0xFF1E293B),
),
const Spacer(), const Spacer(),
InkWell( InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)), onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)),
child: const Padding( child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))),
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.home_outlined,
size: 24,
color: Color(0xFF1E293B),
),
),
), ),
], ],
), ),
@ -303,292 +313,214 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
), ),
), ),
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: Column(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 30), children: <Widget>[
child: Column( Expanded(
crossAxisAlignment: CrossAxisAlignment.start, child: SingleChildScrollView(
children: <Widget>[ padding: const EdgeInsets.fromLTRB(20, 12, 20, 30),
// Profile Card (Mint Green) child: Column(
Container( crossAxisAlignment: CrossAxisAlignment.start,
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: <Widget>[ children: <Widget>[
// Avatar circle with initials // Shown only right after a successful grant (not when just viewing an
Container( // already-granted dependent by tapping into this screen).
width: 52, if (_justGranted && isAllowed) ...<Widget>[
height: 52, Center(
decoration: const BoxDecoration( child: Container(
color: Colors.white, width: 72,
shape: BoxShape.circle, height: 72,
), decoration: const BoxDecoration(color: Color(0xFFE6F7F2), shape: BoxShape.circle),
child: Center( child: Center(child: CustomPaint(size: const Size(36, 36), painter: _ShieldIconPainter(color: const Color(0xFF168A89), isGranted: true))),
child: initials.toText17(
color: const Color(0xFF168A89),
), ),
), ),
), const SizedBox(height: 10),
const SizedBox(width: 14), Center(child: LocaleKeys.mazayaAccessGranted.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF168A89))),
const SizedBox(height: 16),
],
// Dependent Name & Relation // Profile Card (Mint Green)
Expanded( Container(
child: Column( width: double.infinity,
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(16),
mainAxisSize: MainAxisSize.min, 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: <Widget>[ children: <Widget>[
fullName.toText16( // Avatar circle with initials
maxlines: 2, Container(
weight: FontWeight.w500, width: 52,
color: const Color(0xFF1E293B), height: 52,
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: Center(child: initials.toText17(color: const Color(0xFF168A89))),
), ),
if (relation.isNotEmpty) ...<Widget>[ const SizedBox(width: 14),
const SizedBox(height: 3),
relation.toText12( // Dependent Name & Relation
maxLine: 1, Expanded(
weight: FontWeight.w500, child: Column(
color: const Color(0xFF168A89), crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
fullName.toText16(maxlines: 2, weight: FontWeight.w500, color: const Color(0xFF1E293B)),
if (relation.isNotEmpty) ...<Widget>[const SizedBox(height: 3), relation.toText12(maxLine: 1, weight: FontWeight.w500, color: const Color(0xFF168A89))],
],
), ),
], ),
// Verified badge on right
const SizedBox(width: 8),
const Icon(Icons.verified_outlined, color: Color(0xFF059669), size: 24),
], ],
), ),
), ),
// Verified badge on right const SizedBox(height: 16),
const SizedBox(width: 8),
const Icon(
Icons.verified_outlined,
color: Color(0xFF059669),
size: 24,
),
],
),
),
const SizedBox(height: 16), // Key details Card
Container(
// Key details Card width: double.infinity,
Container( padding: const EdgeInsets.all(16),
width: double.infinity, decoration: BoxDecoration(
padding: const EdgeInsets.all(16), color: Colors.white,
decoration: BoxDecoration( borderRadius: BorderRadius.circular(16),
color: Colors.white, border: Border.all(color: const Color(0xFFF1F5F9)),
borderRadius: BorderRadius.circular(16), boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(0, 2))],
border: Border.all(color: const Color(0xFFF1F5F9)), ),
boxShadow: const <BoxShadow>[ child: Column(
BoxShadow( children: <Widget>[
color: Color(0x08000000), Row(
blurRadius: 8, mainAxisAlignment: MainAxisAlignment.spaceBetween,
offset: Offset(0, 2), children: <Widget>[
), LocaleKeys.keyDetails.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
], LocaleKeys.verified.tr().toText13(weight: FontWeight.w500, color: const Color(0xFF059669)),
), ],
child: Column( ),
children: <Widget>[ const SizedBox(height: 14),
Row( const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)),
mainAxisAlignment: MainAxisAlignment.spaceBetween, const SizedBox(height: 14),
children: <Widget>[ Row(
LocaleKeys.keyDetails.tr().toText16( mainAxisAlignment: MainAxisAlignment.spaceBetween,
weight: FontWeight.w500, children: <Widget>[
color: const Color(0xFF1E293B), LocaleKeys.nationalId.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
), formattedId.toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
LocaleKeys.verified.tr().toText13( ],
weight: FontWeight.w500, ),
color: const Color(0xFF059669), const SizedBox(height: 12),
), Row(
], mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: <Widget>[
const SizedBox(height: 14), LocaleKeys.dateOfBirth.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)), formattedDob.toText13(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
const SizedBox(height: 14), ],
Row( ),
mainAxisAlignment: MainAxisAlignment.spaceBetween, ],
children: <Widget>[ ),
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: <Widget>[
LocaleKeys.dateOfBirth.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
formattedDob.toText13(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
],
), ),
],
),
),
const SizedBox(height: 24), const SizedBox(height: 24),
// Services & actions section header // Services & actions section header
LocaleKeys.servicesAndActions.tr().toText17( LocaleKeys.servicesAndActions.tr().toText17(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
weight: FontWeight.w500, const SizedBox(height: 4),
color: const Color(0xFF1E293B), LocaleKeys.startDependentFlowsDesc.tr().toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
),
const SizedBox(height: 4),
LocaleKeys.startDependentFlowsDesc.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
const SizedBox(height: 12), const SizedBox(height: 12),
// Mazaya access Card // Mazaya access Card
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF1F5F9)), border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: const <BoxShadow>[ boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(0, 2))],
BoxShadow( ),
color: Color(0x08000000), child: Column(
blurRadius: 8, crossAxisAlignment: CrossAxisAlignment.start,
offset: Offset(0, 2), children: <Widget>[
), // Header Row: Mazaya access + Badge
], Row(
), mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Column( children: <Widget>[
crossAxisAlignment: CrossAxisAlignment.start, LocaleKeys.mazayaAccess.tr().toText16(weight: FontWeight.w500, color: const Color(0xFF1E293B)),
children: <Widget>[ Container(
// Header Row: Mazaya access + Badge padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
Row( decoration: BoxDecoration(color: isAllowed ? const Color(0xFFECFDF5) : const Color(0xFFFEE2E2), borderRadius: BorderRadius.circular(12)),
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: (isAllowed ? LocaleKeys.active.tr() : LocaleKeys.accessOff.tr()).toText12(
children: <Widget>[ weight: FontWeight.w500,
LocaleKeys.mazayaAccess.tr().toText16( color: isAllowed ? const Color(0xFF059669) : const Color(0xFFEF4444),
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( const SizedBox(height: 6),
weight: FontWeight.w500, LocaleKeys.allowDependentMazayaOffersDesc.tr(args: <String>[firstName.isNotEmpty ? firstName : fullName]).toText13(weight: FontWeight.w400, color: const Color(0xFF64748B)),
color: isAllowed ? const Color(0xFF059669) : const Color(0xFFEF4444), 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: <Widget>[
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: <Widget>[
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: 6),
LocaleKeys.allowDependentMazayaOffersDesc.tr(
args: <String>[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: <Widget>[
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 const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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(
// Action Button: Allow Mazaya Access (Teal) or Revoke Mazaya Access (Red Outline) borderRadius: BorderRadius.circular(12),
InkWell( onTap: _onToggleAccess,
borderRadius: BorderRadius.circular(12), child: Container(
onTap: _onToggleAccess, height: 48,
child: Container( width: double.infinity,
height: 48, decoration: BoxDecoration(
width: double.infinity, color: isAllowed ? const Color(0xFFFEF2F2) : const Color(0xFF22B0A6),
decoration: BoxDecoration( borderRadius: BorderRadius.circular(12),
color: isAllowed ? const Color(0xFFFEF2F2) : const Color(0xFF22B0A6), border: isAllowed ? Border.all(color: const Color(0xFFFECACA), width: 1.2) : null,
borderRadius: BorderRadius.circular(12), ),
border: isAllowed ? Border.all(color: const Color(0xFFFECACA), width: 1.2) : null, child: Row(
), mainAxisAlignment: MainAxisAlignment.center,
child: Row( children: <Widget>[
mainAxisAlignment: MainAxisAlignment.center, SizedBox(width: 18, height: 18, child: CustomPaint(painter: _ShieldIconPainter(color: isAllowed ? const Color(0xFFDC2626) : Colors.white, isGranted: !isAllowed))),
children: <Widget>[ const SizedBox(width: 8),
SizedBox( (isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr()).toText16(
width: 18, weight: FontWeight.w500,
height: 18, color: isAllowed ? const Color(0xFFDC2626) : Colors.white,
child: CustomPaint( ),
painter: _ShieldIconPainter( ],
color: isAllowed ? const Color(0xFFDC2626) : Colors.white,
isGranted: !isAllowed,
),
), ),
), ),
const SizedBox(width: 8), ),
(isAllowed
? LocaleKeys.revokeMazayaAccess.tr() if (isAllowed) ...<Widget>[
: LocaleKeys.allowMazayaAccess.tr()) const SizedBox(height: 12),
.toText16( Center(child: LocaleKeys.revokingPreventsAccessDesc.tr().toText11(weight: FontWeight.w400, color: const Color(0xFF64748B), isCenter: true)),
weight: FontWeight.w500,
color: isAllowed ? const Color(0xFFDC2626) : Colors.white,
),
], ],
), ],
), ),
), ),
if (isAllowed) ...<Widget>[
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,
],
), ),
), ),
); );

@ -1,5 +1,6 @@
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:mohem_flutter_app/api/profile_api_client.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/app_state/app_state.dart';
import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/classes/colors.dart';
@ -12,10 +13,10 @@ 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/provider/dashboard_provider_model.dart';
import 'package:mohem_flutter_app/ui/profile/add_update_family_member.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/ui/profile/dependent_details_screen.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/bottom_sheets/confirm_bottom_sheet.dart';
import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class FamilyMembers extends StatefulWidget { class FamilyMembers extends StatefulWidget {
const FamilyMembers({Key? key}) : super(key: key); const FamilyMembers({Key? key}) : super(key: key);
@ -101,18 +102,10 @@ class _FamilyMembersState extends State<FamilyMembers> {
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
if (action == "Grant") { if (action == "Grant") {
Navigator.push( // Same destination as tapping the card itself, so granting from either entry
context, // point lands on the exact same screen (which now includes the share-link section).
MaterialPageRoute<dynamic>( await Navigator.push(context, MaterialPageRoute<dynamic>(builder: (BuildContext cxt) => DependentDetailsScreen(contact: contact, justGranted: true)));
builder: (BuildContext cxt) => MazayaAccessResultScreen( getEmployeeContacts();
isGranted: true,
dependentName: contact.cONTACTNAME ?? "",
dependentInitials: _getInitials(contact.cONTACTNAME),
nationalId: contact.nATIONALIDENTIFIER,
dateOfBirth: contact.dATEOFBIRTH,
),
),
);
} else { } else {
Utils.showToast(LocaleKeys.revokeMazayaAccess.tr()); Utils.showToast(LocaleKeys.revokeMazayaAccess.tr());
} }
@ -142,11 +135,7 @@ class _FamilyMembersState extends State<FamilyMembers> {
String personId = (item.pERSONID?.toString() ?? "").toLowerCase(); String personId = (item.pERSONID?.toString() ?? "").toLowerCase();
String contactPersonId = (item.cONTACTPERSONID?.toString() ?? "").toLowerCase(); String contactPersonId = (item.cONTACTPERSONID?.toString() ?? "").toLowerCase();
String contactType = (item.cONTACTTYPE ?? "").toLowerCase(); String contactType = (item.cONTACTTYPE ?? "").toLowerCase();
return name.contains(q) || return name.contains(q) || rel.contains(q) || personId.contains(q) || contactPersonId.contains(q) || contactType.contains(q);
rel.contains(q) ||
personId.contains(q) ||
contactPersonId.contains(q) ||
contactType.contains(q);
}).toList(); }).toList();
} }
@ -198,35 +187,15 @@ class _FamilyMembersState extends State<FamilyMembers> {
InkWell( InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.pop(context), onTap: () => Navigator.pop(context),
child: const Padding( child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))),
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.arrow_back_ios_new_rounded,
size: 20,
color: Color(0xFF1E293B),
),
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
(fromMazaya (fromMazaya ? LocaleKeys.myDependents.tr() : LocaleKeys.profile_familyDetails.tr()).toText20(isBold: true, color: const Color(0xFF1E293B)),
? LocaleKeys.myDependents.tr()
: LocaleKeys.profile_familyDetails.tr())
.toText20(
isBold: true,
color: const Color(0xFF1E293B),
),
const Spacer(), const Spacer(),
InkWell( InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)), onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)),
child: const Padding( child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))),
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.home_outlined,
size: 24,
color: Color(0xFF1E293B),
),
),
), ),
], ],
), ),
@ -241,11 +210,7 @@ class _FamilyMembersState extends State<FamilyMembers> {
padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), padding: const EdgeInsets.fromLTRB(20, 8, 20, 12),
child: Container( child: Container(
height: 48, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE2E8F0))),
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
padding: const EdgeInsets.symmetric(horizontal: 14), padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
@ -263,9 +228,7 @@ class _FamilyMembersState extends State<FamilyMembers> {
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
hintText: fromMazaya hintText: fromMazaya ? LocaleKeys.searchDependents.tr() : LocaleKeys.searchFamilyMembers.tr(),
? LocaleKeys.searchDependents.tr()
: LocaleKeys.searchFamilyMembers.tr(),
hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 14), hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 14),
), ),
), ),
@ -278,10 +241,7 @@ class _FamilyMembersState extends State<FamilyMembers> {
_searchQuery = ""; _searchQuery = "";
}); });
}, },
child: const Padding( child: const Padding(padding: EdgeInsets.symmetric(horizontal: 4), child: Icon(Icons.close, color: Color(0xFF94A3B8), size: 18)),
padding: EdgeInsets.symmetric(horizontal: 4),
child: Icon(Icons.close, color: Color(0xFF94A3B8), size: 18),
),
), ),
], ],
), ),
@ -290,45 +250,29 @@ class _FamilyMembersState extends State<FamilyMembers> {
// Dependents list // Dependents list
Expanded( Expanded(
child: _isLoading child:
? Center( _isLoading
child: Image.asset( ? _buildDependentsShimmer()
"assets/images/logos/loading_mohemm_logo.gif", : displayList.isEmpty
height: 96.0,
width: 96.0,
),
)
: displayList.isEmpty
? Utils.getNoDataWidget(context) ? Utils.getNoDataWidget(context)
: ListView.separated( : ListView.separated(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), padding: const EdgeInsets.fromLTRB(20, 4, 20, 20),
separatorBuilder: (BuildContext cxt, int index) => const SizedBox(height: 12), separatorBuilder: (BuildContext cxt, int index) => const SizedBox(height: 12),
itemCount: displayList.length, itemCount: displayList.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return _buildDependentCard( return _buildDependentCard(displayList[index], isAr, canUpdateFamilyMember, canDeleteFamilyMember);
displayList[index], },
isAr, ),
canUpdateFamilyMember,
canDeleteFamilyMember,
);
},
),
), ),
// Bottom action button: + Add another user // Bottom action button: + Add another user
DefaultButton( DefaultButton(
fromMazaya fromMazaya ? LocaleKeys.addAnotherUser.tr() : LocaleKeys.addNewFamilyMember.tr(),
? LocaleKeys.addAnotherUser.tr()
: LocaleKeys.addNewFamilyMember.tr(),
(canAddFamilyMember || fromMazaya) (canAddFamilyMember || fromMazaya)
? () async { ? () async {
await Navigator.pushNamed( await Navigator.pushNamed(context, AppRoutes.addDependentVerify, arguments: <String, dynamic>{'fromMazaya': fromMazaya});
context, getEmployeeContacts();
AppRoutes.addDependentVerify, }
arguments: <String, dynamic>{'fromMazaya': fromMazaya},
);
getEmployeeContacts();
}
: null, : null,
iconData: Icons.add, iconData: Icons.add,
isTextExpanded: false, isTextExpanded: false,
@ -341,279 +285,232 @@ class _FamilyMembersState extends State<FamilyMembers> {
); );
} }
Widget _buildDependentCard( // Same Shimmer.fromColors skeleton-card convention used elsewhere in the app (e.g.
GetEmployeeContactsList item, // dashboard_screen_for_dependet.dart's retailer/category shimmers), shaped like the
bool isAr, // real _buildDependentCard layout so the loading state doesn't jump around once it loads.
bool canUpdate, Widget _buildDependentsShimmer() {
bool canDelete, 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); bool isAllowed = _isAccessAllowed(item);
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
onTap: () async { onTap: () async {
await Navigator.push( await Navigator.push(context, MaterialPageRoute<dynamic>(builder: (BuildContext cxt) => DependentDetailsScreen(contact: item)));
context,
MaterialPageRoute<dynamic>(
builder: (BuildContext cxt) => DependentDetailsScreen(contact: item),
),
);
getEmployeeContacts(); getEmployeeContacts();
}, },
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), padding: const EdgeInsets.fromLTRB(16, 16, 16, 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF1F5F9)), border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: const <BoxShadow>[ boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x0A000000), blurRadius: 8, offset: Offset(0, 3))],
BoxShadow( ),
color: Color(0x0A000000), child: Column(
blurRadius: 8, crossAxisAlignment: CrossAxisAlignment.start,
offset: Offset(0, 3), children: <Widget>[
), // Top row: Avatar, Info, and Status Badge
], Row(
), crossAxisAlignment: CrossAxisAlignment.center,
child: Column( children: <Widget>[
crossAxisAlignment: CrossAxisAlignment.start, // Initials Circle
children: <Widget>[ Container(
// Top row: Avatar, Info, and Status Badge width: 48,
Row( height: 48,
crossAxisAlignment: CrossAxisAlignment.center, decoration: const BoxDecoration(color: Color(0xFFE6F5F3), shape: BoxShape.circle),
children: <Widget>[ child: Center(child: Text(_getInitials(item.cONTACTNAME), style: const TextStyle(color: Color(0xFF168A89), fontWeight: FontWeight.w700, fontSize: 15))),
// Initials Circle
Container(
width: 48,
height: 48,
decoration: const BoxDecoration(
color: Color(0xFFE6F5F3),
shape: BoxShape.circle,
), ),
child: Center( const SizedBox(width: 12),
child: Text(
_getInitials(item.cONTACTNAME), // Name & Details
style: const TextStyle( Expanded(
color: Color(0xFF168A89), child: Column(
fontWeight: FontWeight.w700, crossAxisAlignment: CrossAxisAlignment.start,
fontSize: 15, children: <Widget>[
), Text(item.cONTACTNAME ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B))),
const SizedBox(height: 3),
Row(children: <Widget>[Flexible(child: (item.rELATIONSHIP ?? "").toText12(maxLine: 1, color: const Color(0xFF168A89)))]),
],
), ),
), ),
),
const SizedBox(width: 12),
// Name & Details // Trailing arrow (shown in Mazaya flow): the whole card opens
Expanded( // DependentDetailsScreen on tap, so this just signals that.
child: Column( // Same "arrow_next.svg" + RotatedBox-for-RTL convention used in menus_widget.dart
crossAxisAlignment: CrossAxisAlignment.start, // and services_widget.dart on the dashboard.
children: <Widget>[ if (fromMazaya) ...<Widget>[
Text( const SizedBox(width: 8),
item.cONTACTNAME ?? "", RotatedBox(quarterTurns: isAr ? 2 : 4, child: SvgPicture.asset("assets/images/arrow_next.svg", color: const Color(0xFF94A3B8), width: 14)),
maxLines: 1, ],
overflow: TextOverflow.ellipsis, ],
style: const TextStyle( ),
fontSize: 16,
fontWeight: FontWeight.w600, const SizedBox(height: 14),
color: Color(0xFF1E293B), const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)),
), const SizedBox(height: 12),
),
const SizedBox(height: 3), // Bottom Action Row
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>[ children: <Widget>[
Flexible( Icon(isAllowed ? Icons.close : Icons.check, size: isAllowed ? 16 : 18, color: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89)),
child: (item.rELATIONSHIP ?? "").toText12( const SizedBox(width: 6),
maxLine: 1, Text(
color: const Color(0xFF168A89), 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),
), ),
), ),
], ],
), ),
],
),
),
// Top right pill (shown in Mazaya flow)
if (fromMazaya) ...<Widget>[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isAllowed ? const Color(0xFFE8F8F0) : const Color(0xFFFDE8E8),
borderRadius: BorderRadius.circular(20),
),
child: Text(
isAllowed ? LocaleKeys.active.tr() : LocaleKeys.noAccess.tr(),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: isAllowed ? const Color(0xFF168A89) : const Color(0xFFDC2626),
),
),
),
],
],
),
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>[
] else ...<Widget>[ // Profile Flow: Update and Remove action buttons
// Profile Flow: Update and Remove action buttons Row(
Row( children: <Widget>[
children: <Widget>[ Expanded(
Expanded( child: InkWell(
child: InkWell( borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10), onTap:
onTap: canUpdate canUpdate
? () { ? () {
int? relationId = item.cONTACTRELATIONSHIPID; int? relationId = item.cONTACTRELATIONSHIPID;
if (relationId != null) { if (relationId != null) {
showUpdateAlertDialog(context, relationId, 2, "UPDATE"); showUpdateAlertDialog(context, relationId, 2, "UPDATE");
} }
} }
: null, : null,
child: Container( child: Container(
height: 38, height: 38,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all( border: Border.all(color: canUpdate ? const Color(0xFFCBD5E1) : const Color(0xFFE2E8F0)),
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)),
],
), ),
),
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),
const SizedBox(width: 10), Expanded(
Expanded( child: InkWell(
child: InkWell( borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10), onTap:
onTap: canDelete canDelete
? () { ? () {
int? relationId = item.cONTACTRELATIONSHIPID; int? relationId = item.cONTACTRELATIONSHIPID;
if (relationId != null) { if (relationId != null) {
showRemoveAlertDialog(context, relationId); showRemoveAlertDialog(context, relationId);
} }
} }
: null, : null,
child: Container( child: Container(
height: 38, height: 38,
decoration: BoxDecoration( decoration: BoxDecoration(
color: canDelete ? const Color(0xFFFFF5F5) : Colors.white, color: canDelete ? const Color(0xFFFFF5F5) : Colors.white,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all( border: Border.all(color: canDelete ? const Color(0xFFFFCDD2) : const Color(0xFFE2E8F0)),
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)),
],
), ),
),
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) { void showUpdateAlertDialog(BuildContext context, int relationId, int flag, String actionType) {
Widget cancelButton = TextButton( Widget cancelButton = TextButton(child: LocaleKeys.cancel.tr().toText14(), onPressed: () => Navigator.pop(context));
child: LocaleKeys.cancel.tr().toText14(),
onPressed: () => Navigator.pop(context),
);
Widget continueButton = TextButton( Widget continueButton = TextButton(
child: LocaleKeys.ok.tr().toText14(), child: LocaleKeys.ok.tr().toText14(),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
Navigator.pushNamed( Navigator.pushNamed(context, AppRoutes.addUpdateFamilyMember, arguments: FamilyMemberScreenParams(relationId, flag: 2, actionType: "UPDATE")).then((_) => getEmployeeContacts());
context,
AppRoutes.addUpdateFamilyMember,
arguments: FamilyMemberScreenParams(relationId, flag: 2, actionType: "UPDATE"),
).then((_) => getEmployeeContacts());
}, },
); );
AlertDialog alert = AlertDialog( AlertDialog alert = AlertDialog(title: LocaleKeys.confirm.tr().toText16(isBold: true), content: LocaleKeys.updateMember.tr().toText14(), actions: <Widget>[cancelButton, continueButton]);
title: LocaleKeys.confirm.tr().toText16(isBold: true), showDialog(context: context, builder: (BuildContext context) => alert);
content: LocaleKeys.updateMember.tr().toText14(),
actions: <Widget>[cancelButton, continueButton],
);
showDialog(
context: context,
builder: (BuildContext context) => alert,
);
} }
void showRemoveAlertDialog(BuildContext context, int relationId) { void showRemoveAlertDialog(BuildContext context, int relationId) {
Widget cancelButton = TextButton( Widget cancelButton = TextButton(child: LocaleKeys.cancel.tr().toText14(), onPressed: () => Navigator.pop(context));
child: LocaleKeys.cancel.tr().toText14(),
onPressed: () => Navigator.pop(context),
);
Widget continueButton = TextButton( Widget continueButton = TextButton(
child: LocaleKeys.ok.tr().toText14(), child: LocaleKeys.ok.tr().toText14(),
onPressed: () { onPressed: () {
@ -621,14 +518,7 @@ class _FamilyMembersState extends State<FamilyMembers> {
Navigator.pushNamed(context, AppRoutes.deleteFamilyMember, arguments: relationId).then((_) => getEmployeeContacts()); Navigator.pushNamed(context, AppRoutes.deleteFamilyMember, arguments: relationId).then((_) => getEmployeeContacts());
}, },
); );
AlertDialog alert = AlertDialog( AlertDialog alert = AlertDialog(title: LocaleKeys.confirm.tr().toText16(isBold: true), content: LocaleKeys.removeThisMember.tr().toText14(), actions: <Widget>[cancelButton, continueButton]);
title: LocaleKeys.confirm.tr().toText16(isBold: true), showDialog(context: context, builder: (BuildContext context) => alert);
content: LocaleKeys.removeThisMember.tr().toText14(),
actions: <Widget>[cancelButton, continueButton],
);
showDialog(
context: context,
builder: (BuildContext context) => alert,
);
} }
} }

@ -87,6 +87,15 @@ class MazayaAccessResultScreen extends StatelessWidget {
return channel == 33 ? "https://mohemm.hmg.com/mhmg.html" : "https://mohemm.hmg.com/mohem.html"; return channel == 33 ? "https://mohemm.hmg.com/mhmg.html" : "https://mohemm.hmg.com/mohem.html";
} }
// Channel 33 (HMG) gets the HMG-branded guide, everything else gets the CS-branded one -
// same channel check as getDownloadLink() - crossed with the current app language.
static String getShareGuidePdfAssetPath(bool isAr) {
int channel = AppState().postParamsObject?.channel ?? 31;
String brand = channel == 33 ? "HMG" : "CS";
String lang = isAr ? "ar" : "en";
return "assets/pdf/Mazaya Access Guide $brand $lang.pdf";
}
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();
@ -104,7 +113,6 @@ class MazayaAccessResultScreen extends StatelessWidget {
// Resolve arguments if passed via ModalRoute // Resolve arguments if passed via ModalRoute
Object? routeArgs = ModalRoute.of(context)?.settings.arguments; Object? routeArgs = ModalRoute.of(context)?.settings.arguments;
log("MazayaAccessResultScreen routeArgs: $routeArgs");
bool effectiveGranted = isGranted; bool effectiveGranted = isGranted;
String effectiveName = dependentName; String effectiveName = dependentName;
String? effectiveInitials = dependentInitials; String? effectiveInitials = dependentInitials;
@ -441,16 +449,20 @@ class MazayaAccessResultScreen extends StatelessWidget {
return s; return s;
} }
// Builds the invite text (link + localized instructions + credentials) and shares it // Same message built for both "Share Download Link" and "Copy Link" - they must always
// together with the language-appropriate steps PDF from assets. Falls back to sharing // carry identical content, so this is the single source of truth for it.
// just the text if the PDF can't be loaded/copied for any reason. String _buildMazayaInviteMessage(String linkToShare, String nationalId, String dateOfBirth) {
Future<void> _shareMazayaInvite(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async {
String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId"; String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId";
String message = "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDob(dateOfBirth)}"; return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDob(dateOfBirth)}";
}
// 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<void> _shareMazayaInvite(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async {
String message = _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth);
try { try {
String assetFileName = isAr ? "Mazaya Access Guide Arabic.pdf" : "Mazaya Access Guide English.pdf"; ByteData bytes = await rootBundle.load(getShareGuidePdfAssetPath(isAr));
ByteData bytes = await rootBundle.load("assets/pdf/$assetFileName");
Directory tempDir = await getTemporaryDirectory(); Directory tempDir = await getTemporaryDirectory();
// Write to a space-free filename locally - the asset path above is what actually matters. // Write to a space-free filename locally - the asset path above is what actually matters.
String localFileName = isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf"; String localFileName = isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf";
@ -487,7 +499,7 @@ class MazayaAccessResultScreen extends StatelessWidget {
DefaultButton( DefaultButton(
LocaleKeys.copyLink.tr(), LocaleKeys.copyLink.tr(),
() { () {
Clipboard.setData(ClipboardData(text: linkToShare)); Clipboard.setData(ClipboardData(text: _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth)));
Utils.showToast(LocaleKeys.linkCopied.tr()); Utils.showToast(LocaleKeys.linkCopied.tr());
}, },
colors: const <Color>[Colors.white, Colors.white], colors: const <Color>[Colors.white, Colors.white],

Loading…
Cancel
Save