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/dependent_details_screen.dart

597 lines
22 KiB
Dart

import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:mohem_flutter_app/api/profile_api_client.dart';
import 'package:mohem_flutter_app/classes/utils.dart';
import 'package:mohem_flutter_app/config/routes.dart';
import 'package:mohem_flutter_app/extensions/string_extensions.dart';
import 'package:mohem_flutter_app/generated/locale_keys.g.dart';
import 'package:mohem_flutter_app/models/get_employee_contacts.model.dart';
import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.dart';
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
class _ShieldIconPainter extends CustomPainter {
final Color color;
final bool isGranted;
_ShieldIconPainter({required this.color, required this.isGranted});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
double w = size.width;
double h = size.height;
Path path = Path();
path.moveTo(w * 0.5, h * 0.1);
path.cubicTo(w * 0.72, h * 0.1, w * 0.88, h * 0.18, w * 0.88, h * 0.38);
path.cubicTo(w * 0.88, h * 0.65, w * 0.68, h * 0.84, w * 0.5, h * 0.92);
path.cubicTo(w * 0.32, h * 0.84, w * 0.12, h * 0.65, w * 0.12, h * 0.38);
path.cubicTo(w * 0.12, h * 0.18, w * 0.28, h * 0.1, w * 0.5, h * 0.1);
path.close();
canvas.drawPath(path, paint);
if (isGranted) {
Path checkPath = Path();
checkPath.moveTo(w * 0.34, h * 0.49);
checkPath.lineTo(w * 0.46, h * 0.61);
checkPath.lineTo(w * 0.66, h * 0.39);
canvas.drawPath(checkPath, paint);
} else {
canvas.drawLine(
Offset(w * 0.38, h * 0.38),
Offset(w * 0.62, h * 0.62),
paint,
);
canvas.drawLine(
Offset(w * 0.62, h * 0.38),
Offset(w * 0.38, h * 0.62),
paint,
);
}
}
@override
bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) =>
color != oldDelegate.color || isGranted != oldDelegate.isGranted;
}
class DependentDetailsScreen extends StatefulWidget {
final GetEmployeeContactsList? contact;
const DependentDetailsScreen({Key? key, this.contact}) : super(key: key);
@override
_DependentDetailsScreenState createState() => _DependentDetailsScreenState();
}
class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
GetEmployeeContactsList? _contact;
bool _argsLoaded = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_argsLoaded) {
if (widget.contact != null) {
_contact = widget.contact;
} else {
Object? args = ModalRoute.of(context)?.settings.arguments;
if (args is GetEmployeeContactsList) {
_contact = args;
} else if (args is Map && args['contact'] is GetEmployeeContactsList) {
_contact = args['contact'] as GetEmployeeContactsList;
}
}
_argsLoaded = true;
}
}
bool get _isAccessAllowed {
if (_contact == null) return false;
String? flag = _contact!.mAZAYAACCESSFLG ?? _contact!.mazayaAccess?.toString();
return flag?.trim().toUpperCase() == 'Y';
}
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();
}
String _getFirstName(String? name) {
if (name == null || name.trim().isEmpty) return "";
List<String> parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList();
if (parts.isEmpty) return "";
return parts.first;
}
DateTime? _parseBirthDate(String? dateStr) {
if (dateStr == null || dateStr.trim().isEmpty) return null;
String s = dateStr.trim();
RegExp dateRegex = RegExp(r'/?Date\((-?\d+)(?:[+-]\d+)?\)/?');
Match? match = dateRegex.firstMatch(s);
if (match != null) {
try {
int millis = int.parse(match.group(1)!);
DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true);
return DateTime(dtUtc.year, dtUtc.month, dtUtc.day);
} catch (_) {}
}
if (RegExp(r'^-?\d{10,14}$').hasMatch(s)) {
try {
int millis = int.parse(s);
DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true);
return DateTime(dtUtc.year, dtUtc.month, dtUtc.day);
} catch (_) {}
}
DateTime? dt = DateTime.tryParse(s);
if (dt != null) return DateTime(dt.year, dt.month, dt.day);
List<String> formats = <String>[
'yyyy-MM-dd',
'dd/MM/yyyy',
'MM/dd/yyyy',
'dd-MM-yyyy',
'yyyy/MM/dd',
'dd-MMM-yyyy',
'dd MMM yyyy',
];
for (String fmt in formats) {
try {
DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(s);
return DateTime(parsed.year, parsed.month, parsed.day);
} catch (_) {}
}
return null;
}
String _formatDob(DateTime? dob, String? raw) {
if (dob != null) {
return DateFormat('dd MMM yyyy', 'en_US').format(dob);
}
if (raw != null && raw.trim().isNotEmpty) {
return raw.split(' ').first;
}
return "-";
}
String _formatNationalId(String? nationalId) {
if (nationalId == null || nationalId.trim().isEmpty) return "-";
String clean = nationalId.trim();
String last4 = clean.length >= 4 ? clean.substring(clean.length - 4) : clean;
return LocaleKeys.idEnding.tr(args: <String>[last4]);
}
void _onToggleAccess() {
if (_contact == null) return;
bool isAllowed = _isAccessAllowed;
String dependentName = _contact!.cONTACTNAME ?? "";
ConfirmBottomSheet.show(
context,
title: isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr(),
message: isAllowed
? LocaleKeys.confirmRevokeMazayaAccess.tr(args: <String>[dependentName])
: LocaleKeys.confirmGrantMazayaAccess.tr(args: <String>[dependentName]),
icon: isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline,
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(),
cancelText: LocaleKeys.no.tr(),
onConfirm: _performToggleAccess,
);
}
Future<void> _performToggleAccess() async {
if (_contact == null) return;
bool isAllowed = _isAccessAllowed;
String action = isAllowed ? "Revoke" : "Grant";
try {
Utils.showLoading(context);
await ProfileApiClient().processContactMazayaAccess(
accessAction: action,
contactRelationshipId: _contact!.cONTACTRELATIONSHIPID,
contactPersonId: _contact!.cONTACTPERSONID,
personId: _contact!.pERSONID,
);
setState(() {
_contact!.mAZAYAACCESSFLG = action == "Grant" ? "Y" : "N";
_contact!.mazayaAccess = _contact!.mAZAYAACCESSFLG;
});
Utils.hideLoading(context);
if (mounted) {
if (action == "Grant") {
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) {
Utils.hideLoading(context);
Utils.handleException(ex, context, null);
}
}
@override
Widget build(BuildContext context) {
DateTime? dob = _parseBirthDate(_contact?.dATEOFBIRTH);
String initials = _getInitials(_contact?.cONTACTNAME);
String fullName = _contact?.cONTACTNAME ?? "";
String firstName = _getFirstName(_contact?.cONTACTNAME);
String relation = (_contact?.rELATIONSHIP != null && _contact!.rELATIONSHIP!.trim().isNotEmpty)
? _contact!.rELATIONSHIP!.trim()
: (_contact?.cONTACTTYPE?.trim() ?? "");
String formattedId = _formatNationalId(_contact?.nATIONALIDENTIFIER);
String formattedDob = _formatDob(dob, _contact?.dATEOFBIRTH);
bool isAllowed = _isAccessAllowed;
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),
LocaleKeys.dependentDetails.tr().toText20(
color: const Color(0xFF1E293B),
),
const Spacer(),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.home_outlined,
size: 24,
color: Color(0xFF1E293B),
),
),
),
],
),
),
),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Profile Card (Mint Green)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFE8F7F2),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFBCE6D7), width: 1.2),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// Avatar circle with initials
Container(
width: 52,
height: 52,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Center(
child: initials.toText17(
color: const Color(0xFF168A89),
),
),
),
const SizedBox(width: 14),
// Dependent Name & Relation
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <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,
),
],
),
),
const SizedBox(height: 16),
// Key details Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x08000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
LocaleKeys.keyDetails.tr().toText16(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
LocaleKeys.verified.tr().toText13(
weight: FontWeight.w500,
color: const Color(0xFF059669),
),
],
),
const SizedBox(height: 14),
const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)),
const SizedBox(height: 14),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <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),
// Services & actions section header
LocaleKeys.servicesAndActions.tr().toText17(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
const SizedBox(height: 4),
LocaleKeys.startDependentFlowsDesc.tr().toText13(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
),
const SizedBox(height: 12),
// Mazaya access Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: const <BoxShadow>[
BoxShadow(
color: Color(0x08000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Header Row: Mazaya access + Badge
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
LocaleKeys.mazayaAccess.tr().toText16(
weight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
decoration: BoxDecoration(
color: isAllowed ? const Color(0xFFECFDF5) : const Color(0xFFFEE2E2),
borderRadius: BorderRadius.circular(12),
),
child: (isAllowed ? LocaleKeys.active.tr() : LocaleKeys.accessOff.tr()).toText12(
weight: FontWeight.w500,
color: isAllowed ? const Color(0xFF059669) : const Color(0xFFEF4444),
),
),
],
),
const SizedBox(height: 6),
LocaleKeys.allowDependentMazayaOffersDesc.tr(
args: <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
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(
borderRadius: BorderRadius.circular(12),
onTap: _onToggleAccess,
child: Container(
height: 48,
width: double.infinity,
decoration: BoxDecoration(
color: isAllowed ? const Color(0xFFFEF2F2) : const Color(0xFF22B0A6),
borderRadius: BorderRadius.circular(12),
border: isAllowed ? Border.all(color: const Color(0xFFFECACA), width: 1.2) : null,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 18,
height: 18,
child: CustomPaint(
painter: _ShieldIconPainter(
color: isAllowed ? const Color(0xFFDC2626) : Colors.white,
isGranted: !isAllowed,
),
),
),
const SizedBox(width: 8),
(isAllowed
? LocaleKeys.revokeMazayaAccess.tr()
: LocaleKeys.allowMazayaAccess.tr())
.toText16(
weight: FontWeight.w500,
color: isAllowed ? const Color(0xFFDC2626) : Colors.white,
),
],
),
),
),
if (isAllowed) ...<Widget>[
const SizedBox(height: 12),
Center(
child: LocaleKeys.revokingPreventsAccessDesc.tr().toText11(
weight: FontWeight.w400,
color: const Color(0xFF64748B),
isCenter: true,
),
),
],
],
),
),
],
),
),
),
);
}
}