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