From ae816a1f8fbe28df7b7f8a2c238e9d7a6a978959 Mon Sep 17 00:00:00 2001 From: FaizHashmi Date: Wed, 14 Oct 2026 17:40:37 +0300 Subject: [PATCH] toast gravity change --- assets/langs/ar-SA.json | 3 +- assets/langs/en-US.json | 3 +- lib/classes/consts.dart | 3 +- lib/generated/codegen_loader.g.dart | 6 +- lib/generated/locale_keys.g.dart | 1 + .../dashboard_screen_for_dependet.dart | 24 +++++- lib/ui/profile/dependent_details_screen.dart | 76 ++++++++++++++----- .../profile/mazaya_access_result_screen.dart | 55 ++++++++++---- 8 files changed, 130 insertions(+), 41 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 66a2ba2..a2912a0 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -806,6 +806,7 @@ "locationPermissionRequiredForAttendance": "يجب منح إذن الموقع لتسجيل الحضور", "locationServicesRequiredForAttendance": "يجب تفعيل خدمة الموقع لتسجيل الحضور", "pleaseProvideLocationPermission": "الرجاء منح إذن الموقع", - "microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت." + "microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت.", + "shareAccessGuide": "مشاركة دليل الوصول" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index bc4d260..33cf246 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -806,7 +806,8 @@ "locationPermissionRequiredForAttendance": "You need to give location permission to mark attendance", "locationServicesRequiredForAttendance": "You need to enable location services to mark attendance", "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" } diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index a66b657..da567f7 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -12,7 +12,8 @@ class ApiConsts { static String baseUrl = "https://mohemm.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 = "https://mohemm.hmg.com"; // New Live server //static String eLearningBaseUrl = "https://elearning.hmg.com/moodle_dev/api/"; // Elearning server UAT diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 66fc637..a12943a 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -818,7 +818,8 @@ class CodegenLoader extends AssetLoader{ "locationPermissionRequiredForAttendance": "يجب منح إذن الموقع لتسجيل الحضور", "locationServicesRequiredForAttendance": "يجب تفعيل خدمة الموقع لتسجيل الحضور", "pleaseProvideLocationPermission": "الرجاء منح إذن الموقع", - "microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت." + "microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت.", + "shareAccessGuide": "مشاركة دليل الوصول" }; static const Map _en_US = { "mohemm": "Mohemm", @@ -1624,7 +1625,8 @@ static const Map _en_US = { "locationPermissionRequiredForAttendance": "You need to give location permission to mark attendance", "locationServicesRequiredForAttendance": "You need to enable location services to mark attendance", "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> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 26070e4..07d0587 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -791,5 +791,6 @@ abstract class LocaleKeys { static const locationServicesRequiredForAttendance = 'locationServicesRequiredForAttendance'; static const pleaseProvideLocationPermission = 'pleaseProvideLocationPermission'; static const microphoneAccessRequired = 'microphoneAccessRequired'; + static const shareAccessGuide = 'shareAccessGuide'; } diff --git a/lib/ui/landing/dashboard_screen_for_dependet.dart b/lib/ui/landing/dashboard_screen_for_dependet.dart index e277f9c..b61c799 100644 --- a/lib/ui/landing/dashboard_screen_for_dependet.dart +++ b/lib/ui/landing/dashboard_screen_for_dependet.dart @@ -393,7 +393,7 @@ class _DashboardScreenForDependetState extends State 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), ], ), @@ -516,6 +516,28 @@ class _DashboardScreenForDependetState extends State hintText: LocaleKeys.searchItems.tr(), hintStyle: const TextStyle(color: Color(0xFFACAFB1), fontSize: 14, fontWeight: FontWeight.w400, height: 1.2), 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( + 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), ), diff --git a/lib/ui/profile/dependent_details_screen.dart b/lib/ui/profile/dependent_details_screen.dart index 066c091..9cb0fa6 100644 --- a/lib/ui/profile/dependent_details_screen.dart +++ b/lib/ui/profile/dependent_details_screen.dart @@ -158,10 +158,17 @@ class _DependentDetailsScreenState extends State { DateTime? dt = DateTime.tryParse(s); if (dt != null) return DateTime(dt.year, dt.month, dt.day); - List formats = ['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 formats = ['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) { 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); } catch (_) {} } @@ -171,19 +178,24 @@ class _DependentDetailsScreenState extends State { String _formatDob(DateTime? dob, String? raw) { 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) { - return raw.split(' ').first; + return raw.split(' ').first.replaceAll('/', '-'); } 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) { if (nationalId == null || nationalId.trim().isEmpty) return "-"; String clean = nationalId.replaceAll('-', '').trim(); - String last4 = clean.length >= 4 ? clean.substring(clean.length - 4) : clean; - return LocaleKeys.idEnding.tr(args: [last4]); + if (clean.length <= 3) return clean; + String lastDigits = clean.substring(clean.length - 3); + String mask = List.filled(clean.length - 3, '*').join(); + return '$mask$lastDigits'; } void _onToggleAccess() { @@ -250,24 +262,34 @@ class _DependentDetailsScreenState extends State { 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; + if (raw.contains('T')) return raw.split('T').first.replaceAll('/', '-'); + if (raw.contains(' ')) return raw.split(' ').first.replaceAll('/', '-'); + return raw.replaceAll('/', '-'); } // 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"; + 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()}"; } // 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 _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 _shareMazayaInviteLink(bool isAr, String linkToShare) async { 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 _shareMazayaAccessGuide(bool isAr) async { try { ByteData bytes = await rootBundle.load(MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr)); Directory tempDir = await getTemporaryDirectory(); @@ -275,12 +297,8 @@ class _DependentDetailsScreenState extends State { 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 (_) {} - } + await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)])); + } catch (_) {} } @override @@ -540,9 +558,27 @@ class _DependentDetailsScreenState extends State { ), ), ), - // 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, + // Two separate share actions (not one combined text+file share) - same bottom-sticky + // container convention used elsewhere in the app (e.g. "Join Marathon"), shown once + // access is granted. + if (isAllowed) + Row( + children: [ + // 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(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, ], ), ), diff --git a/lib/ui/profile/mazaya_access_result_screen.dart b/lib/ui/profile/mazaya_access_result_screen.dart index e33597a..f6a2bac 100644 --- a/lib/ui/profile/mazaya_access_result_screen.dart +++ b/lib/ui/profile/mazaya_access_result_screen.dart @@ -440,27 +440,47 @@ class MazayaAccessResultScreen extends StatelessWidget { if (parsed != null) { 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 ['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 (_) {} // 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. - if (s.contains('T')) return s.split('T').first; - if (s.contains(' ')) return s.split(' ').first; - return s; + if (s.contains('T')) return s.split('T').first.replaceAll('/', '-'); + if (s.contains(' ')) return s.split(' ').first.replaceAll('/', '-'); + return s.replaceAll('/', '-'); } // 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, 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)}"; } - // 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 _shareMazayaInvite(bool isAr, String linkToShare, String nationalId, String dateOfBirth) 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 _shareMazayaInviteLink(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async { 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 _shareMazayaAccessGuide(bool isAr) async { try { ByteData bytes = await rootBundle.load(getShareGuidePdfAssetPath(isAr)); Directory tempDir = await getTemporaryDirectory(); @@ -469,13 +489,8 @@ class MazayaAccessResultScreen extends StatelessWidget { File pdfFile = File('${tempDir.path}/$localFileName'); await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes)); - log("final: $message\n\nfile: ${pdfFile.path}"); - await SharePlus.instance.share(ShareParams(text: message, files: [XFile(pdfFile.path)])); - } catch (_) { - try { - await SharePlus.instance.share(ShareParams(text: message)); - } catch (_) {} - } + await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)])); + } catch (_) {} } 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: [ DefaultButton( 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, fontSize: 14, isTextExpanded: false,