From 6a36064af7caed254e23068eb2208b950a0833a9 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 11 Jun 2025 17:11:50 +0530 Subject: [PATCH] feat: Implement project management features across controllers and views - Added DashboardController and ProjectController for managing project data. - Enhanced LayoutController to support project selection and loading states. - Created UserProfileBar for user-specific actions and information. - Refactored app initialization logic to streamline setup and error handling. - Updated layout views to integrate project selection and improve user experience. --- .../dashboard/dashboard_controller.dart | 54 ++ lib/controller/layout/layout_controller.dart | 84 ++- lib/controller/project_controller.dart | 60 ++ lib/helpers/services/app_initializer.dart | 28 + lib/helpers/services/auth_service.dart | 5 +- .../services/storage/local_storage.dart | 9 + lib/main.dart | 114 +--- lib/view/layouts/layout.dart | 517 +++++++----------- lib/view/layouts/user_profile_right_bar.dart | 311 +++++++++++ lib/view/my_app.dart | 95 ++++ 10 files changed, 856 insertions(+), 421 deletions(-) create mode 100644 lib/controller/dashboard/dashboard_controller.dart create mode 100644 lib/controller/project_controller.dart create mode 100644 lib/helpers/services/app_initializer.dart create mode 100644 lib/view/layouts/user_profile_right_bar.dart create mode 100644 lib/view/my_app.dart diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart new file mode 100644 index 0000000..2cd18a1 --- /dev/null +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -0,0 +1,54 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/project_model.dart'; + +final Logger log = Logger(); + +class DashboardController extends GetxController { + RxList projects = [].obs; + RxString? selectedProjectId; + var isProjectListExpanded = false.obs; + RxBool isProjectSelectionExpanded = true.obs; + + void toggleProjectListExpanded() { + isProjectListExpanded.value = !isProjectListExpanded.value; + } + + var isProjectDropdownExpanded = false.obs; + + RxBool isLoading = true.obs; + RxBool isLoadingProjects = true.obs; + RxMap uploadingStates = {}.obs; + + @override + void onInit() { + super.onInit(); + fetchProjects(); + } + + /// Fetches projects and initializes selected project. + Future fetchProjects() async { + isLoadingProjects.value = true; + isLoading.value = true; + + final response = await ApiService.getProjects(); + + if (response != null && response.isNotEmpty) { + projects.assignAll( + response.map((json) => ProjectModel.fromJson(json)).toList()); + selectedProjectId = RxString(projects.first.id.toString()); + log.i("Projects fetched: ${projects.length}"); + } else { + log.w("No projects found or API call failed."); + } + + isLoadingProjects.value = false; + isLoading.value = false; + update(['dashboard_controller']); + } + + void updateSelectedProject(String projectId) { + selectedProjectId?.value = projectId; + } +} diff --git a/lib/controller/layout/layout_controller.dart b/lib/controller/layout/layout_controller.dart index 782b1d3..f754f3e 100644 --- a/lib/controller/layout/layout_controller.dart +++ b/lib/controller/layout/layout_controller.dart @@ -1,23 +1,87 @@ -import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/theme/theme_customizer.dart'; +import 'package:marco/model/project_model.dart'; + + +final Logger log = Logger(); class LayoutController extends GetxController { + // Theme Customization ThemeCustomizer themeCustomizer = ThemeCustomizer(); + // Global Keys final GlobalKey scaffoldKey = GlobalKey(); final GlobalKey> scrollKey = GlobalKey(); - ScrollController scrollController = ScrollController(); + // Scroll + final ScrollController scrollController = ScrollController(); + + // Reactive State + final RxBool isLoading = true.obs; + final RxBool isLoadingProjects = true.obs; + final RxBool isProjectSelectionExpanded = true.obs; + final RxBool isProjectListExpanded = false.obs; + final RxBool isProjectDropdownExpanded = false.obs; + final RxList projects = [].obs; + final RxMap uploadingStates = {}.obs; + + // Selected Project + RxString? selectedProjectId; bool isLastIndex = false; + @override + void onInit() { + super.onInit(); + fetchProjects(); + } + @override void onReady() { super.onReady(); ThemeCustomizer.addListener(onChangeTheme); } + @override + void dispose() { + ThemeCustomizer.removeListener(onChangeTheme); + scrollController.dispose(); + super.dispose(); + } + + // Project Handling + Future fetchProjects() async { + isLoading.value = true; + isLoadingProjects.value = true; + + final response = await ApiService.getProjects(); + + if (response != null && response.isNotEmpty) { + final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList(); + projects.assignAll(fetchedProjects); + selectedProjectId = RxString(fetchedProjects.first.id.toString()); + log.i("Projects fetched: ${fetchedProjects.length}"); + } else { + log.w("No projects found or API call failed."); + } + + isLoadingProjects.value = false; + isLoading.value = false; + update(['dashboard_controller']); + } + + void updateSelectedProject(String projectId) { + selectedProjectId?.value = projectId; + } + + void toggleProjectListExpanded() { + isProjectListExpanded.toggle(); + } + + // Theme Updates void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) { themeCustomizer = newVal; update(); @@ -29,18 +93,12 @@ class LayoutController extends GetxController { } } - enableNotificationShade() { - // SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]); + // Notification Shade (placeholders) + void enableNotificationShade() { + // Add implementation if needed } - disableNotificationShade() { - // SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]); - } - - @override - void dispose() { - super.dispose(); - ThemeCustomizer.removeListener(onChangeTheme); - scrollController.dispose(); + void disableNotificationShade() { + // Add implementation if needed } } diff --git a/lib/controller/project_controller.dart b/lib/controller/project_controller.dart new file mode 100644 index 0000000..805e3d9 --- /dev/null +++ b/lib/controller/project_controller.dart @@ -0,0 +1,60 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/project_model.dart'; +import 'package:marco/helpers/services/storage/local_storage.dart'; + +final Logger log = Logger(); + +class ProjectController extends GetxController { + RxList projects = [].obs; + RxString? selectedProjectId; + RxBool isProjectListExpanded = false.obs; + RxBool isProjectSelectionExpanded = false.obs; + + RxBool isProjectDropdownExpanded = false.obs; + RxBool isLoading = true.obs; + RxBool isLoadingProjects = true.obs; + RxMap uploadingStates = {}.obs; + + @override + void onInit() { + super.onInit(); + fetchProjects(); + } + + /// Fetches projects and initializes selected project. + Future fetchProjects() async { + isLoadingProjects.value = true; + isLoading.value = true; + + final response = await ApiService.getProjects(); + + if (response != null && response.isNotEmpty) { + projects.assignAll( + response.map((json) => ProjectModel.fromJson(json)).toList()); + + String? savedId = LocalStorage.getString('selectedProjectId'); + if (savedId != null && projects.any((p) => p.id == savedId)) { + selectedProjectId = RxString(savedId); + } else { + selectedProjectId = RxString(projects.first.id.toString()); + LocalStorage.saveString('selectedProjectId', projects.first.id.toString()); + } + + isProjectSelectionExpanded.value = false; + log.i("Projects fetched: ${projects.length}"); + } else { + log.w("No projects found or API call failed."); + } + + isLoadingProjects.value = false; + isLoading.value = false; + update(['dashboard_controller']); + } + + void updateSelectedProject(String projectId) { + selectedProjectId?.value = projectId; + LocalStorage.saveString('selectedProjectId', projectId); + } +} diff --git a/lib/helpers/services/app_initializer.dart b/lib/helpers/services/app_initializer.dart new file mode 100644 index 0000000..c5eb844 --- /dev/null +++ b/lib/helpers/services/app_initializer.dart @@ -0,0 +1,28 @@ +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/services/storage/local_storage.dart'; +import 'package:marco/helpers/theme/theme_customizer.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:url_strategy/url_strategy.dart'; +import 'package:logger/logger.dart'; + +final Logger logger = Logger(); + +Future initializeApp() async { + setPathUrlStrategy(); + + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( + statusBarColor: Color.fromARGB(255, 255, 0, 0), + statusBarIconBrightness: Brightness.light, + )); + + await LocalStorage.init(); + await ThemeCustomizer.init(); + Get.put(PermissionController()); + Get.put(ProjectController(), permanent: true); + AppStyle.init(); + + logger.i("App initialization completed successfully."); +} diff --git a/lib/helpers/services/auth_service.dart b/lib/helpers/services/auth_service.dart index 3fe7033..a9e3c53 100644 --- a/lib/helpers/services/auth_service.dart +++ b/lib/helpers/services/auth_service.dart @@ -367,7 +367,10 @@ class AuthService { await LocalStorage.removeMpinToken(); } - Get.put(PermissionController()); + // ✅ Put and load PermissionController + final permissionController = Get.put(PermissionController()); + await permissionController.loadData(jwtToken); + isLoggedIn = true; } } diff --git a/lib/helpers/services/storage/local_storage.dart b/lib/helpers/services/storage/local_storage.dart index c4fc325..e3c0b07 100644 --- a/lib/helpers/services/storage/local_storage.dart +++ b/lib/helpers/services/storage/local_storage.dart @@ -180,4 +180,13 @@ class LocalStorage { static bool? getBool(String key) { return preferences.getBool(key); } + // Save and retrieve String values + static String? getString(String key) { + return preferences.getString(key); +} + +static Future saveString(String key, String value) async { + return preferences.setString(key, value); +} + } diff --git a/lib/main.dart b/lib/main.dart index 1b5de88..7fea3f3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,109 +1,31 @@ import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:get/get.dart'; -import 'package:marco/helpers/extensions/app_localization_delegate.dart'; -import 'package:marco/helpers/services/localizations/language.dart'; -import 'package:marco/helpers/services/navigation_services.dart'; -import 'package:marco/helpers/services/storage/local_storage.dart'; -import 'package:marco/helpers/theme/app_notifier.dart'; -import 'package:marco/helpers/theme/app_theme.dart'; -import 'package:marco/helpers/theme/theme_customizer.dart'; -import 'package:marco/routes.dart'; +import 'package:marco/helpers/services/app_initializer.dart'; +import 'package:marco/view/my_app.dart'; import 'package:provider/provider.dart'; -import 'package:url_strategy/url_strategy.dart'; -import 'package:marco/helpers/services/auth_service.dart'; -import 'package:flutter/services.dart'; +import 'package:marco/helpers/theme/app_notifier.dart'; import 'package:logger/logger.dart'; + final Logger logger = Logger(); Future main() async { WidgetsFlutterBinding.ensureInitialized(); - setPathUrlStrategy(); - - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - statusBarColor: const Color.fromARGB(255, 255, 0, 0), - statusBarIconBrightness: Brightness.light, - )); try { - await LocalStorage.init(); - await ThemeCustomizer.init(); - AppStyle.init(); - logger.i("App initialization completed successfully."); + await initializeApp(); + runApp( + ChangeNotifierProvider( + create: (_) => AppNotifier(), + child: const MyApp(), + ), + ); } catch (e, stacktrace) { - logger.e('Error during app initialization:', - error: e, stackTrace: stacktrace); - return; - } - - runApp(ChangeNotifierProvider( - create: (context) => AppNotifier(), - child: const MyApp(), - )); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - Future _getInitialRoute() async { - if (!AuthService.isLoggedIn) { - logger.i("User not logged in. Routing to /auth/login-option"); - return "/auth/login-option"; - } - logger.i("User is logged in."); - - final bool hasMpin = LocalStorage.getIsMpin(); - logger.i("MPIN enabled: $hasMpin"); - - if (hasMpin) { - await LocalStorage.setBool("mpin_verified", false); - logger.i("Routing to /auth/mpin-auth and setting mpin_verified to false"); - return "/auth/mpin-auth"; - } else { - logger.i("MPIN not enabled. Routing to /home"); - return "/home"; - } - } - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (_, notifier, __) { - return FutureBuilder( - future: _getInitialRoute(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const MaterialApp( - home: Center(child: CircularProgressIndicator()), - ); - } - - return GetMaterialApp( - debugShowCheckedModeBanner: false, - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - themeMode: ThemeCustomizer.instance.theme, - navigatorKey: NavigationService.navigatorKey, - initialRoute: snapshot.data!, - getPages: getPageRoute(), - builder: (context, child) { - NavigationService.registerContext(context); - return Directionality( - textDirection: AppTheme.textDirection, - child: child ?? Container(), - ); - }, - localizationsDelegates: [ - AppLocalizationsDelegate(context), - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: Language.getLocales(), - ); - }, - ); - }, + logger.e('App failed to initialize:', error: e, stackTrace: stacktrace); + runApp( + const MaterialApp( + home: Scaffold( + body: Center(child: Text("Failed to initialize the app.")), + ), + ), ); } } diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index b6d3601..ff144e3 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -1,337 +1,232 @@ -import 'package:flutter_lucide/flutter_lucide.dart'; -import 'package:marco/controller/layout/layout_controller.dart'; -import 'package:marco/helpers/theme/admin_theme.dart'; -import 'package:marco/helpers/theme/app_theme.dart'; -import 'package:marco/helpers/theme/theme_customizer.dart'; -import 'package:marco/helpers/widgets/my_button.dart'; -import 'package:marco/helpers/widgets/my_container.dart'; -import 'package:marco/helpers/widgets/my_dashed_divider.dart'; -import 'package:marco/helpers/widgets/my_responsive.dart'; -import 'package:marco/helpers/widgets/my_spacing.dart'; -import 'package:marco/helpers/widgets/my_text.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:marco/view/layouts/left_bar.dart'; -import 'package:marco/view/layouts/right_bar.dart'; -import 'package:marco/view/layouts/top_bar.dart'; -import 'package:marco/widgets/custom_pop_menu.dart'; +import 'package:marco/controller/layout/layout_controller.dart'; +import 'package:marco/helpers/widgets/my_responsive.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/model/employee_info.dart'; -import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/services/api_endpoints.dart'; - -class Layout extends StatelessWidget { +import 'package:marco/images.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/view/layouts/user_profile_right_bar.dart'; +class Layout extends StatefulWidget { final Widget? child; final Widget? floatingActionButton; + const Layout({super.key, this.child, this.floatingActionButton}); + + @override + State createState() => _LayoutState(); +} + +class _LayoutState extends State { final LayoutController controller = LayoutController(); - final topBarTheme = AdminTheme.theme.topBarTheme; - final contentTheme = AdminTheme.theme.contentTheme; final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo(); + final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage"); + final projectController = Get.find(); - Layout({super.key, this.child, this.floatingActionButton}); - - bool get isBetaEnvironment => ApiEndpoints.baseUrl.contains("stage"); @override Widget build(BuildContext context) { - return MyResponsive(builder: (BuildContext context, _, screenMT) { + return MyResponsive(builder: (context, _, screenMT) { return GetBuilder( - init: controller, - builder: (controller) { - if (screenMT.isMobile || screenMT.isTablet) { - return mobileScreen(); - } else { - return largeScreen(); - } - }); + init: controller, + builder: (_) { + return (screenMT.isMobile || screenMT.isTablet) + ? _buildScaffold(context, isMobile: true) + : _buildScaffold(context); + }, + ); }); } - Widget mobileScreen() { + Widget _buildScaffold(BuildContext context, {bool isMobile = false}) { return Scaffold( key: controller.scaffoldKey, - appBar: AppBar( - elevation: 0, - actions: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MySpacing.width(6), - if (isBetaEnvironment) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 17.0, horizontal: 8.0), - child: Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.blueAccent, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - 'BETA', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ), + endDrawer: UserProfileBar(), + floatingActionButton: widget.floatingActionButton, + body: SafeArea( + child: Column( + children: [ + _buildHeader(context, isMobile), + Expanded( + child: SingleChildScrollView( + key: controller.scrollKey, + padding: EdgeInsets.all(isMobile ? 16 : 32), + child: widget.child, + ), + ), + ], + ), + ), + ); + } + + Widget _buildHeader(BuildContext context, bool isMobile) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: Obx(() { + final isExpanded = projectController.isProjectSelectionExpanded.value; + final selectedProjectId = projectController.selectedProjectId?.value; + final selectedProject = projectController.projects.firstWhereOrNull( + (p) => p.id == selectedProjectId, + ); + + if (selectedProject == null && projectController.projects.isNotEmpty) { + projectController + .updateSelectedProject(projectController.projects.first.id); + } + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(isExpanded ? 16 : 0), + ), + boxShadow: [ + BoxShadow( + color: const Color.fromARGB(255, 67, 73, 84), + blurRadius: 4, + offset: Offset(0, 2), + ), ], ), - MySpacing.width(6), - InkWell( - onTap: () { - Get.toNamed('/dashboard'); + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + Images.logoDark, + height: 50, + width: 50, + fit: BoxFit.contain, + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () => + projectController.isProjectSelectionExpanded.toggle(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + child: MyText.bodyLarge( + selectedProject?.name ?? + "Select Project", + fontWeight: 700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Icon( + isExpanded + ? Icons.arrow_drop_up_outlined + : Icons.arrow_drop_down_outlined, + color: Colors.black, + ), + ], + ), + ), + ], + ), + MyText.bodyMedium( + "Hi, ${employeeInfo?.firstName ?? ''}", + color: Colors.black54, + ), + ], + ), + ), + ), + if (isBetaEnvironment) + Container( + margin: const EdgeInsets.only(left: 8), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.deepPurple, + borderRadius: BorderRadius.circular(6), + ), + child: MyText.bodySmall( + 'BETA', + color: Colors.white, + fontWeight: 700, + ), + ), + IconButton( + icon: Icon(Icons.menu), + onPressed: () => + controller.scaffoldKey.currentState?.openEndDrawer(), + ), + ], + ), + const SizedBox(height: 8), + if (isExpanded) _buildProjectList(context, isMobile), + ], + ), + ); + }), + ); + } + + Widget _buildProjectList(BuildContext context, bool isMobile) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Divider(), + MyText.titleSmall("Switch Project", fontWeight: 600), + const SizedBox(height: 4), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + isMobile ? MediaQuery.of(context).size.height * 0.4 : 400, + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: projectController.projects.length, + itemBuilder: (context, index) { + final project = projectController.projects[index]; + final selectedId = projectController.selectedProjectId?.value; + final isSelected = project.id == selectedId; + + return RadioListTile( + value: project.id, + groupValue: selectedId, + onChanged: (value) { + projectController.updateSelectedProject(value!); + projectController.isProjectSelectionExpanded.value = false; + }, + title: Text( + project.name, + style: TextStyle( + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? Colors.blueAccent : Colors.black87, + ), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 0), + activeColor: Colors.blueAccent, + tileColor: isSelected + ? Colors.blueAccent.withOpacity(0.1) + : Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + visualDensity: const VisualDensity(vertical: -4), + ); }, - borderRadius: BorderRadius.circular(6), - splashColor: contentTheme.primary.withAlpha(20), - child: Padding( - padding: MySpacing.xy(8, 8), - child: Icon( - LucideIcons.layout_dashboard, - size: 18, - color: Colors.blueAccent, - ), - ), ), - MySpacing.width(8), - CustomPopupMenu( - backdrop: true, - onChange: (_) {}, - offsetX: -180, - menu: Padding( - padding: MySpacing.xy(8, 8), - child: Center( - child: Icon( - LucideIcons.bell, - size: 18, - ), - ), - ), - menuBuilder: (_) => buildNotifications(), - ), - MySpacing.width(8), - CustomPopupMenu( - backdrop: true, - onChange: (_) {}, - offsetX: -90, - offsetY: 0, - menu: Padding( - padding: MySpacing.xy(0, 8), - child: MyContainer.rounded( - paddingAll: 0, - child: Avatar( - firstName: employeeInfo?.firstName ?? 'First', - lastName: employeeInfo?.lastName ?? 'Name', - ), - ), - ), - menuBuilder: (_) => buildAccountMenu(), - ), - MySpacing.width(20) - ], - ), - drawer: LeftBar(), - floatingActionButton: floatingActionButton, - body: SingleChildScrollView( - key: controller.scrollKey, - child: child, - ), - ); - } - - Widget largeScreen() { - return Scaffold( - key: controller.scaffoldKey, - endDrawer: RightBar(), - floatingActionButton: floatingActionButton, - body: Row( - children: [ - LeftBar(isCondensed: ThemeCustomizer.instance.leftBarCondensed), - Expanded( - child: Stack( - children: [ - Positioned( - top: 0, - right: 0, - left: 0, - bottom: 0, - child: SingleChildScrollView( - padding: - MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing), - key: controller.scrollKey, - child: child, - ), - ), - Positioned(top: 0, left: 0, right: 0, child: TopBar()), - ], - ), - ), - ], - ), - ); - } - - Widget buildNotifications() { - Widget buildNotification(String title, String description) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.labelLarge(title), - MySpacing.height(4), - MyText.bodySmall(description) - ], - ); - } - - return MyContainer.bordered( - paddingAll: 0, - width: 250, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: MySpacing.xy(16, 12), - child: MyText.titleMedium("Notification", fontWeight: 600), - ), - MyDashedDivider( - height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6), - Padding( - padding: MySpacing.xy(16, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - buildNotification("Welcome to Marco", - "Welcome to Marco, we are glad to have you here"), - MySpacing.height(12), - buildNotification("New update available", - "There is a new update available for your app"), - ], - ), - ), - MyDashedDivider( - height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6), - Padding( - padding: MySpacing.xy(16, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyButton.text( - onPressed: () {}, - splashColor: contentTheme.primary.withAlpha(28), - child: MyText.labelSmall( - "View All", - color: contentTheme.primary, - ), - ), - MyButton.text( - onPressed: () {}, - splashColor: contentTheme.danger.withAlpha(28), - child: MyText.labelSmall( - "Clear", - color: contentTheme.danger, - ), - ), - ], - ), - ) - ], - ), - ); - } - - Widget buildAccountMenu() { - return MyContainer.bordered( - paddingAll: 0, - width: 150, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: MySpacing.xy(8, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyButton( - onPressed:null, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - borderRadiusAll: AppStyle.buttonRadius.medium, - padding: MySpacing.xy(8, 4), - splashColor: contentTheme.onBackground.withAlpha(20), - backgroundColor: const Color.fromARGB(0, 220, 218, 218), - child: Row( - children: [ - Icon( - LucideIcons.user, - size: 14, - color: contentTheme.onBackground, - ), - MySpacing.width(8), - MyText.labelMedium( - "My Account", - fontWeight: 600, - ) - ], - ), - ), - MySpacing.height(4), - MyButton( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed:null, - borderRadiusAll: AppStyle.buttonRadius.medium, - padding: MySpacing.xy(8, 4), - splashColor: contentTheme.onBackground.withAlpha(20), - backgroundColor: const Color.fromARGB(0, 220, 218, 218), - child: Row( - children: [ - Icon( - LucideIcons.settings, - size: 14, - color: contentTheme.onBackground, - ), - MySpacing.width(8), - MyText.labelMedium( - "Settings", - fontWeight: 600, - ) - ], - ), - ), - MyButton( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () async { - await LocalStorage.logout(); - }, - borderRadiusAll: AppStyle.buttonRadius.medium, - padding: MySpacing.xy(8, 4), - splashColor: contentTheme.onBackground.withAlpha(20), - backgroundColor: Colors.transparent, - child: Row( - children: [ - Icon( - LucideIcons.log_out, - size: 14, - color: contentTheme.onBackground, - ), - MySpacing.width(8), - MyText.labelMedium( - "Logout", - fontWeight: 600, - ) - ], - ), - ), - ], - ), - ), - Divider( - height: 1, - thickness: 1, - ), - ], - ), + ), + ], ); } } diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart new file mode 100644 index 0000000..60130ce --- /dev/null +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -0,0 +1,311 @@ +import 'package:flutter/material.dart'; +import 'package:marco/helpers/theme/theme_customizer.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_card.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +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'; + +class UserProfileBar extends StatefulWidget { + final bool isCondensed; + + const UserProfileBar({super.key, this.isCondensed = false}); + + @override + _UserProfileBarState createState() => _UserProfileBarState(); +} + +class _UserProfileBarState extends State + with SingleTickerProviderStateMixin, UIMixin { + final ThemeCustomizer customizer = ThemeCustomizer.instance; + bool isCondensed = false; + EmployeeInfo? employeeInfo; + + @override + void initState() { + super.initState(); + _loadEmployeeInfo(); + } + + void _loadEmployeeInfo() { + setState(() { + employeeInfo = LocalStorage.getEmployeeInfo(); + }); + } + + @override + Widget build(BuildContext context) { + isCondensed = widget.isCondensed; + + return MyCard( + borderRadiusAll: 16, + paddingAll: 0, + shadow: MyShadow(position: MyShadowPosition.centerRight, elevation: 4), + child: AnimatedContainer( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + leftBarTheme.background.withOpacity(0.95), + leftBarTheme.background.withOpacity(0.85), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + width: isCondensed ? 90 : 250, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: Column( + children: [ + userProfileSection(), + MySpacing.height(8), + supportAndSettingsMenu(), + const Spacer(), + logoutButton(), + ], + ), + ), + ); + } + + Widget userProfileSection() { + if (employeeInfo == null) { + return const Padding( + padding: EdgeInsets.all(24.0), + child: Center(child: CircularProgressIndicator()), + ); + } + + return Container( + width: double.infinity, + padding: MySpacing.xy(58, 68), + decoration: BoxDecoration( + color: leftBarTheme.activeItemBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Column( + children: [ + Avatar( + firstName: employeeInfo?.firstName ?? 'First', + lastName: employeeInfo?.lastName ?? 'Name', + size: 60, + ), + MySpacing.height(12), + MyText.labelLarge( + "${employeeInfo?.firstName ?? 'First'} ${employeeInfo?.lastName ?? 'Last'}", + fontWeight: 700, + color: leftBarTheme.activeItemColor, + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + 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"), + ], + ), + ); + } + + Widget menuItem({required IconData icon, required String label}) { + 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), + child: Row( + children: [ + Icon(icon, size: 20, color: leftBarTheme.onBackground), + MySpacing.width(12), + Expanded( + child: MyText.bodyMedium( + label, + color: leftBarTheme.onBackground, + fontWeight: 500, + ), + ), + ], + ), + ), + ); + } + + 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: () => 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"), + ), + ), + ], + ) + ], + ), + ), + ); + }, + ); + + 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), + ), + ); + } + } + }, + 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, + padding: MySpacing.all(16), + decoration: BoxDecoration( + color: leftBarTheme.activeItemBackground, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MyText.bodyMedium( + "Logout", + color: leftBarTheme.activeItemColor, + fontWeight: 600, + ), + MySpacing.width(8), + Icon( + LucideIcons.log_out, + size: 20, + color: leftBarTheme.activeItemColor, + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/my_app.dart b/lib/view/my_app.dart new file mode 100644 index 0000000..dfaa27e --- /dev/null +++ b/lib/view/my_app.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:logger/logger.dart'; + +import 'package:marco/helpers/extensions/app_localization_delegate.dart'; +import 'package:marco/helpers/services/auth_service.dart'; +import 'package:marco/helpers/services/localizations/language.dart'; +import 'package:marco/helpers/services/navigation_services.dart'; +import 'package:marco/helpers/services/storage/local_storage.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/theme/theme_customizer.dart'; +import 'package:marco/helpers/theme/app_notifier.dart'; +import 'package:marco/routes.dart'; + +final Logger logger = Logger(); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + Future _getInitialRoute() async { + try { + if (!AuthService.isLoggedIn) { + logger.i("User not logged in. Routing to /auth/login-option"); + return "/auth/login-option"; + } + + final bool hasMpin = LocalStorage.getIsMpin(); + logger.i("MPIN enabled: $hasMpin"); + + if (hasMpin) { + await LocalStorage.setBool("mpin_verified", false); + logger + .i("Routing to /auth/mpin-auth and setting mpin_verified to false"); + return "/auth/mpin-auth"; + } else { + logger.i("MPIN not enabled. Routing to /home"); + return "/home"; + } + } catch (e, stacktrace) { + logger.e("Error determining initial route", + error: e, stackTrace: stacktrace); + return "/auth/login-option"; + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (_, notifier, __) { + return FutureBuilder( + future: _getInitialRoute(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return const MaterialApp( + home: Center(child: Text("Error determining route")), + ); + } + + if (!snapshot.hasData) { + return const MaterialApp( + home: Center(child: CircularProgressIndicator()), + ); + } + + return GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ThemeCustomizer.instance.theme, + navigatorKey: NavigationService.navigatorKey, + initialRoute: snapshot.data!, + getPages: getPageRoute(), + builder: (context, child) { + NavigationService.registerContext(context); + return Directionality( + textDirection: AppTheme.textDirection, + child: child ?? const SizedBox(), + ); + }, + localizationsDelegates: [ + AppLocalizationsDelegate(context), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: Language.getLocales(), + ); + }, + ); + }, + ); + } +}