Refactor logging mechanism across services and widgets
- Introduced a new `logSafe` function for consistent logging with sensitivity handling. - Replaced direct logger calls with `logSafe` in `api_service.dart`, `app_initializer.dart`, `auth_service.dart`, `permission_service.dart`, and `my_image_compressor.dart`. - Enhanced error handling and logging in various service methods to capture exceptions and provide more context. - Updated image compression logging to include quality and size metrics. - Improved app initialization logging to capture success and error states. - Ensured sensitive information is not logged directly.
This commit is contained in:
parent
e6d05e247e
commit
ec6c24464e
@ -24,47 +24,45 @@ class ForgotPasswordController extends MyController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onForgotPassword() async {
|
Future<void> onForgotPassword() async {
|
||||||
if (!basicValidator.validateForm()) return;
|
if (!basicValidator.validateForm()) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
final data = basicValidator.getData();
|
final data = basicValidator.getData();
|
||||||
final email = data['email']?.toString() ?? '';
|
final email = data['email']?.toString() ?? '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
appLogger.i("Forgot password requested for: $email");
|
logSafe("Forgot password requested for: $email", sensitive: true);
|
||||||
final result = await AuthService.forgotPassword(email);
|
|
||||||
|
|
||||||
if (result == null) {
|
final result = await AuthService.forgotPassword(email);
|
||||||
// Success case
|
|
||||||
|
if (result == null) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: "Password reset link has been sent.",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
await LocalStorage.logout();
|
||||||
|
} else {
|
||||||
|
final errorMessage = result['error'] ?? "Failed to send reset link. Please try again.";
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Failed",
|
||||||
|
message: errorMessage,
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
logSafe("Failed to send reset password email for $email: $errorMessage", level: LogLevel.warning, sensitive: true);
|
||||||
|
}
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
logSafe("Error during forgot password", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Error",
|
||||||
message: "Password reset link has been sent.",
|
message: "Something went wrong. Please try again later.",
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
await LocalStorage.logout();
|
|
||||||
} else {
|
|
||||||
// Failure case with error map
|
|
||||||
final errorMessage = result['error'] ?? "Failed to send reset link. Please try again.";
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Failed",
|
|
||||||
message: errorMessage,
|
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
appLogger.w("Failed to send reset password email for $email: $errorMessage");
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
|
||||||
appLogger.e("Error during forgot password", error: e, stackTrace: stacktrace);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Something went wrong. Please try again later.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void gotoLogIn() {
|
void gotoLogIn() {
|
||||||
Get.offAllNamed('/auth/login-option');
|
Get.offAllNamed('/auth/login-option');
|
||||||
|
@ -55,12 +55,12 @@ class LoginController extends MyController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final loginData = basicValidator.getData();
|
final loginData = basicValidator.getData();
|
||||||
appLogger.i("Attempting login for user: ${loginData['username']}");
|
logSafe("Attempting login for user: ${loginData['username']}", sensitive: true);
|
||||||
|
|
||||||
final errors = await AuthService.loginUser(loginData);
|
final errors = await AuthService.loginUser(loginData);
|
||||||
|
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
appLogger.w("Login failed: $errors");
|
logSafe("Login failed for user: ${loginData['username']} with errors: $errors", level: LogLevel.warning, sensitive: true);
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Login Failed",
|
title: "Login Failed",
|
||||||
@ -73,11 +73,11 @@ class LoginController extends MyController {
|
|||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
} else {
|
} else {
|
||||||
await _handleRememberMe();
|
await _handleRememberMe();
|
||||||
appLogger.i("Login successful: ${loginData['username']}");
|
logSafe("Login successful for user: ${loginData['username']}", sensitive: true);
|
||||||
Get.toNamed('/home');
|
Get.toNamed('/home');
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Exception during login", error: e, stackTrace: stacktrace);
|
logSafe("Exception during login", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Login Error",
|
title: "Login Error",
|
||||||
message: "An unexpected error occurred",
|
message: "An unexpected error occurred",
|
||||||
@ -90,10 +90,8 @@ class LoginController extends MyController {
|
|||||||
|
|
||||||
Future<void> _handleRememberMe() async {
|
Future<void> _handleRememberMe() async {
|
||||||
if (isChecked.value) {
|
if (isChecked.value) {
|
||||||
await LocalStorage.setToken(
|
await LocalStorage.setToken('username', basicValidator.getController('username')!.text);
|
||||||
'username', basicValidator.getController('username')!.text);
|
await LocalStorage.setToken('password', basicValidator.getController('password')!.text);
|
||||||
await LocalStorage.setToken(
|
|
||||||
'password', basicValidator.getController('password')!.text);
|
|
||||||
await LocalStorage.setBool('remember_me', true);
|
await LocalStorage.setBool('remember_me', true);
|
||||||
} else {
|
} else {
|
||||||
await LocalStorage.removeToken('username');
|
await LocalStorage.removeToken('username');
|
||||||
|
@ -5,10 +5,9 @@ import 'package:marco/helpers/services/storage/local_storage.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
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';
|
||||||
|
|
||||||
class MPINController extends GetxController {
|
class MPINController extends GetxController {
|
||||||
|
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
final isNewUser = false.obs;
|
final isNewUser = false.obs;
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
@ -20,17 +19,17 @@ class MPINController extends GetxController {
|
|||||||
final retypeControllers = List.generate(6, (_) => TextEditingController());
|
final retypeControllers = List.generate(6, (_) => TextEditingController());
|
||||||
final retypeFocusNodes = List.generate(6, (_) => FocusNode());
|
final retypeFocusNodes = List.generate(6, (_) => FocusNode());
|
||||||
final RxInt failedAttempts = 0.obs;
|
final RxInt failedAttempts = 0.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
final bool hasMpin = LocalStorage.getIsMpin();
|
final bool hasMpin = LocalStorage.getIsMpin();
|
||||||
isNewUser.value = !hasMpin;
|
isNewUser.value = !hasMpin;
|
||||||
appLogger.i("[MPINController] onInit called. isNewUser: ${isNewUser.value}");
|
logSafe("onInit called. isNewUser: ${isNewUser.value}");
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDigitChanged(String value, int index, {bool isRetype = false}) {
|
void onDigitChanged(String value, int index, {bool isRetype = false}) {
|
||||||
appLogger.i(
|
logSafe("onDigitChanged -> index: $index, value: $value, isRetype: $isRetype", sensitive: true);
|
||||||
"[MPINController] onDigitChanged -> index: $index, value: $value, isRetype: $isRetype");
|
|
||||||
final nodes = isRetype ? retypeFocusNodes : focusNodes;
|
final nodes = isRetype ? retypeFocusNodes : focusNodes;
|
||||||
if (value.isNotEmpty && index < 5) {
|
if (value.isNotEmpty && index < 5) {
|
||||||
nodes[index + 1].requestFocus();
|
nodes[index + 1].requestFocus();
|
||||||
@ -40,15 +39,15 @@ class MPINController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onSubmitMPIN() async {
|
Future<void> onSubmitMPIN() async {
|
||||||
appLogger.i("[MPINController] onSubmitMPIN triggered");
|
logSafe("onSubmitMPIN triggered");
|
||||||
|
|
||||||
if (!formKey.currentState!.validate()) {
|
if (!formKey.currentState!.validate()) {
|
||||||
appLogger.w("[MPINController] Form validation failed");
|
logSafe("Form validation failed", level: LogLevel.warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enteredMPIN = digitControllers.map((c) => c.text).join();
|
final enteredMPIN = digitControllers.map((c) => c.text).join();
|
||||||
appLogger.i("[MPINController] Entered MPIN: $enteredMPIN");
|
logSafe("Entered MPIN: $enteredMPIN", sensitive: true);
|
||||||
|
|
||||||
if (enteredMPIN.length < 6) {
|
if (enteredMPIN.length < 6) {
|
||||||
_showError("Please enter all 6 digits.");
|
_showError("Please enter all 6 digits.");
|
||||||
@ -57,7 +56,7 @@ class MPINController extends GetxController {
|
|||||||
|
|
||||||
if (isNewUser.value) {
|
if (isNewUser.value) {
|
||||||
final retypeMPIN = retypeControllers.map((c) => c.text).join();
|
final retypeMPIN = retypeControllers.map((c) => c.text).join();
|
||||||
appLogger.i("[MPINController] Retyped MPIN: $retypeMPIN");
|
logSafe("Retyped MPIN: $retypeMPIN", sensitive: true);
|
||||||
|
|
||||||
if (retypeMPIN.length < 6) {
|
if (retypeMPIN.length < 6) {
|
||||||
_showError("Please enter all 6 digits in Retype MPIN.");
|
_showError("Please enter all 6 digits in Retype MPIN.");
|
||||||
@ -71,11 +70,11 @@ class MPINController extends GetxController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appLogger.i("[MPINController] MPINs matched. Proceeding to generate MPIN.");
|
logSafe("MPINs matched. Proceeding to generate MPIN.");
|
||||||
final bool success = await generateMPIN(mpin: enteredMPIN);
|
final bool success = await generateMPIN(mpin: enteredMPIN);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
appLogger.i("[MPINController] MPIN generation successful.");
|
logSafe("MPIN generation successful.");
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "MPIN generated successfully. Please login again.",
|
message: "MPIN generated successfully. Please login again.",
|
||||||
@ -83,32 +82,32 @@ class MPINController extends GetxController {
|
|||||||
);
|
);
|
||||||
await LocalStorage.logout();
|
await LocalStorage.logout();
|
||||||
} else {
|
} else {
|
||||||
appLogger.w("[MPINController] MPIN generation failed.");
|
logSafe("MPIN generation failed.", level: LogLevel.warning);
|
||||||
clearFields();
|
clearFields();
|
||||||
clearRetypeFields();
|
clearRetypeFields();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
appLogger.i("[MPINController] Existing user. Proceeding to verify MPIN.");
|
logSafe("Existing user. Proceeding to verify MPIN.");
|
||||||
await verifyMPIN();
|
await verifyMPIN();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onForgotMPIN() async {
|
Future<void> onForgotMPIN() async {
|
||||||
appLogger.i("[MPINController] onForgotMPIN called");
|
logSafe("onForgotMPIN called");
|
||||||
isNewUser.value = true;
|
isNewUser.value = true;
|
||||||
clearFields();
|
clearFields();
|
||||||
clearRetypeFields();
|
clearRetypeFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
void switchToEnterMPIN() {
|
void switchToEnterMPIN() {
|
||||||
appLogger.i("[MPINController] switchToEnterMPIN called");
|
logSafe("switchToEnterMPIN called");
|
||||||
isNewUser.value = false;
|
isNewUser.value = false;
|
||||||
clearFields();
|
clearFields();
|
||||||
clearRetypeFields();
|
clearRetypeFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showError(String message) {
|
void _showError(String message) {
|
||||||
appLogger.e("[MPINController] ERROR: $message");
|
logSafe("ERROR: $message", level: LogLevel.error);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: message,
|
message: message,
|
||||||
@ -118,8 +117,7 @@ class MPINController extends GetxController {
|
|||||||
|
|
||||||
void _navigateToDashboard({String? message}) {
|
void _navigateToDashboard({String? message}) {
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
appLogger
|
logSafe("Navigating to Dashboard with message: $message");
|
||||||
.i("[MPINController] Navigating to Dashboard with message: $message");
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: message,
|
message: message,
|
||||||
@ -130,7 +128,7 @@ class MPINController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void clearFields() {
|
void clearFields() {
|
||||||
appLogger.i("[MPINController] clearFields called");
|
logSafe("clearFields called");
|
||||||
for (final c in digitControllers) {
|
for (final c in digitControllers) {
|
||||||
c.clear();
|
c.clear();
|
||||||
}
|
}
|
||||||
@ -138,7 +136,7 @@ class MPINController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void clearRetypeFields() {
|
void clearRetypeFields() {
|
||||||
appLogger.i("[MPINController] clearRetypeFields called");
|
logSafe("clearRetypeFields called");
|
||||||
for (final c in retypeControllers) {
|
for (final c in retypeControllers) {
|
||||||
c.clear();
|
c.clear();
|
||||||
}
|
}
|
||||||
@ -147,7 +145,7 @@ class MPINController extends GetxController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
appLogger.i("[MPINController] onClose called");
|
logSafe("onClose called");
|
||||||
for (final controller in digitControllers) {
|
for (final controller in digitControllers) {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
}
|
}
|
||||||
@ -168,7 +166,7 @@ class MPINController extends GetxController {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
appLogger.i("[MPINController] generateMPIN started for MPIN: $mpin");
|
logSafe("generateMPIN started");
|
||||||
|
|
||||||
final employeeInfo = LocalStorage.getEmployeeInfo();
|
final employeeInfo = LocalStorage.getEmployeeInfo();
|
||||||
final String? employeeId = employeeInfo?.id;
|
final String? employeeId = employeeInfo?.id;
|
||||||
@ -179,8 +177,7 @@ class MPINController extends GetxController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
appLogger.i(
|
logSafe("Calling AuthService.generateMpin for employeeId: $employeeId", sensitive: true);
|
||||||
"[MPINController] Calling AuthService.generateMpin for employeeId: $employeeId");
|
|
||||||
|
|
||||||
final response = await AuthService.generateMpin(
|
final response = await AuthService.generateMpin(
|
||||||
employeeId: employeeId,
|
employeeId: employeeId,
|
||||||
@ -190,7 +187,7 @@ class MPINController extends GetxController {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
appLogger.i("[MPINController] MPIN generated successfully");
|
logSafe("MPIN generated successfully");
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@ -202,8 +199,7 @@ class MPINController extends GetxController {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
appLogger.w(
|
logSafe("MPIN generation returned error: $response", level: LogLevel.warning);
|
||||||
"[MPINController] MPIN generation returned error response: $response");
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "MPIN Generation Failed",
|
title: "MPIN Generation Failed",
|
||||||
message: "Please check your inputs.",
|
message: "Please check your inputs.",
|
||||||
@ -216,17 +212,17 @@ class MPINController extends GetxController {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
_showError("Failed to generate MPIN: $e");
|
logSafe("Exception in generateMPIN", level: LogLevel.error, error: e);
|
||||||
appLogger.e("[MPINController] Exception in generateMPIN: $e");
|
_showError("Failed to generate MPIN.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifyMPIN() async {
|
Future<void> verifyMPIN() async {
|
||||||
appLogger.i("[MPINController] verifyMPIN triggered");
|
logSafe("verifyMPIN triggered");
|
||||||
|
|
||||||
final enteredMPIN = digitControllers.map((c) => c.text).join();
|
final enteredMPIN = digitControllers.map((c) => c.text).join();
|
||||||
appLogger.i("[MPINController] Entered MPIN: $enteredMPIN");
|
logSafe("Entered MPIN: $enteredMPIN", sensitive: true);
|
||||||
|
|
||||||
if (enteredMPIN.length < 6) {
|
if (enteredMPIN.length < 6) {
|
||||||
_showError("Please enter all 6 digits.");
|
_showError("Please enter all 6 digits.");
|
||||||
@ -251,9 +247,7 @@ class MPINController extends GetxController {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
appLogger.i("[MPINController] MPIN verified successfully.");
|
logSafe("MPIN verified successfully");
|
||||||
|
|
||||||
// Set mpin_verified to true in local storage here:
|
|
||||||
await LocalStorage.setBool('mpin_verified', true);
|
await LocalStorage.setBool('mpin_verified', true);
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
@ -264,7 +258,7 @@ class MPINController extends GetxController {
|
|||||||
_navigateToDashboard();
|
_navigateToDashboard();
|
||||||
} else {
|
} else {
|
||||||
final errorMessage = response["error"] ?? "Invalid MPIN";
|
final errorMessage = response["error"] ?? "Invalid MPIN";
|
||||||
appLogger.w("[MPINController] MPIN verification failed: $errorMessage");
|
logSafe("MPIN verification failed: $errorMessage", level: LogLevel.warning);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
@ -275,8 +269,7 @@ class MPINController extends GetxController {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
final error = "Failed to verify MPIN: $e";
|
logSafe("Exception in verifyMPIN", level: LogLevel.error, error: e);
|
||||||
appLogger.e("[MPINController] Exception in verifyMPIN: $error");
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Something went wrong. Please try again.",
|
message: "Something went wrong. Please try again.",
|
||||||
|
@ -4,7 +4,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.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';
|
||||||
|
|
||||||
class OTPController extends GetxController {
|
class OTPController extends GetxController {
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
@ -25,7 +25,7 @@ class OTPController extends GetxController {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
timer.value = 0;
|
timer.value = 0;
|
||||||
appLogger.i("[OTPController] Initialized");
|
logSafe("[OTPController] Initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -38,18 +38,23 @@ class OTPController extends GetxController {
|
|||||||
for (final node in focusNodes) {
|
for (final node in focusNodes) {
|
||||||
node.dispose();
|
node.dispose();
|
||||||
}
|
}
|
||||||
appLogger.i("[OTPController] Disposed");
|
logSafe("[OTPController] Disposed");
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _sendOTP(String email) async {
|
Future<bool> _sendOTP(String email) async {
|
||||||
appLogger.i("[OTPController] Sending OTP to $email");
|
logSafe("[OTPController] Sending OTP");
|
||||||
final result = await AuthService.generateOtp(email);
|
final result = await AuthService.generateOtp(email);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
appLogger.i("[OTPController] OTP sent successfully to $email");
|
logSafe("[OTPController] OTP sent successfully");
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
appLogger.w("[OTPController] OTP send failed: ${result['error']}");
|
logSafe(
|
||||||
|
"[OTPController] OTP send failed",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
error: result['error'],
|
||||||
|
|
||||||
|
);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: result['error'] ?? "Failed to send OTP",
|
message: result['error'] ?? "Failed to send OTP",
|
||||||
@ -61,10 +66,10 @@ class OTPController extends GetxController {
|
|||||||
|
|
||||||
Future<void> sendOTP() async {
|
Future<void> sendOTP() async {
|
||||||
final userEmail = emailController.text.trim();
|
final userEmail = emailController.text.trim();
|
||||||
appLogger.i("[OTPController] sendOTP called for $userEmail");
|
logSafe("[OTPController] sendOTP called");
|
||||||
|
|
||||||
if (!_validateEmail(userEmail)) {
|
if (!_validateEmail(userEmail)) {
|
||||||
appLogger.w("[OTPController] Invalid email format: $userEmail");
|
logSafe("[OTPController] Invalid email format", level: LogLevel.warning);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Please enter a valid email address",
|
message: "Please enter a valid email address",
|
||||||
@ -89,7 +94,7 @@ class OTPController extends GetxController {
|
|||||||
|
|
||||||
Future<void> onResendOTP() async {
|
Future<void> onResendOTP() async {
|
||||||
if (isResending.value) return;
|
if (isResending.value) return;
|
||||||
appLogger.i("[OTPController] Resending OTP to ${email.value}");
|
logSafe("[OTPController] Resending OTP");
|
||||||
|
|
||||||
isResending.value = true;
|
isResending.value = true;
|
||||||
_clearOTPFields();
|
_clearOTPFields();
|
||||||
@ -103,7 +108,7 @@ class OTPController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onOTPChanged(String value, int index) {
|
void onOTPChanged(String value, int index) {
|
||||||
appLogger.d("[OTPController] OTP field changed: index=$index, value=$value");
|
logSafe("[OTPController] OTP field changed: index=$index", level: LogLevel.debug);
|
||||||
if (value.isNotEmpty) {
|
if (value.isNotEmpty) {
|
||||||
if (index < otpControllers.length - 1) {
|
if (index < otpControllers.length - 1) {
|
||||||
focusNodes[index + 1].requestFocus();
|
focusNodes[index + 1].requestFocus();
|
||||||
@ -119,7 +124,7 @@ class OTPController extends GetxController {
|
|||||||
|
|
||||||
Future<void> verifyOTP() async {
|
Future<void> verifyOTP() async {
|
||||||
final enteredOTP = otpControllers.map((c) => c.text).join();
|
final enteredOTP = otpControllers.map((c) => c.text).join();
|
||||||
appLogger.i("[OTPController] Verifying OTP: $enteredOTP for email: ${email.value}");
|
logSafe("[OTPController] Verifying OTP");
|
||||||
|
|
||||||
final result = await AuthService.verifyOtp(
|
final result = await AuthService.verifyOtp(
|
||||||
email: email.value,
|
email: email.value,
|
||||||
@ -127,23 +132,19 @@ class OTPController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
appLogger.i("[OTPController] OTP verified successfully");
|
logSafe("[OTPController] OTP verified successfully");
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "OTP verified successfully",
|
message: "OTP verified successfully",
|
||||||
type: SnackbarType.success,
|
type: SnackbarType.success,
|
||||||
);
|
);
|
||||||
final bool isMpinEnabled = LocalStorage.getIsMpin();
|
final bool isMpinEnabled = LocalStorage.getIsMpin();
|
||||||
appLogger.i("[OTPController] MPIN Enabled: $isMpinEnabled");
|
logSafe("[OTPController] MPIN Enabled: $isMpinEnabled");
|
||||||
|
|
||||||
if (isMpinEnabled) {
|
Get.offAllNamed('/home');
|
||||||
Get.offAllNamed('/home');
|
|
||||||
} else {
|
|
||||||
Get.offAllNamed('/home');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
final error = result['error'] ?? "Failed to verify OTP";
|
final error = result['error'] ?? "Failed to verify OTP";
|
||||||
appLogger.w("[OTPController] OTP verification failed: $error");
|
logSafe("[OTPController] OTP verification failed", level: LogLevel.warning, error: error, sensitive: true);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: error,
|
message: error,
|
||||||
@ -153,7 +154,7 @@ class OTPController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _clearOTPFields() {
|
void _clearOTPFields() {
|
||||||
appLogger.d("[OTPController] Clearing OTP input fields");
|
logSafe("[OTPController] Clearing OTP input fields", level: LogLevel.debug);
|
||||||
for (final controller in otpControllers) {
|
for (final controller in otpControllers) {
|
||||||
controller.clear();
|
controller.clear();
|
||||||
}
|
}
|
||||||
@ -161,7 +162,7 @@ class OTPController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _startTimer() {
|
void _startTimer() {
|
||||||
appLogger.i("[OTPController] Starting resend timer");
|
logSafe("[OTPController] Starting resend timer");
|
||||||
timer.value = 60;
|
timer.value = 60;
|
||||||
_countdownTimer?.cancel();
|
_countdownTimer?.cancel();
|
||||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
@ -174,7 +175,7 @@ class OTPController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void resetForChangeEmail() {
|
void resetForChangeEmail() {
|
||||||
appLogger.i("[OTPController] Resetting OTP form for change email");
|
logSafe("[OTPController] Resetting OTP form for change email");
|
||||||
|
|
||||||
isOTPSent.value = false;
|
isOTPSent.value = false;
|
||||||
email.value = '';
|
email.value = '';
|
||||||
|
@ -12,7 +12,7 @@ class RegisterAccountController extends MyController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
appLogger.i("[RegisterAccountController] onInit called");
|
logSafe("[RegisterAccountController] onInit called");
|
||||||
|
|
||||||
basicValidator.addField(
|
basicValidator.addField(
|
||||||
'email',
|
'email',
|
||||||
@ -46,32 +46,33 @@ class RegisterAccountController extends MyController {
|
|||||||
Future<void> onLogin() async {
|
Future<void> onLogin() async {
|
||||||
if (basicValidator.validateForm()) {
|
if (basicValidator.validateForm()) {
|
||||||
update();
|
update();
|
||||||
appLogger.i("[RegisterAccountController] Submitting registration data: ${basicValidator.getData()}");
|
final data = basicValidator.getData();
|
||||||
|
logSafe("[RegisterAccountController] Submitting registration data");
|
||||||
|
|
||||||
var errors = await AuthService.loginUser(basicValidator.getData());
|
final errors = await AuthService.loginUser(data);
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
appLogger.w("[RegisterAccountController] Login errors: $errors");
|
logSafe("[RegisterAccountController] Login errors: $errors", level: LogLevel.warning);
|
||||||
basicValidator.addErrors(errors);
|
basicValidator.addErrors(errors);
|
||||||
basicValidator.validateForm();
|
basicValidator.validateForm();
|
||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
appLogger.i("[RegisterAccountController] Redirecting to /starter");
|
logSafe("[RegisterAccountController] Redirecting to /starter");
|
||||||
Get.toNamed('/starter');
|
Get.toNamed('/starter');
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
appLogger.w("[RegisterAccountController] Validation failed");
|
logSafe("[RegisterAccountController] Validation failed", level: LogLevel.warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChangeShowPassword() {
|
void onChangeShowPassword() {
|
||||||
showPassword = !showPassword;
|
showPassword = !showPassword;
|
||||||
appLogger.i("[RegisterAccountController] showPassword toggled: $showPassword");
|
logSafe("[RegisterAccountController] showPassword toggled: $showPassword");
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void gotoLogin() {
|
void gotoLogin() {
|
||||||
appLogger.i("[RegisterAccountController] Navigating to /auth/login-option");
|
logSafe("[RegisterAccountController] Navigating to /auth/login-option");
|
||||||
Get.toNamed('/auth/login-option');
|
Get.toNamed('/auth/login-option');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import 'package:marco/controller/my_controller.dart';
|
|||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class ResetPasswordController extends MyController {
|
class ResetPasswordController extends MyController {
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
@ -14,7 +14,7 @@ class ResetPasswordController extends MyController {
|
|||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
appLogger.i("[ResetPasswordController] onInit called");
|
logSafe("[ResetPasswordController] onInit called");
|
||||||
|
|
||||||
basicValidator.addField(
|
basicValidator.addField(
|
||||||
'password',
|
'password',
|
||||||
@ -33,39 +33,39 @@ class ResetPasswordController extends MyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onResetPassword() async {
|
Future<void> onResetPassword() async {
|
||||||
appLogger.i("[ResetPasswordController] onResetPassword triggered");
|
logSafe("[ResetPasswordController] onResetPassword triggered");
|
||||||
|
|
||||||
if (basicValidator.validateForm()) {
|
if (basicValidator.validateForm()) {
|
||||||
final data = basicValidator.getData();
|
final data = basicValidator.getData();
|
||||||
appLogger.i("[ResetPasswordController] Form data: $data");
|
logSafe("[ResetPasswordController] Reset password form data");
|
||||||
|
|
||||||
update();
|
update();
|
||||||
var errors = await AuthService.loginUser(data);
|
|
||||||
|
|
||||||
|
final errors = await AuthService.loginUser(data); // Consider renaming this to resetPassword() for clarity
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
appLogger.w("[ResetPasswordController] Received errors: $errors");
|
logSafe("[ResetPasswordController] Received errors: $errors", level: LogLevel.warning);
|
||||||
basicValidator.addErrors(errors);
|
basicValidator.addErrors(errors);
|
||||||
basicValidator.validateForm();
|
basicValidator.validateForm();
|
||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
appLogger.i("[ResetPasswordController] Navigating to /home");
|
logSafe("[ResetPasswordController] Navigating to /home");
|
||||||
Get.toNamed('/home');
|
Get.toNamed('/home');
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
appLogger.w("[ResetPasswordController] Form validation failed");
|
logSafe("[ResetPasswordController] Form validation failed", level: LogLevel.warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChangeShowPassword() {
|
void onChangeShowPassword() {
|
||||||
showPassword = !showPassword;
|
showPassword = !showPassword;
|
||||||
appLogger.i("[ResetPasswordController] showPassword toggled: $showPassword");
|
logSafe("[ResetPasswordController] showPassword toggled: $showPassword");
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onConfirmPassword() {
|
void onConfirmPassword() {
|
||||||
confirmPassword = !confirmPassword;
|
confirmPassword = !confirmPassword;
|
||||||
appLogger.i("[ResetPasswordController] confirmPassword toggled: $confirmPassword");
|
logSafe("[ResetPasswordController] confirmPassword toggled: $confirmPassword");
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import 'package:marco/helpers/services/api_service.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
enum Gender {
|
enum Gender {
|
||||||
male,
|
male,
|
||||||
@ -16,7 +16,6 @@ enum Gender {
|
|||||||
const Gender();
|
const Gender();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AddEmployeeController extends MyController {
|
class AddEmployeeController extends MyController {
|
||||||
List<PlatformFile> files = [];
|
List<PlatformFile> files = [];
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
@ -58,15 +57,15 @@ class AddEmployeeController extends MyController {
|
|||||||
"+33": 9,
|
"+33": 9,
|
||||||
"+86": 11,
|
"+86": 11,
|
||||||
};
|
};
|
||||||
String selectedCountryCode = "+91";
|
|
||||||
|
|
||||||
|
String selectedCountryCode = "+91";
|
||||||
bool showOnline = true;
|
bool showOnline = true;
|
||||||
final List<String> categories = [];
|
final List<String> categories = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
appLogger.i("Initializing AddEmployeeController...");
|
logSafe("Initializing AddEmployeeController...");
|
||||||
_initializeFields();
|
_initializeFields();
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
}
|
}
|
||||||
@ -90,41 +89,41 @@ class AddEmployeeController extends MyController {
|
|||||||
required: true,
|
required: true,
|
||||||
controller: TextEditingController(),
|
controller: TextEditingController(),
|
||||||
);
|
);
|
||||||
appLogger.i("Fields initialized for first_name, phone_number, last_name.");
|
logSafe("Fields initialized for first_name, phone_number, last_name.");
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGenderSelected(Gender? gender) {
|
void onGenderSelected(Gender? gender) {
|
||||||
selectedGender = gender;
|
selectedGender = gender;
|
||||||
appLogger.i("Gender selected: ${gender?.name}");
|
logSafe("Gender selected: ${gender?.name}");
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchRoles() async {
|
Future<void> fetchRoles() async {
|
||||||
appLogger.i("Fetching roles...");
|
logSafe("Fetching roles...");
|
||||||
try {
|
try {
|
||||||
final result = await ApiService.getRoles();
|
final result = await ApiService.getRoles();
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
roles = List<Map<String, dynamic>>.from(result);
|
roles = List<Map<String, dynamic>>.from(result);
|
||||||
appLogger.i("Roles fetched successfully.");
|
logSafe("Roles fetched successfully.");
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to fetch roles: null result");
|
logSafe("Failed to fetch roles: null result", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
appLogger.e("Error fetching roles: $e", error: e, stackTrace: st);
|
logSafe("Error fetching roles", level: LogLevel.error, error: e, stackTrace: st);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onRoleSelected(String? roleId) {
|
void onRoleSelected(String? roleId) {
|
||||||
selectedRoleId = roleId;
|
selectedRoleId = roleId;
|
||||||
appLogger.i("Role selected: $roleId");
|
logSafe("Role selected: $roleId");
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> createEmployees() async {
|
Future<bool> createEmployees() async {
|
||||||
appLogger.i("Starting employee creation...");
|
logSafe("Starting employee creation...");
|
||||||
if (selectedGender == null || selectedRoleId == null) {
|
if (selectedGender == null || selectedRoleId == null) {
|
||||||
appLogger.w("Missing gender or role.");
|
logSafe("Missing gender or role.", level: LogLevel.warning);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Missing Fields",
|
title: "Missing Fields",
|
||||||
message: "Please select both Gender and Role.",
|
message: "Please select both Gender and Role.",
|
||||||
@ -135,11 +134,9 @@ class AddEmployeeController extends MyController {
|
|||||||
|
|
||||||
final firstName = basicValidator.getController("first_name")?.text.trim();
|
final firstName = basicValidator.getController("first_name")?.text.trim();
|
||||||
final lastName = basicValidator.getController("last_name")?.text.trim();
|
final lastName = basicValidator.getController("last_name")?.text.trim();
|
||||||
final phoneNumber =
|
final phoneNumber = basicValidator.getController("phone_number")?.text.trim();
|
||||||
basicValidator.getController("phone_number")?.text.trim();
|
|
||||||
|
|
||||||
appLogger.i(
|
logSafe("Creating employee", level: LogLevel.info);
|
||||||
"Creating employee with Name: $firstName $lastName, Phone: $phoneNumber, Gender: ${selectedGender!.name}");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.createEmployee(
|
final response = await ApiService.createEmployee(
|
||||||
@ -151,7 +148,7 @@ class AddEmployeeController extends MyController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
appLogger.i("Employee created successfully.");
|
logSafe("Employee created successfully.");
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "Employee created successfully!",
|
message: "Employee created successfully!",
|
||||||
@ -159,10 +156,10 @@ class AddEmployeeController extends MyController {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to create employee (response false).");
|
logSafe("Failed to create employee (response false)", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
appLogger.e("Error creating employee: $e", error: e, stackTrace: st);
|
logSafe("Error creating employee", level: LogLevel.error, error: e, stackTrace: st);
|
||||||
}
|
}
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
@ -176,9 +173,7 @@ class AddEmployeeController extends MyController {
|
|||||||
Future<bool> _checkAndRequestContactsPermission() async {
|
Future<bool> _checkAndRequestContactsPermission() async {
|
||||||
final status = await Permission.contacts.request();
|
final status = await Permission.contacts.request();
|
||||||
|
|
||||||
if (status.isGranted) {
|
if (status.isGranted) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.isPermanentlyDenied) {
|
if (status.isPermanentlyDenied) {
|
||||||
await openAppSettings();
|
await openAppSettings();
|
||||||
@ -186,8 +181,7 @@ class AddEmployeeController extends MyController {
|
|||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Permission Required",
|
title: "Permission Required",
|
||||||
message:
|
message: "Please allow Contacts permission from settings to pick a contact.",
|
||||||
"Please allow Contacts permission from settings to pick a contact.",
|
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@ -195,17 +189,13 @@ class AddEmployeeController extends MyController {
|
|||||||
|
|
||||||
Future<void> pickContact(BuildContext context) async {
|
Future<void> pickContact(BuildContext context) async {
|
||||||
final permissionGranted = await _checkAndRequestContactsPermission();
|
final permissionGranted = await _checkAndRequestContactsPermission();
|
||||||
|
|
||||||
if (!permissionGranted) return;
|
if (!permissionGranted) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final picked = await FlutterContacts.openExternalPick();
|
final picked = await FlutterContacts.openExternalPick();
|
||||||
|
if (picked == null) return;
|
||||||
|
|
||||||
if (picked == null) return; // User canceled contact picking
|
final contact = await FlutterContacts.getContact(picked.id, withProperties: true);
|
||||||
|
|
||||||
final contact =
|
|
||||||
await FlutterContacts.getContact(picked.id, withProperties: true);
|
|
||||||
|
|
||||||
if (contact == null) {
|
if (contact == null) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@ -223,10 +213,10 @@ class AddEmployeeController extends MyController {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final indiaPhones = contact.phones.where((p) {
|
final indiaPhones = contact.phones.where((p) {
|
||||||
final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), '');
|
final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), '');
|
||||||
return normalized.startsWith('+91') ||
|
return normalized.startsWith('+91') || RegExp(r'^\d{10}$').hasMatch(normalized);
|
||||||
RegExp(r'^\d{10}$').hasMatch(normalized);
|
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
if (indiaPhones.isEmpty) {
|
if (indiaPhones.isEmpty) {
|
||||||
@ -239,7 +229,6 @@ class AddEmployeeController extends MyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? selectedPhone;
|
String? selectedPhone;
|
||||||
|
|
||||||
if (indiaPhones.length == 1) {
|
if (indiaPhones.length == 1) {
|
||||||
selectedPhone = indiaPhones.first.number;
|
selectedPhone = indiaPhones.first.number;
|
||||||
} else {
|
} else {
|
||||||
@ -261,24 +250,16 @@ class AddEmployeeController extends MyController {
|
|||||||
|
|
||||||
if (selectedPhone == null) return;
|
if (selectedPhone == null) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final normalizedPhone = selectedPhone.replaceAll(RegExp(r'[^0-9]'), '');
|
final normalizedPhone = selectedPhone.replaceAll(RegExp(r'[^0-9]'), '');
|
||||||
|
final phoneWithoutCountryCode = normalizedPhone.length > 10
|
||||||
|
? normalizedPhone.substring(normalizedPhone.length - 10)
|
||||||
|
: normalizedPhone;
|
||||||
|
|
||||||
// Remove country code prefix if present, keep only 10 digits
|
basicValidator.getController('phone_number')?.text = phoneWithoutCountryCode;
|
||||||
String phoneWithoutCountryCode;
|
|
||||||
|
|
||||||
if (normalizedPhone.length > 10) {
|
|
||||||
phoneWithoutCountryCode =
|
|
||||||
normalizedPhone.substring(normalizedPhone.length - 10);
|
|
||||||
} else {
|
|
||||||
phoneWithoutCountryCode = normalizedPhone;
|
|
||||||
}
|
|
||||||
|
|
||||||
basicValidator.getController('phone_number')?.text =
|
|
||||||
phoneWithoutCountryCode;
|
|
||||||
|
|
||||||
update();
|
update();
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
appLogger.e("Error fetching contacts: $e", error: e, stackTrace: st);
|
logSafe("Error fetching contacts", level: LogLevel.error, error: e, stackTrace: st);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Failed to fetch contacts.",
|
message: "Failed to fetch contacts.",
|
||||||
|
@ -17,7 +17,6 @@ import 'package:marco/model/attendance_log_view_model.dart';
|
|||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
class AttendanceController extends GetxController {
|
class AttendanceController extends GetxController {
|
||||||
// Data lists
|
|
||||||
List<AttendanceModel> attendances = [];
|
List<AttendanceModel> attendances = [];
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
List<EmployeeModel> employees = [];
|
List<EmployeeModel> employees = [];
|
||||||
@ -25,14 +24,11 @@ class AttendanceController extends GetxController {
|
|||||||
List<RegularizationLogModel> regularizationLogs = [];
|
List<RegularizationLogModel> regularizationLogs = [];
|
||||||
List<AttendanceLogViewModel> attendenceLogsView = [];
|
List<AttendanceLogViewModel> attendenceLogsView = [];
|
||||||
|
|
||||||
// Selected values
|
|
||||||
String selectedTab = 'Employee List';
|
String selectedTab = 'Employee List';
|
||||||
|
|
||||||
// Date range for attendance filtering
|
|
||||||
DateTime? startDateAttendance;
|
DateTime? startDateAttendance;
|
||||||
DateTime? endDateAttendance;
|
DateTime? endDateAttendance;
|
||||||
|
|
||||||
// Loading states
|
|
||||||
RxBool isLoading = true.obs;
|
RxBool isLoading = true.obs;
|
||||||
RxBool isLoadingProjects = true.obs;
|
RxBool isLoadingProjects = true.obs;
|
||||||
RxBool isLoadingEmployees = true.obs;
|
RxBool isLoadingEmployees = true.obs;
|
||||||
@ -40,7 +36,6 @@ class AttendanceController extends GetxController {
|
|||||||
RxBool isLoadingRegularizationLogs = true.obs;
|
RxBool isLoadingRegularizationLogs = true.obs;
|
||||||
RxBool isLoadingLogView = true.obs;
|
RxBool isLoadingLogView = true.obs;
|
||||||
|
|
||||||
// Uploading state per employee (keyed by employeeId)
|
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -58,47 +53,40 @@ class AttendanceController extends GetxController {
|
|||||||
final today = DateTime.now();
|
final today = DateTime.now();
|
||||||
startDateAttendance = today.subtract(const Duration(days: 7));
|
startDateAttendance = today.subtract(const Duration(days: 7));
|
||||||
endDateAttendance = today.subtract(const Duration(days: 1));
|
endDateAttendance = today.subtract(const Duration(days: 1));
|
||||||
appLogger.i("Default date range set: $startDateAttendance to $endDateAttendance");
|
logSafe("Default date range set: $startDateAttendance to $endDateAttendance");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks and requests location permission, returns true if granted.
|
|
||||||
Future<bool> _handleLocationPermission() async {
|
Future<bool> _handleLocationPermission() async {
|
||||||
LocationPermission permission = await Geolocator.checkPermission();
|
LocationPermission permission = await Geolocator.checkPermission();
|
||||||
|
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
appLogger.w('Location permissions are denied');
|
logSafe('Location permissions are denied', level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission == LocationPermission.deniedForever) {
|
if (permission == LocationPermission.deniedForever) {
|
||||||
appLogger.e('Location permissions are permanently denied');
|
logSafe('Location permissions are permanently denied', level: LogLevel.error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches projects and initializes selected project.
|
|
||||||
Future<void> fetchProjects() async {
|
Future<void> fetchProjects() async {
|
||||||
isLoadingProjects.value = true;
|
isLoadingProjects.value = true;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getProjects();
|
final response = await ApiService.getProjects();
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
appLogger.i("Projects fetched: ${projects.length}");
|
logSafe("Projects fetched: ${projects.length}");
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to fetch projects or no projects available.");
|
logSafe("Failed to fetch projects or no projects available.", level: LogLevel.error);
|
||||||
projects = [];
|
projects = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingProjects.value = false;
|
isLoadingProjects.value = false;
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
update(['attendance_dashboard_controller']);
|
update(['attendance_dashboard_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,51 +97,35 @@ class AttendanceController extends GetxController {
|
|||||||
await fetchProjectData(projectId);
|
await fetchProjectData(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches employees, attendance logs and regularization logs for a project.
|
|
||||||
Future<void> fetchProjectData(String? projectId) async {
|
Future<void> fetchProjectData(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
fetchEmployeesByProject(projectId),
|
fetchEmployeesByProject(projectId),
|
||||||
fetchAttendanceLogs(projectId,
|
fetchAttendanceLogs(projectId, dateFrom: startDateAttendance, dateTo: endDateAttendance),
|
||||||
dateFrom: startDateAttendance, dateTo: endDateAttendance),
|
|
||||||
fetchRegularizationLogs(projectId),
|
fetchRegularizationLogs(projectId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
logSafe("Project data fetched for project ID: $projectId");
|
||||||
appLogger.i("Project data fetched for project ID: $projectId");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches employees for the given project.
|
|
||||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoadingEmployees.value = true;
|
isLoadingEmployees.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getEmployeesByProject(projectId);
|
final response = await ApiService.getEmployeesByProject(projectId);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
|
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
|
||||||
|
|
||||||
// Initialize uploading states for employees
|
|
||||||
for (var emp in employees) {
|
for (var emp in employees) {
|
||||||
uploadingStates[emp.id] = false.obs;
|
uploadingStates[emp.id] = false.obs;
|
||||||
}
|
}
|
||||||
|
logSafe("Employees fetched: ${employees.length} for project $projectId");
|
||||||
appLogger.i("Employees fetched: ${employees.length} for project $projectId");
|
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to fetch employees for project $projectId");
|
logSafe("Failed to fetch employees for project $projectId", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingEmployees.value = false;
|
isLoadingEmployees.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Captures image, gets location, and uploads attendance data.
|
|
||||||
/// Returns true on success.
|
|
||||||
Future<bool> captureAndUploadAttendance(
|
Future<bool> captureAndUploadAttendance(
|
||||||
String id,
|
String id,
|
||||||
String employeeId,
|
String employeeId,
|
||||||
@ -165,87 +137,52 @@ class AttendanceController extends GetxController {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
uploadingStates[employeeId]?.value = true;
|
uploadingStates[employeeId]?.value = true;
|
||||||
|
|
||||||
XFile? image;
|
XFile? image;
|
||||||
if (imageCapture) {
|
if (imageCapture) {
|
||||||
image = await ImagePicker().pickImage(
|
image = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 80);
|
||||||
source: ImageSource.camera,
|
|
||||||
imageQuality: 80,
|
|
||||||
);
|
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
appLogger.w("Image capture cancelled.");
|
logSafe("Image capture cancelled.", level: LogLevel.warning);
|
||||||
uploadingStates[employeeId]?.value = false;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
final compressedBytes = await compressImageToUnder100KB(File(image.path));
|
||||||
final compressedBytes =
|
|
||||||
await compressImageToUnder100KB(File(image.path));
|
|
||||||
if (compressedBytes == null) {
|
if (compressedBytes == null) {
|
||||||
appLogger.e("Image compression failed.");
|
logSafe("Image compression failed.", level: LogLevel.error);
|
||||||
uploadingStates[employeeId]?.value = false;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final compressedFile = await saveCompressedImageToFile(compressedBytes);
|
final compressedFile = await saveCompressedImageToFile(compressedBytes);
|
||||||
image = XFile(compressedFile.path);
|
image = XFile(compressedFile.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
final hasLocationPermission = await _handleLocationPermission();
|
final hasLocationPermission = await _handleLocationPermission();
|
||||||
if (!hasLocationPermission) {
|
if (!hasLocationPermission) return false;
|
||||||
uploadingStates[employeeId]?.value = false;
|
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
||||||
return false;
|
final imageName = imageCapture ? ApiService.generateImageName(employeeId, employees.length + 1) : "";
|
||||||
}
|
|
||||||
|
|
||||||
final position = await Geolocator.getCurrentPosition(
|
|
||||||
desiredAccuracy: LocationAccuracy.high,
|
|
||||||
);
|
|
||||||
|
|
||||||
final imageName = imageCapture
|
|
||||||
? ApiService.generateImageName(employeeId, employees.length + 1)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
final result = await ApiService.uploadAttendanceImage(
|
final result = await ApiService.uploadAttendanceImage(
|
||||||
id,
|
id, employeeId, image, position.latitude, position.longitude,
|
||||||
employeeId,
|
imageName: imageName, projectId: projectId, comment: comment,
|
||||||
image,
|
action: action, imageCapture: imageCapture, markTime: markTime,
|
||||||
position.latitude,
|
|
||||||
position.longitude,
|
|
||||||
imageName: imageName,
|
|
||||||
projectId: projectId,
|
|
||||||
comment: comment,
|
|
||||||
action: action,
|
|
||||||
imageCapture: imageCapture,
|
|
||||||
markTime: markTime,
|
|
||||||
);
|
);
|
||||||
|
logSafe("Attendance uploaded for $employeeId, action: $action");
|
||||||
appLogger.i("Attendance uploaded for $employeeId, action: $action");
|
|
||||||
return result;
|
return result;
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Error uploading attendance", error: e, stackTrace: stacktrace);
|
logSafe("Error uploading attendance", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
uploadingStates[employeeId]?.value = false;
|
uploadingStates[employeeId]?.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a date range picker for attendance filtering and fetches logs on selection.
|
Future<void> selectDateRangeForAttendance(BuildContext context, AttendanceController controller) async {
|
||||||
Future<void> selectDateRangeForAttendance(
|
|
||||||
BuildContext context,
|
|
||||||
AttendanceController controller,
|
|
||||||
) async {
|
|
||||||
final today = DateTime.now();
|
final today = DateTime.now();
|
||||||
final todayDateOnly = DateTime(today.year, today.month, today.day);
|
|
||||||
|
|
||||||
final picked = await showDateRangePicker(
|
final picked = await showDateRangePicker(
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: DateTime(2022),
|
firstDate: DateTime(2022),
|
||||||
lastDate: todayDateOnly.subtract(const Duration(days: 1)),
|
lastDate: today.subtract(const Duration(days: 1)),
|
||||||
initialDateRange: DateTimeRange(
|
initialDateRange: DateTimeRange(
|
||||||
start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
|
start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
|
||||||
end: endDateAttendance ??
|
end: endDateAttendance ?? today.subtract(const Duration(days: 1)),
|
||||||
todayDateOnly.subtract(const Duration(days: 1)),
|
|
||||||
),
|
),
|
||||||
builder: (BuildContext context, Widget? child) {
|
builder: (context, child) {
|
||||||
return Center(
|
return Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 400,
|
width: 400,
|
||||||
@ -272,9 +209,7 @@ class AttendanceController extends GetxController {
|
|||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
startDateAttendance = picked.start;
|
startDateAttendance = picked.start;
|
||||||
endDateAttendance = picked.end;
|
endDateAttendance = picked.end;
|
||||||
|
logSafe("Date range selected: $startDateAttendance to $endDateAttendance");
|
||||||
appLogger.i("Date range selected: $startDateAttendance to $endDateAttendance");
|
|
||||||
|
|
||||||
await controller.fetchAttendanceLogs(
|
await controller.fetchAttendanceLogs(
|
||||||
Get.find<ProjectController>().selectedProject?.id,
|
Get.find<ProjectController>().selectedProject?.id,
|
||||||
dateFrom: picked.start,
|
dateFrom: picked.start,
|
||||||
@ -283,49 +218,31 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches attendance logs filtered by project and date range.
|
Future<void> fetchAttendanceLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async {
|
||||||
Future<void> fetchAttendanceLogs(
|
|
||||||
String? projectId, {
|
|
||||||
DateTime? dateFrom,
|
|
||||||
DateTime? dateTo,
|
|
||||||
}) async {
|
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoadingAttendanceLogs.value = true;
|
isLoadingAttendanceLogs.value = true;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
final response = await ApiService.getAttendanceLogs(projectId, dateFrom: dateFrom, dateTo: dateTo);
|
||||||
final response = await ApiService.getAttendanceLogs(
|
|
||||||
projectId,
|
|
||||||
dateFrom: dateFrom,
|
|
||||||
dateTo: dateTo,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendanceLogs =
|
attendanceLogs = response.map((json) => AttendanceLogModel.fromJson(json)).toList();
|
||||||
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
|
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
|
||||||
appLogger.i("Attendance logs fetched: ${attendanceLogs.length}");
|
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to fetch attendance logs for project $projectId");
|
logSafe("Failed to fetch attendance logs for project $projectId", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingAttendanceLogs.value = false;
|
isLoadingAttendanceLogs.value = false;
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Groups attendance logs by check-in date formatted as 'dd MMM yyyy'.
|
|
||||||
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
||||||
final groupedLogs = <String, List<AttendanceLogModel>>{};
|
final groupedLogs = <String, List<AttendanceLogModel>>{};
|
||||||
|
|
||||||
for (var logItem in attendanceLogs) {
|
for (var logItem in attendanceLogs) {
|
||||||
final checkInDate = logItem.checkIn != null
|
final checkInDate = logItem.checkIn != null
|
||||||
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
|
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
|
||||||
: 'Unknown';
|
: 'Unknown';
|
||||||
|
|
||||||
groupedLogs.putIfAbsent(checkInDate, () => []);
|
groupedLogs.putIfAbsent(checkInDate, () => []);
|
||||||
groupedLogs[checkInDate]!.add(logItem);
|
groupedLogs[checkInDate]!.add(logItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
final sortedEntries = groupedLogs.entries.toList()
|
final sortedEntries = groupedLogs.entries.toList()
|
||||||
..sort((a, b) {
|
..sort((a, b) {
|
||||||
if (a.key == 'Unknown') return 1;
|
if (a.key == 'Unknown') return 1;
|
||||||
@ -334,66 +251,43 @@ class AttendanceController extends GetxController {
|
|||||||
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
|
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
|
||||||
return dateB.compareTo(dateA);
|
return dateB.compareTo(dateA);
|
||||||
});
|
});
|
||||||
|
final sortedMap = Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
|
||||||
final sortedMap =
|
logSafe("Logs grouped and sorted by check-in date.");
|
||||||
Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
|
|
||||||
|
|
||||||
appLogger.i("Logs grouped and sorted by check-in date.");
|
|
||||||
return sortedMap;
|
return sortedMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches regularization logs for a project.
|
Future<void> fetchRegularizationLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async {
|
||||||
Future<void> fetchRegularizationLogs(
|
|
||||||
String? projectId, {
|
|
||||||
DateTime? dateFrom,
|
|
||||||
DateTime? dateTo,
|
|
||||||
}) async {
|
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoadingRegularizationLogs.value = true;
|
isLoadingRegularizationLogs.value = true;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getRegularizationLogs(projectId);
|
final response = await ApiService.getRegularizationLogs(projectId);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
regularizationLogs = response
|
regularizationLogs = response.map((json) => RegularizationLogModel.fromJson(json)).toList();
|
||||||
.map((json) => RegularizationLogModel.fromJson(json))
|
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
|
||||||
.toList();
|
|
||||||
appLogger.i("Regularization logs fetched: ${regularizationLogs.length}");
|
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to fetch regularization logs for project $projectId");
|
logSafe("Failed to fetch regularization logs for project $projectId", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingRegularizationLogs.value = false;
|
isLoadingRegularizationLogs.value = false;
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches detailed attendance log view for a specific ID.
|
|
||||||
Future<void> fetchLogsView(String? id) async {
|
Future<void> fetchLogsView(String? id) async {
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
|
|
||||||
isLoadingLogView.value = true;
|
isLoadingLogView.value = true;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getAttendanceLogView(id);
|
final response = await ApiService.getAttendanceLogView(id);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendenceLogsView = response
|
attendenceLogsView = response.map((json) => AttendanceLogViewModel.fromJson(json)).toList();
|
||||||
.map((json) => AttendanceLogViewModel.fromJson(json))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
attendenceLogsView.sort((a, b) {
|
attendenceLogsView.sort((a, b) {
|
||||||
if (a.activityTime == null || b.activityTime == null) return 0;
|
if (a.activityTime == null || b.activityTime == null) return 0;
|
||||||
return b.activityTime!.compareTo(a.activityTime!);
|
return b.activityTime!.compareTo(a.activityTime!);
|
||||||
});
|
});
|
||||||
|
logSafe("Attendance log view fetched for ID: $id");
|
||||||
appLogger.i("Attendance log view fetched for ID: $id");
|
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to fetch attendance log view for ID $id");
|
logSafe("Failed to fetch attendance log view for ID $id", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingLogView.value = false;
|
isLoadingLogView.value = false;
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
import 'package:marco/model/daily_task_model.dart';
|
import 'package:marco/model/daily_task_model.dart';
|
||||||
@ -25,6 +25,7 @@ class DailyTaskController extends GetxController {
|
|||||||
|
|
||||||
RxBool isLoading = true.obs;
|
RxBool isLoading = true.obs;
|
||||||
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -39,43 +40,53 @@ class DailyTaskController extends GetxController {
|
|||||||
final today = DateTime.now();
|
final today = DateTime.now();
|
||||||
startDateTask = today.subtract(const Duration(days: 7));
|
startDateTask = today.subtract(const Duration(days: 7));
|
||||||
endDateTask = today;
|
endDateTask = today;
|
||||||
appLogger.i("Default date range set: $startDateTask to $endDateTask");
|
|
||||||
|
logSafe(
|
||||||
|
"Default date range set: $startDateTask to $endDateTask",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchTaskData(String? projectId) async {
|
Future<void> fetchTaskData(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) {
|
||||||
|
logSafe("fetchTaskData: Skipped, projectId is null", level: LogLevel.warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getDailyTasks(
|
final response = await ApiService.getDailyTasks(
|
||||||
projectId,
|
projectId,
|
||||||
dateFrom: startDateTask,
|
dateFrom: startDateTask,
|
||||||
dateTo: endDateTask,
|
dateTo: endDateTask,
|
||||||
);
|
);
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
groupedDailyTasks.clear();
|
groupedDailyTasks.clear();
|
||||||
|
|
||||||
for (var taskJson in response) {
|
for (var taskJson in response) {
|
||||||
TaskModel task = TaskModel.fromJson(taskJson);
|
final task = TaskModel.fromJson(taskJson);
|
||||||
String assignmentDateKey =
|
final assignmentDateKey =
|
||||||
task.assignmentDate.toIso8601String().split('T')[0];
|
task.assignmentDate.toIso8601String().split('T')[0];
|
||||||
|
|
||||||
if (groupedDailyTasks.containsKey(assignmentDateKey)) {
|
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
|
||||||
groupedDailyTasks[assignmentDateKey]?.add(task);
|
|
||||||
} else {
|
|
||||||
groupedDailyTasks[assignmentDateKey] = [task];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flatten the grouped tasks into the existing dailyTasks list
|
|
||||||
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
|
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
|
||||||
|
|
||||||
appLogger.i("Daily tasks fetched and grouped: ${dailyTasks.length}");
|
logSafe(
|
||||||
|
"Daily tasks fetched and grouped: ${dailyTasks.length} for project $projectId",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
|
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to fetch daily tasks for project $projectId");
|
logSafe(
|
||||||
|
"Failed to fetch daily tasks for project $projectId",
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,18 +99,23 @@ class DailyTaskController extends GetxController {
|
|||||||
firstDate: DateTime(2022),
|
firstDate: DateTime(2022),
|
||||||
lastDate: DateTime.now(),
|
lastDate: DateTime.now(),
|
||||||
initialDateRange: DateTimeRange(
|
initialDateRange: DateTimeRange(
|
||||||
start:
|
start: startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
|
||||||
startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
|
|
||||||
end: endDateTask ?? DateTime.now(),
|
end: endDateTask ?? DateTime.now(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (picked == null) return;
|
if (picked == null) {
|
||||||
|
logSafe("Date range picker cancelled by user.", level: LogLevel.debug);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
startDateTask = picked.start;
|
startDateTask = picked.start;
|
||||||
endDateTask = picked.end;
|
endDateTask = picked.end;
|
||||||
|
|
||||||
appLogger.i("Date range selected: $startDateTask to $endDateTask");
|
logSafe(
|
||||||
|
"Date range selected: $startDateTask to $endDateTask",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
|
|
||||||
await controller.fetchTaskData(controller.selectedProjectId);
|
await controller.fetchTaskData(controller.selectedProjectId);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
class DashboardController extends GetxController {
|
class DashboardController extends GetxController {
|
||||||
// Observables
|
// Observables
|
||||||
final RxList<Map<String, dynamic>> roleWiseData =
|
final RxList<Map<String, dynamic>> roleWiseData = <Map<String, dynamic>>[].obs;
|
||||||
<Map<String, dynamic>>[].obs;
|
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
final RxString selectedRange = '15D'.obs;
|
final RxString selectedRange = '15D'.obs;
|
||||||
final RxBool isChartView = true.obs;
|
final RxBool isChartView = true.obs;
|
||||||
@ -14,31 +13,33 @@ class DashboardController extends GetxController {
|
|||||||
// Inject the ProjectController
|
// Inject the ProjectController
|
||||||
final ProjectController projectController = Get.find<ProjectController>();
|
final ProjectController projectController = Get.find<ProjectController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
// Log to verify order of controller initialization
|
logSafe(
|
||||||
appLogger.i('DashboardController initialized with project ID: ${projectController.selectedProjectId.value}');
|
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
|
||||||
|
level: LogLevel.info,
|
||||||
|
sensitive: true,
|
||||||
|
);
|
||||||
|
|
||||||
if (projectController.selectedProjectId.value.isNotEmpty) {
|
if (projectController.selectedProjectId.value.isNotEmpty) {
|
||||||
fetchRoleWiseAttendance();
|
|
||||||
}
|
|
||||||
|
|
||||||
// React to project change
|
|
||||||
ever<String>(projectController.selectedProjectId, (id) {
|
|
||||||
if (id.isNotEmpty) {
|
|
||||||
appLogger.i('Project changed to $id, fetching attendance');
|
|
||||||
fetchRoleWiseAttendance();
|
fetchRoleWiseAttendance();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// React to range change
|
// React to project change
|
||||||
ever(selectedRange, (_) {
|
ever<String>(projectController.selectedProjectId, (id) {
|
||||||
fetchRoleWiseAttendance();
|
if (id.isNotEmpty) {
|
||||||
});
|
logSafe('Project changed to $id, fetching attendance', level: LogLevel.info, sensitive: true);
|
||||||
}
|
fetchRoleWiseAttendance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// React to range change
|
||||||
|
ever(selectedRange, (_) {
|
||||||
|
fetchRoleWiseAttendance();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
int get rangeDays => _getDaysFromRange(selectedRange.value);
|
int get rangeDays => _getDaysFromRange(selectedRange.value);
|
||||||
|
|
||||||
@ -56,13 +57,16 @@ void onInit() {
|
|||||||
|
|
||||||
void updateRange(String range) {
|
void updateRange(String range) {
|
||||||
selectedRange.value = range;
|
selectedRange.value = range;
|
||||||
|
logSafe('Selected range updated to $range', level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleChartView(bool isChart) {
|
void toggleChartView(bool isChart) {
|
||||||
isChartView.value = isChart;
|
isChartView.value = isChart;
|
||||||
|
logSafe('Chart view toggled to: $isChart', level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshDashboard() async {
|
Future<void> refreshDashboard() async {
|
||||||
|
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
|
||||||
await fetchRoleWiseAttendance();
|
await fetchRoleWiseAttendance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +74,7 @@ void onInit() {
|
|||||||
final String projectId = projectController.selectedProjectId.value;
|
final String projectId = projectController.selectedProjectId.value;
|
||||||
|
|
||||||
if (projectId.isEmpty) {
|
if (projectId.isEmpty) {
|
||||||
appLogger.w('Project ID is empty, skipping API call.');
|
logSafe('Project ID is empty, skipping API call.', level: LogLevel.warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,14 +87,19 @@ void onInit() {
|
|||||||
if (response != null) {
|
if (response != null) {
|
||||||
roleWiseData.value =
|
roleWiseData.value =
|
||||||
response.map((e) => Map<String, dynamic>.from(e)).toList();
|
response.map((e) => Map<String, dynamic>.from(e)).toList();
|
||||||
appLogger.i('Attendance overview fetched successfully.');
|
logSafe('Attendance overview fetched successfully.', level: LogLevel.info);
|
||||||
} else {
|
} else {
|
||||||
appLogger.e('Failed to fetch attendance overview: response is null.');
|
|
||||||
roleWiseData.clear();
|
roleWiseData.clear();
|
||||||
|
logSafe('Failed to fetch attendance overview: response is null.', level: LogLevel.error);
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
appLogger.e('Error fetching attendance overview', error: e, stackTrace: st);
|
|
||||||
roleWiseData.clear();
|
roleWiseData.clear();
|
||||||
|
logSafe(
|
||||||
|
'Error fetching attendance overview',
|
||||||
|
level: LogLevel.error,
|
||||||
|
error: e,
|
||||||
|
stackTrace: st,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/attendance_model.dart';
|
import 'package:marco/model/attendance_model.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
@ -7,7 +7,6 @@ import 'package:marco/model/employee_model.dart';
|
|||||||
import 'package:marco/model/employees/employee_details_model.dart';
|
import 'package:marco/model/employees/employee_details_model.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
|
|
||||||
class EmployeesScreenController extends GetxController {
|
class EmployeesScreenController extends GetxController {
|
||||||
List<AttendanceModel> attendances = [];
|
List<AttendanceModel> attendances = [];
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
@ -18,9 +17,9 @@ class EmployeesScreenController extends GetxController {
|
|||||||
|
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
|
Rxn<EmployeeDetailsModel> selectedEmployeeDetails = Rxn<EmployeeDetailsModel>();
|
||||||
Rxn<EmployeeDetailsModel>();
|
|
||||||
RxBool isLoadingEmployeeDetails = false.obs;
|
RxBool isLoadingEmployeeDetails = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -40,68 +39,113 @@ class EmployeesScreenController extends GetxController {
|
|||||||
|
|
||||||
Future<void> fetchAllProjects() async {
|
Future<void> fetchAllProjects() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
await _handleApiCall(
|
await _handleApiCall(
|
||||||
ApiService.getProjects,
|
ApiService.getProjects,
|
||||||
onSuccess: (data) {
|
onSuccess: (data) {
|
||||||
projects = data.map((json) => ProjectModel.fromJson(json)).toList();
|
projects = data.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
appLogger.i("Projects fetched: ${projects.length} projects loaded.");
|
logSafe(
|
||||||
|
"Projects fetched: ${projects.length} projects loaded.",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onEmpty: () {
|
||||||
|
logSafe("No project data found or API call failed.", level: LogLevel.warning);
|
||||||
},
|
},
|
||||||
onEmpty: () => appLogger.w("No project data found or API call failed."),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearEmployees() {
|
void clearEmployees() {
|
||||||
employees.clear(); // Correct way to clear RxList
|
employees.clear();
|
||||||
appLogger.i("Employees cleared");
|
logSafe("Employees cleared", level: LogLevel.info);
|
||||||
update(['employee_screen_controller']);
|
update(['employee_screen_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchAllEmployees() async {
|
Future<void> fetchAllEmployees() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
await _handleApiCall(
|
await _handleApiCall(
|
||||||
ApiService.getAllEmployees,
|
ApiService.getAllEmployees,
|
||||||
onSuccess: (data) {
|
onSuccess: (data) {
|
||||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||||
appLogger.i("All Employees fetched: ${employees.length} employees loaded.");
|
logSafe(
|
||||||
|
"All Employees fetched: ${employees.length} employees loaded.",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onEmpty: () {
|
onEmpty: () {
|
||||||
employees.clear(); // Always clear on empty
|
employees.clear();
|
||||||
appLogger.w("No Employee data found or API call failed.");
|
logSafe("No Employee data found or API call failed.", level: LogLevel.warning);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
update(['employee_screen_controller']);
|
update(['employee_screen_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
if (projectId == null || projectId.isEmpty) {
|
if (projectId == null || projectId.isEmpty) {
|
||||||
appLogger.e("Project ID is required but was null or empty.");
|
logSafe("Project ID is required but was null or empty.", level: LogLevel.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
await _handleApiCall(
|
await _handleApiCall(
|
||||||
() => ApiService.getAllEmployeesByProject(projectId),
|
() => ApiService.getAllEmployeesByProject(projectId),
|
||||||
onSuccess: (data) {
|
onSuccess: (data) {
|
||||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||||
|
|
||||||
for (var emp in employees) {
|
for (var emp in employees) {
|
||||||
uploadingStates[emp.id] = false.obs;
|
uploadingStates[emp.id] = false.obs;
|
||||||
}
|
}
|
||||||
appLogger.i("Employees fetched: ${employees.length} for project $projectId");
|
|
||||||
|
logSafe(
|
||||||
|
"Employees fetched: ${employees.length} for project $projectId",
|
||||||
|
level: LogLevel.info,
|
||||||
|
sensitive: true,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onEmpty: () {
|
onEmpty: () {
|
||||||
employees.clear();
|
employees.clear();
|
||||||
appLogger.w("No employees found for project $projectId.");
|
logSafe("No employees found for project $projectId.", level: LogLevel.warning, sensitive: true);
|
||||||
|
},
|
||||||
|
onError: (e) {
|
||||||
|
logSafe("Error fetching employees for project $projectId", level: LogLevel.error, error: e, sensitive: true);
|
||||||
},
|
},
|
||||||
onError: (e) =>
|
|
||||||
appLogger.e("Error fetching employees for project $projectId: $e"),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
update(['employee_screen_controller']);
|
update(['employee_screen_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchEmployeeDetails(String? employeeId) async {
|
||||||
|
if (employeeId == null || employeeId.isEmpty) return;
|
||||||
|
|
||||||
|
isLoadingEmployeeDetails.value = true;
|
||||||
|
|
||||||
|
await _handleSingleApiCall(
|
||||||
|
() => ApiService.getEmployeeDetails(employeeId),
|
||||||
|
onSuccess: (data) {
|
||||||
|
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
|
||||||
|
logSafe("Employee details loaded for $employeeId", level: LogLevel.info, sensitive: true);
|
||||||
|
},
|
||||||
|
onEmpty: () {
|
||||||
|
selectedEmployeeDetails.value = null;
|
||||||
|
logSafe("No employee details found for $employeeId", level: LogLevel.warning, sensitive: true);
|
||||||
|
},
|
||||||
|
onError: (e) {
|
||||||
|
selectedEmployeeDetails.value = null;
|
||||||
|
logSafe("Error fetching employee details for $employeeId", level: LogLevel.error, error: e, sensitive: true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
isLoadingEmployeeDetails.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _handleApiCall(
|
Future<void> _handleApiCall(
|
||||||
Future<List<dynamic>?> Function() apiCall, {
|
Future<List<dynamic>?> Function() apiCall, {
|
||||||
required Function(List<dynamic>) onSuccess,
|
required Function(List<dynamic>) onSuccess,
|
||||||
@ -119,32 +163,11 @@ class EmployeesScreenController extends GetxController {
|
|||||||
if (onError != null) {
|
if (onError != null) {
|
||||||
onError(e);
|
onError(e);
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("API call error: $e");
|
logSafe("API call error", level: LogLevel.error, error: e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchEmployeeDetails(String? employeeId) async {
|
|
||||||
if (employeeId == null || employeeId.isEmpty) return;
|
|
||||||
|
|
||||||
isLoadingEmployeeDetails.value = true;
|
|
||||||
|
|
||||||
await _handleSingleApiCall(
|
|
||||||
() => ApiService.getEmployeeDetails(employeeId),
|
|
||||||
onSuccess: (data) {
|
|
||||||
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
|
|
||||||
},
|
|
||||||
onEmpty: () {
|
|
||||||
selectedEmployeeDetails.value = null;
|
|
||||||
},
|
|
||||||
onError: (e) {
|
|
||||||
selectedEmployeeDetails.value = null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
isLoadingEmployeeDetails.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleSingleApiCall(
|
Future<void> _handleSingleApiCall(
|
||||||
Future<Map<String, dynamic>?> Function() apiCall, {
|
Future<Map<String, dynamic>?> Function() apiCall, {
|
||||||
required Function(Map<String, dynamic>) onSuccess,
|
required Function(Map<String, dynamic>) onSuccess,
|
||||||
@ -162,7 +185,7 @@ class EmployeesScreenController extends GetxController {
|
|||||||
if (onError != null) {
|
if (onError != null) {
|
||||||
onError(e);
|
onError(e);
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("API call error: $e");
|
logSafe("API call error", level: LogLevel.error, error: e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LayoutController extends GetxController {
|
class LayoutController extends GetxController {
|
||||||
// Theme Customization
|
// Theme Customization
|
||||||
ThemeCustomizer themeCustomizer = ThemeCustomizer();
|
ThemeCustomizer themeCustomizer = ThemeCustomizer();
|
||||||
@ -51,20 +49,25 @@ class LayoutController extends GetxController {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project Handling
|
/// Fetch project list from API and initialize the selection.
|
||||||
Future<void> fetchProjects() async {
|
Future<void> fetchProjects() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
isLoadingProjects.value = true;
|
isLoadingProjects.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getProjects();
|
try {
|
||||||
|
final response = await ApiService.getProjects();
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
projects.assignAll(fetchedProjects);
|
projects.assignAll(fetchedProjects);
|
||||||
selectedProjectId = RxString(fetchedProjects.first.id.toString());
|
selectedProjectId = RxString(fetchedProjects.first.id.toString());
|
||||||
appLogger.i("Projects fetched: ${fetchedProjects.length}");
|
|
||||||
} else {
|
logSafe("Projects fetched: ${fetchedProjects.length}", level: LogLevel.info);
|
||||||
appLogger.w("No projects found or API call failed.");
|
} else {
|
||||||
|
logSafe("No projects found or API call failed.", level: LogLevel.warning);
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
logSafe("Error fetching projects", level: LogLevel.error, error: e, stackTrace: st);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingProjects.value = false;
|
isLoadingProjects.value = false;
|
||||||
@ -72,32 +75,38 @@ class LayoutController extends GetxController {
|
|||||||
update(['dashboard_controller']);
|
update(['dashboard_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update selected project ID
|
||||||
void updateSelectedProject(String projectId) {
|
void updateSelectedProject(String projectId) {
|
||||||
selectedProjectId?.value = projectId;
|
selectedProjectId?.value = projectId;
|
||||||
|
logSafe("Selected project updated", level: LogLevel.info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle expansion of the project list section
|
||||||
void toggleProjectListExpanded() {
|
void toggleProjectListExpanded() {
|
||||||
isProjectListExpanded.toggle();
|
isProjectListExpanded.toggle();
|
||||||
|
logSafe("Project list expanded: ${isProjectListExpanded.value}", level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme Updates
|
/// Handle theme changes (light/dark, drawer toggles)
|
||||||
void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) {
|
void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) {
|
||||||
themeCustomizer = newVal;
|
themeCustomizer = newVal;
|
||||||
update();
|
update();
|
||||||
|
|
||||||
if (newVal.rightBarOpen) {
|
if (newVal.rightBarOpen) {
|
||||||
scaffoldKey.currentState?.openEndDrawer();
|
scaffoldKey.currentState?.openEndDrawer();
|
||||||
|
logSafe("Theme changed — end drawer opened", level: LogLevel.debug);
|
||||||
} else {
|
} else {
|
||||||
scaffoldKey.currentState?.closeEndDrawer();
|
scaffoldKey.currentState?.closeEndDrawer();
|
||||||
|
logSafe("Theme changed — end drawer closed", level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification Shade (placeholders)
|
/// Optional notification toggles (placeholder)
|
||||||
void enableNotificationShade() {
|
void enableNotificationShade() {
|
||||||
// Add implementation if needed
|
logSafe("Notification shade enabled (not implemented)", level: LogLevel.verbose);
|
||||||
}
|
}
|
||||||
|
|
||||||
void disableNotificationShade() {
|
void disableNotificationShade() {
|
||||||
// Add implementation if needed
|
logSafe("Notification shade disabled (not implemented)", level: LogLevel.verbose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,11 @@ 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/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/permission_service.dart';
|
import 'package:marco/helpers/services/permission_service.dart';
|
||||||
import 'package:marco/model/user_permission.dart';
|
import 'package:marco/model/user_permission.dart';
|
||||||
import 'package:marco/model/employee_info.dart';
|
import 'package:marco/model/employee_info.dart';
|
||||||
import 'package:marco/model/projects_model.dart';
|
import 'package:marco/model/projects_model.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionController extends GetxController {
|
class PermissionController extends GetxController {
|
||||||
var permissions = <UserPermission>[].obs;
|
var permissions = <UserPermission>[].obs;
|
||||||
var employeeInfo = Rxn<EmployeeInfo>();
|
var employeeInfo = Rxn<EmployeeInfo>();
|
||||||
@ -47,9 +44,9 @@ class PermissionController extends GetxController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
appLogger.i("User data successfully stored in SharedPreferences.");
|
logSafe("User data successfully stored in SharedPreferences.");
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Error storing data", error: e, stackTrace: stacktrace);
|
logSafe("Error storing data", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +55,7 @@ class PermissionController extends GetxController {
|
|||||||
if (token?.isNotEmpty ?? false) {
|
if (token?.isNotEmpty ?? false) {
|
||||||
await loadData(token!);
|
await loadData(token!);
|
||||||
} else {
|
} else {
|
||||||
appLogger.w("No token found for loading API data.");
|
logSafe("No token found for loading API data.", level: LogLevel.warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,9 +64,9 @@ class PermissionController extends GetxController {
|
|||||||
final userData = await PermissionService.fetchAllUserData(token);
|
final userData = await PermissionService.fetchAllUserData(token);
|
||||||
_updateState(userData);
|
_updateState(userData);
|
||||||
await _storeData();
|
await _storeData();
|
||||||
appLogger.i("Data loaded and state updated successfully.");
|
logSafe("Data loaded and state updated successfully.");
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Error loading data from API", error: e, stackTrace: stacktrace);
|
logSafe("Error loading data from API", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,45 +75,48 @@ class PermissionController extends GetxController {
|
|||||||
permissions.assignAll(userData['permissions']);
|
permissions.assignAll(userData['permissions']);
|
||||||
employeeInfo.value = userData['employeeInfo'];
|
employeeInfo.value = userData['employeeInfo'];
|
||||||
projectsInfo.assignAll(userData['projects']);
|
projectsInfo.assignAll(userData['projects']);
|
||||||
appLogger.i("State updated with new user data.");
|
|
||||||
|
logSafe("State updated with new user data.", sensitive: true);
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Error updating state", error: e, stackTrace: stacktrace);
|
logSafe("Error updating state", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _getAuthToken() async {
|
Future<String?> _getAuthToken() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.getString('jwt_token');
|
final token = prefs.getString('jwt_token');
|
||||||
|
logSafe("Auth token retrieved successfully.", sensitive: true);
|
||||||
|
return token;
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Error retrieving auth token", error: e, stackTrace: stacktrace);
|
logSafe("Error retrieving auth token", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startAutoRefresh() {
|
void _startAutoRefresh() {
|
||||||
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
|
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
|
||||||
appLogger.i("Auto-refresh triggered.");
|
logSafe("Auto-refresh triggered.");
|
||||||
await _loadDataFromAPI();
|
await _loadDataFromAPI();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasPermission(String permissionId) {
|
bool hasPermission(String permissionId) {
|
||||||
final hasPerm = permissions.any((p) => p.id == permissionId);
|
final hasPerm = permissions.any((p) => p.id == permissionId);
|
||||||
appLogger.d("Checking permission $permissionId: $hasPerm");
|
logSafe("Checking permission $permissionId: $hasPerm", level: LogLevel.debug);
|
||||||
return hasPerm;
|
return hasPerm;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isUserAssignedToProject(String projectId) {
|
bool isUserAssignedToProject(String projectId) {
|
||||||
final assigned = projectsInfo.any((project) => project.id == projectId);
|
final assigned = projectsInfo.any((project) => project.id == projectId);
|
||||||
appLogger.d("Checking project assignment for $projectId: $assigned");
|
logSafe("Checking project assignment for $projectId: $assigned", level: LogLevel.debug);
|
||||||
return assigned;
|
return assigned;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
_refreshTimer?.cancel();
|
_refreshTimer?.cancel();
|
||||||
appLogger.i("PermissionController disposed and timer cancelled.");
|
logSafe("PermissionController disposed and timer cancelled.");
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,9 @@ class ProjectController extends GetxController {
|
|||||||
RxBool isLoading = true.obs;
|
RxBool isLoading = true.obs;
|
||||||
RxBool isLoadingProjects = true.obs;
|
RxBool isLoadingProjects = true.obs;
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
GlobalProjectModel? get selectedProject {
|
GlobalProjectModel? get selectedProject {
|
||||||
if (selectedProjectId.value.isEmpty) return null;
|
if (selectedProjectId.value.isEmpty) return null;
|
||||||
|
|
||||||
return projects.firstWhereOrNull((p) => p.id == selectedProjectId.value);
|
return projects.firstWhereOrNull((p) => p.id == selectedProjectId.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +36,10 @@ class ProjectController extends GetxController {
|
|||||||
isLoadingProjects.value = false;
|
isLoadingProjects.value = false;
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
uploadingStates.clear();
|
uploadingStates.clear();
|
||||||
|
|
||||||
LocalStorage.saveString('selectedProjectId', '');
|
LocalStorage.saveString('selectedProjectId', '');
|
||||||
|
|
||||||
|
logSafe("Projects cleared and UI states reset.");
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,20 +52,21 @@ class ProjectController extends GetxController {
|
|||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
projects.assignAll(
|
projects.assignAll(
|
||||||
response.map((json) => GlobalProjectModel.fromJson(json)).toList());
|
response.map((json) => GlobalProjectModel.fromJson(json)).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
String? savedId = LocalStorage.getString('selectedProjectId');
|
String? savedId = LocalStorage.getString('selectedProjectId');
|
||||||
if (savedId != null && projects.any((p) => p.id == savedId)) {
|
if (savedId != null && projects.any((p) => p.id == savedId)) {
|
||||||
selectedProjectId.value = savedId; // ✅ update value only
|
selectedProjectId.value = savedId;
|
||||||
} else {
|
} else {
|
||||||
selectedProjectId.value = projects.first.id.toString(); // ✅
|
selectedProjectId.value = projects.first.id.toString();
|
||||||
LocalStorage.saveString('selectedProjectId', selectedProjectId.value);
|
LocalStorage.saveString('selectedProjectId', selectedProjectId.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
isProjectSelectionExpanded.value = false;
|
isProjectSelectionExpanded.value = false;
|
||||||
appLogger.i("Projects fetched: ${projects.length}");
|
logSafe("Projects fetched: ${projects.length}");
|
||||||
} else {
|
} else {
|
||||||
appLogger.w("No projects found or API call failed.");
|
logSafe("No projects found or API call failed.", level: LogLevel.warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingProjects.value = false;
|
isLoadingProjects.value = false;
|
||||||
@ -72,8 +76,8 @@ class ProjectController extends GetxController {
|
|||||||
|
|
||||||
Future<void> updateSelectedProject(String projectId) async {
|
Future<void> updateSelectedProject(String projectId) async {
|
||||||
selectedProjectId.value = projectId;
|
selectedProjectId.value = projectId;
|
||||||
|
|
||||||
await LocalStorage.saveString('selectedProjectId', projectId);
|
await LocalStorage.saveString('selectedProjectId', projectId);
|
||||||
|
logSafe("Selected project updated to $projectId");
|
||||||
update(['selected_project']);
|
update(['selected_project']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
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/model/dailyTaskPlaning/master_work_category_model.dart';
|
import 'package:marco/model/dailyTaskPlaning/master_work_category_model.dart';
|
||||||
|
|
||||||
|
|
||||||
class AddTaskController extends GetxController {
|
class AddTaskController extends GetxController {
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
@ -19,6 +18,7 @@ class AddTaskController extends GetxController {
|
|||||||
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs;
|
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs;
|
||||||
|
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -29,15 +29,11 @@ class AddTaskController extends GetxController {
|
|||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return 'This field is required';
|
return 'This field is required';
|
||||||
}
|
}
|
||||||
if (fieldType == "target") {
|
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
|
||||||
if (int.tryParse(value.trim()) == null) {
|
return 'Please enter a valid number';
|
||||||
return 'Please enter a valid number';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (fieldType == "description") {
|
if (fieldType == "description" && value.trim().length < 5) {
|
||||||
if (value.trim().length < 5) {
|
return 'Description must be at least 5 characters';
|
||||||
return 'Description must be at least 5 characters';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -49,7 +45,7 @@ class AddTaskController extends GetxController {
|
|||||||
required List<String> taskTeam,
|
required List<String> taskTeam,
|
||||||
DateTime? assignmentDate,
|
DateTime? assignmentDate,
|
||||||
}) async {
|
}) async {
|
||||||
appLogger.i("Starting assign task...");
|
logSafe("Starting task assignment...", level: LogLevel.info);
|
||||||
|
|
||||||
final response = await ApiService.assignDailyTask(
|
final response = await ApiService.assignDailyTask(
|
||||||
workItemId: workItemId,
|
workItemId: workItemId,
|
||||||
@ -60,7 +56,7 @@ class AddTaskController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
appLogger.i("Task assigned successfully.");
|
logSafe("Task assigned successfully.", level: LogLevel.info);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "Task assigned successfully!",
|
message: "Task assigned successfully!",
|
||||||
@ -68,7 +64,7 @@ class AddTaskController extends GetxController {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to assign task.");
|
logSafe("Failed to assign task.", level: LogLevel.error);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Failed to assign task.",
|
message: "Failed to assign task.",
|
||||||
@ -87,7 +83,7 @@ class AddTaskController extends GetxController {
|
|||||||
required String categoryId,
|
required String categoryId,
|
||||||
DateTime? assignmentDate,
|
DateTime? assignmentDate,
|
||||||
}) async {
|
}) async {
|
||||||
appLogger.i("Creating new task...");
|
logSafe("Creating new task...", level: LogLevel.info);
|
||||||
|
|
||||||
final response = await ApiService.createTask(
|
final response = await ApiService.createTask(
|
||||||
parentTaskId: parentTaskId,
|
parentTaskId: parentTaskId,
|
||||||
@ -97,11 +93,10 @@ class AddTaskController extends GetxController {
|
|||||||
activityId: activityId,
|
activityId: activityId,
|
||||||
assignmentDate: assignmentDate,
|
assignmentDate: assignmentDate,
|
||||||
categoryId: categoryId,
|
categoryId: categoryId,
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
appLogger.i("Task created successfully.");
|
logSafe("Task created successfully.", level: LogLevel.info);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "Task created successfully!",
|
message: "Task created successfully!",
|
||||||
@ -109,7 +104,7 @@ class AddTaskController extends GetxController {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to create task.");
|
logSafe("Failed to create task.", level: LogLevel.error);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Failed to create task.",
|
message: "Failed to create task.",
|
||||||
@ -122,9 +117,9 @@ class AddTaskController extends GetxController {
|
|||||||
Future<void> fetchWorkMasterCategories() async {
|
Future<void> fetchWorkMasterCategories() async {
|
||||||
isLoadingWorkMasterCategories.value = true;
|
isLoadingWorkMasterCategories.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getMasterWorkCategories();
|
try {
|
||||||
if (response != null) {
|
final response = await ApiService.getMasterWorkCategories();
|
||||||
try {
|
if (response != null) {
|
||||||
final dataList = response['data'] ?? [];
|
final dataList = response['data'] ?? [];
|
||||||
|
|
||||||
final parsedList = List<WorkCategoryModel>.from(
|
final parsedList = List<WorkCategoryModel>.from(
|
||||||
@ -132,19 +127,17 @@ class AddTaskController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
workMasterCategories.assignAll(parsedList);
|
workMasterCategories.assignAll(parsedList);
|
||||||
final Map<String, String> mapped = {
|
final mapped = {for (var item in parsedList) item.id: item.name};
|
||||||
for (var item in parsedList) item.id: item.name,
|
|
||||||
};
|
|
||||||
categoryIdNameMap.assignAll(mapped);
|
categoryIdNameMap.assignAll(mapped);
|
||||||
|
|
||||||
appLogger.i("Work categories fetched: ${dataList.length}");
|
logSafe("Work categories fetched: ${dataList.length}", level: LogLevel.info);
|
||||||
} catch (e) {
|
} else {
|
||||||
appLogger.e("Error parsing work categories: $e");
|
logSafe("No work categories found or API call failed.", level: LogLevel.warning);
|
||||||
workMasterCategories.clear();
|
|
||||||
categoryIdNameMap.clear();
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e, st) {
|
||||||
appLogger.w("No work categories found or API call failed.");
|
logSafe("Error parsing work categories", level: LogLevel.error, error: e, stackTrace: st);
|
||||||
|
workMasterCategories.clear();
|
||||||
|
categoryIdNameMap.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingWorkMasterCategories.value = false;
|
isLoadingWorkMasterCategories.value = false;
|
||||||
@ -154,5 +147,6 @@ class AddTaskController extends GetxController {
|
|||||||
void selectCategory(String id) {
|
void selectCategory(String id) {
|
||||||
selectedCategoryId.value = id;
|
selectedCategoryId.value = id;
|
||||||
selectedCategoryName.value = categoryIdNameMap[id];
|
selectedCategoryName.value = categoryIdNameMap[id];
|
||||||
|
logSafe("Category selected", level: LogLevel.debug, sensitive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,26 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
import 'package:marco/model/dailyTaskPlaning/daily_task_planing_model.dart';
|
import 'package:marco/model/dailyTaskPlaning/daily_task_planing_model.dart';
|
||||||
import 'package:marco/model/employee_model.dart';
|
import 'package:marco/model/employee_model.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class DailyTaskPlaningController extends GetxController {
|
class DailyTaskPlaningController extends GetxController {
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
List<EmployeeModel> employees = [];
|
List<EmployeeModel> employees = [];
|
||||||
List<TaskPlanningDetailsModel> dailyTasks = [];
|
List<TaskPlanningDetailsModel> dailyTasks = [];
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
|
||||||
|
|
||||||
List<Map<String, dynamic>> roles = [];
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
RxnString selectedRoleId = RxnString();
|
|
||||||
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
|
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
|
||||||
|
|
||||||
void updateSelectedEmployees() {
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
final selected =
|
List<Map<String, dynamic>> roles = [];
|
||||||
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
|
|
||||||
selectedEmployees.value = selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
RxnString selectedRoleId = RxnString();
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -41,114 +36,113 @@ class DailyTaskPlaningController extends GetxController {
|
|||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return 'This field is required';
|
return 'This field is required';
|
||||||
}
|
}
|
||||||
if (fieldType == "target") {
|
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
|
||||||
if (int.tryParse(value.trim()) == null) {
|
return 'Please enter a valid number';
|
||||||
return 'Please enter a valid number';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (fieldType == "description") {
|
if (fieldType == "description" && value.trim().length < 5) {
|
||||||
if (value.trim().length < 5) {
|
return 'Description must be at least 5 characters';
|
||||||
return 'Description must be at least 5 characters';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchRoles() async {
|
void updateSelectedEmployees() {
|
||||||
appLogger.i("Fetching roles...");
|
final selected = employees
|
||||||
final result = await ApiService.getRoles();
|
.where((e) => uploadingStates[e.id]?.value == true)
|
||||||
if (result != null) {
|
.toList();
|
||||||
roles = List<Map<String, dynamic>>.from(result);
|
selectedEmployees.value = selected;
|
||||||
appLogger.i("Roles fetched successfully.");
|
logSafe("Updated selected employees", level: LogLevel.debug, sensitive: true);
|
||||||
update();
|
|
||||||
} else {
|
|
||||||
appLogger.e("Failed to fetch roles.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onRoleSelected(String? roleId) {
|
void onRoleSelected(String? roleId) {
|
||||||
selectedRoleId.value = roleId;
|
selectedRoleId.value = roleId;
|
||||||
appLogger.i("Role selected: $roleId");
|
logSafe("Role selected", level: LogLevel.info, sensitive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchRoles() async {
|
||||||
|
logSafe("Fetching roles...", level: LogLevel.info);
|
||||||
|
final result = await ApiService.getRoles();
|
||||||
|
if (result != null) {
|
||||||
|
roles = List<Map<String, dynamic>>.from(result);
|
||||||
|
logSafe("Roles fetched successfully", level: LogLevel.info);
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
logSafe("Failed to fetch roles", level: LogLevel.error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> assignDailyTask({
|
Future<bool> assignDailyTask({
|
||||||
required String workItemId,
|
required String workItemId,
|
||||||
required int plannedTask,
|
required int plannedTask,
|
||||||
required String description,
|
required String description,
|
||||||
required List<String> taskTeam,
|
required List<String> taskTeam,
|
||||||
DateTime? assignmentDate,
|
DateTime? assignmentDate,
|
||||||
}) async {
|
}) async {
|
||||||
appLogger.i("Starting assign task...");
|
logSafe("Starting assign task...", level: LogLevel.info);
|
||||||
|
|
||||||
final response = await ApiService.assignDailyTask(
|
final response = await ApiService.assignDailyTask(
|
||||||
workItemId: workItemId,
|
workItemId: workItemId,
|
||||||
plannedTask: plannedTask,
|
plannedTask: plannedTask,
|
||||||
description: description,
|
description: description,
|
||||||
taskTeam: taskTeam,
|
taskTeam: taskTeam,
|
||||||
assignmentDate: assignmentDate,
|
assignmentDate: assignmentDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
appLogger.i("Task assigned successfully.");
|
logSafe("Task assigned successfully", level: LogLevel.info);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "Task assigned successfully!",
|
message: "Task assigned successfully!",
|
||||||
type: SnackbarType.success,
|
type: SnackbarType.success,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
appLogger.e("Failed to assign task.");
|
logSafe("Failed to assign task", level: LogLevel.error);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Failed to assign task.",
|
message: "Failed to assign task.",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> fetchProjects() async {
|
Future<void> fetchProjects() async {
|
||||||
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
final response = await ApiService.getProjects();
|
final response = await ApiService.getProjects();
|
||||||
if (response?.isEmpty ?? true) {
|
if (response?.isEmpty ?? true) {
|
||||||
appLogger.w("No project data found or API call failed.");
|
logSafe("No project data found or API call failed", level: LogLevel.warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
|
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
appLogger.i("Projects fetched: ${projects.length} projects loaded.");
|
logSafe("Projects fetched: ${projects.length} projects loaded", level: LogLevel.info);
|
||||||
update();
|
update();
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
appLogger.e("Error fetching projects", error: e, stackTrace: stack);
|
logSafe("Error fetching projects", level: LogLevel.error, error: e, stackTrace: stack);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchTaskData(String? projectId) async {
|
Future<void> fetchTaskData(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) {
|
||||||
|
logSafe("Project ID is null", level: LogLevel.warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
final response = await ApiService.getDailyTasksDetails(projectId);
|
final response = await ApiService.getDailyTasksDetails(projectId);
|
||||||
if (response != null) {
|
final data = response?['data'];
|
||||||
final data = response['data'];
|
if (data != null) {
|
||||||
if (data != null) {
|
dailyTasks = [TaskPlanningDetailsModel.fromJson(data)];
|
||||||
dailyTasks = [TaskPlanningDetailsModel.fromJson(data)];
|
logSafe("Daily task Planning Details fetched", level: LogLevel.info, sensitive: true);
|
||||||
appLogger.i("Daily task Planning Details fetched.");
|
|
||||||
} else {
|
|
||||||
appLogger.e("Data field is null");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
appLogger.e(
|
logSafe("Data field is null", level: LogLevel.warning);
|
||||||
"Failed to fetch daily task planning Details for project $projectId");
|
|
||||||
}
|
}
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
appLogger.e("Error fetching daily task data", error: e, stackTrace: stack);
|
logSafe("Error fetching daily task data", level: LogLevel.error, error: e, stackTrace: stack);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
update();
|
update();
|
||||||
@ -157,7 +151,7 @@ class DailyTaskPlaningController extends GetxController {
|
|||||||
|
|
||||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
if (projectId == null || projectId.isEmpty) {
|
if (projectId == null || projectId.isEmpty) {
|
||||||
appLogger.e("Project ID is required but was null or empty.");
|
logSafe("Project ID is required but was null or empty", level: LogLevel.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,21 +159,22 @@ class DailyTaskPlaningController extends GetxController {
|
|||||||
try {
|
try {
|
||||||
final response = await ApiService.getAllEmployeesByProject(projectId);
|
final response = await ApiService.getAllEmployeesByProject(projectId);
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
employees =
|
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
|
||||||
response.map((json) => EmployeeModel.fromJson(json)).toList();
|
|
||||||
for (var emp in employees) {
|
for (var emp in employees) {
|
||||||
uploadingStates[emp.id] = false.obs;
|
uploadingStates[emp.id] = false.obs;
|
||||||
}
|
}
|
||||||
appLogger.i("Employees fetched: ${employees.length} for project $projectId");
|
logSafe("Employees fetched: ${employees.length} for project $projectId",
|
||||||
|
level: LogLevel.info, sensitive: true);
|
||||||
} else {
|
} else {
|
||||||
appLogger.w("No employees found for project $projectId.");
|
|
||||||
employees = [];
|
employees = [];
|
||||||
|
logSafe("No employees found for project $projectId", level: LogLevel.warning, sensitive: true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, stack) {
|
||||||
appLogger.e("Error fetching employees for project $projectId: $e");
|
logSafe("Error fetching employees for project $projectId",
|
||||||
|
level: LogLevel.error, error: e, stackTrace: stack, sensitive: true);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
update();
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
||||||
@ -14,13 +14,9 @@ import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/dailyTaskPlaning/work_status_model.dart';
|
import 'package:marco/model/dailyTaskPlaning/work_status_model.dart';
|
||||||
|
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
enum ApiStatus { idle, loading, success, failure }
|
||||||
|
|
||||||
class ReportTaskActionController extends MyController {
|
class ReportTaskActionController extends MyController {
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
// Reactive State
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
|
final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
|
||||||
final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
|
final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
|
||||||
@ -37,9 +33,6 @@ class ReportTaskActionController extends MyController {
|
|||||||
|
|
||||||
final RxString selectedWorkStatusName = ''.obs;
|
final RxString selectedWorkStatusName = ''.obs;
|
||||||
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
// Controllers & Validators
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
|
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
@ -72,13 +65,10 @@ class ReportTaskActionController extends MyController {
|
|||||||
approvedTaskController,
|
approvedTaskController,
|
||||||
];
|
];
|
||||||
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
// Lifecycle Hooks
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
appLogger.i("Initializing ReportTaskController...");
|
logSafe("Initializing ReportTaskController...");
|
||||||
_initializeFormFields();
|
_initializeFormFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,12 +77,10 @@ class ReportTaskActionController extends MyController {
|
|||||||
for (final controller in _allControllers) {
|
for (final controller in _allControllers) {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
}
|
}
|
||||||
|
logSafe("Disposed all text controllers in ReportTaskActionController.");
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
// Form Field Setup
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
void _initializeFormFields() {
|
void _initializeFormFields() {
|
||||||
basicValidator
|
basicValidator
|
||||||
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
||||||
@ -109,9 +97,6 @@ class ReportTaskActionController extends MyController {
|
|||||||
..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
|
..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
// Task Approval Logic
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
Future<bool> approveTask({
|
Future<bool> approveTask({
|
||||||
required String projectId,
|
required String projectId,
|
||||||
required String comment,
|
required String comment,
|
||||||
@ -119,14 +104,11 @@ class ReportTaskActionController extends MyController {
|
|||||||
required String approvedTaskCount,
|
required String approvedTaskCount,
|
||||||
List<File>? images,
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
appLogger.i("Starting task approval...");
|
logSafe("approveTask() started", sensitive: false);
|
||||||
appLogger.i("Project ID: $projectId");
|
|
||||||
appLogger.i("Comment: $comment");
|
|
||||||
appLogger.i("Report Action ID: $reportActionId");
|
|
||||||
appLogger.i("Approved Task Count: $approvedTaskCount");
|
|
||||||
|
|
||||||
if (projectId.isEmpty || reportActionId.isEmpty) {
|
if (projectId.isEmpty || reportActionId.isEmpty) {
|
||||||
_showError("Project ID and Report Action ID are required.");
|
_showError("Project ID and Report Action ID are required.");
|
||||||
|
logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,25 +117,29 @@ class ReportTaskActionController extends MyController {
|
|||||||
|
|
||||||
if (approvedTaskInt == null) {
|
if (approvedTaskInt == null) {
|
||||||
_showError("Invalid approved task count.");
|
_showError("Invalid approved task count.");
|
||||||
|
logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
|
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
|
||||||
_showError("Approved task count cannot exceed completed work.");
|
_showError("Approved task count cannot exceed completed work.");
|
||||||
|
logSafe("Validation failed: approved > completed", level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comment.trim().isEmpty) {
|
if (comment.trim().isEmpty) {
|
||||||
_showError("Comment is required.");
|
_showError("Comment is required.");
|
||||||
|
logSafe("Comment field is empty", level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
reportStatus.value = ApiStatus.loading;
|
reportStatus.value = ApiStatus.loading;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
logSafe("Calling _prepareImages() for approval...");
|
||||||
final imageData = await _prepareImages(images);
|
final imageData = await _prepareImages(images);
|
||||||
|
|
||||||
|
logSafe("Calling ApiService.approveTask()");
|
||||||
final success = await ApiService.approveTask(
|
final success = await ApiService.approveTask(
|
||||||
id: projectId,
|
id: projectId,
|
||||||
workStatus: reportActionId,
|
workStatus: reportActionId,
|
||||||
@ -163,15 +149,17 @@ class ReportTaskActionController extends MyController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
logSafe("Task approved successfully");
|
||||||
_showSuccess("Task approved successfully!");
|
_showSuccess("Task approved successfully!");
|
||||||
await taskController.fetchTaskData(projectId);
|
await taskController.fetchTaskData(projectId);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
logSafe("API returned failure on approveTask", level: LogLevel.error);
|
||||||
_showError("Failed to approve task.");
|
_showError("Failed to approve task.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, st) {
|
||||||
appLogger.e("Error approving task: $e");
|
logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
|
||||||
_showError("An error occurred.");
|
_showError("An error occurred.");
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@ -182,26 +170,26 @@ class ReportTaskActionController extends MyController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
// Comment Task Logic
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
Future<void> commentTask({
|
Future<void> commentTask({
|
||||||
required String projectId,
|
required String projectId,
|
||||||
required String comment,
|
required String comment,
|
||||||
List<File>? images,
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
appLogger.i("Starting task comment...");
|
logSafe("commentTask() started", sensitive: false);
|
||||||
|
|
||||||
if (commentController.text.trim().isEmpty) {
|
if (commentController.text.trim().isEmpty) {
|
||||||
_showError("Comment is required.");
|
_showError("Comment is required.");
|
||||||
|
logSafe("Comment field is empty", level: LogLevel.warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
|
logSafe("Calling _prepareImages() for comment...");
|
||||||
final imageData = await _prepareImages(images);
|
final imageData = await _prepareImages(images);
|
||||||
|
|
||||||
|
logSafe("Calling ApiService.commentTask()");
|
||||||
final success = await ApiService.commentTask(
|
final success = await ApiService.commentTask(
|
||||||
id: projectId,
|
id: projectId,
|
||||||
comment: commentController.text.trim(),
|
comment: commentController.text.trim(),
|
||||||
@ -211,31 +199,32 @@ class ReportTaskActionController extends MyController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
logSafe("Comment added successfully");
|
||||||
_showSuccess("Task commented successfully!");
|
_showSuccess("Task commented successfully!");
|
||||||
await taskController.fetchTaskData(projectId);
|
await taskController.fetchTaskData(projectId);
|
||||||
} else {
|
} else {
|
||||||
|
logSafe("API returned failure on commentTask", level: LogLevel.error);
|
||||||
_showError("Failed to comment task.");
|
_showError("Failed to comment task.");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, st) {
|
||||||
appLogger.e("Error commenting task: $e");
|
logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
|
||||||
_showError("An error occurred while commenting the task.");
|
_showError("An error occurred while commenting the task.");
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
// API Helpers
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
Future<void> fetchWorkStatuses() async {
|
Future<void> fetchWorkStatuses() async {
|
||||||
|
logSafe("Fetching work statuses...");
|
||||||
isLoadingWorkStatus.value = true;
|
isLoadingWorkStatus.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getWorkStatus();
|
final response = await ApiService.getWorkStatus();
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
final model = WorkStatusResponseModel.fromJson(response);
|
final model = WorkStatusResponseModel.fromJson(response);
|
||||||
workStatus.assignAll(model.data);
|
workStatus.assignAll(model.data);
|
||||||
|
logSafe("Fetched ${model.data.length} work statuses");
|
||||||
} else {
|
} else {
|
||||||
appLogger.w("No work statuses found or API call failed.");
|
logSafe("No work statuses found or API call failed", level: LogLevel.warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingWorkStatus.value = false;
|
isLoadingWorkStatus.value = false;
|
||||||
@ -243,8 +232,12 @@ class ReportTaskActionController extends MyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async {
|
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async {
|
||||||
if (images == null || images.isEmpty) return null;
|
if (images == null || images.isEmpty) {
|
||||||
|
logSafe("_prepareImages: No images selected.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logSafe("_prepareImages: Compressing and encoding images...");
|
||||||
final results = await Future.wait(images.map((file) async {
|
final results = await Future.wait(images.map((file) async {
|
||||||
final compressedBytes = await compressImageToUnder100KB(file);
|
final compressedBytes = await compressImageToUnder100KB(file);
|
||||||
if (compressedBytes == null) return null;
|
if (compressedBytes == null) return null;
|
||||||
@ -258,6 +251,7 @@ class ReportTaskActionController extends MyController {
|
|||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
|
||||||
return results.whereType<Map<String, dynamic>>().toList();
|
return results.whereType<Map<String, dynamic>>().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,33 +266,28 @@ class ReportTaskActionController extends MyController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
// Image Picker Utils
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
Future<void> pickImages({required bool fromCamera}) async {
|
Future<void> pickImages({required bool fromCamera}) async {
|
||||||
|
logSafe("Opening image picker...");
|
||||||
if (fromCamera) {
|
if (fromCamera) {
|
||||||
final pickedFile = await _picker.pickImage(
|
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
|
||||||
source: ImageSource.camera,
|
|
||||||
imageQuality: 75,
|
|
||||||
);
|
|
||||||
if (pickedFile != null) {
|
if (pickedFile != null) {
|
||||||
selectedImages.add(File(pickedFile.path));
|
selectedImages.add(File(pickedFile.path));
|
||||||
|
logSafe("Image added from camera: ${pickedFile.path}", sensitive: true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
||||||
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
||||||
|
logSafe("${pickedFiles.length} images added from gallery.", sensitive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeImageAt(int index) {
|
void removeImageAt(int index) {
|
||||||
if (index >= 0 && index < selectedImages.length) {
|
if (index >= 0 && index < selectedImages.length) {
|
||||||
|
logSafe("Removing image at index $index", sensitive: true);
|
||||||
selectedImages.removeAt(index);
|
selectedImages.removeAt(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
// Snackbar Feedback
|
|
||||||
// ────────────────────────────────────────────────
|
|
||||||
void _showError(String message) => showAppSnackbar(
|
void _showError(String message) => showAppSnackbar(
|
||||||
title: "Error", message: message, type: SnackbarType.error);
|
title: "Error", message: message, type: SnackbarType.error);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import 'package:marco/controller/my_controller.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@ -12,11 +12,9 @@ import 'dart:io';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||||
|
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
enum ApiStatus { idle, loading, success, failure }
|
||||||
|
|
||||||
final DailyTaskPlaningController taskController =
|
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
|
||||||
Get.put(DailyTaskPlaningController());
|
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
|
|
||||||
class ReportTaskController extends MyController {
|
class ReportTaskController extends MyController {
|
||||||
@ -28,7 +26,6 @@ class ReportTaskController extends MyController {
|
|||||||
|
|
||||||
RxList<File> selectedImages = <File>[].obs;
|
RxList<File> selectedImages = <File>[].obs;
|
||||||
|
|
||||||
// Controllers for each form field
|
|
||||||
final assignedDateController = TextEditingController();
|
final assignedDateController = TextEditingController();
|
||||||
final workAreaController = TextEditingController();
|
final workAreaController = TextEditingController();
|
||||||
final activityController = TextEditingController();
|
final activityController = TextEditingController();
|
||||||
@ -44,50 +41,37 @@ class ReportTaskController extends MyController {
|
|||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
appLogger.i("Initializing ReportTaskController...");
|
logSafe("Initializing ReportTaskController...");
|
||||||
|
basicValidator
|
||||||
basicValidator.addField('assigned_date',
|
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
||||||
label: "Assigned Date", controller: assignedDateController);
|
..addField('work_area', label: "Work Area", controller: workAreaController)
|
||||||
basicValidator.addField('work_area',
|
..addField('activity', label: "Activity", controller: activityController)
|
||||||
label: "Work Area", controller: workAreaController);
|
..addField('team_size', label: "Team Size", controller: teamSizeController)
|
||||||
basicValidator.addField('activity',
|
..addField('task_id', label: "Task Id", controller: taskIdController)
|
||||||
label: "Activity", controller: activityController);
|
..addField('assigned', label: "Assigned", controller: assignedController)
|
||||||
basicValidator.addField('team_size',
|
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
|
||||||
label: "Team Size", controller: teamSizeController);
|
..addField('comment', label: "Comment", required: true, controller: commentController)
|
||||||
basicValidator.addField('task_id',
|
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
|
||||||
label: "Task Id", controller: taskIdController);
|
..addField('team_members', label: "Team Members", controller: teamMembersController)
|
||||||
basicValidator.addField('assigned',
|
..addField('planned_work', label: "Planned Work", controller: plannedWorkController);
|
||||||
label: "Assigned", controller: assignedController);
|
logSafe("Form fields initialized.");
|
||||||
basicValidator.addField('completed_work',
|
|
||||||
label: "Completed Work",
|
|
||||||
required: true,
|
|
||||||
controller: completedWorkController);
|
|
||||||
basicValidator.addField('comment',
|
|
||||||
label: "Comment", required: true, controller: commentController);
|
|
||||||
basicValidator.addField('assigned_by',
|
|
||||||
label: "Assigned By", controller: assignedByController);
|
|
||||||
basicValidator.addField('team_members',
|
|
||||||
label: "Team Members", controller: teamMembersController);
|
|
||||||
basicValidator.addField('planned_work',
|
|
||||||
label: "Planned Work", controller: plannedWorkController);
|
|
||||||
|
|
||||||
appLogger.i(
|
|
||||||
"Fields initialized for assigned_date, work_area, activity, team_size, assigned, completed_work, and comment.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
assignedDateController.dispose();
|
[
|
||||||
workAreaController.dispose();
|
assignedDateController,
|
||||||
activityController.dispose();
|
workAreaController,
|
||||||
teamSizeController.dispose();
|
activityController,
|
||||||
taskIdController.dispose();
|
teamSizeController,
|
||||||
assignedController.dispose();
|
taskIdController,
|
||||||
completedWorkController.dispose();
|
assignedController,
|
||||||
commentController.dispose();
|
completedWorkController,
|
||||||
assignedByController.dispose();
|
commentController,
|
||||||
teamMembersController.dispose();
|
assignedByController,
|
||||||
plannedWorkController.dispose();
|
teamMembersController,
|
||||||
|
plannedWorkController,
|
||||||
|
].forEach((controller) => controller.dispose());
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,36 +83,16 @@ class ReportTaskController extends MyController {
|
|||||||
required DateTime reportedDate,
|
required DateTime reportedDate,
|
||||||
List<File>? images,
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
appLogger.i("Starting task report...");
|
logSafe("Reporting task for projectId", sensitive: true);
|
||||||
|
|
||||||
final completedWork = completedWorkController.text.trim();
|
final completedWork = completedWorkController.text.trim();
|
||||||
|
if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) {
|
||||||
if (completedWork.isEmpty) {
|
_showError("Completed work must be a positive number.");
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Completed work is required.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final completedWorkInt = int.tryParse(completedWork);
|
|
||||||
if (completedWorkInt == null || completedWorkInt < 0) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Completed work must be a positive integer.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final commentField = commentController.text.trim();
|
final commentField = commentController.text.trim();
|
||||||
if (commentField.isEmpty) {
|
if (commentField.isEmpty) {
|
||||||
showAppSnackbar(
|
_showError("Comment is required.");
|
||||||
title: "Error",
|
|
||||||
message: "Comment is required.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,63 +100,30 @@ class ReportTaskController extends MyController {
|
|||||||
reportStatus.value = ApiStatus.loading;
|
reportStatus.value = ApiStatus.loading;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
List<Map<String, dynamic>>? imageData;
|
final imageData = await _prepareImages(images, "task report");
|
||||||
if (images != null && images.isNotEmpty) {
|
|
||||||
final imageFutures = images.map((file) async {
|
|
||||||
final compressedBytes = await compressImageToUnder100KB(file);
|
|
||||||
if (compressedBytes == null) return null;
|
|
||||||
|
|
||||||
final base64Image = base64Encode(compressedBytes);
|
|
||||||
final fileName = file.path.split('/').last;
|
|
||||||
final contentType = _getContentTypeFromFileName(fileName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
"fileName": fileName,
|
|
||||||
"base64Data": base64Image,
|
|
||||||
"contentType": contentType,
|
|
||||||
"fileSize": compressedBytes.lengthInBytes,
|
|
||||||
"description": "Image uploaded for task report",
|
|
||||||
};
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final results = await Future.wait(imageFutures);
|
|
||||||
imageData = results.whereType<Map<String, dynamic>>().toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
final success = await ApiService.reportTask(
|
final success = await ApiService.reportTask(
|
||||||
id: projectId,
|
id: projectId,
|
||||||
comment: commentField,
|
comment: commentField,
|
||||||
completedTask: completedWorkInt,
|
completedTask: int.parse(completedWork),
|
||||||
checkList: checklist,
|
checkList: checklist,
|
||||||
images: imageData,
|
images: imageData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
reportStatus.value = ApiStatus.success;
|
reportStatus.value = ApiStatus.success;
|
||||||
showAppSnackbar(
|
_showSuccess("Task reported successfully!");
|
||||||
title: "Success",
|
|
||||||
message: "Task reported successfully!",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
await taskController.fetchTaskData(projectId);
|
await taskController.fetchTaskData(projectId);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
reportStatus.value = ApiStatus.failure;
|
reportStatus.value = ApiStatus.failure;
|
||||||
showAppSnackbar(
|
_showError("Failed to report task.");
|
||||||
title: "Error",
|
|
||||||
message: "Failed to report task.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
appLogger.e("Error reporting task: $e");
|
logSafe("Exception while reporting task", level: LogLevel.error, error: e, stackTrace: s);
|
||||||
reportStatus.value = ApiStatus.failure;
|
reportStatus.value = ApiStatus.failure;
|
||||||
showAppSnackbar(
|
_showError("An error occurred while reporting the task.");
|
||||||
title: "Error",
|
|
||||||
message: "An error occurred while reporting the task.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@ -202,121 +133,116 @@ class ReportTaskController extends MyController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getContentTypeFromFileName(String fileName) {
|
|
||||||
final ext = fileName.split('.').last.toLowerCase();
|
|
||||||
switch (ext) {
|
|
||||||
case 'jpg':
|
|
||||||
case 'jpeg':
|
|
||||||
return 'image/jpeg';
|
|
||||||
case 'png':
|
|
||||||
return 'image/png';
|
|
||||||
case 'webp':
|
|
||||||
return 'image/webp';
|
|
||||||
case 'gif':
|
|
||||||
return 'image/gif';
|
|
||||||
default:
|
|
||||||
return 'application/octet-stream';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> commentTask({
|
Future<void> commentTask({
|
||||||
required String projectId,
|
required String projectId,
|
||||||
required String comment,
|
required String comment,
|
||||||
List<File>? images,
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
appLogger.i("Starting task comment...");
|
logSafe("Submitting comment for project", sensitive: true);
|
||||||
|
|
||||||
final commentField = commentController.text.trim();
|
final commentField = commentController.text.trim();
|
||||||
if (commentField.isEmpty) {
|
if (commentField.isEmpty) {
|
||||||
showAppSnackbar(
|
_showError("Comment is required.");
|
||||||
title: "Error",
|
|
||||||
message: "Comment is required.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
List<Map<String, dynamic>>? imageData;
|
final imageData = await _prepareImages(images, "task comment");
|
||||||
|
|
||||||
if (images != null && images.isNotEmpty) {
|
|
||||||
final imageFutures = images.map((file) async {
|
|
||||||
final compressedBytes = await compressImageToUnder100KB(file);
|
|
||||||
if (compressedBytes == null) return null;
|
|
||||||
|
|
||||||
final base64Image = base64Encode(compressedBytes);
|
|
||||||
final fileName = file.path.split('/').last;
|
|
||||||
final contentType = _getContentTypeFromFileName(fileName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
"fileName": fileName,
|
|
||||||
"base64Data": base64Image,
|
|
||||||
"contentType": contentType,
|
|
||||||
"fileSize": compressedBytes.lengthInBytes,
|
|
||||||
"description": "Image uploaded for task comment",
|
|
||||||
};
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final results = await Future.wait(imageFutures);
|
|
||||||
imageData = results.whereType<Map<String, dynamic>>().toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
final success = await ApiService.commentTask(
|
final success = await ApiService.commentTask(
|
||||||
id: projectId,
|
id: projectId,
|
||||||
comment: commentField,
|
comment: commentField,
|
||||||
images: imageData,
|
images: imageData,
|
||||||
).timeout(const Duration(seconds: 30), onTimeout: () {
|
).timeout(const Duration(seconds: 30), onTimeout: () {
|
||||||
appLogger.e("Request timed out.");
|
logSafe("Task comment request timed out.", level: LogLevel.error);
|
||||||
throw Exception("Request timed out.");
|
throw Exception("Request timed out.");
|
||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
showAppSnackbar(
|
_showSuccess("Task commented successfully!");
|
||||||
title: "Success",
|
|
||||||
message: "Task commented successfully!",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
await taskController.fetchTaskData(projectId);
|
await taskController.fetchTaskData(projectId);
|
||||||
} else {
|
} else {
|
||||||
showAppSnackbar(
|
_showError("Failed to comment task.");
|
||||||
title: "Error",
|
|
||||||
message: "Failed to comment task.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
appLogger.e("Error commenting task: $e");
|
logSafe("Exception while commenting task", level: LogLevel.error, error: e, stackTrace: s);
|
||||||
showAppSnackbar(
|
_showError("An error occurred while commenting the task.");
|
||||||
title: "Error",
|
|
||||||
message: "An error occurred while commenting the task.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pickImages({required bool fromCamera}) async {
|
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images, String context) async {
|
||||||
if (fromCamera) {
|
if (images == null || images.isEmpty) return null;
|
||||||
final pickedFile = await _picker.pickImage(
|
|
||||||
source: ImageSource.camera,
|
logSafe("Preparing images for $context upload...");
|
||||||
imageQuality: 75,
|
|
||||||
);
|
final results = await Future.wait(images.map((file) async {
|
||||||
if (pickedFile != null) {
|
try {
|
||||||
selectedImages.add(File(pickedFile.path));
|
final compressed = await compressImageToUnder100KB(file);
|
||||||
|
if (compressed == null) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
"fileName": file.path.split('/').last,
|
||||||
|
"base64Data": base64Encode(compressed),
|
||||||
|
"contentType": _getContentTypeFromFileName(file.path),
|
||||||
|
"fileSize": compressed.lengthInBytes,
|
||||||
|
"description": "Image uploaded for $context",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("Image processing failed: ${file.path}", level: LogLevel.warning, error: e);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
}));
|
||||||
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
|
||||||
if (pickedFiles.isNotEmpty) {
|
return results.whereType<Map<String, dynamic>>().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getContentTypeFromFileName(String fileName) {
|
||||||
|
final ext = fileName.split('.').last.toLowerCase();
|
||||||
|
return switch (ext) {
|
||||||
|
'jpg' || 'jpeg' => 'image/jpeg',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'webp' => 'image/webp',
|
||||||
|
'gif' => 'image/gif',
|
||||||
|
_ => 'application/octet-stream',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pickImages({required bool fromCamera}) async {
|
||||||
|
try {
|
||||||
|
if (fromCamera) {
|
||||||
|
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
|
||||||
|
if (pickedFile != null) {
|
||||||
|
selectedImages.add(File(pickedFile.path));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
||||||
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
||||||
}
|
}
|
||||||
|
logSafe("Images picked: ${selectedImages.length}", sensitive: true);
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("Error picking images", level: LogLevel.warning, error: e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeImageAt(int index) {
|
void removeImageAt(int index) {
|
||||||
if (index >= 0 && index < selectedImages.length) {
|
if (index >= 0 && index < selectedImages.length) {
|
||||||
selectedImages.removeAt(index);
|
selectedImages.removeAt(index);
|
||||||
|
logSafe("Removed image at index $index");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showError(String message) => showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: message,
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
|
||||||
|
void _showSuccess(String message) => showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: message,
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,63 +9,60 @@ import 'package:marco/helpers/services/api_endpoints.dart';
|
|||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
static const Duration timeout = Duration(seconds: 30);
|
static const Duration timeout = Duration(seconds: 30);
|
||||||
static const bool enableLogs = true;
|
static const bool enableLogs = true;
|
||||||
static const Duration extendedTimeout = Duration(seconds: 60);
|
static const Duration extendedTimeout = Duration(seconds: 60);
|
||||||
|
|
||||||
// === Helpers ===
|
static Future<String?> _getToken() async {
|
||||||
|
final token = await LocalStorage.getJwtToken();
|
||||||
|
|
||||||
static Future<String?> _getToken() async {
|
if (token == null) {
|
||||||
final token = await LocalStorage.getJwtToken();
|
logSafe("No JWT token found.");
|
||||||
|
return null;
|
||||||
if (token == null) {
|
|
||||||
if (enableLogs) appLogger.w("No JWT token found.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if the token is expired
|
|
||||||
if (JwtDecoder.isExpired(token)) {
|
|
||||||
_log("Access token is expired. Attempting refresh...");
|
|
||||||
final refreshed = await AuthService.refreshToken();
|
|
||||||
if (refreshed) {
|
|
||||||
return await LocalStorage.getJwtToken();
|
|
||||||
} else {
|
|
||||||
_log("Token refresh failed. Logging out...");
|
|
||||||
await LocalStorage.logout();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if token is about to expire in < 2 minutes
|
try {
|
||||||
final expirationDate = JwtDecoder.getExpirationDate(token);
|
if (JwtDecoder.isExpired(token)) {
|
||||||
final now = DateTime.now();
|
logSafe("Access token is expired. Attempting refresh...");
|
||||||
final difference = expirationDate.difference(now);
|
final refreshed = await AuthService.refreshToken();
|
||||||
|
if (refreshed) {
|
||||||
if (difference.inMinutes < 2) {
|
return await LocalStorage.getJwtToken();
|
||||||
_log("Access token is about to expire in ${difference.inSeconds}s. Refreshing...");
|
} else {
|
||||||
final refreshed = await AuthService.refreshToken();
|
logSafe("Token refresh failed. Logging out...");
|
||||||
if (refreshed) {
|
await LocalStorage.logout();
|
||||||
return await LocalStorage.getJwtToken();
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final expirationDate = JwtDecoder.getExpirationDate(token);
|
||||||
|
final now = DateTime.now();
|
||||||
|
final difference = expirationDate.difference(now);
|
||||||
|
|
||||||
|
if (difference.inMinutes < 2) {
|
||||||
|
logSafe(
|
||||||
|
"Access token is about to expire in ${difference.inSeconds}s. Refreshing...");
|
||||||
|
final refreshed = await AuthService.refreshToken();
|
||||||
|
if (refreshed) {
|
||||||
|
return await LocalStorage.getJwtToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("Token decoding error: $e", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
return token;
|
||||||
_log("Token decoding error: $e");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, String> _headers(String token) => {
|
static Map<String, String> _headers(String token) => {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer $token',
|
'Authorization': 'Bearer $token',
|
||||||
};
|
};
|
||||||
|
|
||||||
static void _log(String message) {
|
static void _log(String message) {
|
||||||
if (enableLogs) appLogger.i(message);
|
if (enableLogs) logSafe(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
static dynamic _parseResponse(http.Response response, {String label = ''}) {
|
static dynamic _parseResponse(http.Response response, {String label = ''}) {
|
||||||
@ -82,29 +79,27 @@ class ApiService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static dynamic _parseResponseForAllData(http.Response response,
|
static dynamic _parseResponseForAllData(http.Response response,
|
||||||
{String label = ''}) {
|
{String label = ''}) {
|
||||||
_log("$label Response: ${response.body}");
|
_log("$label Response: ${response.body}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final body = response.body.trim();
|
final body = response.body.trim();
|
||||||
if (body.isEmpty) throw FormatException("Empty response body");
|
if (body.isEmpty) throw FormatException("Empty response body");
|
||||||
|
|
||||||
final json = jsonDecode(body);
|
final json = jsonDecode(body);
|
||||||
|
if (response.statusCode == 200 && json['success'] == true) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.statusCode == 200 && json['success'] == true) {
|
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
|
||||||
return json;
|
} catch (e) {
|
||||||
|
_log("Response parsing error [$label]: $e");
|
||||||
}
|
}
|
||||||
|
|
||||||
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
|
return null;
|
||||||
} catch (e) {
|
|
||||||
_log("Response parsing error [$label]: $e");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static Future<http.Response?> _getRequest(
|
static Future<http.Response?> _getRequest(
|
||||||
String endpoint, {
|
String endpoint, {
|
||||||
Map<String, String>? queryParams,
|
Map<String, String>? queryParams,
|
||||||
@ -115,22 +110,22 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
|
|
||||||
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
|
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
|
||||||
.replace(queryParameters: queryParams);
|
.replace(queryParameters: queryParams);
|
||||||
_log("GET $uri");
|
logSafe("GET $uri");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response =
|
final response =
|
||||||
await http.get(uri, headers: _headers(token)).timeout(timeout);
|
await http.get(uri, headers: _headers(token)).timeout(timeout);
|
||||||
if (response.statusCode == 401 && !hasRetried) {
|
if (response.statusCode == 401 && !hasRetried) {
|
||||||
_log("Unauthorized. Attempting token refresh...");
|
logSafe("Unauthorized. Attempting token refresh...");
|
||||||
if (await AuthService.refreshToken()) {
|
if (await AuthService.refreshToken()) {
|
||||||
return await _getRequest(endpoint,
|
return await _getRequest(endpoint,
|
||||||
queryParams: queryParams, hasRetried: true);
|
queryParams: queryParams, hasRetried: true);
|
||||||
}
|
}
|
||||||
_log("Token refresh failed.");
|
logSafe("Token refresh failed.");
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log("HTTP GET Exception: $e");
|
logSafe("HTTP GET Exception: $e", level: LogLevel.error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,7 +140,8 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
if (token == null) return null;
|
if (token == null) return null;
|
||||||
|
|
||||||
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
||||||
_log("POST $uri\nHeaders: ${_headers(token)}\nBody: $body");
|
logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
|
||||||
|
sensitive: true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await http
|
final response = await http
|
||||||
@ -153,7 +149,7 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
.timeout(customTimeout);
|
.timeout(customTimeout);
|
||||||
|
|
||||||
if (response.statusCode == 401 && !hasRetried) {
|
if (response.statusCode == 401 && !hasRetried) {
|
||||||
_log("Unauthorized POST. Attempting token refresh...");
|
logSafe("Unauthorized POST. Attempting token refresh...");
|
||||||
if (await AuthService.refreshToken()) {
|
if (await AuthService.refreshToken()) {
|
||||||
return await _postRequest(endpoint, body,
|
return await _postRequest(endpoint, body,
|
||||||
customTimeout: customTimeout, hasRetried: true);
|
customTimeout: customTimeout, hasRetried: true);
|
||||||
@ -161,12 +157,12 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log("HTTP POST Exception: $e");
|
logSafe("HTTP POST Exception: $e", level: LogLevel.error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Dashboard Endpoints ===
|
// === Dashboard Endpoints ===
|
||||||
|
|
||||||
static Future<List<dynamic>?> getDashboardAttendanceOverview(
|
static Future<List<dynamic>?> getDashboardAttendanceOverview(
|
||||||
String projectId, int days) async {
|
String projectId, int days) async {
|
||||||
@ -263,7 +259,7 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
"base64Data": base64Encode(bytes),
|
"base64Data": base64Encode(bytes),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log("Image encoding error: $e");
|
logSafe("Image encoding error: $e", level: LogLevel.error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,7 +274,7 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
final json = jsonDecode(response.body);
|
final json = jsonDecode(response.body);
|
||||||
if (response.statusCode == 200 && json['success'] == true) return true;
|
if (response.statusCode == 200 && json['success'] == true) return true;
|
||||||
|
|
||||||
_log("Failed to upload image: ${json['message'] ?? 'Unknown error'}");
|
logSafe("Failed to upload image: ${json['message'] ?? 'Unknown error'}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,7 +385,7 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
Get.back();
|
Get.back();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_log("Failed to report task: ${json['message'] ?? 'Unknown error'}");
|
logSafe("Failed to report task: ${json['message'] ?? 'Unknown error'}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,19 +439,19 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
Get.back();
|
Get.back();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_log("Failed to assign daily task: ${json['message'] ?? 'Unknown error'}");
|
logSafe(
|
||||||
|
"Failed to assign daily task: ${json['message'] ?? 'Unknown error'}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>?> getWorkStatus() async {
|
static Future<Map<String, dynamic>?> getWorkStatus() async {
|
||||||
final res = await _getRequest(ApiEndpoints.getWorkStatus);
|
final res = await _getRequest(ApiEndpoints.getWorkStatus);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
_log('Work Status API returned null');
|
logSafe('Work Status API returned null');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log('Work Status raw response: ${res.body}');
|
logSafe('Work Status raw response: ${res.body}');
|
||||||
|
|
||||||
return _parseResponseForAllData(res, label: 'Work Status')
|
return _parseResponseForAllData(res, label: 'Work Status')
|
||||||
as Map<String, dynamic>?;
|
as Map<String, dynamic>?;
|
||||||
}
|
}
|
||||||
@ -465,6 +461,7 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
res != null
|
res != null
|
||||||
? _parseResponseForAllData(res, label: 'Master Work Categories')
|
? _parseResponseForAllData(res, label: 'Master Work Categories')
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
static Future<bool> approveTask({
|
static Future<bool> approveTask({
|
||||||
required String id,
|
required String id,
|
||||||
required String comment,
|
required String comment,
|
||||||
@ -517,7 +514,7 @@ static dynamic _parseResponseForAllData(http.Response response,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log("Failed to create task: ${json['message'] ?? 'Unknown error'}");
|
logSafe("Failed to create task: ${json['message'] ?? 'Unknown error'}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,23 +6,43 @@ import 'package:marco/helpers/services/storage/local_storage.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';
|
||||||
import 'package:url_strategy/url_strategy.dart';
|
import 'package:url_strategy/url_strategy.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> initializeApp() async {
|
Future<void> initializeApp() async {
|
||||||
setPathUrlStrategy();
|
try {
|
||||||
|
logSafe("Starting app initialization...");
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
setPathUrlStrategy();
|
||||||
statusBarColor: Color.fromARGB(255, 255, 0, 0),
|
logSafe("URL strategy set.");
|
||||||
statusBarIconBrightness: Brightness.light,
|
|
||||||
));
|
|
||||||
|
|
||||||
await LocalStorage.init();
|
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||||
await ThemeCustomizer.init();
|
statusBarColor: Color.fromARGB(255, 255, 0, 0),
|
||||||
Get.put(PermissionController());
|
statusBarIconBrightness: Brightness.light,
|
||||||
Get.put(ProjectController(), permanent: true);
|
));
|
||||||
AppStyle.init();
|
logSafe("System UI overlay style set.");
|
||||||
|
|
||||||
appLogger.i("App initialization completed successfully.");
|
await LocalStorage.init();
|
||||||
|
logSafe("Local storage initialized.");
|
||||||
|
|
||||||
|
await ThemeCustomizer.init();
|
||||||
|
logSafe("Theme customizer initialized.");
|
||||||
|
|
||||||
|
Get.put(PermissionController());
|
||||||
|
logSafe("PermissionController injected.");
|
||||||
|
|
||||||
|
Get.put(ProjectController(), permanent: true);
|
||||||
|
logSafe("ProjectController injected as permanent.");
|
||||||
|
|
||||||
|
AppStyle.init();
|
||||||
|
logSafe("AppStyle initialized.");
|
||||||
|
|
||||||
|
logSafe("App initialization completed successfully.");
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
logSafe("Error during app initialization",
|
||||||
|
level: LogLevel.error,
|
||||||
|
error: e,
|
||||||
|
stackTrace: stacktrace,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,21 @@ late final FileLogOutput fileLogOutput;
|
|||||||
/// Initialize logging (call once in `main()`)
|
/// Initialize logging (call once in `main()`)
|
||||||
Future<void> initLogging() async {
|
Future<void> initLogging() async {
|
||||||
await requestStoragePermission();
|
await requestStoragePermission();
|
||||||
|
|
||||||
fileLogOutput = FileLogOutput();
|
fileLogOutput = FileLogOutput();
|
||||||
|
|
||||||
appLogger = Logger(
|
appLogger = Logger(
|
||||||
printer: SimpleFileLogPrinter(),
|
printer: PrettyPrinter(
|
||||||
output: fileLogOutput,
|
methodCount: 0,
|
||||||
|
printTime: true,
|
||||||
|
colors: true,
|
||||||
|
printEmojis: true,
|
||||||
|
),
|
||||||
|
output: MultiOutput([
|
||||||
|
ConsoleOutput(), // ✅ Console will use the top-level PrettyPrinter
|
||||||
|
fileLogOutput, // ✅ File will still use the SimpleFileLogPrinter
|
||||||
|
]),
|
||||||
|
level: Level.debug,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,6 +38,34 @@ Future<void> requestStoragePermission() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Safe logger wrapper
|
||||||
|
void logSafe(
|
||||||
|
String message, {
|
||||||
|
LogLevel level = LogLevel.info,
|
||||||
|
dynamic error,
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
bool sensitive = false,
|
||||||
|
}) {
|
||||||
|
if (sensitive) return;
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.debug:
|
||||||
|
appLogger.d(message, error: error, stackTrace: stackTrace);
|
||||||
|
break;
|
||||||
|
case LogLevel.warning:
|
||||||
|
appLogger.w(message, error: error, stackTrace: stackTrace);
|
||||||
|
break;
|
||||||
|
case LogLevel.error:
|
||||||
|
appLogger.e(message, error: error, stackTrace: stackTrace);
|
||||||
|
break;
|
||||||
|
case LogLevel.verbose:
|
||||||
|
appLogger.v(message, error: error, stackTrace: stackTrace);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
appLogger.i(message, error: error, stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Custom log output that writes to a local `.txt` file
|
/// Custom log output that writes to a local `.txt` file
|
||||||
class FileLogOutput extends LogOutput {
|
class FileLogOutput extends LogOutput {
|
||||||
File? _logFile;
|
File? _logFile;
|
||||||
@ -54,6 +93,9 @@ class FileLogOutput extends LogOutput {
|
|||||||
@override
|
@override
|
||||||
void output(OutputEvent event) async {
|
void output(OutputEvent event) async {
|
||||||
await _init();
|
await _init();
|
||||||
|
|
||||||
|
if (event.lines.isEmpty) return;
|
||||||
|
|
||||||
final logMessage = event.lines.join('\n') + '\n';
|
final logMessage = event.lines.join('\n') + '\n';
|
||||||
await _logFile!.writeAsString(
|
await _logFile!.writeAsString(
|
||||||
logMessage,
|
logMessage,
|
||||||
@ -97,11 +139,18 @@ class FileLogOutput extends LogOutput {
|
|||||||
class SimpleFileLogPrinter extends LogPrinter {
|
class SimpleFileLogPrinter extends LogPrinter {
|
||||||
@override
|
@override
|
||||||
List<String> log(LogEvent event) {
|
List<String> log(LogEvent event) {
|
||||||
|
final message = event.message.toString();
|
||||||
|
|
||||||
|
if (message.contains('[SENSITIVE]')) return [];
|
||||||
|
|
||||||
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
|
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
|
||||||
final level = event.level.name.toUpperCase();
|
final level = event.level.name.toUpperCase();
|
||||||
final message = event.message;
|
|
||||||
final error = event.error != null ? ' | ERROR: ${event.error}' : '';
|
final error = event.error != null ? ' | ERROR: ${event.error}' : '';
|
||||||
final stack = event.stackTrace != null ? '\nSTACKTRACE:\n${event.stackTrace}' : '';
|
final stack =
|
||||||
|
event.stackTrace != null ? '\nSTACKTRACE:\n${event.stackTrace}' : '';
|
||||||
return ['[$timestamp] [$level] $message$error$stack'];
|
return ['[$timestamp] [$level] $message$error$stack'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional log level enum for better type safety
|
||||||
|
enum LogLevel { debug, info, warning, error, verbose }
|
||||||
|
@ -6,7 +6,7 @@ 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/api_endpoints.dart';
|
import 'package:marco/helpers/services/api_endpoints.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';
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||||
@ -19,6 +19,7 @@ class AuthService {
|
|||||||
/// Login with email and password
|
/// Login with email and password
|
||||||
static Future<Map<String, String>?> loginUser(Map<String, dynamic> data) async {
|
static Future<Map<String, String>?> loginUser(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
|
logSafe("Attempting login...");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse("$_baseUrl/auth/login-mobile"),
|
Uri.parse("$_baseUrl/auth/login-mobile"),
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
@ -30,12 +31,14 @@ class AuthService {
|
|||||||
await _handleLoginSuccess(responseData['data']);
|
await _handleLoginSuccess(responseData['data']);
|
||||||
return null;
|
return null;
|
||||||
} else if (response.statusCode == 401) {
|
} else if (response.statusCode == 401) {
|
||||||
|
logSafe("Invalid login credentials.", level: LogLevel.warning);
|
||||||
return {"password": "Invalid email or password"};
|
return {"password": "Invalid email or password"};
|
||||||
} else {
|
} else {
|
||||||
|
logSafe("Login error: ${responseData['message']}", level: LogLevel.warning);
|
||||||
return {"error": responseData['message'] ?? "Unexpected error occurred"};
|
return {"error": responseData['message'] ?? "Unexpected error occurred"};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Login error: $e");
|
logSafe("Login exception", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return {"error": "Network error. Please check your connection."};
|
return {"error": "Network error. Please check your connection."};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,7 +49,7 @@ class AuthService {
|
|||||||
final refreshToken = await LocalStorage.getRefreshToken();
|
final refreshToken = await LocalStorage.getRefreshToken();
|
||||||
|
|
||||||
if (accessToken == null || refreshToken == null || accessToken.isEmpty || refreshToken.isEmpty) {
|
if (accessToken == null || refreshToken == null || accessToken.isEmpty || refreshToken.isEmpty) {
|
||||||
appLogger.w("Missing access/refresh token.");
|
logSafe("Missing access or refresh token.", level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +59,7 @@ class AuthService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logSafe("Refreshing token...");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse("$_baseUrl/auth/refresh-token"),
|
Uri.parse("$_baseUrl/auth/refresh-token"),
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
@ -67,14 +71,14 @@ class AuthService {
|
|||||||
await LocalStorage.setJwtToken(data['data']['token']);
|
await LocalStorage.setJwtToken(data['data']['token']);
|
||||||
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
|
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
|
||||||
await LocalStorage.setLoggedInUser(true);
|
await LocalStorage.setLoggedInUser(true);
|
||||||
appLogger.i("Token refreshed.");
|
logSafe("Token refreshed successfully.");
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
appLogger.w("Refresh token failed: ${data['message']}");
|
logSafe("Refresh token failed: ${data['message']}", level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Token refresh error: $e");
|
logSafe("Token refresh exception", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,6 +86,7 @@ class AuthService {
|
|||||||
/// Forgot password
|
/// Forgot password
|
||||||
static Future<Map<String, String>?> forgotPassword(String email) async {
|
static Future<Map<String, String>?> forgotPassword(String email) async {
|
||||||
try {
|
try {
|
||||||
|
logSafe("Forgot password requested.");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse("$_baseUrl/auth/forgot-password"),
|
Uri.parse("$_baseUrl/auth/forgot-password"),
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
@ -91,8 +96,8 @@ class AuthService {
|
|||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||||
return {"error": data['message'] ?? "Failed to send reset link."};
|
return {"error": data['message'] ?? "Failed to send reset link."};
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Forgot password error: $e");
|
logSafe("Forgot password error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return {"error": "Network error. Please check your connection."};
|
return {"error": "Network error. Please check your connection."};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,6 +105,7 @@ class AuthService {
|
|||||||
/// Request demo
|
/// Request demo
|
||||||
static Future<Map<String, String>?> requestDemo(Map<String, dynamic> demoData) async {
|
static Future<Map<String, String>?> requestDemo(Map<String, dynamic> demoData) async {
|
||||||
try {
|
try {
|
||||||
|
logSafe("Submitting demo request...");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse("$_baseUrl/market/inquiry"),
|
Uri.parse("$_baseUrl/market/inquiry"),
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
@ -109,8 +115,8 @@ class AuthService {
|
|||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||||
return {"error": data['message'] ?? "Failed to submit demo request."};
|
return {"error": data['message'] ?? "Failed to submit demo request."};
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Request demo error: $e");
|
logSafe("Request demo error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return {"error": "Network error. Please check your connection."};
|
return {"error": "Network error. Please check your connection."};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,6 +124,7 @@ class AuthService {
|
|||||||
/// Get list of industries
|
/// Get list of industries
|
||||||
static Future<List<Map<String, dynamic>>?> getIndustries() async {
|
static Future<List<Map<String, dynamic>>?> getIndustries() async {
|
||||||
try {
|
try {
|
||||||
|
logSafe("Fetching industries list...");
|
||||||
final response = await http.get(
|
final response = await http.get(
|
||||||
Uri.parse("$_baseUrl/market/industries"),
|
Uri.parse("$_baseUrl/market/industries"),
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
@ -128,8 +135,8 @@ class AuthService {
|
|||||||
return List<Map<String, dynamic>>.from(data['data']);
|
return List<Map<String, dynamic>>.from(data['data']);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Get industries error: $e");
|
logSafe("Get industries error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,6 +149,7 @@ class AuthService {
|
|||||||
final token = await LocalStorage.getJwtToken();
|
final token = await LocalStorage.getJwtToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logSafe("Generating MPIN...");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse("$_baseUrl/auth/generate-mpin"),
|
Uri.parse("$_baseUrl/auth/generate-mpin"),
|
||||||
headers: {
|
headers: {
|
||||||
@ -154,8 +162,8 @@ class AuthService {
|
|||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||||
return {"error": data['message'] ?? "Failed to generate MPIN."};
|
return {"error": data['message'] ?? "Failed to generate MPIN."};
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Generate MPIN error: $e");
|
logSafe("Generate MPIN error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return {"error": "Network error. Please check your connection."};
|
return {"error": "Network error. Please check your connection."};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,6 +179,7 @@ class AuthService {
|
|||||||
final token = await LocalStorage.getJwtToken();
|
final token = await LocalStorage.getJwtToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logSafe("Verifying MPIN...");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse("$_baseUrl/auth/login-mpin"),
|
Uri.parse("$_baseUrl/auth/login-mpin"),
|
||||||
headers: {
|
headers: {
|
||||||
@ -187,8 +196,8 @@ class AuthService {
|
|||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||||
return {"error": data['message'] ?? "MPIN verification failed."};
|
return {"error": data['message'] ?? "MPIN verification failed."};
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Verify MPIN error: $e");
|
logSafe("Verify MPIN error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return {"error": "Network error. Please check your connection."};
|
return {"error": "Network error. Please check your connection."};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,6 +205,7 @@ class AuthService {
|
|||||||
/// Generate OTP
|
/// Generate OTP
|
||||||
static Future<Map<String, String>?> generateOtp(String email) async {
|
static Future<Map<String, String>?> generateOtp(String email) async {
|
||||||
try {
|
try {
|
||||||
|
logSafe("Generating OTP for email...");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse("$_baseUrl/auth/send-otp"),
|
Uri.parse("$_baseUrl/auth/send-otp"),
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
@ -205,8 +215,8 @@ class AuthService {
|
|||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
if (response.statusCode == 200 && data['success'] == true) return null;
|
if (response.statusCode == 200 && data['success'] == true) return null;
|
||||||
return {"error": data['message'] ?? "Failed to generate OTP."};
|
return {"error": data['message'] ?? "Failed to generate OTP."};
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Generate OTP error: $e");
|
logSafe("Generate OTP error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return {"error": "Network error. Please check your connection."};
|
return {"error": "Network error. Please check your connection."};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,6 +227,7 @@ class AuthService {
|
|||||||
required String otp,
|
required String otp,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
logSafe("Verifying OTP...");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse("$_baseUrl/auth/login-otp"),
|
Uri.parse("$_baseUrl/auth/login-otp"),
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
@ -229,14 +240,16 @@ class AuthService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {"error": data['message'] ?? "OTP verification failed."};
|
return {"error": data['message'] ?? "OTP verification failed."};
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Verify OTP error: $e");
|
logSafe("Verify OTP error", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return {"error": "Network error. Please check your connection."};
|
return {"error": "Network error. Please check your connection."};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle login success flow
|
/// Handle login success flow
|
||||||
static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async {
|
static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async {
|
||||||
|
logSafe("Processing login success...");
|
||||||
|
|
||||||
final jwtToken = data['token'];
|
final jwtToken = data['token'];
|
||||||
final refreshToken = data['refreshToken'];
|
final refreshToken = data['refreshToken'];
|
||||||
final mpinToken = data['mpinToken'];
|
final mpinToken = data['mpinToken'];
|
||||||
@ -256,9 +269,10 @@ class AuthService {
|
|||||||
|
|
||||||
final permissionController = Get.put(PermissionController());
|
final permissionController = Get.put(PermissionController());
|
||||||
await permissionController.loadData(jwtToken);
|
await permissionController.loadData(jwtToken);
|
||||||
|
|
||||||
await Get.find<ProjectController>().fetchProjects();
|
await Get.find<ProjectController>().fetchProjects();
|
||||||
|
|
||||||
isLoggedIn = true;
|
isLoggedIn = true;
|
||||||
appLogger.i("Login success initialized.");
|
logSafe("Login flow completed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import 'dart:convert';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/model/user_permission.dart';
|
import 'package:marco/model/user_permission.dart';
|
||||||
import 'package:marco/model/employee_info.dart';
|
import 'package:marco/model/employee_info.dart';
|
||||||
import 'package:marco/model/projects_model.dart';
|
import 'package:marco/model/projects_model.dart';
|
||||||
@ -10,18 +10,19 @@ import 'package:marco/helpers/services/storage/local_storage.dart';
|
|||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionService {
|
class PermissionService {
|
||||||
static final Map<String, Map<String, dynamic>> _userDataCache = {};
|
static final Map<String, Map<String, dynamic>> _userDataCache = {};
|
||||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||||
|
|
||||||
/// Fetches all user-related data (permissions, employee info, projects)
|
/// Fetches all user-related data (permissions, employee info, projects)
|
||||||
static Future<Map<String, dynamic>> fetchAllUserData(
|
static Future<Map<String, dynamic>> fetchAllUserData(
|
||||||
String token, {
|
String token, {
|
||||||
bool hasRetried = false,
|
bool hasRetried = false,
|
||||||
}) async {
|
}) async {
|
||||||
// Return cached data if already available
|
logSafe("Fetching user data...", sensitive: true);
|
||||||
|
|
||||||
if (_userDataCache.containsKey(token)) {
|
if (_userDataCache.containsKey(token)) {
|
||||||
|
logSafe("User data cache hit.", sensitive: true);
|
||||||
return _userDataCache[token]!;
|
return _userDataCache[token]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,8 +31,10 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await http.get(uri, headers: headers);
|
final response = await http.get(uri, headers: headers);
|
||||||
|
final statusCode = response.statusCode;
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (statusCode == 200) {
|
||||||
|
logSafe("User data fetched successfully.");
|
||||||
final data = json.decode(response.body)['data'];
|
final data = json.decode(response.body)['data'];
|
||||||
|
|
||||||
final result = {
|
final result = {
|
||||||
@ -44,8 +47,9 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle 401 by attempting a single retry with refreshed token
|
if (statusCode == 401 && !hasRetried) {
|
||||||
if (response.statusCode == 401 && !hasRetried) {
|
logSafe("Unauthorized. Attempting token refresh...", level: LogLevel.warning);
|
||||||
|
|
||||||
final refreshed = await AuthService.refreshToken();
|
final refreshed = await AuthService.refreshToken();
|
||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
final newToken = await LocalStorage.getJwtToken();
|
final newToken = await LocalStorage.getJwtToken();
|
||||||
@ -55,19 +59,23 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _handleUnauthorized();
|
await _handleUnauthorized();
|
||||||
|
logSafe("Token refresh failed. Redirecting to login.", level: LogLevel.warning);
|
||||||
throw Exception('Unauthorized. Token refresh failed.');
|
throw Exception('Unauthorized. Token refresh failed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
final error = json.decode(response.body)['message'] ?? 'Unknown error';
|
final error = json.decode(response.body)['message'] ?? 'Unknown error';
|
||||||
|
logSafe("Failed to fetch user data: $error", level: LogLevel.warning);
|
||||||
throw Exception('Failed to fetch user data: $error');
|
throw Exception('Failed to fetch user data: $error');
|
||||||
} catch (e) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e('Error fetching user data: $e');
|
logSafe("Exception while fetching user data", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears auth data and redirects to login
|
/// Clears auth data and redirects to login
|
||||||
static Future<void> _handleUnauthorized() async {
|
static Future<void> _handleUnauthorized() async {
|
||||||
|
logSafe("Clearing tokens and redirecting to login due to unauthorized access.", level: LogLevel.warning);
|
||||||
|
|
||||||
await LocalStorage.removeToken('jwt_token');
|
await LocalStorage.removeToken('jwt_token');
|
||||||
await LocalStorage.removeToken('refresh_token');
|
await LocalStorage.removeToken('refresh_token');
|
||||||
await LocalStorage.setLoggedInUser(false);
|
await LocalStorage.setLoggedInUser(false);
|
||||||
@ -76,6 +84,7 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
|
|||||||
|
|
||||||
/// Converts raw permission data into list of `UserPermission`
|
/// Converts raw permission data into list of `UserPermission`
|
||||||
static List<UserPermission> _parsePermissions(List<dynamic> permissions) {
|
static List<UserPermission> _parsePermissions(List<dynamic> permissions) {
|
||||||
|
logSafe("Parsing user permissions...");
|
||||||
return permissions
|
return permissions
|
||||||
.map((id) => UserPermission.fromJson({'id': id}))
|
.map((id) => UserPermission.fromJson({'id': id}))
|
||||||
.toList();
|
.toList();
|
||||||
@ -83,11 +92,13 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
|
|||||||
|
|
||||||
/// Converts raw employee JSON into `EmployeeInfo`
|
/// Converts raw employee JSON into `EmployeeInfo`
|
||||||
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> data) {
|
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> data) {
|
||||||
|
logSafe("Parsing employee info...");
|
||||||
return EmployeeInfo.fromJson(data);
|
return EmployeeInfo.fromJson(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts raw projects JSON into list of `ProjectInfo`
|
/// Converts raw projects JSON into list of `ProjectInfo`
|
||||||
static List<ProjectInfo> _parseProjectsInfo(List<dynamic> projects) {
|
static List<ProjectInfo> _parseProjectsInfo(List<dynamic> projects) {
|
||||||
|
logSafe("Parsing projects info...");
|
||||||
return projects.map((proj) => ProjectInfo.fromJson(proj)).toList();
|
return projects.map((proj) => ProjectInfo.fromJson(proj)).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,40 +8,61 @@ import 'package:marco/helpers/services/app_logger.dart';
|
|||||||
|
|
||||||
|
|
||||||
Future<Uint8List?> compressImageToUnder100KB(File file) async {
|
Future<Uint8List?> compressImageToUnder100KB(File file) async {
|
||||||
int quality = 40;
|
int quality = 40;
|
||||||
Uint8List? result;
|
Uint8List? result;
|
||||||
|
|
||||||
const int maxWidth = 800;
|
const int maxWidth = 800;
|
||||||
const int maxHeight = 800;
|
const int maxHeight = 800;
|
||||||
|
|
||||||
|
logSafe("Starting image compression...", sensitive: true);
|
||||||
|
|
||||||
while (quality >= 10) {
|
while (quality >= 10) {
|
||||||
result = await FlutterImageCompress.compressWithFile(
|
try {
|
||||||
file.absolute.path,
|
result = await FlutterImageCompress.compressWithFile(
|
||||||
quality: quality,
|
file.absolute.path,
|
||||||
minWidth: maxWidth,
|
quality: quality,
|
||||||
minHeight: maxHeight,
|
minWidth: maxWidth,
|
||||||
format: CompressFormat.jpeg,
|
minHeight: maxHeight,
|
||||||
);
|
format: CompressFormat.jpeg,
|
||||||
|
);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
appLogger.i('Quality: $quality, Size: ${(result.lengthInBytes / 1024).toStringAsFixed(2)} KB');
|
logSafe(
|
||||||
|
'Compression quality: $quality, size: ${(result.lengthInBytes / 1024).toStringAsFixed(2)} KB',
|
||||||
|
);
|
||||||
|
|
||||||
if (result.lengthInBytes <= 100 * 1024) {
|
if (result.lengthInBytes <= 100 * 1024) {
|
||||||
return result;
|
logSafe("Image compressed successfully under 100KB.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logSafe("Compression returned null at quality $quality", level: LogLevel.warning);
|
||||||
}
|
}
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
logSafe("Compression error at quality $quality", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
quality -= 10;
|
quality -= 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logSafe("Failed to compress image under 100KB. Returning best effort result.", level: LogLevel.warning);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File> saveCompressedImageToFile(Uint8List bytes) async {
|
Future<File> saveCompressedImageToFile(Uint8List bytes) async {
|
||||||
final tempDir = await getTemporaryDirectory();
|
try {
|
||||||
final filePath = path.join(
|
final tempDir = await getTemporaryDirectory();
|
||||||
tempDir.path,
|
final filePath = path.join(
|
||||||
'compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
|
tempDir.path,
|
||||||
);
|
'compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||||||
final file = File(filePath);
|
);
|
||||||
return await file.writeAsBytes(bytes);
|
final file = File(filePath);
|
||||||
|
final savedFile = await file.writeAsBytes(bytes);
|
||||||
|
|
||||||
|
logSafe("Compressed image saved to ${savedFile.path}", sensitive: true);
|
||||||
|
return savedFile;
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
logSafe("Error saving compressed image", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,13 @@ import 'package:marco/helpers/services/app_logger.dart';
|
|||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
await initLogging();
|
await initLogging();
|
||||||
|
logSafe("App starting...");
|
||||||
appLogger.i("App starting...");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await initializeApp();
|
await initializeApp();
|
||||||
|
logSafe("App initialized successfully.");
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ChangeNotifierProvider<AppNotifier>(
|
ChangeNotifierProvider<AppNotifier>(
|
||||||
create: (_) => AppNotifier(),
|
create: (_) => AppNotifier(),
|
||||||
@ -21,12 +22,21 @@ Future<void> main() async {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e('App failed to initialize:', error: e, stackTrace: stacktrace);
|
logSafe('App failed to initialize.',
|
||||||
|
level: LogLevel.error,
|
||||||
|
error: e,
|
||||||
|
stackTrace: stacktrace,
|
||||||
|
);
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
const MaterialApp(
|
const MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: Center(child: Text("Failed to initialize the app.")),
|
body: Center(
|
||||||
|
child: Text(
|
||||||
|
"Failed to initialize the app.",
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/extensions/app_localization_delegate.dart';
|
import 'package:marco/helpers/extensions/app_localization_delegate.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';
|
||||||
@ -14,33 +14,30 @@ import 'package:marco/helpers/theme/theme_customizer.dart';
|
|||||||
import 'package:marco/helpers/theme/app_notifier.dart';
|
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||||
import 'package:marco/routes.dart';
|
import 'package:marco/routes.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
Future<String> _getInitialRoute() async {
|
Future<String> _getInitialRoute() async {
|
||||||
try {
|
try {
|
||||||
if (!AuthService.isLoggedIn) {
|
if (!AuthService.isLoggedIn) {
|
||||||
appLogger.i("User not logged in. Routing to /auth/login-option");
|
logSafe("User not logged in. Routing to /auth/login-option");
|
||||||
return "/auth/login-option";
|
return "/auth/login-option";
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool hasMpin = LocalStorage.getIsMpin();
|
final bool hasMpin = LocalStorage.getIsMpin();
|
||||||
appLogger.i("MPIN enabled: $hasMpin");
|
logSafe("MPIN enabled: $hasMpin", sensitive: true);
|
||||||
|
|
||||||
if (hasMpin) {
|
if (hasMpin) {
|
||||||
await LocalStorage.setBool("mpin_verified", false);
|
await LocalStorage.setBool("mpin_verified", false);
|
||||||
appLogger
|
logSafe("Routing to /auth/mpin-auth and setting mpin_verified to false");
|
||||||
.i("Routing to /auth/mpin-auth and setting mpin_verified to false");
|
|
||||||
return "/auth/mpin-auth";
|
return "/auth/mpin-auth";
|
||||||
} else {
|
} else {
|
||||||
appLogger.i("MPIN not enabled. Routing to /home");
|
logSafe("MPIN not enabled. Routing to /dashboard");
|
||||||
return "/dashboard";
|
return "/dashboard";
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
appLogger.e("Error determining initial route",
|
logSafe("Error determining initial route",
|
||||||
error: e, stackTrace: stacktrace);
|
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
return "/auth/login-option";
|
return "/auth/login-option";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,6 +50,8 @@ class MyApp extends StatelessWidget {
|
|||||||
future: _getInitialRoute(),
|
future: _getInitialRoute(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
|
logSafe("FutureBuilder snapshot error",
|
||||||
|
level: LogLevel.error, error: snapshot.error);
|
||||||
return const MaterialApp(
|
return const MaterialApp(
|
||||||
home: Center(child: Text("Error determining route")),
|
home: Center(child: Text("Error determining route")),
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user