feat: Refactor DynamicMenuController to remove caching and auto-refresh; update error handling in DashboardScreen and UserProfileBar
This commit is contained in:
parent
be908a5251
commit
61acbb019b
@ -3,7 +3,6 @@ import 'package:get/get.dart';
|
||||
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
|
||||
class DynamicMenuController extends GetxController {
|
||||
// UI reactive states
|
||||
@ -12,30 +11,14 @@ class DynamicMenuController extends GetxController {
|
||||
final RxString errorMessage = ''.obs;
|
||||
final RxList<MenuItem> menuItems = <MenuItem>[].obs;
|
||||
|
||||
Timer? _autoRefreshTimer;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// ✅ Load cached menus immediately (so user doesn’t 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
|
||||
// Fetch menus directly from API at startup
|
||||
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 {
|
||||
isLoading.value = true;
|
||||
hasError.value = false;
|
||||
@ -44,13 +27,9 @@ class DynamicMenuController extends GetxController {
|
||||
try {
|
||||
final responseData = await ApiService.getMenuApi();
|
||||
if (responseData != null) {
|
||||
// Parse JSON into MenuResponse
|
||||
final menuResponse = MenuResponse.fromJson(responseData);
|
||||
menuItems.assignAll(menuResponse.data);
|
||||
|
||||
// Save for offline use
|
||||
await LocalStorage.setMenus(menuItems);
|
||||
|
||||
logSafe("✅ Menu loaded from API with ${menuItems.length} items");
|
||||
} else {
|
||||
_handleApiFailure("Menu API returned null response");
|
||||
@ -65,26 +44,19 @@ class DynamicMenuController extends GetxController {
|
||||
void _handleApiFailure(String logMessage) {
|
||||
logSafe(logMessage, level: LogLevel.error);
|
||||
|
||||
final cachedMenus = LocalStorage.getMenus();
|
||||
if (cachedMenus.isNotEmpty) {
|
||||
menuItems.assignAll(cachedMenus);
|
||||
errorMessage.value = "⚠️ Using offline menus (latest sync failed)";
|
||||
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();
|
||||
}
|
||||
// No cache available, show error state
|
||||
hasError.value = true;
|
||||
errorMessage.value = "❌ Unable to load menus. Please try again later.";
|
||||
menuItems.clear();
|
||||
}
|
||||
|
||||
bool isMenuAllowed(String menuName) {
|
||||
final menu = menuItems.firstWhereOrNull((m) => m.name == menuName);
|
||||
return menu?.available ?? false; // default false if not found
|
||||
return menu?.available ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_autoRefreshTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
@ -40,8 +40,7 @@ class DashboardScreen extends StatefulWidget {
|
||||
class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
final DashboardController dashboardController =
|
||||
Get.put(DashboardController(), permanent: true);
|
||||
final DynamicMenuController menuController =
|
||||
Get.put(DynamicMenuController(), permanent: true);
|
||||
final DynamicMenuController menuController = Get.put(DynamicMenuController());
|
||||
|
||||
bool hasMpin = true;
|
||||
|
||||
@ -243,7 +242,8 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
||||
if (menuController.isLoading.value) {
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
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)
|
||||
Widget _buildStatCard(_StatItem statItem, bool isProjectSelected) {
|
||||
const double cardWidth = 80;
|
||||
const double cardHeight = 70;
|
||||
/// Stat Card (Compact with wrapping text)
|
||||
Widget _buildStatCard(_StatItem statItem, bool isProjectSelected) {
|
||||
const double cardWidth = 80;
|
||||
const double cardHeight = 70;
|
||||
|
||||
// ✅ Attendance should always be enabled
|
||||
final bool isEnabled = statItem.title == "Attendance" || isProjectSelected;
|
||||
// ✅ Attendance should always be enabled
|
||||
final bool isEnabled = statItem.title == "Attendance" || isProjectSelected;
|
||||
|
||||
return Opacity(
|
||||
opacity: isEnabled ? 1.0 : 0.4,
|
||||
child: IgnorePointer(
|
||||
ignoring: !isEnabled,
|
||||
child: InkWell(
|
||||
onTap: () => _handleStatCardTap(statItem, isEnabled),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: MyCard.bordered(
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
paddingAll: 4,
|
||||
borderRadiusAll: 8,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildStatCardIconCompact(statItem),
|
||||
MySpacing.height(4),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
statItem.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
overflow: TextOverflow.visible,
|
||||
return Opacity(
|
||||
opacity: isEnabled ? 1.0 : 0.4,
|
||||
child: IgnorePointer(
|
||||
ignoring: !isEnabled,
|
||||
child: InkWell(
|
||||
onTap: () => _handleStatCardTap(statItem, isEnabled),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: MyCard.bordered(
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
paddingAll: 4,
|
||||
borderRadiusAll: 8,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildStatCardIconCompact(statItem),
|
||||
MySpacing.height(4),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
statItem.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
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
|
||||
Widget _buildStatCardIconCompact(_StatItem statItem) {
|
||||
@ -378,7 +378,6 @@ void _handleStatCardTap(_StatItem statItem, bool isEnabled) {
|
||||
}
|
||||
|
||||
/// Handle Tap
|
||||
|
||||
}
|
||||
|
||||
class _StatItem {
|
||||
|
@ -10,7 +10,6 @@ import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/model/employees/employee_info.dart';
|
||||
import 'package:marco/controller/auth/mpin_controller.dart';
|
||||
import 'package:marco/view/employees/employee_profile_screen.dart';
|
||||
import 'package:marco/view/document/user_document_screen.dart';
|
||||
|
||||
class UserProfileBar extends StatefulWidget {
|
||||
final bool isCondensed;
|
||||
@ -178,12 +177,6 @@ class _UserProfileBarState extends State<UserProfileBar>
|
||||
onTap: _onProfileTap,
|
||||
),
|
||||
SizedBox(height: spacingHeight),
|
||||
_menuItemRow(
|
||||
icon: LucideIcons.file_text,
|
||||
label: 'My Documents',
|
||||
onTap: _onDocumentsTap,
|
||||
),
|
||||
SizedBox(height: spacingHeight),
|
||||
_menuItemRow(
|
||||
icon: LucideIcons.settings,
|
||||
label: 'Settings',
|
||||
@ -250,13 +243,6 @@ class _UserProfileBarState extends State<UserProfileBar>
|
||||
));
|
||||
}
|
||||
|
||||
void _onDocumentsTap() {
|
||||
Get.to(() => UserDocumentsPage(
|
||||
entityId: "${employeeInfo.id}",
|
||||
isEmployee: true,
|
||||
));
|
||||
}
|
||||
|
||||
void _onMpinTap() {
|
||||
final controller = Get.put(MPINController());
|
||||
if (hasMpin) controller.setChangeMpinMode();
|
||||
|
Loading…
x
Reference in New Issue
Block a user