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.
This commit is contained in:
parent
52afa7735e
commit
6a36064af7
54
lib/controller/dashboard/dashboard_controller.dart
Normal file
54
lib/controller/dashboard/dashboard_controller.dart
Normal file
@ -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<ProjectModel> projects = <ProjectModel>[].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<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches projects and initializes selected project.
|
||||||
|
Future<void> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,23 +1,87 @@
|
|||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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 {
|
class LayoutController extends GetxController {
|
||||||
|
// Theme Customization
|
||||||
ThemeCustomizer themeCustomizer = ThemeCustomizer();
|
ThemeCustomizer themeCustomizer = ThemeCustomizer();
|
||||||
|
|
||||||
|
// Global Keys
|
||||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||||
final GlobalKey<State<StatefulWidget>> scrollKey = GlobalKey();
|
final GlobalKey<State<StatefulWidget>> 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<ProjectModel> projects = <ProjectModel>[].obs;
|
||||||
|
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
|
// Selected Project
|
||||||
|
RxString? selectedProjectId;
|
||||||
|
|
||||||
bool isLastIndex = false;
|
bool isLastIndex = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchProjects();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() {
|
||||||
super.onReady();
|
super.onReady();
|
||||||
ThemeCustomizer.addListener(onChangeTheme);
|
ThemeCustomizer.addListener(onChangeTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
ThemeCustomizer.removeListener(onChangeTheme);
|
||||||
|
scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project Handling
|
||||||
|
Future<void> 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) {
|
void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) {
|
||||||
themeCustomizer = newVal;
|
themeCustomizer = newVal;
|
||||||
update();
|
update();
|
||||||
@ -29,18 +93,12 @@ class LayoutController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enableNotificationShade() {
|
// Notification Shade (placeholders)
|
||||||
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
void enableNotificationShade() {
|
||||||
|
// Add implementation if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
disableNotificationShade() {
|
void disableNotificationShade() {
|
||||||
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
|
// Add implementation if needed
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
ThemeCustomizer.removeListener(onChangeTheme);
|
|
||||||
scrollController.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
lib/controller/project_controller.dart
Normal file
60
lib/controller/project_controller.dart
Normal file
@ -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<ProjectModel> projects = <ProjectModel>[].obs;
|
||||||
|
RxString? selectedProjectId;
|
||||||
|
RxBool isProjectListExpanded = false.obs;
|
||||||
|
RxBool isProjectSelectionExpanded = false.obs;
|
||||||
|
|
||||||
|
RxBool isProjectDropdownExpanded = false.obs;
|
||||||
|
RxBool isLoading = true.obs;
|
||||||
|
RxBool isLoadingProjects = true.obs;
|
||||||
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches projects and initializes selected project.
|
||||||
|
Future<void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/helpers/services/app_initializer.dart
Normal file
28
lib/helpers/services/app_initializer.dart
Normal file
@ -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<void> 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.");
|
||||||
|
}
|
||||||
@ -367,7 +367,10 @@ class AuthService {
|
|||||||
await LocalStorage.removeMpinToken();
|
await LocalStorage.removeMpinToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
Get.put(PermissionController());
|
// ✅ Put and load PermissionController
|
||||||
|
final permissionController = Get.put(PermissionController());
|
||||||
|
await permissionController.loadData(jwtToken);
|
||||||
|
|
||||||
isLoggedIn = true;
|
isLoggedIn = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -180,4 +180,13 @@ class LocalStorage {
|
|||||||
static bool? getBool(String key) {
|
static bool? getBool(String key) {
|
||||||
return preferences.getBool(key);
|
return preferences.getBool(key);
|
||||||
}
|
}
|
||||||
|
// Save and retrieve String values
|
||||||
|
static String? getString(String key) {
|
||||||
|
return preferences.getString(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> saveString(String key, String value) async {
|
||||||
|
return preferences.setString(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
114
lib/main.dart
114
lib/main.dart
@ -1,109 +1,31 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:marco/helpers/services/app_initializer.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:marco/view/my_app.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:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_strategy/url_strategy.dart';
|
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
final Logger logger = Logger();
|
final Logger logger = Logger();
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
setPathUrlStrategy();
|
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
|
||||||
statusBarColor: const Color.fromARGB(255, 255, 0, 0),
|
|
||||||
statusBarIconBrightness: Brightness.light,
|
|
||||||
));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await LocalStorage.init();
|
await initializeApp();
|
||||||
await ThemeCustomizer.init();
|
runApp(
|
||||||
AppStyle.init();
|
ChangeNotifierProvider<AppNotifier>(
|
||||||
logger.i("App initialization completed successfully.");
|
create: (_) => AppNotifier(),
|
||||||
|
child: const MyApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logger.e('Error during app initialization:',
|
logger.e('App failed to initialize:', error: e, stackTrace: stacktrace);
|
||||||
error: e, stackTrace: stacktrace);
|
runApp(
|
||||||
return;
|
const MaterialApp(
|
||||||
}
|
home: Scaffold(
|
||||||
|
body: Center(child: Text("Failed to initialize the app.")),
|
||||||
runApp(ChangeNotifierProvider<AppNotifier>(
|
),
|
||||||
create: (context) => AppNotifier(),
|
),
|
||||||
child: const MyApp(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
|
||||||
const MyApp({super.key});
|
|
||||||
|
|
||||||
Future<String> _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<AppNotifier>(
|
|
||||||
builder: (_, notifier, __) {
|
|
||||||
return FutureBuilder<String>(
|
|
||||||
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(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/view/layouts/left_bar.dart';
|
import 'package:marco/controller/layout/layout_controller.dart';
|
||||||
import 'package:marco/view/layouts/right_bar.dart';
|
import 'package:marco/helpers/widgets/my_responsive.dart';
|
||||||
import 'package:marco/view/layouts/top_bar.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/widgets/custom_pop_menu.dart';
|
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/model/employee_info.dart';
|
import 'package:marco/model/employee_info.dart';
|
||||||
import 'package:marco/helpers/widgets/avatar.dart';
|
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||||
|
import 'package:marco/images.dart';
|
||||||
class Layout extends StatelessWidget {
|
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? child;
|
||||||
final Widget? floatingActionButton;
|
final Widget? floatingActionButton;
|
||||||
|
|
||||||
|
const Layout({super.key, this.child, this.floatingActionButton});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<Layout> createState() => _LayoutState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LayoutState extends State<Layout> {
|
||||||
final LayoutController controller = LayoutController();
|
final LayoutController controller = LayoutController();
|
||||||
final topBarTheme = AdminTheme.theme.topBarTheme;
|
|
||||||
final contentTheme = AdminTheme.theme.contentTheme;
|
|
||||||
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo();
|
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo();
|
||||||
|
final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
|
||||||
|
final projectController = Get.find<ProjectController>();
|
||||||
|
|
||||||
Layout({super.key, this.child, this.floatingActionButton});
|
|
||||||
|
|
||||||
bool get isBetaEnvironment => ApiEndpoints.baseUrl.contains("stage");
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MyResponsive(builder: (BuildContext context, _, screenMT) {
|
return MyResponsive(builder: (context, _, screenMT) {
|
||||||
return GetBuilder(
|
return GetBuilder(
|
||||||
init: controller,
|
init: controller,
|
||||||
builder: (controller) {
|
builder: (_) {
|
||||||
if (screenMT.isMobile || screenMT.isTablet) {
|
return (screenMT.isMobile || screenMT.isTablet)
|
||||||
return mobileScreen();
|
? _buildScaffold(context, isMobile: true)
|
||||||
} else {
|
: _buildScaffold(context);
|
||||||
return largeScreen();
|
},
|
||||||
}
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget mobileScreen() {
|
Widget _buildScaffold(BuildContext context, {bool isMobile = false}) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: controller.scaffoldKey,
|
key: controller.scaffoldKey,
|
||||||
appBar: AppBar(
|
endDrawer: UserProfileBar(),
|
||||||
elevation: 0,
|
floatingActionButton: widget.floatingActionButton,
|
||||||
actions: [
|
body: SafeArea(
|
||||||
Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
_buildHeader(context, isMobile),
|
||||||
MySpacing.width(6),
|
Expanded(
|
||||||
if (isBetaEnvironment)
|
child: SingleChildScrollView(
|
||||||
Padding(
|
key: controller.scrollKey,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: EdgeInsets.all(isMobile ? 16 : 32),
|
||||||
vertical: 17.0, horizontal: 8.0),
|
child: widget.child,
|
||||||
child: Container(
|
),
|
||||||
padding:
|
),
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
],
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: Colors.blueAccent,
|
),
|
||||||
borderRadius: BorderRadius.circular(4),
|
);
|
||||||
),
|
}
|
||||||
child: Text(
|
|
||||||
'BETA',
|
Widget _buildHeader(BuildContext context, bool isMobile) {
|
||||||
style: TextStyle(
|
return Padding(
|
||||||
color: Colors.white,
|
padding: const EdgeInsets.symmetric(horizontal: 0),
|
||||||
fontWeight: FontWeight.bold,
|
child: Obx(() {
|
||||||
fontSize: 12,
|
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),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
InkWell(
|
child: Column(
|
||||||
onTap: () {
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Get.toNamed('/dashboard');
|
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<String>(
|
||||||
|
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
311
lib/view/layouts/user_profile_right_bar.dart
Normal file
311
lib/view/layouts/user_profile_right_bar.dart
Normal file
@ -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<UserProfileBar>
|
||||||
|
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<bool>(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
lib/view/my_app.dart
Normal file
95
lib/view/my_app.dart
Normal file
@ -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<String> _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<AppNotifier>(
|
||||||
|
builder: (_, notifier, __) {
|
||||||
|
return FutureBuilder<String>(
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user