diff --git a/lib/controller/auth/mpin_controller.dart b/lib/controller/auth/mpin_controller.dart index cde5bc0..f045310 100644 --- a/lib/controller/auth/mpin_controller.dart +++ b/lib/controller/auth/mpin_controller.dart @@ -10,14 +10,17 @@ import 'package:marco/helpers/services/app_logger.dart'; class MPINController extends GetxController { final MyFormValidator basicValidator = MyFormValidator(); final isNewUser = false.obs; + final isChangeMpin = false.obs; final RxBool isLoading = false.obs; final formKey = GlobalKey(); - final digitControllers = List.generate(6, (_) => TextEditingController()); - final focusNodes = List.generate(6, (_) => FocusNode()); + // Updated to 4-digit MPIN + final digitControllers = List.generate(4, (_) => TextEditingController()); + final focusNodes = List.generate(4, (_) => FocusNode()); + + final retypeControllers = List.generate(4, (_) => TextEditingController()); + final retypeFocusNodes = List.generate(4, (_) => FocusNode()); - final retypeControllers = List.generate(6, (_) => TextEditingController()); - final retypeFocusNodes = List.generate(6, (_) => FocusNode()); final RxInt failedAttempts = 0.obs; @override @@ -28,16 +31,27 @@ class MPINController extends GetxController { logSafe("onInit called. isNewUser: ${isNewUser.value}"); } + /// Enable Change MPIN mode + void setChangeMpinMode() { + isChangeMpin.value = true; + isNewUser.value = false; + clearFields(); + clearRetypeFields(); + logSafe("setChangeMpinMode activated"); + } + + /// Handle digit entry and focus movement void onDigitChanged(String value, int index, {bool isRetype = false}) { - logSafe("onDigitChanged -> index: $index, value: $value, isRetype: $isRetype", ); + logSafe("onDigitChanged -> index: $index, value: $value, isRetype: $isRetype"); final nodes = isRetype ? retypeFocusNodes : focusNodes; - if (value.isNotEmpty && index < 5) { + if (value.isNotEmpty && index < 3) { nodes[index + 1].requestFocus(); } else if (value.isEmpty && index > 0) { nodes[index - 1].requestFocus(); } } + /// Submit MPIN for verification or generation Future onSubmitMPIN() async { logSafe("onSubmitMPIN triggered"); @@ -47,19 +61,19 @@ class MPINController extends GetxController { } final enteredMPIN = digitControllers.map((c) => c.text).join(); - logSafe("Entered MPIN: $enteredMPIN", ); + logSafe("Entered MPIN: $enteredMPIN"); - if (enteredMPIN.length < 6) { - _showError("Please enter all 6 digits."); + if (enteredMPIN.length < 4) { + _showError("Please enter all 4 digits."); return; } - if (isNewUser.value) { + if (isNewUser.value || isChangeMpin.value) { final retypeMPIN = retypeControllers.map((c) => c.text).join(); - logSafe("Retyped MPIN: $retypeMPIN", ); + logSafe("Retyped MPIN: $retypeMPIN"); - if (retypeMPIN.length < 6) { - _showError("Please enter all 6 digits in Retype MPIN."); + if (retypeMPIN.length < 4) { + _showError("Please enter all 4 digits in Retype MPIN."); return; } @@ -70,19 +84,20 @@ class MPINController extends GetxController { return; } - logSafe("MPINs matched. Proceeding to generate MPIN."); final bool success = await generateMPIN(mpin: enteredMPIN); if (success) { - logSafe("MPIN generation successful."); + logSafe("MPIN generation/change successful."); showAppSnackbar( title: "Success", - message: "MPIN generated successfully. Please login again.", + message: isChangeMpin.value + ? "MPIN changed successfully." + : "MPIN generated successfully. Please login again.", type: SnackbarType.success, ); await LocalStorage.logout(); } else { - logSafe("MPIN generation failed.", level: LogLevel.warning); + logSafe("MPIN generation/change failed.", level: LogLevel.warning); clearFields(); clearRetypeFields(); } @@ -92,20 +107,25 @@ class MPINController extends GetxController { } } + /// Forgot MPIN Future onForgotMPIN() async { logSafe("onForgotMPIN called"); isNewUser.value = true; + isChangeMpin.value = false; clearFields(); clearRetypeFields(); } + /// Switch to login/enter MPIN screen void switchToEnterMPIN() { logSafe("switchToEnterMPIN called"); isNewUser.value = false; + isChangeMpin.value = false; clearFields(); clearRetypeFields(); } + /// Show error snackbar void _showError(String message) { logSafe("ERROR: $message", level: LogLevel.error); showAppSnackbar( @@ -115,6 +135,7 @@ class MPINController extends GetxController { ); } + /// Navigate to dashboard void _navigateToDashboard({String? message}) { if (message != null) { logSafe("Navigating to Dashboard with message: $message"); @@ -127,6 +148,7 @@ class MPINController extends GetxController { Get.offAll(() => const DashboardScreen()); } + /// Clear the primary MPIN fields void clearFields() { logSafe("clearFields called"); for (final c in digitControllers) { @@ -135,6 +157,7 @@ class MPINController extends GetxController { focusNodes.first.requestFocus(); } + /// Clear the retype MPIN fields void clearRetypeFields() { logSafe("clearRetypeFields called"); for (final c in retypeControllers) { @@ -143,6 +166,7 @@ class MPINController extends GetxController { retypeFocusNodes.first.requestFocus(); } + /// Cleanup @override void onClose() { logSafe("onClose called"); @@ -161,9 +185,8 @@ class MPINController extends GetxController { super.onClose(); } - Future generateMPIN({ - required String mpin, - }) async { + /// Generate MPIN for new user/change MPIN + Future generateMPIN({required String mpin}) async { try { isLoading.value = true; logSafe("generateMPIN started"); @@ -177,7 +200,7 @@ class MPINController extends GetxController { return false; } - logSafe("Calling AuthService.generateMpin for employeeId: $employeeId", ); + logSafe("Calling AuthService.generateMpin for employeeId: $employeeId"); final response = await AuthService.generateMpin( employeeId: employeeId, @@ -187,21 +210,11 @@ class MPINController extends GetxController { isLoading.value = false; if (response == null) { - logSafe("MPIN generated successfully"); - - showAppSnackbar( - title: "Success", - message: "MPIN generated successfully. Please login again.", - type: SnackbarType.success, - ); - - await LocalStorage.logout(); - return true; } else { logSafe("MPIN generation returned error: $response", level: LogLevel.warning); showAppSnackbar( - title: "MPIN Generation Failed", + title: "MPIN Operation Failed", message: "Please check your inputs.", type: SnackbarType.error, ); @@ -213,19 +226,20 @@ class MPINController extends GetxController { } catch (e) { isLoading.value = false; logSafe("Exception in generateMPIN", level: LogLevel.error, error: e); - _showError("Failed to generate MPIN."); + _showError("Failed to process MPIN."); return false; } } + /// Verify MPIN for existing user Future verifyMPIN() async { logSafe("verifyMPIN triggered"); final enteredMPIN = digitControllers.map((c) => c.text).join(); - logSafe("Entered MPIN: $enteredMPIN", ); + logSafe("Entered MPIN: $enteredMPIN"); - if (enteredMPIN.length < 6) { - _showError("Please enter all 6 digits."); + if (enteredMPIN.length < 4) { + _showError("Please enter all 4 digits."); return; } @@ -278,6 +292,7 @@ class MPINController extends GetxController { } } + /// Increment failed attempts and warn void onInvalidMPIN() { failedAttempts.value++; if (failedAttempts.value >= 3) { diff --git a/lib/view/auth/mpin_auth_screen.dart b/lib/view/auth/mpin_auth_screen.dart index c53164d..d1d332c 100644 --- a/lib/view/auth/mpin_auth_screen.dart +++ b/lib/view/auth/mpin_auth_screen.dart @@ -139,6 +139,7 @@ class _MPINAuthScreenState extends State Widget _buildMPINCard(MPINController controller) { return Obx(() { final isNewUser = controller.isNewUser.value; + final isChangeMpin = controller.isChangeMpin.value; return Container( padding: const EdgeInsets.all(24), @@ -156,7 +157,9 @@ class _MPINAuthScreenState extends State child: Column( children: [ MyText( - isNewUser ? 'Generate MPIN' : 'Enter MPIN', + isChangeMpin + ? 'Change MPIN' + : (isNewUser ? 'Generate MPIN' : 'Enter MPIN'), fontSize: 20, fontWeight: 700, color: Colors.black87, @@ -164,17 +167,19 @@ class _MPINAuthScreenState extends State ), const SizedBox(height: 10), MyText( - isNewUser - ? 'Set your 6-digit MPIN for quick login.' - : 'Enter your 6-digit MPIN to continue.', + isChangeMpin + ? 'Set a new 6-digit MPIN for your account.' + : (isNewUser + ? 'Set your 6-digit MPIN for quick login.' + : 'Enter your 6-digit MPIN to continue.'), fontSize: 14, color: Colors.black54, textAlign: TextAlign.center, ), const SizedBox(height: 30), - _buildMPINForm(controller, isNewUser), + _buildMPINForm(controller, isNewUser || isChangeMpin), const SizedBox(height: 32), - _buildSubmitButton(controller, isNewUser), + _buildSubmitButton(controller, isNewUser, isChangeMpin), const SizedBox(height: 20), _buildFooterOptions(controller, isNewUser), ], @@ -183,13 +188,13 @@ class _MPINAuthScreenState extends State }); } - Widget _buildMPINForm(MPINController controller, bool isNewUser) { + Widget _buildMPINForm(MPINController controller, bool showRetype) { return Form( key: controller.formKey, child: Column( children: [ _buildDigitRow(controller, isRetype: false), - if (isNewUser) ...[ + if (showRetype) ...[ const SizedBox(height: 20), MyText( 'Retype MPIN', @@ -206,11 +211,9 @@ class _MPINAuthScreenState extends State } Widget _buildDigitRow(MPINController controller, {required bool isRetype}) { - return Wrap( - alignment: WrapAlignment.center, - spacing: 0, - runSpacing: 12, - children: List.generate(6, (index) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(4, (index) { return _buildDigitBox(controller, index, isRetype); }), ); @@ -225,29 +228,33 @@ class _MPINAuthScreenState extends State : controller.focusNodes[index]; return Container( - margin: const EdgeInsets.symmetric(horizontal: 6), - width: 30, - height: 55, + margin: const EdgeInsets.symmetric(horizontal: 8), + width: 48, + height: 60, child: TextFormField( controller: textController, focusNode: focusNode, - obscureText: true, + obscureText: false, // Digits are visible maxLength: 1, textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - letterSpacing: 8, ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (value) { controller.onDigitChanged(value, index, isRetype: isRetype); + // Auto-submit only in verification mode if (!isRetype) { final isComplete = controller.digitControllers.every((c) => c.text.isNotEmpty); - if (isComplete && !controller.isLoading.value) { + + if (isComplete && + !controller.isLoading.value && + !controller.isNewUser.value && + !controller.isChangeMpin.value) { controller.onSubmitMPIN(); } } @@ -265,7 +272,8 @@ class _MPINAuthScreenState extends State ); } - Widget _buildSubmitButton(MPINController controller, bool isNewUser) { + Widget _buildSubmitButton( + MPINController controller, bool isNewUser, bool isChangeMpin) { return Obx(() { return MyButton.rounded( onPressed: controller.isLoading.value ? null : controller.onSubmitMPIN, @@ -285,7 +293,9 @@ class _MPINAuthScreenState extends State ), ) : MyText.bodyMedium( - isNewUser ? 'Generate MPIN' : 'Submit MPIN', + isChangeMpin + ? 'Change MPIN' + : (isNewUser ? 'Generate MPIN' : 'Submit MPIN'), color: Colors.white, fontWeight: 700, fontSize: 16, @@ -296,12 +306,13 @@ class _MPINAuthScreenState extends State Widget _buildFooterOptions(MPINController controller, bool isNewUser) { return Obx(() { + final isChangeMpin = controller.isChangeMpin.value; final showBackToLogin = - controller.failedAttempts.value >= 3 && !isNewUser; + controller.failedAttempts.value >= 3 && !isNewUser && !isChangeMpin; return Column( children: [ - if (isNewUser) + if (isNewUser || isChangeMpin) TextButton.icon( onPressed: () => Get.toNamed('/dashboard'), icon: const Icon(Icons.arrow_back, @@ -359,8 +370,8 @@ class _WavePainter extends CustomPainter { final path1 = Path() ..moveTo(0, size.height * 0.2) - ..quadraticBezierTo( - size.width * 0.25, size.height * 0.05, size.width * 0.5, size.height * 0.15) + ..quadraticBezierTo(size.width * 0.25, size.height * 0.05, + size.width * 0.5, size.height * 0.15) ..quadraticBezierTo( size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1) ..lineTo(size.width, 0) diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index de064f4..ef0b8d6 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -6,14 +6,14 @@ import 'package:marco/controller/project_controller.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/my_shadow.dart'; -import 'package:marco/helpers/widgets/my_button.dart'; +// import 'package:marco/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/view/dashboard/dashboard_chart.dart'; import 'package:marco/view/layouts/layout.dart'; - + class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @@ -74,71 +74,70 @@ class _DashboardScreenState extends State with UIMixin { crossAxisAlignment: CrossAxisAlignment.start, children: [ AttendanceDashboardChart(), - MySpacing.height(300), ], ), ), ); }, ), - if (!hasMpin) ...[ - MyCard( - borderRadiusAll: 12, - paddingAll: 16, - shadow: MyShadow(elevation: 2), - color: Colors.red.withOpacity(0.05), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.warning_amber_rounded, - color: Colors.redAccent, size: 28), - MySpacing.width(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyMedium( - "MPIN Not Generated", - color: Colors.redAccent, - fontWeight: 700, - ), - MySpacing.height(4), - MyText.bodySmall( - "To secure your account, please generate your MPIN now.", - color: contentTheme.onBackground.withOpacity(0.8), - ), - MySpacing.height(10), - Align( - alignment: Alignment.center, - child: MyButton.rounded( - onPressed: () { - Get.toNamed("/auth/mpin-auth"); - }, - backgroundColor: contentTheme.brandRed, - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 10), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.lock_outline, - size: 18, color: Colors.white), - MySpacing.width(8), - MyText.bodyMedium( - "Generate MPIN", - color: Colors.white, - fontWeight: 600, - ), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ), - ], + // if (!hasMpin) ...[ + // MyCard( + // borderRadiusAll: 12, + // paddingAll: 16, + // shadow: MyShadow(elevation: 2), + // color: Colors.red.withOpacity(0.05), + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // const Icon(Icons.warning_amber_rounded, + // color: Colors.redAccent, size: 28), + // MySpacing.width(12), + // Expanded( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // MyText.bodyMedium( + // "MPIN Not Generated", + // color: Colors.redAccent, + // fontWeight: 700, + // ), + // MySpacing.height(4), + // MyText.bodySmall( + // "To secure your account, please generate your MPIN now.", + // color: contentTheme.onBackground.withOpacity(0.8), + // ), + // MySpacing.height(10), + // Align( + // alignment: Alignment.center, + // child: MyButton.rounded( + // onPressed: () { + // Get.toNamed("/auth/mpin-auth"); + // }, + // backgroundColor: contentTheme.brandRed, + // padding: const EdgeInsets.symmetric( + // horizontal: 20, vertical: 10), + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // const Icon(Icons.lock_outline, + // size: 18, color: Colors.white), + // MySpacing.width(8), + // MyText.bodyMedium( + // "Generate MPIN", + // color: Colors.white, + // fontWeight: 600, + // ), + // ], + // ), + // ), + // ), + // ], + // ), + // ), + // ], + // ), + // ), + // ], ], ), ), diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index 4000606..44db173 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -26,6 +26,23 @@ class _LayoutState extends State { final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage"); final projectController = Get.find(); + bool hasMpin = true; + + @override + void initState() { + super.initState(); + _checkMpinStatus(); + } + + Future _checkMpinStatus() async { + final bool mpinStatus = await LocalStorage.getIsMpin(); + if (mounted) { + setState(() { + hasMpin = mpinStatus; + }); + } + } + @override Widget build(BuildContext context) { return MyResponsive(builder: (context, _, screenMT) { @@ -43,7 +60,7 @@ class _LayoutState extends State { Widget _buildScaffold(BuildContext context, {bool isMobile = false}) { return Scaffold( key: controller.scaffoldKey, - endDrawer: UserProfileBar(), + endDrawer: const UserProfileBar(), floatingActionButton: widget.floatingActionButton, body: SafeArea( child: GestureDetector( @@ -68,28 +85,7 @@ class _LayoutState extends State { ), ], ), - Obx(() { - if (!projectController.isProjectSelectionExpanded.value) { - return const SizedBox.shrink(); - } - return Positioned( - top: 95, - left: 16, - right: 16, - child: Material( - elevation: 4, - borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(10), - child: _buildProjectList(context, isMobile), - ), - ), - ); - }), + _buildProjectDropdown(context, isMobile), ], ), ), @@ -97,6 +93,7 @@ class _LayoutState extends State { ); } + /// Header Section Widget _buildHeader(BuildContext context, bool isMobile) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), @@ -236,11 +233,32 @@ class _LayoutState extends State { fontWeight: 700, ), ), - IconButton( - icon: const Icon(Icons.menu), - onPressed: () => - controller.scaffoldKey.currentState?.openEndDrawer(), - ), + Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + IconButton( + icon: const Icon(Icons.menu), + onPressed: () => controller.scaffoldKey.currentState + ?.openEndDrawer(), + ), + if (!hasMpin) + Positioned( + right: 10, + top: 10, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.redAccent, + shape: BoxShape.circle, + border: + Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ) ], ), ), @@ -262,6 +280,7 @@ class _LayoutState extends State { ); } + /// Loading Skeleton for Header Widget _buildLoadingSkeleton() { return Card( elevation: 4, @@ -312,6 +331,32 @@ class _LayoutState extends State { ); } + /// Project List Popup + Widget _buildProjectDropdown(BuildContext context, bool isMobile) { + return Obx(() { + if (!projectController.isProjectSelectionExpanded.value) { + return const SizedBox.shrink(); + } + return Positioned( + top: 95, + left: 16, + right: 16, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(10), + child: _buildProjectList(context, isMobile), + ), + ), + ); + }); + } + Widget _buildProjectList(BuildContext context, bool isMobile) { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index 8b37bbf..d85674e 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -9,6 +9,8 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:marco/model/employee_info.dart'; import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/auth/mpin_controller.dart'; class UserProfileBar extends StatefulWidget { final bool isCondensed; @@ -24,11 +26,13 @@ class _UserProfileBarState extends State final ThemeCustomizer customizer = ThemeCustomizer.instance; bool isCondensed = false; EmployeeInfo? employeeInfo; + bool hasMpin = true; @override void initState() { super.initState(); _loadEmployeeInfo(); + _checkMpinStatus(); } void _loadEmployeeInfo() { @@ -37,6 +41,13 @@ class _UserProfileBarState extends State }); } + Future _checkMpinStatus() async { + final bool mpinStatus = await LocalStorage.getIsMpin(); + setState(() { + hasMpin = mpinStatus; + }); + } + @override Widget build(BuildContext context) { isCondensed = widget.isCondensed; @@ -44,19 +55,23 @@ class _UserProfileBarState extends State return MyCard( borderRadiusAll: 16, paddingAll: 0, - shadow: MyShadow(position: MyShadowPosition.centerRight, elevation: 4), + shadow: MyShadow( + position: MyShadowPosition.centerRight, + elevation: 6, + blurRadius: 12, + ), child: AnimatedContainer( decoration: BoxDecoration( gradient: LinearGradient( colors: [ - leftBarTheme.background.withOpacity(0.95), - leftBarTheme.background.withOpacity(0.85), + leftBarTheme.background.withOpacity(0.97), + leftBarTheme.background.withOpacity(0.88), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), - width: isCondensed ? 90 : 250, + width: isCondensed ? 90 : 260, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, child: SafeArea( @@ -65,9 +80,10 @@ class _UserProfileBarState extends State left: false, right: false, child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ userProfileSection(), - MySpacing.height(8), + MySpacing.height(16), supportAndSettingsMenu(), const Spacer(), logoutButton(), @@ -78,17 +94,18 @@ class _UserProfileBarState extends State ); } + /// User Profile Section - Avatar + Name Widget userProfileSection() { if (employeeInfo == null) { return const Padding( - padding: EdgeInsets.all(24.0), + padding: EdgeInsets.symmetric(vertical: 32), child: Center(child: CircularProgressIndicator()), ); } return Container( width: double.infinity, - padding: MySpacing.xy(58, 68), + padding: MySpacing.fromLTRB(20, 50, 30, 50), decoration: BoxDecoration( color: leftBarTheme.activeItemBackground, borderRadius: const BorderRadius.only( @@ -96,55 +113,102 @@ class _UserProfileBarState extends State topRight: Radius.circular(16), ), ), - child: Column( + child: Row( children: [ Avatar( - firstName: employeeInfo?.firstName ?? 'First', - lastName: employeeInfo?.lastName ?? 'Name', - size: 60, + firstName: employeeInfo?.firstName ?? 'F', + lastName: employeeInfo?.lastName ?? 'N', + size: 50, ), - MySpacing.height(12), - MyText.labelLarge( - "${employeeInfo?.firstName ?? 'First'} ${employeeInfo?.lastName ?? 'Last'}", - fontWeight: 700, - color: leftBarTheme.activeItemColor, - textAlign: TextAlign.center, + MySpacing.width(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium( + "${employeeInfo?.firstName ?? 'First'} ${employeeInfo?.lastName ?? 'Last'}", + fontWeight: 700, + color: leftBarTheme.activeItemColor, + ), + ], + ), ), ], ), ); } + /// Menu Section with Settings, Support & MPIN Widget supportAndSettingsMenu() { return Padding( padding: MySpacing.xy(16, 16), child: Column( children: [ - menuItem(icon: LucideIcons.settings, label: "Settings"), - MySpacing.height(12), - menuItem(icon: LucideIcons.badge_help, label: "Support"), + menuItem( + icon: LucideIcons.settings, + label: "Settings", + ), + MySpacing.height(14), + menuItem( + icon: LucideIcons.badge_help, + label: "Support", + ), + MySpacing.height(14), + menuItem( + icon: LucideIcons.lock, + label: hasMpin ? "Change MPIN" : "Set MPIN", + iconColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, + labelColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, + onTap: () { + final controller = Get.put(MPINController()); + if (hasMpin) { + controller.setChangeMpinMode(); + } + Navigator.pushNamed(context, "/auth/mpin-auth"); + }, + filled: true, + ), ], ), ); } - Widget menuItem({required IconData icon, required String label}) { + Widget menuItem({ + required IconData icon, + required String label, + Color? iconColor, + Color? labelColor, + VoidCallback? onTap, + bool filled = false, + }) { return InkWell( - onTap: () {}, - borderRadius: BorderRadius.circular(10), - hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.2), - splashColor: leftBarTheme.activeItemBackground.withOpacity(0.3), - child: Padding( - padding: MySpacing.xy(12, 10), + onTap: onTap, + borderRadius: BorderRadius.circular(12), + hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25), + splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35), + child: Container( + padding: MySpacing.xy(14, 12), + decoration: BoxDecoration( + color: filled + ? leftBarTheme.activeItemBackground.withOpacity(0.15) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: filled + ? leftBarTheme.activeItemBackground.withOpacity(0.3) + : Colors.transparent, + width: 1, + ), + ), child: Row( children: [ - Icon(icon, size: 20, color: leftBarTheme.onBackground), - MySpacing.width(12), + Icon(icon, size: 22, color: iconColor ?? leftBarTheme.onBackground), + MySpacing.width(14), Expanded( child: MyText.bodyMedium( label, - color: leftBarTheme.onBackground, - fontWeight: 500, + color: labelColor ?? leftBarTheme.onBackground, + fontWeight: 600, ), ), ], @@ -153,142 +217,19 @@ class _UserProfileBarState extends State ); } + /// Logout Button Widget logoutButton() { return InkWell( onTap: () async { - bool? confirm = await showDialog( - context: context, - builder: (context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 28), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - LucideIcons.log_out, - size: 48, - color: Colors.redAccent, - ), - const SizedBox(height: 16), - Text( - "Logout Confirmation", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - const SizedBox(height: 12), - Text( - "Are you sure you want to logout?\nYou will need to login again to continue.", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.7), - ), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: TextButton( - onPressed: () => Navigator.pop(context, false), - style: TextButton.styleFrom( - foregroundColor: Colors.grey.shade700, - ), - child: const Text("Cancel"), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: () async { - await LocalStorage.logout(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.redAccent, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text("Logout"), - ), - ), - ], - ) - ], - ), - ), - ); - }, - ); - - if (confirm == true) { - // Show animated loader dialog - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Dialog( - backgroundColor: Colors.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - children: const [ - CircularProgressIndicator(), - SizedBox(height: 12), - Text( - "Logging you out...", - style: TextStyle(color: Colors.white), - ) - ], - ), - ), - ); - - await LocalStorage.logout(); - - if (mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(LucideIcons.check, color: Colors.green), - const SizedBox(width: 12), - const Text("You’ve been logged out successfully."), - ], - ), - backgroundColor: Colors.grey.shade900, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - duration: const Duration(seconds: 3), - ), - ); - } - } + await _showLogoutConfirmation(); }, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), ), - hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.2), - splashColor: leftBarTheme.activeItemBackground.withOpacity(0.3), - child: AnimatedContainer( - duration: const Duration(milliseconds: 150), - curve: Curves.easeInOut, - width: double.infinity, + hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25), + splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35), + child: Container( padding: MySpacing.all(16), decoration: BoxDecoration( color: leftBarTheme.activeItemBackground, @@ -316,4 +257,78 @@ class _UserProfileBarState extends State ), ); } + + Future _showLogoutConfirmation() async { + bool? confirm = await showDialog( + context: context, + builder: (context) => _buildLogoutDialog(context), + ); + + if (confirm == true) { + await LocalStorage.logout(); + } + } + + Widget _buildLogoutDialog(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.log_out, size: 48, color: Colors.redAccent), + const SizedBox(height: 16), + Text( + "Logout Confirmation", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onBackground, + ), + ), + const SizedBox(height: 12), + Text( + "Are you sure you want to logout?\nYou will need to login again to continue.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(context, false), + style: TextButton.styleFrom( + foregroundColor: Colors.grey.shade700, + ), + child: const Text("Cancel"), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text("Logout"), + ), + ), + ], + ), + ], + ), + ), + ); + } }