toast gravity change

wala_plus_with_dialog_removal
FaizHashmi 3 weeks ago
parent 371a3ecb74
commit ae816a1f8f

@ -806,6 +806,7 @@
"locationPermissionRequiredForAttendance": "يجب منح إذن الموقع لتسجيل الحضور", "locationPermissionRequiredForAttendance": "يجب منح إذن الموقع لتسجيل الحضور",
"locationServicesRequiredForAttendance": "يجب تفعيل خدمة الموقع لتسجيل الحضور", "locationServicesRequiredForAttendance": "يجب تفعيل خدمة الموقع لتسجيل الحضور",
"pleaseProvideLocationPermission": "الرجاء منح إذن الموقع", "pleaseProvideLocationPermission": "الرجاء منح إذن الموقع",
"microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت." "microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت.",
"shareAccessGuide": "مشاركة دليل الوصول"
} }

@ -806,7 +806,8 @@
"locationPermissionRequiredForAttendance": "You need to give location permission to mark attendance", "locationPermissionRequiredForAttendance": "You need to give location permission to mark attendance",
"locationServicesRequiredForAttendance": "You need to enable location services to mark attendance", "locationServicesRequiredForAttendance": "You need to enable location services to mark attendance",
"pleaseProvideLocationPermission": "Please provide location permission", "pleaseProvideLocationPermission": "Please provide location permission",
"microphoneAccessRequired": "The app needs microphone access to be able to record audio." "microphoneAccessRequired": "The app needs microphone access to be able to record audio.",
"shareAccessGuide": "Share Access Guide"
} }

@ -12,7 +12,8 @@ class ApiConsts {
static String baseUrl = "https://mohemm.hmg.com"; // New Live server static String baseUrl = "https://mohemm.hmg.com"; // New Live server
static String ssoBaseUrl = "https://sso.hmg.com"; // New Live server static String ssoBaseUrl = "https://sso.hmg.com"; // New Live server
// //
// static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver // static String baseUrl = "https://uat.hmgwebservices.com"; //
// UAT ser343622ver
// static String baseUrl = "http://10.20.200.111:1010/"; // static String baseUrl = "http://10.20.200.111:1010/";
// static String baseUrl = "https://mohemm.hmg.com"; // New Live server // static String baseUrl = "https://mohemm.hmg.com"; // New Live server
//static String eLearningBaseUrl = "https://elearning.hmg.com/moodle_dev/api/"; // Elearning server UAT //static String eLearningBaseUrl = "https://elearning.hmg.com/moodle_dev/api/"; // Elearning server UAT

@ -818,7 +818,8 @@ class CodegenLoader extends AssetLoader{
"locationPermissionRequiredForAttendance": "يجب منح إذن الموقع لتسجيل الحضور", "locationPermissionRequiredForAttendance": "يجب منح إذن الموقع لتسجيل الحضور",
"locationServicesRequiredForAttendance": "يجب تفعيل خدمة الموقع لتسجيل الحضور", "locationServicesRequiredForAttendance": "يجب تفعيل خدمة الموقع لتسجيل الحضور",
"pleaseProvideLocationPermission": "الرجاء منح إذن الموقع", "pleaseProvideLocationPermission": "الرجاء منح إذن الموقع",
"microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت." "microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت.",
"shareAccessGuide": "مشاركة دليل الوصول"
}; };
static const Map<String,dynamic> _en_US = { static const Map<String,dynamic> _en_US = {
"mohemm": "Mohemm", "mohemm": "Mohemm",
@ -1624,7 +1625,8 @@ static const Map<String,dynamic> _en_US = {
"locationPermissionRequiredForAttendance": "You need to give location permission to mark attendance", "locationPermissionRequiredForAttendance": "You need to give location permission to mark attendance",
"locationServicesRequiredForAttendance": "You need to enable location services to mark attendance", "locationServicesRequiredForAttendance": "You need to enable location services to mark attendance",
"pleaseProvideLocationPermission": "Please provide location permission", "pleaseProvideLocationPermission": "Please provide location permission",
"microphoneAccessRequired": "The app needs microphone access to be able to record audio." "microphoneAccessRequired": "The app needs microphone access to be able to record audio.",
"shareAccessGuide": "Share Access Guide"
}; };
static const Map<String, Map<String,dynamic>> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; static const Map<String, Map<String,dynamic>> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US};
} }

@ -791,5 +791,6 @@ abstract class LocaleKeys {
static const locationServicesRequiredForAttendance = 'locationServicesRequiredForAttendance'; static const locationServicesRequiredForAttendance = 'locationServicesRequiredForAttendance';
static const pleaseProvideLocationPermission = 'pleaseProvideLocationPermission'; static const pleaseProvideLocationPermission = 'pleaseProvideLocationPermission';
static const microphoneAccessRequired = 'microphoneAccessRequired'; static const microphoneAccessRequired = 'microphoneAccessRequired';
static const shareAccessGuide = 'shareAccessGuide';
} }

@ -393,7 +393,7 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
child: Icon(isAr ? Icons.arrow_forward_ios_rounded : Icons.arrow_back_ios_new_rounded, size: 20, color: const Color(0xFF1E293B)), child: Icon(isAr ? Icons.arrow_forward_ios_rounded : Icons.arrow_back_ios_new_rounded, size: 20, color: const Color(0xFF1E293B)),
), ),
), ),
const SizedBox(width: 8) , const SizedBox(width: 8),
SvgPicture.asset("assets/icons/mazaya_brand.svg", height: 48, fit: BoxFit.contain), SvgPicture.asset("assets/icons/mazaya_brand.svg", height: 48, fit: BoxFit.contain),
], ],
), ),
@ -516,6 +516,28 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
hintText: LocaleKeys.searchItems.tr(), hintText: LocaleKeys.searchItems.tr(),
hintStyle: const TextStyle(color: Color(0xFFACAFB1), fontSize: 14, fontWeight: FontWeight.w400, height: 1.2), hintStyle: const TextStyle(color: Color(0xFFACAFB1), fontSize: 14, fontWeight: FontWeight.w400, height: 1.2),
border: InputBorder.none, border: InputBorder.none,
// Listens to the controller directly (rather than relying on _onSearchChanged's
// own setState elsewhere) so the clear icon reliably shows/hides on every
// keystroke, independent of the search debounce/fetch timing.
suffixIcon: ValueListenableBuilder<TextEditingValue>(
valueListenable: _searchController,
builder: (BuildContext context, TextEditingValue value, Widget? child) {
if (value.text.isEmpty) return const SizedBox.shrink();
return IconButton(
icon: const Icon(Icons.clear, size: 18, color: Color(0xFFACAFB1)),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
_searchController.clear();
_onSearchChanged("");
},
);
},
),
// max, not min - this search bar's outer Container is a fixed height (70), so the
// icon must never be forced taller than the TextField's own natural height or it
// overflows the available space.
suffixIconConstraints: const BoxConstraints(maxWidth: 24, maxHeight: 24),
), ),
style: const TextStyle(color: Color(0xFF2A333C), fontSize: 14, fontWeight: FontWeight.w400), style: const TextStyle(color: Color(0xFF2A333C), fontSize: 14, fontWeight: FontWeight.w400),
), ),

@ -158,10 +158,17 @@ 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>['yyyy-MM-dd', 'dd/MM/yyyy', 'MM/dd/yyyy', 'dd-MM-yyyy', 'yyyy/MM/dd', 'dd-MMM-yyyy', 'dd MMM yyyy']; // DATE_OF_BIRTH comes back as "M/d/yyyy h:mm:ss a" (e.g. "10/25/1997 12:00:00 AM") - strip
// that clock-time suffix first, since none of the date-only patterns below can match it
// with it still attached (parseStrict requires the whole string to match the pattern).
String dateOnly = s.replaceFirst(RegExp(r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$', caseSensitive: false), '').trim();
// MM/dd/yyyy before dd/MM/yyyy - the real source is confirmed month-first, so for an
// ambiguous case like "5/6/1990" this must resolve to May 6, not be misread as 5 June.
List<String> formats = <String>['yyyy-MM-dd', 'MM/dd/yyyy', 'dd/MM/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(dateOnly);
return DateTime(parsed.year, parsed.month, parsed.day); return DateTime(parsed.year, parsed.month, parsed.day);
} catch (_) {} } catch (_) {}
} }
@ -171,19 +178,24 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
String _formatDob(DateTime? dob, String? raw) { String _formatDob(DateTime? dob, String? raw) {
if (dob != null) { if (dob != null) {
return DateFormat('dd MMM yyyy', 'en_US').format(dob); return DateFormat('dd-MM-yyyy', 'en_US').format(dob);
} }
if (raw != null && raw.trim().isNotEmpty) { if (raw != null && raw.trim().isNotEmpty) {
return raw.split(' ').first; return raw.split(' ').first.replaceAll('/', '-');
} }
return "-"; return "-";
} }
// Masks everything but the last 3 digits, e.g. "2561615457" -> "*******457" - used both for
// the on-screen Key Details display and the share message, so the full ID is never shown or
// sent in plain text anywhere.
String _formatNationalId(String? nationalId) { String _formatNationalId(String? nationalId) {
if (nationalId == null || nationalId.trim().isEmpty) return "-"; if (nationalId == null || nationalId.trim().isEmpty) return "-";
String clean = nationalId.replaceAll('-', '').trim(); String clean = nationalId.replaceAll('-', '').trim();
String last4 = clean.length >= 4 ? clean.substring(clean.length - 4) : clean; if (clean.length <= 3) return clean;
return LocaleKeys.idEnding.tr(args: <String>[last4]); String lastDigits = clean.substring(clean.length - 3);
String mask = List<String>.filled(clean.length - 3, '*').join();
return '$mask$lastDigits';
} }
void _onToggleAccess() { void _onToggleAccess() {
@ -250,24 +262,34 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
if (dob != null) return DateFormat('dd-MM-yyyy').format(dob); if (dob != null) return DateFormat('dd-MM-yyyy').format(dob);
String raw = _contact?.dATEOFBIRTH?.trim() ?? ""; String raw = _contact?.dATEOFBIRTH?.trim() ?? "";
if (raw.isEmpty) return ""; if (raw.isEmpty) return "";
if (raw.contains('T')) return raw.split('T').first; if (raw.contains('T')) return raw.split('T').first.replaceAll('/', '-');
if (raw.contains(' ')) return raw.split(' ').first; if (raw.contains(' ')) return raw.split(' ').first.replaceAll('/', '-');
return raw; return raw.replaceAll('/', '-');
} }
// Same message built for both "Share Download Link" and "Copy Link" - they must always // 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. // carry identical content, so this is the single source of truth for it.
String _buildMazayaInviteMessage(String linkToShare) { String _buildMazayaInviteMessage(String linkToShare) {
String? nationalId = _contact?.nATIONALIDENTIFIER?.trim(); String? nationalId = _contact?.nATIONALIDENTIFIER?.trim();
String nationalIdLine = (nationalId == null || nationalId.isEmpty) ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId"; String nationalIdLine = (nationalId == null || nationalId.isEmpty) ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}";
return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDobForShare()}"; 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. // 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. // 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 { // Text-only share (the invite message + link). Kept separate from the guide share below
// because some targets (e.g. WhatsApp) drop the accompanying `text` entirely once a file is
// attached to the same share - splitting them guarantees the message itself is always visible.
Future<void> _shareMazayaInviteLink(bool isAr, String linkToShare) async {
String message = _buildMazayaInviteMessage(linkToShare); String message = _buildMazayaInviteMessage(linkToShare);
try {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
// File-only share of the bundled access guide PDF - no accompanying text, so every target
// (including WhatsApp) just attaches the file with nothing to drop.
Future<void> _shareMazayaAccessGuide(bool isAr) async {
try { try {
ByteData bytes = await rootBundle.load(MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr)); ByteData bytes = await rootBundle.load(MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr));
Directory tempDir = await getTemporaryDirectory(); Directory tempDir = await getTemporaryDirectory();
@ -275,12 +297,8 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
File pdfFile = File('${tempDir.path}/$localFileName'); File pdfFile = File('${tempDir.path}/$localFileName');
await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes)); await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes));
await SharePlus.instance.share(ShareParams(text: message, files: [XFile(pdfFile.path)])); await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)]));
} catch (_) { } catch (_) {}
try {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
} }
@override @override
@ -540,9 +558,27 @@ class _DependentDetailsScreenState extends State<DependentDetailsScreen> {
), ),
), ),
), ),
// Share download link - same bottom-sticky button convention used elsewhere in // Two separate share actions (not one combined text+file share) - same bottom-sticky
// the app (e.g. "Join Marathon"), shown once access is granted. // container convention used elsewhere in the app (e.g. "Join Marathon"), shown once
if (isAllowed) DefaultButton(LocaleKeys.shareDownloadLink.tr(), () => _shareMazayaInvite(isAr, linkToShare)).insideContainer, // access is granted.
if (isAllowed)
Row(
children: <Widget>[
// Secondary - same bg/border/text colors as the name+relation card at the top
// of this screen, for visual consistency within this page.
Expanded(
child: DefaultButton(
LocaleKeys.shareAccessGuide.tr(),
() => _shareMazayaAccessGuide(isAr),
colors: const <Color>[Color(0xFFE8F7F2), Color(0xFFE8F7F2)],
border: Border.all(color: const Color(0xFFBCE6D7), width: 1.2),
textColor: const Color(0xFF168A89),
),
),
const SizedBox(width: 12),
Expanded(child: DefaultButton(LocaleKeys.shareDownloadLink.tr(), () => _shareMazayaInviteLink(isAr, linkToShare))),
],
).insideContainer,
], ],
), ),
), ),

@ -440,27 +440,47 @@ class MazayaAccessResultScreen extends StatelessWidget {
if (parsed != null) { if (parsed != null) {
return DateFormat('dd-MM-yyyy').format(parsed); return DateFormat('dd-MM-yyyy').format(parsed);
} }
// DATE_OF_BIRTH comes back as "M/d/yyyy h:mm:ss a" (e.g. "10/25/1997 12:00:00 AM") -
// DateTime.tryParse above doesn't understand that shape, so strip the clock-time suffix
// and parse the date part explicitly. MM/dd/yyyy before dd/MM/yyyy since the real source
// is confirmed month-first - an ambiguous case like "5/6/1990" must resolve to May 6.
String dateOnly = s.replaceFirst(RegExp(r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$', caseSensitive: false), '').trim();
for (String fmt in <String>['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy-MM-dd', 'dd-MM-yyyy']) {
try {
DateTime dateOnlyParsed = DateFormat(fmt, 'en_US').parseStrict(dateOnly);
return DateFormat('dd-MM-yyyy').format(dateOnlyParsed);
} catch (_) {}
}
} catch (_) {} } catch (_) {}
// Fallback: strip a time component off a plain "yyyy-MM-dd HH:mm:ss" / "yyyy-MM-ddTHH:mm:ss" // Fallback: strip a time component off a plain "yyyy-MM-dd HH:mm:ss" / "yyyy-MM-ddTHH:mm:ss"
// string we couldn't otherwise parse, rather than showing it as-is with the time attached. // string we couldn't otherwise parse, rather than showing it as-is with the time attached.
if (s.contains('T')) return s.split('T').first; if (s.contains('T')) return s.split('T').first.replaceAll('/', '-');
if (s.contains(' ')) return s.split(' ').first; if (s.contains(' ')) return s.split(' ').first.replaceAll('/', '-');
return s; return s.replaceAll('/', '-');
} }
// Same message built for both "Share Download Link" and "Copy Link" - they must always // 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. // carry identical content, so this is the single source of truth for it.
String _buildMazayaInviteMessage(String linkToShare, String nationalId, String dateOfBirth) { String _buildMazayaInviteMessage(String linkToShare, String nationalId, String dateOfBirth) {
String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: $nationalId"; String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}";
return "$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. // Text-only share (the invite message + link). Kept separate from the guide share below
// Falls back to sharing just the text if the PDF can't be loaded/copied for any reason. // because some targets (e.g. WhatsApp) drop the accompanying `text` entirely once a file is
Future<void> _shareMazayaInvite(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async { // attached to the same share - splitting them guarantees the message itself is always visible.
Future<void> _shareMazayaInviteLink(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async {
String message = _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth); String message = _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth);
try {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
// File-only share of the bundled access guide PDF - no accompanying text, so every target
// (including WhatsApp) just attaches the file with nothing to drop.
Future<void> _shareMazayaAccessGuide(bool isAr) async {
try { try {
ByteData bytes = await rootBundle.load(getShareGuidePdfAssetPath(isAr)); ByteData bytes = await rootBundle.load(getShareGuidePdfAssetPath(isAr));
Directory tempDir = await getTemporaryDirectory(); Directory tempDir = await getTemporaryDirectory();
@ -469,13 +489,8 @@ class MazayaAccessResultScreen extends StatelessWidget {
File pdfFile = File('${tempDir.path}/$localFileName'); File pdfFile = File('${tempDir.path}/$localFileName');
await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes)); await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes));
log("final: $message\n\nfile: ${pdfFile.path}"); await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)]));
await SharePlus.instance.share(ShareParams(text: message, files: [XFile(pdfFile.path)])); } catch (_) {}
} catch (_) {
try {
await SharePlus.instance.share(ShareParams(text: message));
} catch (_) {}
}
} }
Widget _buildBottomButtons(BuildContext context, bool isAr, String fontFamily, bool granted, String linkToShare, String nationalId, String dateOfBirth) { Widget _buildBottomButtons(BuildContext context, bool isAr, String fontFamily, bool granted, String linkToShare, String nationalId, String dateOfBirth) {
@ -488,7 +503,17 @@ class MazayaAccessResultScreen extends StatelessWidget {
children: <Widget>[ children: <Widget>[
DefaultButton( DefaultButton(
LocaleKeys.shareDownloadLink.tr(), LocaleKeys.shareDownloadLink.tr(),
() => _shareMazayaInvite(isAr, linkToShare, nationalId, dateOfBirth), () => _shareMazayaInviteLink(isAr, linkToShare, nationalId, dateOfBirth),
trailingIconData: isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded,
fontSize: 14,
isTextExpanded: false,
height: 48,
borderRadius: 12,
),
const SizedBox(height: 10),
DefaultButton(
LocaleKeys.shareAccessGuide.tr(),
() => _shareMazayaAccessGuide(isAr),
trailingIconData: isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded, trailingIconData: isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded,
fontSize: 14, fontSize: 14,
isTextExpanded: false, isTextExpanded: false,

Loading…
Cancel
Save