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:
Vaibhav Surve 2025-06-11 17:11:50 +05:30
parent 52afa7735e
commit 6a36064af7
10 changed files with 856 additions and 421 deletions

View 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;
}
}

View File

@ -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<ScaffoldState> scaffoldKey = 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;
@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<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) {
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
}
}

View 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);
}
}

View 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.");
}

View File

@ -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;
}
}

View File

@ -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<bool> saveString(String key, String value) async {
return preferences.setString(key, value);
}
}

View File

@ -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<void> 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<AppNotifier>(
create: (_) => AppNotifier(),
child: const MyApp(),
),
);
} catch (e, stacktrace) {
logger.e('Error during app initialization:',
error: e, stackTrace: stacktrace);
return;
}
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(),
);
},
);
},
logger.e('App failed to initialize:', error: e, stackTrace: stacktrace);
runApp(
const MaterialApp(
home: Scaffold(
body: Center(child: Text("Failed to initialize the app.")),
),
),
);
}
}

View File

@ -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<Layout> createState() => _LayoutState();
}
class _LayoutState extends State<Layout> {
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<ProjectController>();
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<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,
),
],
),
),
],
);
}
}

View 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("Youve 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
View 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(),
);
},
);
},
);
}
}