feat: Refactor DynamicMenuController to remove caching and auto-refresh; update error handling in DashboardScreen and UserProfileBar

This commit is contained in:
Vaibhav Surve 2025-09-12 12:22:12 +05:30
parent be908a5251
commit 61acbb019b
3 changed files with 63 additions and 106 deletions

View File

@ -3,7 +3,6 @@ import 'package:get/get.dart';
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart'; import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
class DynamicMenuController extends GetxController { class DynamicMenuController extends GetxController {
// UI reactive states // UI reactive states
@ -12,30 +11,14 @@ class DynamicMenuController extends GetxController {
final RxString errorMessage = ''.obs; final RxString errorMessage = ''.obs;
final RxList<MenuItem> menuItems = <MenuItem>[].obs; final RxList<MenuItem> menuItems = <MenuItem>[].obs;
Timer? _autoRefreshTimer;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// Fetch menus directly from API at startup
// Load cached menus immediately (so user doesnt see empty state)
final cachedMenus = LocalStorage.getMenus();
if (cachedMenus.isNotEmpty) {
menuItems.assignAll(cachedMenus);
logSafe("Loaded ${cachedMenus.length} menus from cache at startup");
}
// Fetch from API in background
fetchMenu(); fetchMenu();
// Auto refresh every 15 minutes
_autoRefreshTimer = Timer.periodic(
const Duration(minutes: 15),
(_) => fetchMenu(),
);
} }
/// Fetch dynamic menu from API with cache fallback /// Fetch dynamic menu from API (no local cache)
Future<void> fetchMenu() async { Future<void> fetchMenu() async {
isLoading.value = true; isLoading.value = true;
hasError.value = false; hasError.value = false;
@ -44,13 +27,9 @@ class DynamicMenuController extends GetxController {
try { try {
final responseData = await ApiService.getMenuApi(); final responseData = await ApiService.getMenuApi();
if (responseData != null) { if (responseData != null) {
// Parse JSON into MenuResponse
final menuResponse = MenuResponse.fromJson(responseData); final menuResponse = MenuResponse.fromJson(responseData);
menuItems.assignAll(menuResponse.data); menuItems.assignAll(menuResponse.data);
// Save for offline use
await LocalStorage.setMenus(menuItems);
logSafe("✅ Menu loaded from API with ${menuItems.length} items"); logSafe("✅ Menu loaded from API with ${menuItems.length} items");
} else { } else {
_handleApiFailure("Menu API returned null response"); _handleApiFailure("Menu API returned null response");
@ -65,26 +44,19 @@ class DynamicMenuController extends GetxController {
void _handleApiFailure(String logMessage) { void _handleApiFailure(String logMessage) {
logSafe(logMessage, level: LogLevel.error); logSafe(logMessage, level: LogLevel.error);
final cachedMenus = LocalStorage.getMenus(); // No cache available, show error state
if (cachedMenus.isNotEmpty) { hasError.value = true;
menuItems.assignAll(cachedMenus); errorMessage.value = "❌ Unable to load menus. Please try again later.";
errorMessage.value = "⚠️ Using offline menus (latest sync failed)"; menuItems.clear();
logSafe("Loaded ${menuItems.length} menus from cache after failure");
} else {
hasError.value = true;
errorMessage.value = "❌ Unable to load menus. Please try again later.";
menuItems.clear();
}
} }
bool isMenuAllowed(String menuName) { bool isMenuAllowed(String menuName) {
final menu = menuItems.firstWhereOrNull((m) => m.name == menuName); final menu = menuItems.firstWhereOrNull((m) => m.name == menuName);
return menu?.available ?? false; // default false if not found return menu?.available ?? false;
} }
@override @override
void onClose() { void onClose() {
_autoRefreshTimer?.cancel();
super.onClose(); super.onClose();
} }
} }

View File

@ -40,8 +40,7 @@ class DashboardScreen extends StatefulWidget {
class _DashboardScreenState extends State<DashboardScreen> with UIMixin { class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
final DashboardController dashboardController = final DashboardController dashboardController =
Get.put(DashboardController(), permanent: true); Get.put(DashboardController(), permanent: true);
final DynamicMenuController menuController = final DynamicMenuController menuController = Get.put(DynamicMenuController());
Get.put(DynamicMenuController(), permanent: true);
bool hasMpin = true; bool hasMpin = true;
@ -243,7 +242,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
if (menuController.isLoading.value) { if (menuController.isLoading.value) {
return _buildLoadingSkeleton(context); return _buildLoadingSkeleton(context);
} }
if (menuController.hasError.value) { if (menuController.hasError.value && menuController.menuItems.isEmpty) {
// Only show error if there are no menus at all
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Center( child: Center(
@ -299,70 +299,70 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
} }
/// Stat Card (Compact with wrapping text) /// Stat Card (Compact with wrapping text)
/// Stat Card (Compact with wrapping text) /// Stat Card (Compact with wrapping text)
Widget _buildStatCard(_StatItem statItem, bool isProjectSelected) { Widget _buildStatCard(_StatItem statItem, bool isProjectSelected) {
const double cardWidth = 80; const double cardWidth = 80;
const double cardHeight = 70; const double cardHeight = 70;
// Attendance should always be enabled // Attendance should always be enabled
final bool isEnabled = statItem.title == "Attendance" || isProjectSelected; final bool isEnabled = statItem.title == "Attendance" || isProjectSelected;
return Opacity( return Opacity(
opacity: isEnabled ? 1.0 : 0.4, opacity: isEnabled ? 1.0 : 0.4,
child: IgnorePointer( child: IgnorePointer(
ignoring: !isEnabled, ignoring: !isEnabled,
child: InkWell( child: InkWell(
onTap: () => _handleStatCardTap(statItem, isEnabled), onTap: () => _handleStatCardTap(statItem, isEnabled),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: MyCard.bordered( child: MyCard.bordered(
width: cardWidth, width: cardWidth,
height: cardHeight, height: cardHeight,
paddingAll: 4, paddingAll: 4,
borderRadiusAll: 8, borderRadiusAll: 8,
border: Border.all(color: Colors.grey.withOpacity(0.15)), border: Border.all(color: Colors.grey.withOpacity(0.15)),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
_buildStatCardIconCompact(statItem), _buildStatCardIconCompact(statItem),
MySpacing.height(4), MySpacing.height(4),
Expanded( Expanded(
child: Center( child: Center(
child: Text( child: Text(
statItem.title, statItem.title,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 10, fontSize: 10,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
),
maxLines: 2,
softWrap: true,
), ),
maxLines: 2,
softWrap: true,
), ),
), ),
), ],
], ),
), ),
), ),
), ),
),
);
}
/// Handle Tap
void _handleStatCardTap(_StatItem statItem, bool isEnabled) {
if (!isEnabled) {
Get.defaultDialog(
title: "No Project Selected",
middleText: "You need to select a project before accessing this section.",
confirm: ElevatedButton(
onPressed: () => Get.back(),
child: const Text("OK"),
),
); );
} else {
Get.toNamed(statItem.route);
} }
}
/// Handle Tap
void _handleStatCardTap(_StatItem statItem, bool isEnabled) {
if (!isEnabled) {
Get.defaultDialog(
title: "No Project Selected",
middleText:
"You need to select a project before accessing this section.",
confirm: ElevatedButton(
onPressed: () => Get.back(),
child: const Text("OK"),
),
);
} else {
Get.toNamed(statItem.route);
}
}
/// Compact Icon /// Compact Icon
Widget _buildStatCardIconCompact(_StatItem statItem) { Widget _buildStatCardIconCompact(_StatItem statItem) {
@ -378,7 +378,6 @@ void _handleStatCardTap(_StatItem statItem, bool isEnabled) {
} }
/// Handle Tap /// Handle Tap
} }
class _StatItem { class _StatItem {

View File

@ -10,7 +10,6 @@ import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/model/employees/employee_info.dart'; import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/controller/auth/mpin_controller.dart'; import 'package:marco/controller/auth/mpin_controller.dart';
import 'package:marco/view/employees/employee_profile_screen.dart'; import 'package:marco/view/employees/employee_profile_screen.dart';
import 'package:marco/view/document/user_document_screen.dart';
class UserProfileBar extends StatefulWidget { class UserProfileBar extends StatefulWidget {
final bool isCondensed; final bool isCondensed;
@ -178,12 +177,6 @@ class _UserProfileBarState extends State<UserProfileBar>
onTap: _onProfileTap, onTap: _onProfileTap,
), ),
SizedBox(height: spacingHeight), SizedBox(height: spacingHeight),
_menuItemRow(
icon: LucideIcons.file_text,
label: 'My Documents',
onTap: _onDocumentsTap,
),
SizedBox(height: spacingHeight),
_menuItemRow( _menuItemRow(
icon: LucideIcons.settings, icon: LucideIcons.settings,
label: 'Settings', label: 'Settings',
@ -250,13 +243,6 @@ class _UserProfileBarState extends State<UserProfileBar>
)); ));
} }
void _onDocumentsTap() {
Get.to(() => UserDocumentsPage(
entityId: "${employeeInfo.id}",
isEmployee: true,
));
}
void _onMpinTap() { void _onMpinTap() {
final controller = Get.put(MPINController()); final controller = Get.put(MPINController());
if (hasMpin) controller.setChangeMpinMode(); if (hasMpin) controller.setChangeMpinMode();