feat: Re-enable Firebase Messaging and FCM token handling; improve local storage initialization and logging #69
@ -79,8 +79,7 @@ class LoginController extends MyController {
|
|||||||
enableRemoteLogging();
|
enableRemoteLogging();
|
||||||
logSafe("✅ Remote logging enabled after login.");
|
logSafe("✅ Remote logging enabled after login.");
|
||||||
|
|
||||||
// ✅ Commented out FCM token registration after login
|
|
||||||
/*
|
|
||||||
final fcmToken = await LocalStorage.getFcmToken();
|
final fcmToken = await LocalStorage.getFcmToken();
|
||||||
if (fcmToken?.isNotEmpty ?? false) {
|
if (fcmToken?.isNotEmpty ?? false) {
|
||||||
final success = await AuthService.registerDeviceToken(fcmToken!);
|
final success = await AuthService.registerDeviceToken(fcmToken!);
|
||||||
@ -90,7 +89,7 @@ class LoginController extends MyController {
|
|||||||
: "⚠️ Failed to register FCM token after login.",
|
: "⚠️ Failed to register FCM token after login.",
|
||||||
level: LogLevel.warning);
|
level: LogLevel.warning);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
logSafe("Login successful for user: ${loginData['username']}");
|
logSafe("Login successful for user: ${loginData['username']}");
|
||||||
Get.toNamed('/home');
|
Get.toNamed('/home');
|
||||||
|
@ -6,7 +6,7 @@ import 'package:marco/helpers/widgets/my_form_validator.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
// import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; // 🔴 Commented out
|
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
|
||||||
|
|
||||||
class MPINController extends GetxController {
|
class MPINController extends GetxController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
@ -256,14 +256,12 @@ class MPINController extends GetxController {
|
|||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// ✅ Fetch FCM Token here (DISABLED)
|
final fcmToken = await FirebaseNotificationService().getFcmToken();
|
||||||
// final fcmToken = await FirebaseNotificationService().getFcmToken();
|
|
||||||
|
|
||||||
final response = await AuthService.verifyMpin(
|
final response = await AuthService.verifyMpin(
|
||||||
mpin: enteredMPIN,
|
mpin: enteredMPIN,
|
||||||
mpinToken: mpinToken,
|
mpinToken: mpinToken,
|
||||||
// fcmToken: fcmToken ?? '', // 🔴 Commented out
|
fcmToken: fcmToken ?? '',
|
||||||
fcmToken: '', // ✅ Passing empty string instead
|
|
||||||
);
|
);
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:url_strategy/url_strategy.dart';
|
import 'package:url_strategy/url_strategy.dart';
|
||||||
// import 'package:firebase_core/firebase_core.dart'; // ❌ Commented out Firebase
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
// import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; // ❌ Commented out FCM
|
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
|
||||||
import 'package:marco/helpers/services/device_info_service.dart';
|
import 'package:marco/helpers/services/device_info_service.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
@ -20,7 +20,7 @@ Future<void> initializeApp() async {
|
|||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
_setupUI(),
|
_setupUI(),
|
||||||
// _setupFirebase(), // ❌ Commented out Firebase init
|
_setupFirebase(),
|
||||||
_setupLocalStorage(),
|
_setupLocalStorage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ Future<void> initializeApp() async {
|
|||||||
await _handleAuthTokens();
|
await _handleAuthTokens();
|
||||||
await _setupTheme();
|
await _setupTheme();
|
||||||
await _setupControllers();
|
await _setupControllers();
|
||||||
// await _setupFirebaseMessaging(); // ❌ Commented out FCM init
|
await _setupFirebaseMessaging();
|
||||||
|
|
||||||
_finalizeAppStyle();
|
_finalizeAppStyle();
|
||||||
|
|
||||||
@ -56,17 +56,19 @@ Future<void> _setupUI() async {
|
|||||||
logSafe("💡 UI setup completed.");
|
logSafe("💡 UI setup completed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ❌ Commented out Firebase setup
|
|
||||||
/*
|
|
||||||
Future<void> _setupFirebase() async {
|
Future<void> _setupFirebase() async {
|
||||||
await Firebase.initializeApp();
|
await Firebase.initializeApp();
|
||||||
logSafe("💡 Firebase initialized.");
|
logSafe("💡 Firebase initialized.");
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
Future<void> _setupLocalStorage() async {
|
Future<void> _setupLocalStorage() async {
|
||||||
await LocalStorage.init();
|
if (!LocalStorage.isInitialized) {
|
||||||
logSafe("💡 Local storage initialized.");
|
await LocalStorage.init();
|
||||||
|
logSafe("💡 Local storage initialized.");
|
||||||
|
} else {
|
||||||
|
logSafe("ℹ️ Local storage already initialized, skipping.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setupDeviceInfo() async {
|
Future<void> _setupDeviceInfo() async {
|
||||||
@ -118,12 +120,12 @@ Future<void> _setupControllers() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ❌ Commented out Firebase Messaging setup
|
// ❌ Commented out Firebase Messaging setup
|
||||||
/*
|
|
||||||
Future<void> _setupFirebaseMessaging() async {
|
Future<void> _setupFirebaseMessaging() async {
|
||||||
await FirebaseNotificationService().initialize();
|
await FirebaseNotificationService().initialize();
|
||||||
logSafe("💡 Firebase Messaging initialized.");
|
logSafe("💡 Firebase Messaging initialized.");
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
void _finalizeAppStyle() {
|
void _finalizeAppStyle() {
|
||||||
AppStyle.init();
|
AppStyle.init();
|
||||||
|
@ -54,7 +54,20 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final body = {"fcmToken": fcmToken};
|
final body = {"fcmToken": fcmToken};
|
||||||
|
final headers = {
|
||||||
|
..._headers,
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
};
|
||||||
|
final endpoint = "$_baseUrl/auth/set/device-token";
|
||||||
|
|
||||||
|
// 🔹 Log request details
|
||||||
|
logSafe("📡 Device Token API Request");
|
||||||
|
logSafe("➡️ Endpoint: $endpoint");
|
||||||
|
logSafe("➡️ Headers: ${jsonEncode(headers)}");
|
||||||
|
logSafe("➡️ Payload: ${jsonEncode(body)}");
|
||||||
|
|
||||||
final data = await _post("/auth/set/device-token", body, authToken: token);
|
final data = await _post("/auth/set/device-token", body, authToken: token);
|
||||||
|
|
||||||
if (data != null && data['success'] == true) {
|
if (data != null && data['success'] == true) {
|
||||||
logSafe("✅ Device token registered successfully.");
|
logSafe("✅ Device token registered successfully.");
|
||||||
return true;
|
return true;
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:googleapis_auth/auth_io.dart';
|
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
|
||||||
|
|
||||||
import 'package:marco/helpers/services/local_notification_service.dart';
|
import 'package:marco/helpers/services/local_notification_service.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
@ -15,10 +11,6 @@ class FirebaseNotificationService {
|
|||||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
||||||
final Logger _logger = Logger();
|
final Logger _logger = Logger();
|
||||||
|
|
||||||
static const _fcmScopes = [
|
|
||||||
'https://www.googleapis.com/auth/firebase.messaging',
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Initialize FCM (Firebase.initializeApp() should be called once globally)
|
/// Initialize FCM (Firebase.initializeApp() should be called once globally)
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
_logger.i('✅ FirebaseMessaging initializing...');
|
_logger.i('✅ FirebaseMessaging initializing...');
|
||||||
@ -119,79 +111,7 @@ class FirebaseNotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a test notification using FCM v1 API
|
|
||||||
Future<void> sendTestNotification(String deviceToken) async {
|
|
||||||
try {
|
|
||||||
final client = await _getAuthenticatedHttpClient();
|
|
||||||
if (client == null) return;
|
|
||||||
|
|
||||||
final projectId = await _getProjectId();
|
|
||||||
if (projectId == null) return;
|
|
||||||
|
|
||||||
_logger.i('🏗 Firebase Project ID: $projectId');
|
|
||||||
|
|
||||||
final url = Uri.parse(
|
|
||||||
'https://fcm.googleapis.com/v1/projects/$projectId/messages:send');
|
|
||||||
final payload = _buildNotificationPayload(deviceToken);
|
|
||||||
|
|
||||||
final response = await client.post(
|
|
||||||
url,
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: jsonEncode(payload),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
_logger.i('✅ Test notification sent successfully');
|
|
||||||
} else {
|
|
||||||
_logger.e('❌ Send failed: ${response.statusCode} ${response.body}');
|
|
||||||
}
|
|
||||||
|
|
||||||
client.close();
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.e('❌ Error sending notification', error: e, stackTrace: s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Authenticated HTTP client using service account
|
|
||||||
Future<http.Client?> _getAuthenticatedHttpClient() async {
|
|
||||||
try {
|
|
||||||
final credentials = ServiceAccountCredentials.fromJson(
|
|
||||||
json.decode(await rootBundle.loadString('assets/service-account.json')),
|
|
||||||
);
|
|
||||||
return clientViaServiceAccount(credentials, _fcmScopes);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.e('❌ Failed to authenticate', error: e, stackTrace: s);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get Project ID from service account
|
|
||||||
Future<String?> _getProjectId() async {
|
|
||||||
try {
|
|
||||||
final jsonMap = json
|
|
||||||
.decode(await rootBundle.loadString('assets/service-account.json'));
|
|
||||||
return jsonMap['project_id'];
|
|
||||||
} catch (e) {
|
|
||||||
_logger.e('❌ Failed to load project_id: $e');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build FCM v1 payload
|
|
||||||
Map<String, dynamic> _buildNotificationPayload(String token) => {
|
|
||||||
"message": {
|
|
||||||
"token": token,
|
|
||||||
"notification": {
|
|
||||||
"title": "Test Notification",
|
|
||||||
"body": "This is a test message from Flutter (v1 API)"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"click_action": "FLUTTER_NOTIFICATION_CLICK",
|
|
||||||
"type": "expense_updated", // Example
|
|
||||||
"expense_id": "1234"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Handle tap on notification
|
/// Handle tap on notification
|
||||||
void _handleNotificationTap(RemoteMessage message) {
|
void _handleNotificationTap(RemoteMessage message) {
|
||||||
|
@ -6,6 +6,10 @@ import 'package:marco/controller/task_planning/daily_task_controller.dart';
|
|||||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
||||||
import 'package:marco/controller/expense/expense_detail_controller.dart';
|
import 'package:marco/controller/expense/expense_detail_controller.dart';
|
||||||
|
import 'package:marco/controller/directory/directory_controller.dart';
|
||||||
|
import 'package:marco/controller/directory/notes_controller.dart';
|
||||||
|
import 'package:marco/controller/document/user_document_controller.dart';
|
||||||
|
import 'package:marco/controller/document/document_details_controller.dart';
|
||||||
|
|
||||||
/// Handles incoming FCM notification actions and updates UI/controllers.
|
/// Handles incoming FCM notification actions and updates UI/controllers.
|
||||||
class NotificationActionHandler {
|
class NotificationActionHandler {
|
||||||
@ -37,7 +41,7 @@ class NotificationActionHandler {
|
|||||||
static void _handleByType(String type, Map<String, dynamic> data) {
|
static void _handleByType(String type, Map<String, dynamic> data) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'expense_updated':
|
case 'expense_updated':
|
||||||
// No specific handler yet
|
_handleExpenseUpdated(data);
|
||||||
break;
|
break;
|
||||||
case 'attendance_updated':
|
case 'attendance_updated':
|
||||||
_handleAttendanceUpdated(data);
|
_handleAttendanceUpdated(data);
|
||||||
@ -51,12 +55,14 @@ class NotificationActionHandler {
|
|||||||
static void _handleByKeyword(
|
static void _handleByKeyword(
|
||||||
String keyword, String? action, Map<String, dynamic> data) {
|
String keyword, String? action, Map<String, dynamic> data) {
|
||||||
switch (keyword) {
|
switch (keyword) {
|
||||||
|
/// 🔹 Attendance
|
||||||
case 'Attendance':
|
case 'Attendance':
|
||||||
if (_isAttendanceAction(action)) {
|
if (_isAttendanceAction(action)) {
|
||||||
_handleAttendanceUpdated(data);
|
_handleAttendanceUpdated(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
/// 🔹 Tasks
|
||||||
case 'Report_Task':
|
case 'Report_Task':
|
||||||
_handleTaskUpdated(data, isComment: false);
|
_handleTaskUpdated(data, isComment: false);
|
||||||
break;
|
break;
|
||||||
@ -64,11 +70,7 @@ class NotificationActionHandler {
|
|||||||
case 'Task_Comment':
|
case 'Task_Comment':
|
||||||
_handleTaskUpdated(data, isComment: true);
|
_handleTaskUpdated(data, isComment: true);
|
||||||
break;
|
break;
|
||||||
case 'Expenses_Modified':
|
|
||||||
_handleExpenseUpdated(data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// ✅ New cases
|
|
||||||
case 'Task_Modified':
|
case 'Task_Modified':
|
||||||
case 'WorkArea_Modified':
|
case 'WorkArea_Modified':
|
||||||
case 'Floor_Modified':
|
case 'Floor_Modified':
|
||||||
@ -76,11 +78,41 @@ class NotificationActionHandler {
|
|||||||
_handleTaskPlanningUpdated(data);
|
_handleTaskPlanningUpdated(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
/// 🔹 Expenses
|
||||||
|
case 'Expenses_Modified':
|
||||||
|
_handleExpenseUpdated(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
/// 🔹 Documents
|
||||||
|
case 'Employee_Document_Modified':
|
||||||
|
case 'Project_Document_Modified':
|
||||||
|
_handleDocumentModified(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
/// 🔹 Directory / Contacts
|
||||||
|
case 'Contact_Modified':
|
||||||
|
_handleContactModified(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Contact_Note_Modified':
|
||||||
|
_handleContactNoteModified(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Bucket_Modified':
|
||||||
|
_handleBucketModified(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Bucket_Assigned':
|
||||||
|
_handleBucketAssigned(data);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
_logger.w('⚠️ Unhandled notification keyword: $keyword');
|
_logger.w('⚠️ Unhandled notification keyword: $keyword');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ---------------------- HANDLERS ----------------------
|
||||||
|
|
||||||
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
|
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
|
||||||
final projectId = data['ProjectId'];
|
final projectId = data['ProjectId'];
|
||||||
if (projectId == null) {
|
if (projectId == null) {
|
||||||
@ -99,7 +131,6 @@ class NotificationActionHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates the set of allowed Attendance actions
|
|
||||||
static bool _isAttendanceAction(String? action) {
|
static bool _isAttendanceAction(String? action) {
|
||||||
const validActions = {
|
const validActions = {
|
||||||
'CHECK_IN',
|
'CHECK_IN',
|
||||||
@ -132,7 +163,6 @@ class NotificationActionHandler {
|
|||||||
// Update Expense Detail (if open and matches this expenseId)
|
// Update Expense Detail (if open and matches this expenseId)
|
||||||
_safeControllerUpdate<ExpenseDetailController>(
|
_safeControllerUpdate<ExpenseDetailController>(
|
||||||
onFound: (controller) async {
|
onFound: (controller) async {
|
||||||
// only refresh if the open screen is for this expense
|
|
||||||
if (controller.expense.value?.id == expenseId) {
|
if (controller.expense.value?.id == expenseId) {
|
||||||
await controller.fetchExpenseDetails();
|
await controller.fetchExpenseDetails();
|
||||||
_logger
|
_logger
|
||||||
@ -166,7 +196,97 @@ class NotificationActionHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic reusable method for safe GetX controller access + log handling
|
/// ---------------------- DOCUMENT HANDLER ----------------------
|
||||||
|
static void _handleDocumentModified(Map<String, dynamic> data) {
|
||||||
|
final entityTypeId = data['EntityTypeId'];
|
||||||
|
final entityId = data['EntityId'];
|
||||||
|
|
||||||
|
if (entityTypeId == null || entityId == null) {
|
||||||
|
_logger.w(
|
||||||
|
"⚠️ Document update received without EntityTypeId/EntityId: $data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh document list
|
||||||
|
_safeControllerUpdate<DocumentController>(
|
||||||
|
onFound: (controller) async {
|
||||||
|
await controller.fetchDocuments(
|
||||||
|
entityTypeId: entityTypeId,
|
||||||
|
entityId: entityId,
|
||||||
|
reset: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
notFoundMessage: '⚠️ DocumentController not found, cannot refresh list.',
|
||||||
|
successMessage: '✅ DocumentController refreshed from notification.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh document details (if open and matches)
|
||||||
|
// Refresh document details (if open and matches)
|
||||||
|
final documentId = data['DocumentId'];
|
||||||
|
if (documentId != null) {
|
||||||
|
_safeControllerUpdate<DocumentDetailsController>(
|
||||||
|
onFound: (controller) async {
|
||||||
|
if (controller.documentDetails.value?.data?.id == documentId) {
|
||||||
|
await controller.fetchDocumentDetails(documentId);
|
||||||
|
_logger.i(
|
||||||
|
"✅ DocumentDetailsController refreshed for Document $documentId");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notFoundMessage: 'ℹ️ DocumentDetailsController not active, skipping.',
|
||||||
|
successMessage: '✅ DocumentDetailsController checked for refresh.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ---------------------- DIRECTORY HANDLERS ----------------------
|
||||||
|
|
||||||
|
static void _handleContactModified(Map<String, dynamic> data) {
|
||||||
|
_safeControllerUpdate<DirectoryController>(
|
||||||
|
onFound: (controller) => controller.fetchContacts(),
|
||||||
|
notFoundMessage: '⚠️ DirectoryController not found, cannot refresh.',
|
||||||
|
successMessage: '✅ Directory contacts refreshed from notification.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _handleContactNoteModified(Map<String, dynamic> data) {
|
||||||
|
final contactId = data['contactId'];
|
||||||
|
|
||||||
|
_safeControllerUpdate<DirectoryController>(
|
||||||
|
onFound: (controller) {
|
||||||
|
if (contactId != null) {
|
||||||
|
controller.fetchCommentsForContact(contactId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notFoundMessage:
|
||||||
|
'⚠️ DirectoryController not found, cannot refresh notes.',
|
||||||
|
successMessage: '✅ Directory comments refreshed from notification.',
|
||||||
|
);
|
||||||
|
|
||||||
|
_safeControllerUpdate<NotesController>(
|
||||||
|
onFound: (controller) => controller.fetchNotes(),
|
||||||
|
notFoundMessage: '⚠️ NotesController not found, cannot refresh.',
|
||||||
|
successMessage: '✅ Notes refreshed from notification.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _handleBucketModified(Map<String, dynamic> data) {
|
||||||
|
_safeControllerUpdate<DirectoryController>(
|
||||||
|
onFound: (controller) => controller.fetchBuckets(),
|
||||||
|
notFoundMessage: '⚠️ DirectoryController not found, cannot refresh.',
|
||||||
|
successMessage: '✅ Buckets refreshed from notification.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _handleBucketAssigned(Map<String, dynamic> data) {
|
||||||
|
_safeControllerUpdate<DirectoryController>(
|
||||||
|
onFound: (controller) => controller.fetchBuckets(),
|
||||||
|
notFoundMessage: '⚠️ DirectoryController not found, cannot refresh.',
|
||||||
|
successMessage: '✅ Bucket assignments refreshed from notification.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ---------------------- UTILITY ----------------------
|
||||||
|
|
||||||
static void _safeControllerUpdate<T>({
|
static void _safeControllerUpdate<T>({
|
||||||
required void Function(T controller) onFound,
|
required void Function(T controller) onFound,
|
||||||
required String notFoundMessage,
|
required String notFoundMessage,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/localizations/language.dart';
|
import 'package:marco/helpers/services/localizations/language.dart';
|
||||||
@ -19,10 +20,13 @@ class LocalStorage {
|
|||||||
static const String _employeeInfoKey = "employee_info";
|
static const String _employeeInfoKey = "employee_info";
|
||||||
static const String _mpinTokenKey = "mpinToken";
|
static const String _mpinTokenKey = "mpinToken";
|
||||||
static const String _isMpinKey = "isMpin";
|
static const String _isMpinKey = "isMpin";
|
||||||
static const String _fcmTokenKey = 'fcm_token';
|
static const String _fcmTokenKey = "fcm_token";
|
||||||
static const String _menuStorageKey = "dynamic_menus";
|
static const String _menuStorageKey = "dynamic_menus";
|
||||||
|
|
||||||
static SharedPreferences? _preferencesInstance;
|
static SharedPreferences? _preferencesInstance;
|
||||||
|
static bool _initialized = false;
|
||||||
|
|
||||||
|
static bool get isInitialized => _initialized;
|
||||||
|
|
||||||
static SharedPreferences get preferences {
|
static SharedPreferences get preferences {
|
||||||
if (_preferencesInstance == null) {
|
if (_preferencesInstance == null) {
|
||||||
@ -31,49 +35,54 @@ class LocalStorage {
|
|||||||
return _preferencesInstance!;
|
return _preferencesInstance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialization
|
/// Initialization (idempotent)
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
|
if (_initialized) return;
|
||||||
_preferencesInstance = await SharedPreferences.getInstance();
|
_preferencesInstance = await SharedPreferences.getInstance();
|
||||||
await initData();
|
await _initData();
|
||||||
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> initData() async {
|
static Future<void> _initData() async {
|
||||||
AuthService.isLoggedIn = preferences.getBool(_loggedInUserKey) ?? false;
|
AuthService.isLoggedIn = preferences.getBool(_loggedInUserKey) ?? false;
|
||||||
ThemeCustomizer.fromJSON(preferences.getString(_themeCustomizerKey));
|
ThemeCustomizer.fromJSON(preferences.getString(_themeCustomizerKey));
|
||||||
}
|
}
|
||||||
/// ================== Sidebar Menu ==================
|
|
||||||
static Future<bool> setMenus(List<MenuItem> menus) async {
|
|
||||||
try {
|
|
||||||
final jsonList = menus.map((e) => e.toJson()).toList();
|
|
||||||
return preferences.setString(_menuStorageKey, jsonEncode(jsonList));
|
|
||||||
} catch (e) {
|
|
||||||
print("Error saving menus: $e");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<MenuItem> getMenus() {
|
// ================== Sidebar Menu ==================
|
||||||
final storedJson = preferences.getString(_menuStorageKey);
|
static Future<bool> setMenus(List<MenuItem> menus) async {
|
||||||
if (storedJson == null) return [];
|
try {
|
||||||
try {
|
final jsonList = menus.map((e) => e.toJson()).toList();
|
||||||
return (jsonDecode(storedJson) as List)
|
return preferences.setString(_menuStorageKey, jsonEncode(jsonList));
|
||||||
.map((e) => MenuItem.fromJson(e as Map<String, dynamic>))
|
} catch (e) {
|
||||||
.toList();
|
print("Error saving menus: $e");
|
||||||
} catch (e) {
|
return false;
|
||||||
print("Error loading menus: $e");
|
}
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
|
||||||
|
|
||||||
/// ================== User Permissions ==================
|
static List<MenuItem> getMenus() {
|
||||||
static Future<bool> setUserPermissions(
|
if (!_initialized) return [];
|
||||||
List<UserPermission> permissions) async {
|
final storedJson = preferences.getString(_menuStorageKey);
|
||||||
|
if (storedJson == null) return [];
|
||||||
|
try {
|
||||||
|
return (jsonDecode(storedJson) as List)
|
||||||
|
.map((e) => MenuItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
print("Error loading menus: $e");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
||||||
|
|
||||||
|
// ================== User Permissions ==================
|
||||||
|
static Future<bool> setUserPermissions(List<UserPermission> permissions) async {
|
||||||
final jsonList = permissions.map((e) => e.toJson()).toList();
|
final jsonList = permissions.map((e) => e.toJson()).toList();
|
||||||
return preferences.setString(_userPermissionsKey, jsonEncode(jsonList));
|
return preferences.setString(_userPermissionsKey, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<UserPermission> getUserPermissions() {
|
static List<UserPermission> getUserPermissions() {
|
||||||
|
if (!_initialized) return [];
|
||||||
final storedJson = preferences.getString(_userPermissionsKey);
|
final storedJson = preferences.getString(_userPermissionsKey);
|
||||||
if (storedJson == null) return [];
|
if (storedJson == null) return [];
|
||||||
return (jsonDecode(storedJson) as List)
|
return (jsonDecode(storedJson) as List)
|
||||||
@ -84,11 +93,12 @@ static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
|||||||
static Future<bool> removeUserPermissions() =>
|
static Future<bool> removeUserPermissions() =>
|
||||||
preferences.remove(_userPermissionsKey);
|
preferences.remove(_userPermissionsKey);
|
||||||
|
|
||||||
/// ================== Employee Info ==================
|
// ================== Employee Info ==================
|
||||||
static Future<bool> setEmployeeInfo(EmployeeInfo employeeInfo) =>
|
static Future<bool> setEmployeeInfo(EmployeeInfo employeeInfo) =>
|
||||||
preferences.setString(_employeeInfoKey, jsonEncode(employeeInfo.toJson()));
|
preferences.setString(_employeeInfoKey, jsonEncode(employeeInfo.toJson()));
|
||||||
|
|
||||||
static EmployeeInfo? getEmployeeInfo() {
|
static EmployeeInfo? getEmployeeInfo() {
|
||||||
|
if (!_initialized) return null;
|
||||||
final storedJson = preferences.getString(_employeeInfoKey);
|
final storedJson = preferences.getString(_employeeInfoKey);
|
||||||
return storedJson == null
|
return storedJson == null
|
||||||
? null
|
? null
|
||||||
@ -98,7 +108,7 @@ static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
|||||||
static Future<bool> removeEmployeeInfo() =>
|
static Future<bool> removeEmployeeInfo() =>
|
||||||
preferences.remove(_employeeInfoKey);
|
preferences.remove(_employeeInfoKey);
|
||||||
|
|
||||||
/// ================== Login / Logout ==================
|
// ================== Login / Logout ==================
|
||||||
static Future<bool> setLoggedInUser(bool loggedIn) =>
|
static Future<bool> setLoggedInUser(bool loggedIn) =>
|
||||||
preferences.setBool(_loggedInUserKey, loggedIn);
|
preferences.setBool(_loggedInUserKey, loggedIn);
|
||||||
|
|
||||||
@ -110,7 +120,6 @@ static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
|||||||
final refreshToken = getRefreshToken();
|
final refreshToken = getRefreshToken();
|
||||||
final fcmToken = getFcmToken();
|
final fcmToken = getFcmToken();
|
||||||
|
|
||||||
// Call API only if both tokens exist
|
|
||||||
if (refreshToken != null && fcmToken != null) {
|
if (refreshToken != null && fcmToken != null) {
|
||||||
await AuthService.logoutApi(refreshToken, fcmToken);
|
await AuthService.logoutApi(refreshToken, fcmToken);
|
||||||
}
|
}
|
||||||
@ -118,7 +127,6 @@ static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
|||||||
print("Logout API error: $e");
|
print("Logout API error: $e");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ===== Local Cleanup =====
|
|
||||||
await removeLoggedInUser();
|
await removeLoggedInUser();
|
||||||
await removeToken(_jwtTokenKey);
|
await removeToken(_jwtTokenKey);
|
||||||
await removeToken(_refreshTokenKey);
|
await removeToken(_refreshTokenKey);
|
||||||
@ -126,7 +134,7 @@ static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
|||||||
await removeEmployeeInfo();
|
await removeEmployeeInfo();
|
||||||
await removeMpinToken();
|
await removeMpinToken();
|
||||||
await removeIsMpin();
|
await removeIsMpin();
|
||||||
await removeMenus(); // clear menus on logout
|
await removeMenus();
|
||||||
await preferences.remove("mpin_verified");
|
await preferences.remove("mpin_verified");
|
||||||
await preferences.remove(_languageKey);
|
await preferences.remove(_languageKey);
|
||||||
await preferences.remove(_themeCustomizerKey);
|
await preferences.remove(_themeCustomizerKey);
|
||||||
@ -139,20 +147,22 @@ static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
|||||||
Get.offAllNamed('/auth/login-option');
|
Get.offAllNamed('/auth/login-option');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ================== Theme & Language ==================
|
// ================== Theme & Language ==================
|
||||||
static Future<bool> setCustomizer(ThemeCustomizer themeCustomizer) =>
|
static Future<bool> setCustomizer(ThemeCustomizer themeCustomizer) =>
|
||||||
preferences.setString(_themeCustomizerKey, themeCustomizer.toJSON());
|
preferences.setString(_themeCustomizerKey, themeCustomizer.toJSON());
|
||||||
|
|
||||||
static Future<bool> setLanguage(Language language) =>
|
static Future<bool> setLanguage(Language language) =>
|
||||||
preferences.setString(_languageKey, language.locale.languageCode);
|
preferences.setString(_languageKey, language.locale.languageCode);
|
||||||
|
|
||||||
static String? getLanguage() => preferences.getString(_languageKey);
|
static String? getLanguage() =>
|
||||||
|
_initialized ? preferences.getString(_languageKey) : null;
|
||||||
|
|
||||||
/// ================== Tokens ==================
|
// ================== Tokens ==================
|
||||||
static Future<bool> setToken(String key, String token) =>
|
static Future<bool> setToken(String key, String token) =>
|
||||||
preferences.setString(key, token);
|
preferences.setString(key, token);
|
||||||
|
|
||||||
static String? getToken(String key) => preferences.getString(key);
|
static String? getToken(String key) =>
|
||||||
|
_initialized ? preferences.getString(key) : null;
|
||||||
|
|
||||||
static Future<bool> removeToken(String key) => preferences.remove(key);
|
static Future<bool> removeToken(String key) => preferences.remove(key);
|
||||||
|
|
||||||
@ -166,34 +176,39 @@ static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
|||||||
|
|
||||||
static String? getRefreshToken() => getToken(_refreshTokenKey);
|
static String? getRefreshToken() => getToken(_refreshTokenKey);
|
||||||
|
|
||||||
/// ================== FCM Token ==================
|
// ================== FCM Token ==================
|
||||||
static Future<void> setFcmToken(String token) =>
|
static Future<void> setFcmToken(String token) =>
|
||||||
preferences.setString(_fcmTokenKey, token);
|
preferences.setString(_fcmTokenKey, token);
|
||||||
|
|
||||||
static String? getFcmToken() => preferences.getString(_fcmTokenKey);
|
static String? getFcmToken() =>
|
||||||
|
_initialized ? preferences.getString(_fcmTokenKey) : null;
|
||||||
|
|
||||||
/// ================== MPIN ==================
|
// ================== MPIN ==================
|
||||||
static Future<bool> setMpinToken(String token) =>
|
static Future<bool> setMpinToken(String token) =>
|
||||||
preferences.setString(_mpinTokenKey, token);
|
preferences.setString(_mpinTokenKey, token);
|
||||||
|
|
||||||
static String? getMpinToken() => preferences.getString(_mpinTokenKey);
|
static String? getMpinToken() =>
|
||||||
|
_initialized ? preferences.getString(_mpinTokenKey) : null;
|
||||||
|
|
||||||
static Future<bool> removeMpinToken() => preferences.remove(_mpinTokenKey);
|
static Future<bool> removeMpinToken() => preferences.remove(_mpinTokenKey);
|
||||||
|
|
||||||
static Future<bool> setIsMpin(bool value) =>
|
static Future<bool> setIsMpin(bool value) =>
|
||||||
preferences.setBool(_isMpinKey, value);
|
preferences.setBool(_isMpinKey, value);
|
||||||
|
|
||||||
static bool getIsMpin() => preferences.getBool(_isMpinKey) ?? false;
|
static bool getIsMpin() =>
|
||||||
|
_initialized ? preferences.getBool(_isMpinKey) ?? false : false;
|
||||||
|
|
||||||
static Future<bool> removeIsMpin() => preferences.remove(_isMpinKey);
|
static Future<bool> removeIsMpin() => preferences.remove(_isMpinKey);
|
||||||
|
|
||||||
/// ================== Generic Set/Get ==================
|
// ================== Generic Set/Get ==================
|
||||||
static Future<bool> setBool(String key, bool value) =>
|
static Future<bool> setBool(String key, bool value) =>
|
||||||
preferences.setBool(key, value);
|
preferences.setBool(key, value);
|
||||||
|
|
||||||
static bool? getBool(String key) => preferences.getBool(key);
|
static bool? getBool(String key) =>
|
||||||
|
_initialized ? preferences.getBool(key) : null;
|
||||||
|
|
||||||
static String? getString(String key) => preferences.getString(key);
|
static String? getString(String key) =>
|
||||||
|
_initialized ? preferences.getString(key) : null;
|
||||||
|
|
||||||
static Future<bool> saveString(String key, String value) =>
|
static Future<bool> saveString(String key, String value) =>
|
||||||
preferences.setString(key, value);
|
preferences.setString(key, value);
|
||||||
|
@ -7,13 +7,22 @@ import 'package:marco/view/my_app.dart';
|
|||||||
import 'package:marco/helpers/theme/app_notifier.dart';
|
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/view/layouts/offline_screen.dart';
|
import 'package:marco/view/layouts/offline_screen.dart';
|
||||||
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// Initialize logging system
|
||||||
await initLogging();
|
await initLogging();
|
||||||
logSafe("App starting...");
|
logSafe("App starting...");
|
||||||
|
|
||||||
|
// ✅ Ensure local storage is ready before enabling remote logging
|
||||||
|
await LocalStorage.init();
|
||||||
|
logSafe("💡 Local storage initialized (early init for logging).");
|
||||||
|
|
||||||
|
// Now safe to enable remote logging
|
||||||
enableRemoteLogging();
|
enableRemoteLogging();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await initializeApp();
|
await initializeApp();
|
||||||
logSafe("App initialized successfully.");
|
logSafe("App initialized successfully.");
|
||||||
|
@ -16,8 +16,6 @@ import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
||||||
import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart';
|
import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart';
|
||||||
|
|
||||||
// import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; // ❌ Commented out
|
|
||||||
|
|
||||||
class DashboardScreen extends StatefulWidget {
|
class DashboardScreen extends StatefulWidget {
|
||||||
const DashboardScreen({super.key});
|
const DashboardScreen({super.key});
|
||||||
|
|
||||||
@ -63,20 +61,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// ❌ Commented out FCM Test Button
|
|
||||||
/*
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final fcmService = FirebaseNotificationService();
|
|
||||||
final token = await fcmService.getFcmToken();
|
|
||||||
if (token != null) {
|
|
||||||
await fcmService.sendTestNotification(token);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text("Send Test Notification"),
|
|
||||||
),
|
|
||||||
MySpacing.height(10),
|
|
||||||
*/
|
|
||||||
_buildDashboardStats(context),
|
_buildDashboardStats(context),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@ -235,7 +219,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stat Card
|
|
||||||
/// Dashboard Statistics Section with Compact Cards
|
/// Dashboard Statistics Section with Compact Cards
|
||||||
Widget _buildDashboardStats(BuildContext context) {
|
Widget _buildDashboardStats(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user