import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:miler/views/Dashboard/profile/informations/profile.dart'; import 'package:miler/views/Dashboard/profile/informations/notifications_page.dart'; import 'package:miler/views/Dashboard/profile/informations/help_center.dart'; import 'package:miler/views/Dashboard/profile/informations/faq.dart'; import 'package:miler/views/Dashboard/profile/informations/rider_rewards_page.dart'; import 'package:miler/views/Dashboard/profile/informations/terms_condition.dart'; import 'package:miler/views/Dashboard/profile/informations/saved_address.dart'; import 'package:miler/views/Dashboard/profile/informations/order_alert_sound.dart'; import 'package:miler/views/Dashboard/summary/summary.dart'; import 'package:miler/views/helpers/constants/Font_constant.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/helpers/constants/design_constants.dart'; import 'package:miler/views/helpers/widgets/miler_app_bar.dart'; import 'package:miler/views/helpers/widgets/app_widgets.dart'; import 'package:miler/views/helpers/widgets/page_transitions.dart'; import 'package:miler/views/helpers/widgets/settings_ui.dart'; import 'package:miler/widget/Bottom_page.dart'; import 'package:miler/views/onboardscreens/Sign_in.dart'; import 'package:miler/controllers/profile_controller.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/controllers/rewards_controller.dart'; import 'package:miler/controllers/summary_controller.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @override State createState() => _ProfilePageState(); } class _ProfilePageState extends State { late final ProfileController _profileController = Get.isRegistered() ? Get.find() : Get.put(ProfileController(), permanent: true); final RewardsController _rewardsController = Get.isRegistered() ? Get.find() : Get.put(RewardsController()); final SummaryController _summaryController = Get.isRegistered() ? Get.find() : Get.put(SummaryController()); String _name = ''; String _email = ''; String _contact = ''; String _appVersion = ''; /// Name and phone come off disk. Until they land, the card used to state /// "Rider Name / Not added" as if that were the answer — so the rider's first /// glimpse of his own account said his number was missing. bool _prefsLoaded = false; @override void initState() { super.initState(); _loadProfilePrefs(); _loadAppVersion(); ever(_profileController.userName, (_) => _assignFromController()); ever(_profileController.userEmail, (_) => _assignFromController()); ever(_profileController.userContact, (_) => _assignFromController()); _profileController.loadFromPrefs(); } Future _loadAppVersion() async { try { final info = await PackageInfo.fromPlatform(); if (!mounted) return; setState( () => _appVersion = 'v${info.version} (Build ${info.buildNumber})', ); } catch (_) {} } Future _loadProfilePrefs() async { final prefs = await SharedPreferences.getInstance(); if (!mounted) return; setState(() { _name = prefs.getString('user_name') ?? 'Rider Name'; _email = prefs.getString('user_email') ?? 'rider@miler.com'; _contact = prefs.getString('contactno') ?? 'Not added'; _prefsLoaded = true; }); final userId = prefs.getInt('userid') ?? 0; if (userId > 0) { _rewardsController.fetchBonusSummary(userId); _summaryController.fetchSummaryStats(userId); } } void _assignFromController() { setState(() { if (_profileController.userName.value.trim().isNotEmpty) { _name = _profileController.userName.value.trim(); } if (_profileController.userEmail.value.trim().isNotEmpty) { _email = _profileController.userEmail.value.trim(); } if (_profileController.userContact.value.trim().isNotEmpty) { _contact = _profileController.userContact.value.trim(); } }); } @override Widget build(BuildContext context) { return Scaffold( // ── The same white as the other tabs ── // // This was [ColorConstants.surface] — #FCF9F8, a warm off-white — while // Home and Activity are on [ColorConstants.pureSurface]. On Bookings the // difference never shows, because cards cover the page; Account is bare // rows on the bare background, so the whole tab read a shade dirtier than // the three either side of it. Four tabs of one app should not disagree // about what colour the paper is. backgroundColor: ColorConstants.pureSurface, // Account was the odd tab out: no top bar at all, just a 28sp inline // heading scrolling away with the content, while the other three sat // under a fixed branded bar. Four tabs of one app should not disagree // about where the page title lives. appBar: MilerAppBar( title: 'Account', trailing: _circleIconButton( Icons.notifications_none_rounded, () => openSheet(context, const NotificationsPage()), semanticLabel: 'Notifications', ), ), // ── Pull to refresh ── // // A pull here used to drag the page off the bottom of the screen and // spring back having done nothing: `BouncingScrollPhysics` forces the iOS // rubber-band on every platform, and rubber-band overscroll is unbounded // unless a RefreshIndicator catches the gesture. The gesture responded, // so it read as a refresh that had failed. Home was fixed this way; this // is the same fix, and now the pull actually re-reads the profile, the // points balance and the pickup count. body: SettingsBody( onRefresh: _refresh, children: [ // ── Who you are ── _profileCard(), SizedBox(height: 16.h), // ── The three numbers worth a tap ── _quickActions(), SizedBox(height: 22.h), // ── Earnings ── // // It used to be a tab of its own. A pay figure is a once-or-twice-a- // day read, and a permanent quarter of the nav bar is the most // expensive place in the app to keep one — so it lives here, at the // top of Account, where the rest of "things about me rather than // about this shift" already lives. The tab it vacated now shows what // he has finished today, which is the glance he actually makes // between stops. const SettingsLabel('Money'), SettingsGroup( rows: [ SettingsRow( icon: Icons.account_balance_wallet_outlined, title: 'Earnings', subtitle: 'Today, this week and this month', onTap: () => openSheet(context, const Summary()), ), ], ), SizedBox(height: 18.h), const SettingsLabel('Account'), SettingsGroup( rows: [ SettingsRow( icon: Icons.person_outline_rounded, title: 'Edit Profile', subtitle: _email.isNotEmpty ? _email : null, onTap: () => openSheet(context, const Profile()), ), SettingsRow( icon: Icons.location_on_outlined, title: 'Saved Address', subtitle: 'Where your shift starts and ends', onTap: () => openSheet(context, const SavedAddressPage()), ), SettingsRow( icon: Icons.notifications_active_outlined, title: 'Alert Sound', subtitle: 'What a new booking sounds like', onTap: () => openSheet(context, const OrderAlertSoundPage()), ), SettingsRow( icon: Icons.notifications_none_rounded, title: 'Notifications', onTap: () => openSheet(context, const NotificationsPage()), ), ], ), SizedBox(height: 22.h), const SettingsLabel('Support'), SettingsGroup( rows: [ SettingsRow( icon: Icons.headset_mic_outlined, title: 'Help & Support', subtitle: 'Message the team — we reply within a day', onTap: () => openSheet(context, const HelpCenter()), ), SettingsRow( icon: Icons.quiz_outlined, title: 'FAQ', subtitle: 'Answers to the usual questions', onTap: () => openSheet(context, const FaqPage()), ), SettingsRow( icon: Icons.description_outlined, title: 'Terms & Conditions', onTap: () => openSheet(context, const TermsCondition()), ), ], ), SizedBox(height: 16.h), // ── SOS ── _buildSOSBanner(), SizedBox(height: 18.h), SettingsGroup( rows: [ SettingsRow( icon: Icons.logout_rounded, title: 'Logout', danger: true, onTap: () => _showLogoutDialog(context), ), ], ), SizedBox(height: 22.h), Center( child: Text( "Miler ${_appVersion.isNotEmpty ? _appVersion : ''}\n© 2026 Doormile Logistics" .trim(), textAlign: TextAlign.center, style: TextStyle( fontSize: 11.sp, color: ColorConstants.onSurfaceVariant, fontFamily: FontConstants.fontFamily, height: 1.6, ), ), ), SizedBox(height: 20.h), ], ), ); } /// Re-reads everything the page shows. Deliberately awaits, so the spinner /// stays down until the numbers are actually new. Future _refresh() async { await _profileController.loadFromPrefs(); await _loadProfilePrefs(); await _loadAppVersion(); } // ─── Section label ─────────────────────────────────────────────────────────── // ─── Header icon button ────────────────────────────────────────────────────── Widget _circleIconButton( IconData icon, VoidCallback onTap, { required String semanticLabel, }) { return PressScale( onTap: onTap, semanticLabel: semanticLabel, minTarget: true, child: Container( width: 42.w, height: 42.w, decoration: BoxDecoration( color: ColorConstants.surfaceContainerLowest, shape: BoxShape.circle, border: Border.all(color: ColorConstants.dividerColor), boxShadow: [ BoxShadow( color: ColorConstants.onSurface.withValues(alpha: 0.05), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Icon(icon, color: ColorConstants.onSurface, size: 22.sp), ), ); } // ─── Profile card ──────────────────────────────────────────────────────────── Widget _profileCard() { return PressScale( onTap: () => openSheet(context, const Profile()), child: Container( padding: EdgeInsets.all(16.w), decoration: SettingsCard.decoration(), child: Row( children: [ Obx(() { final path = _profileController.imagePath.value; bool hasImage = false; try { hasImage = path.isNotEmpty && File(path).existsSync(); } catch (_) {} return Container( width: 64.w, height: 64.w, decoration: BoxDecoration( shape: BoxShape.circle, color: ColorConstants.neutralLight, image: hasImage ? DecorationImage( image: FileImage(File(path)), fit: BoxFit.cover, ) : null, ), child: !hasImage ? Icon( Icons.person_rounded, color: ColorConstants.onSurfaceVariant, size: 34.sp, ) : null, ); }), SizedBox(width: 14.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!_prefsLoaded) ...[ MilerShimmer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SkeletonBone(width: 150.w, height: 18.h), SizedBox(height: 7.h), SkeletonBone(width: 110.w, height: 13.h), ], ), ), ] else ...[ Text( _name.isNotEmpty ? _name : "Rider Name", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w700, color: ColorConstants.onSurface, fontFamily: FontConstants.fontFamily, ), ), SizedBox(height: 3.h), Text( _contact.isNotEmpty ? _contact : "Not added", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13.sp, color: ColorConstants.onSurfaceVariant, fontFamily: FontConstants.fontFamily, ), ), ], SizedBox(height: 8.h), Container( padding: EdgeInsets.symmetric( horizontal: 10.w, vertical: 4.h, ), decoration: BoxDecoration( color: ColorConstants.primary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(DesignConstants.radiusXl), ), child: Text( "View profile", style: TextStyle( fontSize: 11.5.sp, fontWeight: FontWeight.w700, color: ColorConstants.primary, fontFamily: FontConstants.fontFamily, ), ), ), ], ), ), Icon( Icons.chevron_right_rounded, color: ColorConstants.onSurfaceVariant, size: 22.sp, ), ], ), ), ); } // ─── Quick actions ─────────────────────────────────────────────────────────── Widget _quickActions() { return Obx( () => Row( children: [ _quickCard( Icons.card_giftcard_rounded, "Rewards", value: _formatCount(_rewardsController.totalPoints.value), loading: _rewardsController.isLoading.value, onTap: () => openSheet(context, const RiderRewardsPage()), ), SizedBox(width: 12.w), _quickCard( Icons.local_shipping_rounded, "Pickups", value: _formatCount(_summaryController.total.value), loading: _summaryController.isLoading.value, // Was `onTap: () {}` — a card that looks tappable, sits next to // two that are, and does nothing. It shows the rider's lifetime // pickup count, so it goes where that count comes from. onTap: () => BottomPage.goToTab(1), ), SizedBox(width: 12.w), _quickCard( Icons.headset_mic_rounded, "Help", value: "24/7", onTap: () => openSheet(context, const HelpCenter()), ), ], ), ); } Widget _quickCard( IconData icon, String label, { String? value, VoidCallback? onTap, bool loading = false, }) { return Expanded( child: PressScale( onTap: onTap, child: Container( padding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 6.w), decoration: SettingsCard.decoration(), child: Column( children: [ Icon(icon, color: ColorConstants.primary, size: 24.sp), SizedBox(height: 8.h), if (value != null) ...[ // A stale zero reads as a real number. While the count is in // flight, show that it is in flight. if (loading) Padding( padding: EdgeInsets.symmetric(vertical: 3.h), child: MilerShimmer( child: SkeletonBone(width: 34.w, height: 15.h), ), ) else Text( value, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w800, color: ColorConstants.onSurface, fontFamily: FontConstants.fontFamily, ), ), SizedBox(height: 1.h), ], Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12.sp, fontWeight: value != null ? FontWeight.w500 : FontWeight.w600, color: value != null ? ColorConstants.onSurfaceVariant : ColorConstants.onSurface, fontFamily: FontConstants.fontFamily, ), ), ], ), ), ), ); } /// Formats an integer with thousands separators (e.g. 12450 -> "12,450"). String _formatCount(int value) { final s = value.toString(); final buf = StringBuffer(); for (int i = 0; i < s.length; i++) { if (i > 0 && (s.length - i) % 3 == 0) buf.write(','); buf.write(s[i]); } return buf.toString(); } /// India's emergency number. This was **911**, which does nothing on an /// Indian network — the one control in the app that only matters when a rider /// has been hit or threatened, wired to a number that cannot answer. static const String _emergencyNumber = '112'; /// Fails loudly. `canLaunchUrl` returning false used to end the story: the /// rider taps SOS in an emergency and the app does nothing at all. Future _callEmergency() async { final Uri url = Uri(scheme: 'tel', path: _emergencyNumber); bool launched = false; try { if (await canLaunchUrl(url)) { launched = await launchUrl(url); } } catch (_) { launched = false; } if (!launched && mounted) { AppFeedback.error( context, 'Could not place the call. Dial $_emergencyNumber from your phone.', duration: const Duration(seconds: 8), ); } } // ─── SOS banner ───────────────────────────────────────────────────────────── Widget _buildSOSBanner() { return GestureDetector( onTap: _callEmergency, child: InfoBanner( text: "Emergency Response — Tap to call SOS if you are in immediate danger.", icon: Icons.warning_amber_rounded, color: ColorConstants.primary, ), ); } } // ─── Logout dialog ──────────────────────────────────────────────────────────── void _showLogoutDialog(BuildContext context) { showAppDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: ColorConstants.surfaceContainerLowest, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(DesignConstants.radiusXl), ), title: Text( "Logout", style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w700, fontFamily: FontConstants.fontFamily, color: ColorConstants.onSurface, ), ), content: Text( "Are you sure you want to logout?", style: TextStyle( fontSize: 14.sp, color: ColorConstants.onSurfaceVariant, fontFamily: FontConstants.fontFamily, ), ), actionsPadding: EdgeInsets.fromLTRB(20.w, 4.h, 20.w, 18.h), actions: [ Row( children: [ Expanded( child: MilerButton( label: "Cancel", onPressed: () => Navigator.pop(context), variant: MilerButtonVariant.outlined, color: ColorConstants.onSurfaceVariant, height: ButtonSizes.secondary, ), ), SizedBox(width: 12.w), Expanded( child: MilerButton( label: "Logout", onPressed: () async { final prefs = await SharedPreferences.getInstance(); prefs.setBool('logged_out', true); Get.offAll(() => const SignIn()); }, height: ButtonSizes.secondary, ), ), ], ), ], ); }, ); }