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:
Vaibhav Surve 2025-06-25 12:10:57 +05:30
parent e6d05e247e
commit ec6c24464e
26 changed files with 895 additions and 943 deletions

View File

@ -24,47 +24,45 @@ class ForgotPasswordController extends MyController {
); );
} }
Future<void> onForgotPassword() async { Future<void> onForgotPassword() async {
if (!basicValidator.validateForm()) return; if (!basicValidator.validateForm()) return;
isLoading.value = true; isLoading.value = true;
final data = basicValidator.getData(); final data = basicValidator.getData();
final email = data['email']?.toString() ?? ''; final email = data['email']?.toString() ?? '';
try { try {
appLogger.i("Forgot password requested for: $email"); logSafe("Forgot password requested for: $email", sensitive: true);
final result = await AuthService.forgotPassword(email);
if (result == null) { final result = await AuthService.forgotPassword(email);
// Success case
if (result == null) {
showAppSnackbar(
title: "Success",
message: "Password reset link has been sent.",
type: SnackbarType.success,
);
await LocalStorage.logout();
} else {
final errorMessage = result['error'] ?? "Failed to send reset link. Please try again.";
showAppSnackbar(
title: "Failed",
message: errorMessage,
type: SnackbarType.error,
);
logSafe("Failed to send reset password email for $email: $errorMessage", level: LogLevel.warning, sensitive: true);
}
} catch (e, stacktrace) {
logSafe("Error during forgot password", level: LogLevel.error, error: e, stackTrace: stacktrace);
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Error",
message: "Password reset link has been sent.", message: "Something went wrong. Please try again later.",
type: SnackbarType.success,
);
await LocalStorage.logout();
} else {
// Failure case with error map
final errorMessage = result['error'] ?? "Failed to send reset link. Please try again.";
showAppSnackbar(
title: "Failed",
message: errorMessage,
type: SnackbarType.error, type: SnackbarType.error,
); );
appLogger.w("Failed to send reset password email for $email: $errorMessage"); } finally {
isLoading.value = false;
} }
} catch (e, stacktrace) {
appLogger.e("Error during forgot password", error: e, stackTrace: stacktrace);
showAppSnackbar(
title: "Error",
message: "Something went wrong. Please try again later.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
} }
}
void gotoLogIn() { void gotoLogIn() {
Get.offAllNamed('/auth/login-option'); Get.offAllNamed('/auth/login-option');

View File

@ -55,12 +55,12 @@ class LoginController extends MyController {
try { try {
final loginData = basicValidator.getData(); final loginData = basicValidator.getData();
appLogger.i("Attempting login for user: ${loginData['username']}"); logSafe("Attempting login for user: ${loginData['username']}", sensitive: true);
final errors = await AuthService.loginUser(loginData); final errors = await AuthService.loginUser(loginData);
if (errors != null) { if (errors != null) {
appLogger.w("Login failed: $errors"); logSafe("Login failed for user: ${loginData['username']} with errors: $errors", level: LogLevel.warning, sensitive: true);
showAppSnackbar( showAppSnackbar(
title: "Login Failed", title: "Login Failed",
@ -73,11 +73,11 @@ class LoginController extends MyController {
basicValidator.clearErrors(); basicValidator.clearErrors();
} else { } else {
await _handleRememberMe(); await _handleRememberMe();
appLogger.i("Login successful: ${loginData['username']}"); logSafe("Login successful for user: ${loginData['username']}", sensitive: true);
Get.toNamed('/home'); Get.toNamed('/home');
} }
} catch (e, stacktrace) { } catch (e, stacktrace) {
appLogger.e("Exception during login", error: e, stackTrace: stacktrace); logSafe("Exception during login", level: LogLevel.error, error: e, stackTrace: stacktrace);
showAppSnackbar( showAppSnackbar(
title: "Login Error", title: "Login Error",
message: "An unexpected error occurred", message: "An unexpected error occurred",
@ -90,10 +90,8 @@ class LoginController extends MyController {
Future<void> _handleRememberMe() async { Future<void> _handleRememberMe() async {
if (isChecked.value) { if (isChecked.value) {
await LocalStorage.setToken( await LocalStorage.setToken('username', basicValidator.getController('username')!.text);
'username', basicValidator.getController('username')!.text); await LocalStorage.setToken('password', basicValidator.getController('password')!.text);
await LocalStorage.setToken(
'password', basicValidator.getController('password')!.text);
await LocalStorage.setBool('remember_me', true); await LocalStorage.setBool('remember_me', true);
} else { } else {
await LocalStorage.removeToken('username'); await LocalStorage.removeToken('username');

View File

@ -5,10 +5,9 @@ import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/view/dashboard/dashboard_screen.dart'; import 'package:marco/view/dashboard/dashboard_screen.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class MPINController extends GetxController { class MPINController extends GetxController {
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();
final isNewUser = false.obs; final isNewUser = false.obs;
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
@ -20,17 +19,17 @@ class MPINController extends GetxController {
final retypeControllers = List.generate(6, (_) => TextEditingController()); final retypeControllers = List.generate(6, (_) => TextEditingController());
final retypeFocusNodes = List.generate(6, (_) => FocusNode()); final retypeFocusNodes = List.generate(6, (_) => FocusNode());
final RxInt failedAttempts = 0.obs; final RxInt failedAttempts = 0.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
final bool hasMpin = LocalStorage.getIsMpin(); final bool hasMpin = LocalStorage.getIsMpin();
isNewUser.value = !hasMpin; isNewUser.value = !hasMpin;
appLogger.i("[MPINController] onInit called. isNewUser: ${isNewUser.value}"); logSafe("onInit called. isNewUser: ${isNewUser.value}");
} }
void onDigitChanged(String value, int index, {bool isRetype = false}) { void onDigitChanged(String value, int index, {bool isRetype = false}) {
appLogger.i( logSafe("onDigitChanged -> index: $index, value: $value, isRetype: $isRetype", sensitive: true);
"[MPINController] onDigitChanged -> index: $index, value: $value, isRetype: $isRetype");
final nodes = isRetype ? retypeFocusNodes : focusNodes; final nodes = isRetype ? retypeFocusNodes : focusNodes;
if (value.isNotEmpty && index < 5) { if (value.isNotEmpty && index < 5) {
nodes[index + 1].requestFocus(); nodes[index + 1].requestFocus();
@ -40,15 +39,15 @@ class MPINController extends GetxController {
} }
Future<void> onSubmitMPIN() async { Future<void> onSubmitMPIN() async {
appLogger.i("[MPINController] onSubmitMPIN triggered"); logSafe("onSubmitMPIN triggered");
if (!formKey.currentState!.validate()) { if (!formKey.currentState!.validate()) {
appLogger.w("[MPINController] Form validation failed"); logSafe("Form validation failed", level: LogLevel.warning);
return; return;
} }
final enteredMPIN = digitControllers.map((c) => c.text).join(); final enteredMPIN = digitControllers.map((c) => c.text).join();
appLogger.i("[MPINController] Entered MPIN: $enteredMPIN"); logSafe("Entered MPIN: $enteredMPIN", sensitive: true);
if (enteredMPIN.length < 6) { if (enteredMPIN.length < 6) {
_showError("Please enter all 6 digits."); _showError("Please enter all 6 digits.");
@ -57,7 +56,7 @@ class MPINController extends GetxController {
if (isNewUser.value) { if (isNewUser.value) {
final retypeMPIN = retypeControllers.map((c) => c.text).join(); final retypeMPIN = retypeControllers.map((c) => c.text).join();
appLogger.i("[MPINController] Retyped MPIN: $retypeMPIN"); logSafe("Retyped MPIN: $retypeMPIN", sensitive: true);
if (retypeMPIN.length < 6) { if (retypeMPIN.length < 6) {
_showError("Please enter all 6 digits in Retype MPIN."); _showError("Please enter all 6 digits in Retype MPIN.");
@ -71,11 +70,11 @@ class MPINController extends GetxController {
return; return;
} }
appLogger.i("[MPINController] MPINs matched. Proceeding to generate MPIN."); logSafe("MPINs matched. Proceeding to generate MPIN.");
final bool success = await generateMPIN(mpin: enteredMPIN); final bool success = await generateMPIN(mpin: enteredMPIN);
if (success) { if (success) {
appLogger.i("[MPINController] MPIN generation successful."); logSafe("MPIN generation successful.");
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "MPIN generated successfully. Please login again.", message: "MPIN generated successfully. Please login again.",
@ -83,32 +82,32 @@ class MPINController extends GetxController {
); );
await LocalStorage.logout(); await LocalStorage.logout();
} else { } else {
appLogger.w("[MPINController] MPIN generation failed."); logSafe("MPIN generation failed.", level: LogLevel.warning);
clearFields(); clearFields();
clearRetypeFields(); clearRetypeFields();
} }
} else { } else {
appLogger.i("[MPINController] Existing user. Proceeding to verify MPIN."); logSafe("Existing user. Proceeding to verify MPIN.");
await verifyMPIN(); await verifyMPIN();
} }
} }
Future<void> onForgotMPIN() async { Future<void> onForgotMPIN() async {
appLogger.i("[MPINController] onForgotMPIN called"); logSafe("onForgotMPIN called");
isNewUser.value = true; isNewUser.value = true;
clearFields(); clearFields();
clearRetypeFields(); clearRetypeFields();
} }
void switchToEnterMPIN() { void switchToEnterMPIN() {
appLogger.i("[MPINController] switchToEnterMPIN called"); logSafe("switchToEnterMPIN called");
isNewUser.value = false; isNewUser.value = false;
clearFields(); clearFields();
clearRetypeFields(); clearRetypeFields();
} }
void _showError(String message) { void _showError(String message) {
appLogger.e("[MPINController] ERROR: $message"); logSafe("ERROR: $message", level: LogLevel.error);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: message, message: message,
@ -118,8 +117,7 @@ class MPINController extends GetxController {
void _navigateToDashboard({String? message}) { void _navigateToDashboard({String? message}) {
if (message != null) { if (message != null) {
appLogger logSafe("Navigating to Dashboard with message: $message");
.i("[MPINController] Navigating to Dashboard with message: $message");
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: message, message: message,
@ -130,7 +128,7 @@ class MPINController extends GetxController {
} }
void clearFields() { void clearFields() {
appLogger.i("[MPINController] clearFields called"); logSafe("clearFields called");
for (final c in digitControllers) { for (final c in digitControllers) {
c.clear(); c.clear();
} }
@ -138,7 +136,7 @@ class MPINController extends GetxController {
} }
void clearRetypeFields() { void clearRetypeFields() {
appLogger.i("[MPINController] clearRetypeFields called"); logSafe("clearRetypeFields called");
for (final c in retypeControllers) { for (final c in retypeControllers) {
c.clear(); c.clear();
} }
@ -147,7 +145,7 @@ class MPINController extends GetxController {
@override @override
void onClose() { void onClose() {
appLogger.i("[MPINController] onClose called"); logSafe("onClose called");
for (final controller in digitControllers) { for (final controller in digitControllers) {
controller.dispose(); controller.dispose();
} }
@ -168,7 +166,7 @@ class MPINController extends GetxController {
}) async { }) async {
try { try {
isLoading.value = true; isLoading.value = true;
appLogger.i("[MPINController] generateMPIN started for MPIN: $mpin"); logSafe("generateMPIN started");
final employeeInfo = LocalStorage.getEmployeeInfo(); final employeeInfo = LocalStorage.getEmployeeInfo();
final String? employeeId = employeeInfo?.id; final String? employeeId = employeeInfo?.id;
@ -179,8 +177,7 @@ class MPINController extends GetxController {
return false; return false;
} }
appLogger.i( logSafe("Calling AuthService.generateMpin for employeeId: $employeeId", sensitive: true);
"[MPINController] Calling AuthService.generateMpin for employeeId: $employeeId");
final response = await AuthService.generateMpin( final response = await AuthService.generateMpin(
employeeId: employeeId, employeeId: employeeId,
@ -190,7 +187,7 @@ class MPINController extends GetxController {
isLoading.value = false; isLoading.value = false;
if (response == null) { if (response == null) {
appLogger.i("[MPINController] MPIN generated successfully"); logSafe("MPIN generated successfully");
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
@ -202,8 +199,7 @@ class MPINController extends GetxController {
return true; return true;
} else { } else {
appLogger.w( logSafe("MPIN generation returned error: $response", level: LogLevel.warning);
"[MPINController] MPIN generation returned error response: $response");
showAppSnackbar( showAppSnackbar(
title: "MPIN Generation Failed", title: "MPIN Generation Failed",
message: "Please check your inputs.", message: "Please check your inputs.",
@ -216,17 +212,17 @@ class MPINController extends GetxController {
} }
} catch (e) { } catch (e) {
isLoading.value = false; isLoading.value = false;
_showError("Failed to generate MPIN: $e"); logSafe("Exception in generateMPIN", level: LogLevel.error, error: e);
appLogger.e("[MPINController] Exception in generateMPIN: $e"); _showError("Failed to generate MPIN.");
return false; return false;
} }
} }
Future<void> verifyMPIN() async { Future<void> verifyMPIN() async {
appLogger.i("[MPINController] verifyMPIN triggered"); logSafe("verifyMPIN triggered");
final enteredMPIN = digitControllers.map((c) => c.text).join(); final enteredMPIN = digitControllers.map((c) => c.text).join();
appLogger.i("[MPINController] Entered MPIN: $enteredMPIN"); logSafe("Entered MPIN: $enteredMPIN", sensitive: true);
if (enteredMPIN.length < 6) { if (enteredMPIN.length < 6) {
_showError("Please enter all 6 digits."); _showError("Please enter all 6 digits.");
@ -251,9 +247,7 @@ class MPINController extends GetxController {
isLoading.value = false; isLoading.value = false;
if (response == null) { if (response == null) {
appLogger.i("[MPINController] MPIN verified successfully."); logSafe("MPIN verified successfully");
// Set mpin_verified to true in local storage here:
await LocalStorage.setBool('mpin_verified', true); await LocalStorage.setBool('mpin_verified', true);
showAppSnackbar( showAppSnackbar(
@ -264,7 +258,7 @@ class MPINController extends GetxController {
_navigateToDashboard(); _navigateToDashboard();
} else { } else {
final errorMessage = response["error"] ?? "Invalid MPIN"; final errorMessage = response["error"] ?? "Invalid MPIN";
appLogger.w("[MPINController] MPIN verification failed: $errorMessage"); logSafe("MPIN verification failed: $errorMessage", level: LogLevel.warning);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: errorMessage, message: errorMessage,
@ -275,8 +269,7 @@ class MPINController extends GetxController {
} }
} catch (e) { } catch (e) {
isLoading.value = false; isLoading.value = false;
final error = "Failed to verify MPIN: $e"; logSafe("Exception in verifyMPIN", level: LogLevel.error, error: e);
appLogger.e("[MPINController] Exception in verifyMPIN: $error");
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Something went wrong. Please try again.", message: "Something went wrong. Please try again.",

View File

@ -4,7 +4,7 @@ import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class OTPController extends GetxController { class OTPController extends GetxController {
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
@ -25,7 +25,7 @@ class OTPController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
timer.value = 0; timer.value = 0;
appLogger.i("[OTPController] Initialized"); logSafe("[OTPController] Initialized");
} }
@override @override
@ -38,18 +38,23 @@ class OTPController extends GetxController {
for (final node in focusNodes) { for (final node in focusNodes) {
node.dispose(); node.dispose();
} }
appLogger.i("[OTPController] Disposed"); logSafe("[OTPController] Disposed");
super.onClose(); super.onClose();
} }
Future<bool> _sendOTP(String email) async { Future<bool> _sendOTP(String email) async {
appLogger.i("[OTPController] Sending OTP to $email"); logSafe("[OTPController] Sending OTP");
final result = await AuthService.generateOtp(email); final result = await AuthService.generateOtp(email);
if (result == null) { if (result == null) {
appLogger.i("[OTPController] OTP sent successfully to $email"); logSafe("[OTPController] OTP sent successfully");
return true; return true;
} else { } else {
appLogger.w("[OTPController] OTP send failed: ${result['error']}"); logSafe(
"[OTPController] OTP send failed",
level: LogLevel.warning,
error: result['error'],
);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: result['error'] ?? "Failed to send OTP", message: result['error'] ?? "Failed to send OTP",
@ -61,10 +66,10 @@ class OTPController extends GetxController {
Future<void> sendOTP() async { Future<void> sendOTP() async {
final userEmail = emailController.text.trim(); final userEmail = emailController.text.trim();
appLogger.i("[OTPController] sendOTP called for $userEmail"); logSafe("[OTPController] sendOTP called");
if (!_validateEmail(userEmail)) { if (!_validateEmail(userEmail)) {
appLogger.w("[OTPController] Invalid email format: $userEmail"); logSafe("[OTPController] Invalid email format", level: LogLevel.warning);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Please enter a valid email address", message: "Please enter a valid email address",
@ -89,7 +94,7 @@ class OTPController extends GetxController {
Future<void> onResendOTP() async { Future<void> onResendOTP() async {
if (isResending.value) return; if (isResending.value) return;
appLogger.i("[OTPController] Resending OTP to ${email.value}"); logSafe("[OTPController] Resending OTP");
isResending.value = true; isResending.value = true;
_clearOTPFields(); _clearOTPFields();
@ -103,7 +108,7 @@ class OTPController extends GetxController {
} }
void onOTPChanged(String value, int index) { void onOTPChanged(String value, int index) {
appLogger.d("[OTPController] OTP field changed: index=$index, value=$value"); logSafe("[OTPController] OTP field changed: index=$index", level: LogLevel.debug);
if (value.isNotEmpty) { if (value.isNotEmpty) {
if (index < otpControllers.length - 1) { if (index < otpControllers.length - 1) {
focusNodes[index + 1].requestFocus(); focusNodes[index + 1].requestFocus();
@ -119,7 +124,7 @@ class OTPController extends GetxController {
Future<void> verifyOTP() async { Future<void> verifyOTP() async {
final enteredOTP = otpControllers.map((c) => c.text).join(); final enteredOTP = otpControllers.map((c) => c.text).join();
appLogger.i("[OTPController] Verifying OTP: $enteredOTP for email: ${email.value}"); logSafe("[OTPController] Verifying OTP");
final result = await AuthService.verifyOtp( final result = await AuthService.verifyOtp(
email: email.value, email: email.value,
@ -127,23 +132,19 @@ class OTPController extends GetxController {
); );
if (result == null) { if (result == null) {
appLogger.i("[OTPController] OTP verified successfully"); logSafe("[OTPController] OTP verified successfully");
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "OTP verified successfully", message: "OTP verified successfully",
type: SnackbarType.success, type: SnackbarType.success,
); );
final bool isMpinEnabled = LocalStorage.getIsMpin(); final bool isMpinEnabled = LocalStorage.getIsMpin();
appLogger.i("[OTPController] MPIN Enabled: $isMpinEnabled"); logSafe("[OTPController] MPIN Enabled: $isMpinEnabled");
if (isMpinEnabled) { Get.offAllNamed('/home');
Get.offAllNamed('/home');
} else {
Get.offAllNamed('/home');
}
} else { } else {
final error = result['error'] ?? "Failed to verify OTP"; final error = result['error'] ?? "Failed to verify OTP";
appLogger.w("[OTPController] OTP verification failed: $error"); logSafe("[OTPController] OTP verification failed", level: LogLevel.warning, error: error, sensitive: true);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: error, message: error,
@ -153,7 +154,7 @@ class OTPController extends GetxController {
} }
void _clearOTPFields() { void _clearOTPFields() {
appLogger.d("[OTPController] Clearing OTP input fields"); logSafe("[OTPController] Clearing OTP input fields", level: LogLevel.debug);
for (final controller in otpControllers) { for (final controller in otpControllers) {
controller.clear(); controller.clear();
} }
@ -161,7 +162,7 @@ class OTPController extends GetxController {
} }
void _startTimer() { void _startTimer() {
appLogger.i("[OTPController] Starting resend timer"); logSafe("[OTPController] Starting resend timer");
timer.value = 60; timer.value = 60;
_countdownTimer?.cancel(); _countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
@ -174,7 +175,7 @@ class OTPController extends GetxController {
} }
void resetForChangeEmail() { void resetForChangeEmail() {
appLogger.i("[OTPController] Resetting OTP form for change email"); logSafe("[OTPController] Resetting OTP form for change email");
isOTPSent.value = false; isOTPSent.value = false;
email.value = ''; email.value = '';

View File

@ -12,7 +12,7 @@ class RegisterAccountController extends MyController {
@override @override
void onInit() { void onInit() {
appLogger.i("[RegisterAccountController] onInit called"); logSafe("[RegisterAccountController] onInit called");
basicValidator.addField( basicValidator.addField(
'email', 'email',
@ -46,32 +46,33 @@ class RegisterAccountController extends MyController {
Future<void> onLogin() async { Future<void> onLogin() async {
if (basicValidator.validateForm()) { if (basicValidator.validateForm()) {
update(); update();
appLogger.i("[RegisterAccountController] Submitting registration data: ${basicValidator.getData()}"); final data = basicValidator.getData();
logSafe("[RegisterAccountController] Submitting registration data");
var errors = await AuthService.loginUser(basicValidator.getData()); final errors = await AuthService.loginUser(data);
if (errors != null) { if (errors != null) {
appLogger.w("[RegisterAccountController] Login errors: $errors"); logSafe("[RegisterAccountController] Login errors: $errors", level: LogLevel.warning);
basicValidator.addErrors(errors); basicValidator.addErrors(errors);
basicValidator.validateForm(); basicValidator.validateForm();
basicValidator.clearErrors(); basicValidator.clearErrors();
} }
appLogger.i("[RegisterAccountController] Redirecting to /starter"); logSafe("[RegisterAccountController] Redirecting to /starter");
Get.toNamed('/starter'); Get.toNamed('/starter');
update(); update();
} else { } else {
appLogger.w("[RegisterAccountController] Validation failed"); logSafe("[RegisterAccountController] Validation failed", level: LogLevel.warning);
} }
} }
void onChangeShowPassword() { void onChangeShowPassword() {
showPassword = !showPassword; showPassword = !showPassword;
appLogger.i("[RegisterAccountController] showPassword toggled: $showPassword"); logSafe("[RegisterAccountController] showPassword toggled: $showPassword");
update(); update();
} }
void gotoLogin() { void gotoLogin() {
appLogger.i("[RegisterAccountController] Navigating to /auth/login-option"); logSafe("[RegisterAccountController] Navigating to /auth/login-option");
Get.toNamed('/auth/login-option'); Get.toNamed('/auth/login-option');
} }
} }

View File

@ -4,7 +4,7 @@ import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart'; import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class ResetPasswordController extends MyController { class ResetPasswordController extends MyController {
MyFormValidator basicValidator = MyFormValidator(); MyFormValidator basicValidator = MyFormValidator();
@ -14,7 +14,7 @@ class ResetPasswordController extends MyController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
appLogger.i("[ResetPasswordController] onInit called"); logSafe("[ResetPasswordController] onInit called");
basicValidator.addField( basicValidator.addField(
'password', 'password',
@ -33,39 +33,39 @@ class ResetPasswordController extends MyController {
} }
Future<void> onResetPassword() async { Future<void> onResetPassword() async {
appLogger.i("[ResetPasswordController] onResetPassword triggered"); logSafe("[ResetPasswordController] onResetPassword triggered");
if (basicValidator.validateForm()) { if (basicValidator.validateForm()) {
final data = basicValidator.getData(); final data = basicValidator.getData();
appLogger.i("[ResetPasswordController] Form data: $data"); logSafe("[ResetPasswordController] Reset password form data");
update(); update();
var errors = await AuthService.loginUser(data);
final errors = await AuthService.loginUser(data); // Consider renaming this to resetPassword() for clarity
if (errors != null) { if (errors != null) {
appLogger.w("[ResetPasswordController] Received errors: $errors"); logSafe("[ResetPasswordController] Received errors: $errors", level: LogLevel.warning);
basicValidator.addErrors(errors); basicValidator.addErrors(errors);
basicValidator.validateForm(); basicValidator.validateForm();
basicValidator.clearErrors(); basicValidator.clearErrors();
} }
appLogger.i("[ResetPasswordController] Navigating to /home"); logSafe("[ResetPasswordController] Navigating to /home");
Get.toNamed('/home'); Get.toNamed('/home');
update(); update();
} else { } else {
appLogger.w("[ResetPasswordController] Form validation failed"); logSafe("[ResetPasswordController] Form validation failed", level: LogLevel.warning);
} }
} }
void onChangeShowPassword() { void onChangeShowPassword() {
showPassword = !showPassword; showPassword = !showPassword;
appLogger.i("[ResetPasswordController] showPassword toggled: $showPassword"); logSafe("[ResetPasswordController] showPassword toggled: $showPassword");
update(); update();
} }
void onConfirmPassword() { void onConfirmPassword() {
confirmPassword = !confirmPassword; confirmPassword = !confirmPassword;
appLogger.i("[ResetPasswordController] confirmPassword toggled: $confirmPassword"); logSafe("[ResetPasswordController] confirmPassword toggled: $confirmPassword");
update(); update();
} }
} }

View File

@ -6,7 +6,7 @@ import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
enum Gender { enum Gender {
male, male,
@ -16,7 +16,6 @@ enum Gender {
const Gender(); const Gender();
} }
class AddEmployeeController extends MyController { class AddEmployeeController extends MyController {
List<PlatformFile> files = []; List<PlatformFile> files = [];
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();
@ -58,15 +57,15 @@ class AddEmployeeController extends MyController {
"+33": 9, "+33": 9,
"+86": 11, "+86": 11,
}; };
String selectedCountryCode = "+91";
String selectedCountryCode = "+91";
bool showOnline = true; bool showOnline = true;
final List<String> categories = []; final List<String> categories = [];
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
appLogger.i("Initializing AddEmployeeController..."); logSafe("Initializing AddEmployeeController...");
_initializeFields(); _initializeFields();
fetchRoles(); fetchRoles();
} }
@ -90,41 +89,41 @@ class AddEmployeeController extends MyController {
required: true, required: true,
controller: TextEditingController(), controller: TextEditingController(),
); );
appLogger.i("Fields initialized for first_name, phone_number, last_name."); logSafe("Fields initialized for first_name, phone_number, last_name.");
} }
void onGenderSelected(Gender? gender) { void onGenderSelected(Gender? gender) {
selectedGender = gender; selectedGender = gender;
appLogger.i("Gender selected: ${gender?.name}"); logSafe("Gender selected: ${gender?.name}");
update(); update();
} }
Future<void> fetchRoles() async { Future<void> fetchRoles() async {
appLogger.i("Fetching roles..."); logSafe("Fetching roles...");
try { try {
final result = await ApiService.getRoles(); final result = await ApiService.getRoles();
if (result != null) { if (result != null) {
roles = List<Map<String, dynamic>>.from(result); roles = List<Map<String, dynamic>>.from(result);
appLogger.i("Roles fetched successfully."); logSafe("Roles fetched successfully.");
update(); update();
} else { } else {
appLogger.e("Failed to fetch roles: null result"); logSafe("Failed to fetch roles: null result", level: LogLevel.error);
} }
} catch (e, st) { } catch (e, st) {
appLogger.e("Error fetching roles: $e", error: e, stackTrace: st); logSafe("Error fetching roles", level: LogLevel.error, error: e, stackTrace: st);
} }
} }
void onRoleSelected(String? roleId) { void onRoleSelected(String? roleId) {
selectedRoleId = roleId; selectedRoleId = roleId;
appLogger.i("Role selected: $roleId"); logSafe("Role selected: $roleId");
update(); update();
} }
Future<bool> createEmployees() async { Future<bool> createEmployees() async {
appLogger.i("Starting employee creation..."); logSafe("Starting employee creation...");
if (selectedGender == null || selectedRoleId == null) { if (selectedGender == null || selectedRoleId == null) {
appLogger.w("Missing gender or role."); logSafe("Missing gender or role.", level: LogLevel.warning);
showAppSnackbar( showAppSnackbar(
title: "Missing Fields", title: "Missing Fields",
message: "Please select both Gender and Role.", message: "Please select both Gender and Role.",
@ -135,11 +134,9 @@ class AddEmployeeController extends MyController {
final firstName = basicValidator.getController("first_name")?.text.trim(); final firstName = basicValidator.getController("first_name")?.text.trim();
final lastName = basicValidator.getController("last_name")?.text.trim(); final lastName = basicValidator.getController("last_name")?.text.trim();
final phoneNumber = final phoneNumber = basicValidator.getController("phone_number")?.text.trim();
basicValidator.getController("phone_number")?.text.trim();
appLogger.i( logSafe("Creating employee", level: LogLevel.info);
"Creating employee with Name: $firstName $lastName, Phone: $phoneNumber, Gender: ${selectedGender!.name}");
try { try {
final response = await ApiService.createEmployee( final response = await ApiService.createEmployee(
@ -151,7 +148,7 @@ class AddEmployeeController extends MyController {
); );
if (response == true) { if (response == true) {
appLogger.i("Employee created successfully."); logSafe("Employee created successfully.");
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "Employee created successfully!", message: "Employee created successfully!",
@ -159,10 +156,10 @@ class AddEmployeeController extends MyController {
); );
return true; return true;
} else { } else {
appLogger.e("Failed to create employee (response false)."); logSafe("Failed to create employee (response false)", level: LogLevel.error);
} }
} catch (e, st) { } catch (e, st) {
appLogger.e("Error creating employee: $e", error: e, stackTrace: st); logSafe("Error creating employee", level: LogLevel.error, error: e, stackTrace: st);
} }
showAppSnackbar( showAppSnackbar(
@ -176,9 +173,7 @@ class AddEmployeeController extends MyController {
Future<bool> _checkAndRequestContactsPermission() async { Future<bool> _checkAndRequestContactsPermission() async {
final status = await Permission.contacts.request(); final status = await Permission.contacts.request();
if (status.isGranted) { if (status.isGranted) return true;
return true;
}
if (status.isPermanentlyDenied) { if (status.isPermanentlyDenied) {
await openAppSettings(); await openAppSettings();
@ -186,8 +181,7 @@ class AddEmployeeController extends MyController {
showAppSnackbar( showAppSnackbar(
title: "Permission Required", title: "Permission Required",
message: message: "Please allow Contacts permission from settings to pick a contact.",
"Please allow Contacts permission from settings to pick a contact.",
type: SnackbarType.warning, type: SnackbarType.warning,
); );
return false; return false;
@ -195,17 +189,13 @@ class AddEmployeeController extends MyController {
Future<void> pickContact(BuildContext context) async { Future<void> pickContact(BuildContext context) async {
final permissionGranted = await _checkAndRequestContactsPermission(); final permissionGranted = await _checkAndRequestContactsPermission();
if (!permissionGranted) return; if (!permissionGranted) return;
try { try {
final picked = await FlutterContacts.openExternalPick(); final picked = await FlutterContacts.openExternalPick();
if (picked == null) return;
if (picked == null) return; // User canceled contact picking final contact = await FlutterContacts.getContact(picked.id, withProperties: true);
final contact =
await FlutterContacts.getContact(picked.id, withProperties: true);
if (contact == null) { if (contact == null) {
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
@ -223,10 +213,10 @@ class AddEmployeeController extends MyController {
); );
return; return;
} }
final indiaPhones = contact.phones.where((p) { final indiaPhones = contact.phones.where((p) {
final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), ''); final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), '');
return normalized.startsWith('+91') || return normalized.startsWith('+91') || RegExp(r'^\d{10}$').hasMatch(normalized);
RegExp(r'^\d{10}$').hasMatch(normalized);
}).toList(); }).toList();
if (indiaPhones.isEmpty) { if (indiaPhones.isEmpty) {
@ -239,7 +229,6 @@ class AddEmployeeController extends MyController {
} }
String? selectedPhone; String? selectedPhone;
if (indiaPhones.length == 1) { if (indiaPhones.length == 1) {
selectedPhone = indiaPhones.first.number; selectedPhone = indiaPhones.first.number;
} else { } else {
@ -261,24 +250,16 @@ class AddEmployeeController extends MyController {
if (selectedPhone == null) return; if (selectedPhone == null) return;
} }
final normalizedPhone = selectedPhone.replaceAll(RegExp(r'[^0-9]'), ''); final normalizedPhone = selectedPhone.replaceAll(RegExp(r'[^0-9]'), '');
final phoneWithoutCountryCode = normalizedPhone.length > 10
? normalizedPhone.substring(normalizedPhone.length - 10)
: normalizedPhone;
// Remove country code prefix if present, keep only 10 digits basicValidator.getController('phone_number')?.text = phoneWithoutCountryCode;
String phoneWithoutCountryCode;
if (normalizedPhone.length > 10) {
phoneWithoutCountryCode =
normalizedPhone.substring(normalizedPhone.length - 10);
} else {
phoneWithoutCountryCode = normalizedPhone;
}
basicValidator.getController('phone_number')?.text =
phoneWithoutCountryCode;
update(); update();
} catch (e, st) { } catch (e, st) {
appLogger.e("Error fetching contacts: $e", error: e, stackTrace: st); logSafe("Error fetching contacts", level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to fetch contacts.", message: "Failed to fetch contacts.",

View File

@ -17,7 +17,6 @@ import 'package:marco/model/attendance_log_view_model.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
class AttendanceController extends GetxController { class AttendanceController extends GetxController {
// Data lists
List<AttendanceModel> attendances = []; List<AttendanceModel> attendances = [];
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
List<EmployeeModel> employees = []; List<EmployeeModel> employees = [];
@ -25,14 +24,11 @@ class AttendanceController extends GetxController {
List<RegularizationLogModel> regularizationLogs = []; List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = []; List<AttendanceLogViewModel> attendenceLogsView = [];
// Selected values
String selectedTab = 'Employee List'; String selectedTab = 'Employee List';
// Date range for attendance filtering
DateTime? startDateAttendance; DateTime? startDateAttendance;
DateTime? endDateAttendance; DateTime? endDateAttendance;
// Loading states
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
RxBool isLoadingProjects = true.obs; RxBool isLoadingProjects = true.obs;
RxBool isLoadingEmployees = true.obs; RxBool isLoadingEmployees = true.obs;
@ -40,7 +36,6 @@ class AttendanceController extends GetxController {
RxBool isLoadingRegularizationLogs = true.obs; RxBool isLoadingRegularizationLogs = true.obs;
RxBool isLoadingLogView = true.obs; RxBool isLoadingLogView = true.obs;
// Uploading state per employee (keyed by employeeId)
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
@override @override
@ -58,47 +53,40 @@ class AttendanceController extends GetxController {
final today = DateTime.now(); final today = DateTime.now();
startDateAttendance = today.subtract(const Duration(days: 7)); startDateAttendance = today.subtract(const Duration(days: 7));
endDateAttendance = today.subtract(const Duration(days: 1)); endDateAttendance = today.subtract(const Duration(days: 1));
appLogger.i("Default date range set: $startDateAttendance to $endDateAttendance"); logSafe("Default date range set: $startDateAttendance to $endDateAttendance");
} }
/// Checks and requests location permission, returns true if granted.
Future<bool> _handleLocationPermission() async { Future<bool> _handleLocationPermission() async {
LocationPermission permission = await Geolocator.checkPermission(); LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
appLogger.w('Location permissions are denied'); logSafe('Location permissions are denied', level: LogLevel.warning);
return false; return false;
} }
} }
if (permission == LocationPermission.deniedForever) { if (permission == LocationPermission.deniedForever) {
appLogger.e('Location permissions are permanently denied'); logSafe('Location permissions are permanently denied', level: LogLevel.error);
return false; return false;
} }
return true; return true;
} }
/// Fetches projects and initializes selected project.
Future<void> fetchProjects() async { Future<void> fetchProjects() async {
isLoadingProjects.value = true; isLoadingProjects.value = true;
isLoading.value = true; isLoading.value = true;
final response = await ApiService.getProjects(); final response = await ApiService.getProjects();
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList(); projects = response.map((json) => ProjectModel.fromJson(json)).toList();
appLogger.i("Projects fetched: ${projects.length}"); logSafe("Projects fetched: ${projects.length}");
} else { } else {
appLogger.e("Failed to fetch projects or no projects available."); logSafe("Failed to fetch projects or no projects available.", level: LogLevel.error);
projects = []; projects = [];
} }
isLoadingProjects.value = false; isLoadingProjects.value = false;
isLoading.value = false; isLoading.value = false;
update(['attendance_dashboard_controller']); update(['attendance_dashboard_controller']);
} }
@ -109,51 +97,35 @@ class AttendanceController extends GetxController {
await fetchProjectData(projectId); await fetchProjectData(projectId);
} }
/// Fetches employees, attendance logs and regularization logs for a project.
Future<void> fetchProjectData(String? projectId) async { Future<void> fetchProjectData(String? projectId) async {
if (projectId == null) return; if (projectId == null) return;
isLoading.value = true; isLoading.value = true;
await Future.wait([ await Future.wait([
fetchEmployeesByProject(projectId), fetchEmployeesByProject(projectId),
fetchAttendanceLogs(projectId, fetchAttendanceLogs(projectId, dateFrom: startDateAttendance, dateTo: endDateAttendance),
dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchRegularizationLogs(projectId), fetchRegularizationLogs(projectId),
]); ]);
isLoading.value = false; isLoading.value = false;
logSafe("Project data fetched for project ID: $projectId");
appLogger.i("Project data fetched for project ID: $projectId");
} }
/// Fetches employees for the given project.
Future<void> fetchEmployeesByProject(String? projectId) async { Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null) return; if (projectId == null) return;
isLoadingEmployees.value = true; isLoadingEmployees.value = true;
final response = await ApiService.getEmployeesByProject(projectId); final response = await ApiService.getEmployeesByProject(projectId);
if (response != null) { if (response != null) {
employees = response.map((json) => EmployeeModel.fromJson(json)).toList(); employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
// Initialize uploading states for employees
for (var emp in employees) { for (var emp in employees) {
uploadingStates[emp.id] = false.obs; uploadingStates[emp.id] = false.obs;
} }
logSafe("Employees fetched: ${employees.length} for project $projectId");
appLogger.i("Employees fetched: ${employees.length} for project $projectId");
update(); update();
} else { } else {
appLogger.e("Failed to fetch employees for project $projectId"); logSafe("Failed to fetch employees for project $projectId", level: LogLevel.error);
} }
isLoadingEmployees.value = false; isLoadingEmployees.value = false;
} }
/// Captures image, gets location, and uploads attendance data.
/// Returns true on success.
Future<bool> captureAndUploadAttendance( Future<bool> captureAndUploadAttendance(
String id, String id,
String employeeId, String employeeId,
@ -165,87 +137,52 @@ class AttendanceController extends GetxController {
}) async { }) async {
try { try {
uploadingStates[employeeId]?.value = true; uploadingStates[employeeId]?.value = true;
XFile? image; XFile? image;
if (imageCapture) { if (imageCapture) {
image = await ImagePicker().pickImage( image = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 80);
source: ImageSource.camera,
imageQuality: 80,
);
if (image == null) { if (image == null) {
appLogger.w("Image capture cancelled."); logSafe("Image capture cancelled.", level: LogLevel.warning);
uploadingStates[employeeId]?.value = false;
return false; return false;
} }
final compressedBytes = await compressImageToUnder100KB(File(image.path));
final compressedBytes =
await compressImageToUnder100KB(File(image.path));
if (compressedBytes == null) { if (compressedBytes == null) {
appLogger.e("Image compression failed."); logSafe("Image compression failed.", level: LogLevel.error);
uploadingStates[employeeId]?.value = false;
return false; return false;
} }
final compressedFile = await saveCompressedImageToFile(compressedBytes); final compressedFile = await saveCompressedImageToFile(compressedBytes);
image = XFile(compressedFile.path); image = XFile(compressedFile.path);
} }
final hasLocationPermission = await _handleLocationPermission(); final hasLocationPermission = await _handleLocationPermission();
if (!hasLocationPermission) { if (!hasLocationPermission) return false;
uploadingStates[employeeId]?.value = false; final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
return false; final imageName = imageCapture ? ApiService.generateImageName(employeeId, employees.length + 1) : "";
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
final imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1)
: "";
final result = await ApiService.uploadAttendanceImage( final result = await ApiService.uploadAttendanceImage(
id, id, employeeId, image, position.latitude, position.longitude,
employeeId, imageName: imageName, projectId: projectId, comment: comment,
image, action: action, imageCapture: imageCapture, markTime: markTime,
position.latitude,
position.longitude,
imageName: imageName,
projectId: projectId,
comment: comment,
action: action,
imageCapture: imageCapture,
markTime: markTime,
); );
logSafe("Attendance uploaded for $employeeId, action: $action");
appLogger.i("Attendance uploaded for $employeeId, action: $action");
return result; return result;
} catch (e, stacktrace) { } catch (e, stacktrace) {
appLogger.e("Error uploading attendance", error: e, stackTrace: stacktrace); logSafe("Error uploading attendance", level: LogLevel.error, error: e, stackTrace: stacktrace);
return false; return false;
} finally { } finally {
uploadingStates[employeeId]?.value = false; uploadingStates[employeeId]?.value = false;
} }
} }
/// Opens a date range picker for attendance filtering and fetches logs on selection. Future<void> selectDateRangeForAttendance(BuildContext context, AttendanceController controller) async {
Future<void> selectDateRangeForAttendance(
BuildContext context,
AttendanceController controller,
) async {
final today = DateTime.now(); final today = DateTime.now();
final todayDateOnly = DateTime(today.year, today.month, today.day);
final picked = await showDateRangePicker( final picked = await showDateRangePicker(
context: context, context: context,
firstDate: DateTime(2022), firstDate: DateTime(2022),
lastDate: todayDateOnly.subtract(const Duration(days: 1)), lastDate: today.subtract(const Duration(days: 1)),
initialDateRange: DateTimeRange( initialDateRange: DateTimeRange(
start: startDateAttendance ?? today.subtract(const Duration(days: 7)), start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
end: endDateAttendance ?? end: endDateAttendance ?? today.subtract(const Duration(days: 1)),
todayDateOnly.subtract(const Duration(days: 1)),
), ),
builder: (BuildContext context, Widget? child) { builder: (context, child) {
return Center( return Center(
child: SizedBox( child: SizedBox(
width: 400, width: 400,
@ -272,9 +209,7 @@ class AttendanceController extends GetxController {
if (picked != null) { if (picked != null) {
startDateAttendance = picked.start; startDateAttendance = picked.start;
endDateAttendance = picked.end; endDateAttendance = picked.end;
logSafe("Date range selected: $startDateAttendance to $endDateAttendance");
appLogger.i("Date range selected: $startDateAttendance to $endDateAttendance");
await controller.fetchAttendanceLogs( await controller.fetchAttendanceLogs(
Get.find<ProjectController>().selectedProject?.id, Get.find<ProjectController>().selectedProject?.id,
dateFrom: picked.start, dateFrom: picked.start,
@ -283,49 +218,31 @@ class AttendanceController extends GetxController {
} }
} }
/// Fetches attendance logs filtered by project and date range. Future<void> fetchAttendanceLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async {
Future<void> fetchAttendanceLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return; if (projectId == null) return;
isLoadingAttendanceLogs.value = true; isLoadingAttendanceLogs.value = true;
isLoading.value = true; isLoading.value = true;
final response = await ApiService.getAttendanceLogs(projectId, dateFrom: dateFrom, dateTo: dateTo);
final response = await ApiService.getAttendanceLogs(
projectId,
dateFrom: dateFrom,
dateTo: dateTo,
);
if (response != null) { if (response != null) {
attendanceLogs = attendanceLogs = response.map((json) => AttendanceLogModel.fromJson(json)).toList();
response.map((json) => AttendanceLogModel.fromJson(json)).toList(); logSafe("Attendance logs fetched: ${attendanceLogs.length}");
appLogger.i("Attendance logs fetched: ${attendanceLogs.length}");
update(); update();
} else { } else {
appLogger.e("Failed to fetch attendance logs for project $projectId"); logSafe("Failed to fetch attendance logs for project $projectId", level: LogLevel.error);
} }
isLoadingAttendanceLogs.value = false; isLoadingAttendanceLogs.value = false;
isLoading.value = false; isLoading.value = false;
} }
/// Groups attendance logs by check-in date formatted as 'dd MMM yyyy'.
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() { Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{}; final groupedLogs = <String, List<AttendanceLogModel>>{};
for (var logItem in attendanceLogs) { for (var logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null final checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!) ? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown'; : 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []); groupedLogs.putIfAbsent(checkInDate, () => []);
groupedLogs[checkInDate]!.add(logItem); groupedLogs[checkInDate]!.add(logItem);
} }
final sortedEntries = groupedLogs.entries.toList() final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) { ..sort((a, b) {
if (a.key == 'Unknown') return 1; if (a.key == 'Unknown') return 1;
@ -334,66 +251,43 @@ class AttendanceController extends GetxController {
final dateB = DateFormat('dd MMM yyyy').parse(b.key); final dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA); return dateB.compareTo(dateA);
}); });
final sortedMap = Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
final sortedMap = logSafe("Logs grouped and sorted by check-in date.");
Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
appLogger.i("Logs grouped and sorted by check-in date.");
return sortedMap; return sortedMap;
} }
/// Fetches regularization logs for a project. Future<void> fetchRegularizationLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async {
Future<void> fetchRegularizationLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return; if (projectId == null) return;
isLoadingRegularizationLogs.value = true; isLoadingRegularizationLogs.value = true;
isLoading.value = true; isLoading.value = true;
final response = await ApiService.getRegularizationLogs(projectId); final response = await ApiService.getRegularizationLogs(projectId);
if (response != null) { if (response != null) {
regularizationLogs = response regularizationLogs = response.map((json) => RegularizationLogModel.fromJson(json)).toList();
.map((json) => RegularizationLogModel.fromJson(json)) logSafe("Regularization logs fetched: ${regularizationLogs.length}");
.toList();
appLogger.i("Regularization logs fetched: ${regularizationLogs.length}");
update(); update();
} else { } else {
appLogger.e("Failed to fetch regularization logs for project $projectId"); logSafe("Failed to fetch regularization logs for project $projectId", level: LogLevel.error);
} }
isLoadingRegularizationLogs.value = false; isLoadingRegularizationLogs.value = false;
isLoading.value = false; isLoading.value = false;
} }
/// Fetches detailed attendance log view for a specific ID.
Future<void> fetchLogsView(String? id) async { Future<void> fetchLogsView(String? id) async {
if (id == null) return; if (id == null) return;
isLoadingLogView.value = true; isLoadingLogView.value = true;
isLoading.value = true; isLoading.value = true;
final response = await ApiService.getAttendanceLogView(id); final response = await ApiService.getAttendanceLogView(id);
if (response != null) { if (response != null) {
attendenceLogsView = response attendenceLogsView = response.map((json) => AttendanceLogViewModel.fromJson(json)).toList();
.map((json) => AttendanceLogViewModel.fromJson(json))
.toList();
attendenceLogsView.sort((a, b) { attendenceLogsView.sort((a, b) {
if (a.activityTime == null || b.activityTime == null) return 0; if (a.activityTime == null || b.activityTime == null) return 0;
return b.activityTime!.compareTo(a.activityTime!); return b.activityTime!.compareTo(a.activityTime!);
}); });
logSafe("Attendance log view fetched for ID: $id");
appLogger.i("Attendance log view fetched for ID: $id");
update(); update();
} else { } else {
appLogger.e("Failed to fetch attendance log view for ID $id"); logSafe("Failed to fetch attendance log view for ID $id", level: LogLevel.error);
} }
isLoadingLogView.value = false; isLoadingLogView.value = false;
isLoading.value = false; isLoading.value = false;
} }

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/project_model.dart'; import 'package:marco/model/project_model.dart';
import 'package:marco/model/daily_task_model.dart'; import 'package:marco/model/daily_task_model.dart';
@ -25,6 +25,7 @@ class DailyTaskController extends GetxController {
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
Map<String, List<TaskModel>> groupedDailyTasks = {}; Map<String, List<TaskModel>> groupedDailyTasks = {};
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -39,43 +40,53 @@ class DailyTaskController extends GetxController {
final today = DateTime.now(); final today = DateTime.now();
startDateTask = today.subtract(const Duration(days: 7)); startDateTask = today.subtract(const Duration(days: 7));
endDateTask = today; endDateTask = today;
appLogger.i("Default date range set: $startDateTask to $endDateTask");
logSafe(
"Default date range set: $startDateTask to $endDateTask",
level: LogLevel.info,
);
} }
Future<void> fetchTaskData(String? projectId) async { Future<void> fetchTaskData(String? projectId) async {
if (projectId == null) return; if (projectId == null) {
logSafe("fetchTaskData: Skipped, projectId is null", level: LogLevel.warning);
return;
}
isLoading.value = true; isLoading.value = true;
final response = await ApiService.getDailyTasks( final response = await ApiService.getDailyTasks(
projectId, projectId,
dateFrom: startDateTask, dateFrom: startDateTask,
dateTo: endDateTask, dateTo: endDateTask,
); );
isLoading.value = false; isLoading.value = false;
if (response != null) { if (response != null) {
groupedDailyTasks.clear(); groupedDailyTasks.clear();
for (var taskJson in response) { for (var taskJson in response) {
TaskModel task = TaskModel.fromJson(taskJson); final task = TaskModel.fromJson(taskJson);
String assignmentDateKey = final assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0]; task.assignmentDate.toIso8601String().split('T')[0];
if (groupedDailyTasks.containsKey(assignmentDateKey)) { groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
groupedDailyTasks[assignmentDateKey]?.add(task);
} else {
groupedDailyTasks[assignmentDateKey] = [task];
}
} }
// Flatten the grouped tasks into the existing dailyTasks list
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList(); dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
appLogger.i("Daily tasks fetched and grouped: ${dailyTasks.length}"); logSafe(
"Daily tasks fetched and grouped: ${dailyTasks.length} for project $projectId",
level: LogLevel.info,
);
update(); update();
} else { } else {
appLogger.e("Failed to fetch daily tasks for project $projectId"); logSafe(
"Failed to fetch daily tasks for project $projectId",
level: LogLevel.error,
);
} }
} }
@ -88,18 +99,23 @@ class DailyTaskController extends GetxController {
firstDate: DateTime(2022), firstDate: DateTime(2022),
lastDate: DateTime.now(), lastDate: DateTime.now(),
initialDateRange: DateTimeRange( initialDateRange: DateTimeRange(
start: start: startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
end: endDateTask ?? DateTime.now(), end: endDateTask ?? DateTime.now(),
), ),
); );
if (picked == null) return; if (picked == null) {
logSafe("Date range picker cancelled by user.", level: LogLevel.debug);
return;
}
startDateTask = picked.start; startDateTask = picked.start;
endDateTask = picked.end; endDateTask = picked.end;
appLogger.i("Date range selected: $startDateTask to $endDateTask"); logSafe(
"Date range selected: $startDateTask to $endDateTask",
level: LogLevel.info,
);
await controller.fetchTaskData(controller.selectedProjectId); await controller.fetchTaskData(controller.selectedProjectId);
} }

View File

@ -1,12 +1,11 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
class DashboardController extends GetxController { class DashboardController extends GetxController {
// Observables // Observables
final RxList<Map<String, dynamic>> roleWiseData = final RxList<Map<String, dynamic>> roleWiseData = <Map<String, dynamic>>[].obs;
<Map<String, dynamic>>[].obs;
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
final RxString selectedRange = '15D'.obs; final RxString selectedRange = '15D'.obs;
final RxBool isChartView = true.obs; final RxBool isChartView = true.obs;
@ -14,31 +13,33 @@ class DashboardController extends GetxController {
// Inject the ProjectController // Inject the ProjectController
final ProjectController projectController = Get.find<ProjectController>(); final ProjectController projectController = Get.find<ProjectController>();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// Log to verify order of controller initialization logSafe(
appLogger.i('DashboardController initialized with project ID: ${projectController.selectedProjectId.value}'); 'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
level: LogLevel.info,
sensitive: true,
);
if (projectController.selectedProjectId.value.isNotEmpty) { if (projectController.selectedProjectId.value.isNotEmpty) {
fetchRoleWiseAttendance();
}
// React to project change
ever<String>(projectController.selectedProjectId, (id) {
if (id.isNotEmpty) {
appLogger.i('Project changed to $id, fetching attendance');
fetchRoleWiseAttendance(); fetchRoleWiseAttendance();
} }
});
// React to range change // React to project change
ever(selectedRange, (_) { ever<String>(projectController.selectedProjectId, (id) {
fetchRoleWiseAttendance(); if (id.isNotEmpty) {
}); logSafe('Project changed to $id, fetching attendance', level: LogLevel.info, sensitive: true);
} fetchRoleWiseAttendance();
}
});
// React to range change
ever(selectedRange, (_) {
fetchRoleWiseAttendance();
});
}
int get rangeDays => _getDaysFromRange(selectedRange.value); int get rangeDays => _getDaysFromRange(selectedRange.value);
@ -56,13 +57,16 @@ void onInit() {
void updateRange(String range) { void updateRange(String range) {
selectedRange.value = range; selectedRange.value = range;
logSafe('Selected range updated to $range', level: LogLevel.debug);
} }
void toggleChartView(bool isChart) { void toggleChartView(bool isChart) {
isChartView.value = isChart; isChartView.value = isChart;
logSafe('Chart view toggled to: $isChart', level: LogLevel.debug);
} }
Future<void> refreshDashboard() async { Future<void> refreshDashboard() async {
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
await fetchRoleWiseAttendance(); await fetchRoleWiseAttendance();
} }
@ -70,7 +74,7 @@ void onInit() {
final String projectId = projectController.selectedProjectId.value; final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) { if (projectId.isEmpty) {
appLogger.w('Project ID is empty, skipping API call.'); logSafe('Project ID is empty, skipping API call.', level: LogLevel.warning);
return; return;
} }
@ -83,14 +87,19 @@ void onInit() {
if (response != null) { if (response != null) {
roleWiseData.value = roleWiseData.value =
response.map((e) => Map<String, dynamic>.from(e)).toList(); response.map((e) => Map<String, dynamic>.from(e)).toList();
appLogger.i('Attendance overview fetched successfully.'); logSafe('Attendance overview fetched successfully.', level: LogLevel.info);
} else { } else {
appLogger.e('Failed to fetch attendance overview: response is null.');
roleWiseData.clear(); roleWiseData.clear();
logSafe('Failed to fetch attendance overview: response is null.', level: LogLevel.error);
} }
} catch (e, st) { } catch (e, st) {
appLogger.e('Error fetching attendance overview', error: e, stackTrace: st);
roleWiseData.clear(); roleWiseData.clear();
logSafe(
'Error fetching attendance overview',
level: LogLevel.error,
error: e,
stackTrace: st,
);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@ -1,5 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.dart'; import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/project_model.dart'; import 'package:marco/model/project_model.dart';
@ -7,7 +7,6 @@ import 'package:marco/model/employee_model.dart';
import 'package:marco/model/employees/employee_details_model.dart'; import 'package:marco/model/employees/employee_details_model.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
class EmployeesScreenController extends GetxController { class EmployeesScreenController extends GetxController {
List<AttendanceModel> attendances = []; List<AttendanceModel> attendances = [];
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
@ -18,9 +17,9 @@ class EmployeesScreenController extends GetxController {
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
Rxn<EmployeeDetailsModel> selectedEmployeeDetails = Rxn<EmployeeDetailsModel> selectedEmployeeDetails = Rxn<EmployeeDetailsModel>();
Rxn<EmployeeDetailsModel>();
RxBool isLoadingEmployeeDetails = false.obs; RxBool isLoadingEmployeeDetails = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -40,68 +39,113 @@ class EmployeesScreenController extends GetxController {
Future<void> fetchAllProjects() async { Future<void> fetchAllProjects() async {
isLoading.value = true; isLoading.value = true;
await _handleApiCall( await _handleApiCall(
ApiService.getProjects, ApiService.getProjects,
onSuccess: (data) { onSuccess: (data) {
projects = data.map((json) => ProjectModel.fromJson(json)).toList(); projects = data.map((json) => ProjectModel.fromJson(json)).toList();
appLogger.i("Projects fetched: ${projects.length} projects loaded."); logSafe(
"Projects fetched: ${projects.length} projects loaded.",
level: LogLevel.info,
);
},
onEmpty: () {
logSafe("No project data found or API call failed.", level: LogLevel.warning);
}, },
onEmpty: () => appLogger.w("No project data found or API call failed."),
); );
isLoading.value = false; isLoading.value = false;
update(); update();
} }
void clearEmployees() { void clearEmployees() {
employees.clear(); // Correct way to clear RxList employees.clear();
appLogger.i("Employees cleared"); logSafe("Employees cleared", level: LogLevel.info);
update(['employee_screen_controller']); update(['employee_screen_controller']);
} }
Future<void> fetchAllEmployees() async { Future<void> fetchAllEmployees() async {
isLoading.value = true; isLoading.value = true;
await _handleApiCall( await _handleApiCall(
ApiService.getAllEmployees, ApiService.getAllEmployees,
onSuccess: (data) { onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
appLogger.i("All Employees fetched: ${employees.length} employees loaded."); logSafe(
"All Employees fetched: ${employees.length} employees loaded.",
level: LogLevel.info,
);
}, },
onEmpty: () { onEmpty: () {
employees.clear(); // Always clear on empty employees.clear();
appLogger.w("No Employee data found or API call failed."); logSafe("No Employee data found or API call failed.", level: LogLevel.warning);
}, },
); );
isLoading.value = false; isLoading.value = false;
update(['employee_screen_controller']); update(['employee_screen_controller']);
} }
Future<void> fetchEmployeesByProject(String? projectId) async { Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) { if (projectId == null || projectId.isEmpty) {
appLogger.e("Project ID is required but was null or empty."); logSafe("Project ID is required but was null or empty.", level: LogLevel.error);
return; return;
} }
isLoading.value = true; isLoading.value = true;
await _handleApiCall( await _handleApiCall(
() => ApiService.getAllEmployeesByProject(projectId), () => ApiService.getAllEmployeesByProject(projectId),
onSuccess: (data) { onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
for (var emp in employees) { for (var emp in employees) {
uploadingStates[emp.id] = false.obs; uploadingStates[emp.id] = false.obs;
} }
appLogger.i("Employees fetched: ${employees.length} for project $projectId");
logSafe(
"Employees fetched: ${employees.length} for project $projectId",
level: LogLevel.info,
sensitive: true,
);
}, },
onEmpty: () { onEmpty: () {
employees.clear(); employees.clear();
appLogger.w("No employees found for project $projectId."); logSafe("No employees found for project $projectId.", level: LogLevel.warning, sensitive: true);
},
onError: (e) {
logSafe("Error fetching employees for project $projectId", level: LogLevel.error, error: e, sensitive: true);
}, },
onError: (e) =>
appLogger.e("Error fetching employees for project $projectId: $e"),
); );
isLoading.value = false; isLoading.value = false;
update(['employee_screen_controller']); update(['employee_screen_controller']);
} }
Future<void> fetchEmployeeDetails(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return;
isLoadingEmployeeDetails.value = true;
await _handleSingleApiCall(
() => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
logSafe("Employee details loaded for $employeeId", level: LogLevel.info, sensitive: true);
},
onEmpty: () {
selectedEmployeeDetails.value = null;
logSafe("No employee details found for $employeeId", level: LogLevel.warning, sensitive: true);
},
onError: (e) {
selectedEmployeeDetails.value = null;
logSafe("Error fetching employee details for $employeeId", level: LogLevel.error, error: e, sensitive: true);
},
);
isLoadingEmployeeDetails.value = false;
}
Future<void> _handleApiCall( Future<void> _handleApiCall(
Future<List<dynamic>?> Function() apiCall, { Future<List<dynamic>?> Function() apiCall, {
required Function(List<dynamic>) onSuccess, required Function(List<dynamic>) onSuccess,
@ -119,32 +163,11 @@ class EmployeesScreenController extends GetxController {
if (onError != null) { if (onError != null) {
onError(e); onError(e);
} else { } else {
appLogger.e("API call error: $e"); logSafe("API call error", level: LogLevel.error, error: e);
} }
} }
} }
Future<void> fetchEmployeeDetails(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return;
isLoadingEmployeeDetails.value = true;
await _handleSingleApiCall(
() => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
},
onEmpty: () {
selectedEmployeeDetails.value = null;
},
onError: (e) {
selectedEmployeeDetails.value = null;
},
);
isLoadingEmployeeDetails.value = false;
}
Future<void> _handleSingleApiCall( Future<void> _handleSingleApiCall(
Future<Map<String, dynamic>?> Function() apiCall, { Future<Map<String, dynamic>?> Function() apiCall, {
required Function(Map<String, dynamic>) onSuccess, required Function(Map<String, dynamic>) onSuccess,
@ -162,7 +185,7 @@ class EmployeesScreenController extends GetxController {
if (onError != null) { if (onError != null) {
onError(e); onError(e);
} else { } else {
appLogger.e("API call error: $e"); logSafe("API call error", level: LogLevel.error, error: e);
} }
} }
} }

View File

@ -1,12 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/model/project_model.dart'; import 'package:marco/model/project_model.dart';
class LayoutController extends GetxController { class LayoutController extends GetxController {
// Theme Customization // Theme Customization
ThemeCustomizer themeCustomizer = ThemeCustomizer(); ThemeCustomizer themeCustomizer = ThemeCustomizer();
@ -51,20 +49,25 @@ class LayoutController extends GetxController {
super.dispose(); super.dispose();
} }
// Project Handling /// Fetch project list from API and initialize the selection.
Future<void> fetchProjects() async { Future<void> fetchProjects() async {
isLoading.value = true; isLoading.value = true;
isLoadingProjects.value = true; isLoadingProjects.value = true;
final response = await ApiService.getProjects(); try {
final response = await ApiService.getProjects();
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList(); final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
projects.assignAll(fetchedProjects); projects.assignAll(fetchedProjects);
selectedProjectId = RxString(fetchedProjects.first.id.toString()); selectedProjectId = RxString(fetchedProjects.first.id.toString());
appLogger.i("Projects fetched: ${fetchedProjects.length}");
} else { logSafe("Projects fetched: ${fetchedProjects.length}", level: LogLevel.info);
appLogger.w("No projects found or API call failed."); } else {
logSafe("No projects found or API call failed.", level: LogLevel.warning);
}
} catch (e, st) {
logSafe("Error fetching projects", level: LogLevel.error, error: e, stackTrace: st);
} }
isLoadingProjects.value = false; isLoadingProjects.value = false;
@ -72,32 +75,38 @@ class LayoutController extends GetxController {
update(['dashboard_controller']); update(['dashboard_controller']);
} }
/// Update selected project ID
void updateSelectedProject(String projectId) { void updateSelectedProject(String projectId) {
selectedProjectId?.value = projectId; selectedProjectId?.value = projectId;
logSafe("Selected project updated", level: LogLevel.info);
} }
/// Toggle expansion of the project list section
void toggleProjectListExpanded() { void toggleProjectListExpanded() {
isProjectListExpanded.toggle(); isProjectListExpanded.toggle();
logSafe("Project list expanded: ${isProjectListExpanded.value}", level: LogLevel.debug);
} }
// Theme Updates /// Handle theme changes (light/dark, drawer toggles)
void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) { void onChangeTheme(ThemeCustomizer oldVal, ThemeCustomizer newVal) {
themeCustomizer = newVal; themeCustomizer = newVal;
update(); update();
if (newVal.rightBarOpen) { if (newVal.rightBarOpen) {
scaffoldKey.currentState?.openEndDrawer(); scaffoldKey.currentState?.openEndDrawer();
logSafe("Theme changed — end drawer opened", level: LogLevel.debug);
} else { } else {
scaffoldKey.currentState?.closeEndDrawer(); scaffoldKey.currentState?.closeEndDrawer();
logSafe("Theme changed — end drawer closed", level: LogLevel.debug);
} }
} }
// Notification Shade (placeholders) /// Optional notification toggles (placeholder)
void enableNotificationShade() { void enableNotificationShade() {
// Add implementation if needed logSafe("Notification shade enabled (not implemented)", level: LogLevel.verbose);
} }
void disableNotificationShade() { void disableNotificationShade() {
// Add implementation if needed logSafe("Notification shade disabled (not implemented)", level: LogLevel.verbose);
} }
} }

View File

@ -3,14 +3,11 @@ import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/permission_service.dart'; import 'package:marco/helpers/services/permission_service.dart';
import 'package:marco/model/user_permission.dart'; import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employee_info.dart'; import 'package:marco/model/employee_info.dart';
import 'package:marco/model/projects_model.dart'; import 'package:marco/model/projects_model.dart';
class PermissionController extends GetxController { class PermissionController extends GetxController {
var permissions = <UserPermission>[].obs; var permissions = <UserPermission>[].obs;
var employeeInfo = Rxn<EmployeeInfo>(); var employeeInfo = Rxn<EmployeeInfo>();
@ -47,9 +44,9 @@ class PermissionController extends GetxController {
); );
} }
appLogger.i("User data successfully stored in SharedPreferences."); logSafe("User data successfully stored in SharedPreferences.");
} catch (e, stacktrace) { } catch (e, stacktrace) {
appLogger.e("Error storing data", error: e, stackTrace: stacktrace); logSafe("Error storing data", level: LogLevel.error, error: e, stackTrace: stacktrace);
} }
} }
@ -58,7 +55,7 @@ class PermissionController extends GetxController {
if (token?.isNotEmpty ?? false) { if (token?.isNotEmpty ?? false) {
await loadData(token!); await loadData(token!);
} else { } else {
appLogger.w("No token found for loading API data."); logSafe("No token found for loading API data.", level: LogLevel.warning);
} }
} }
@ -67,9 +64,9 @@ class PermissionController extends GetxController {
final userData = await PermissionService.fetchAllUserData(token); final userData = await PermissionService.fetchAllUserData(token);
_updateState(userData); _updateState(userData);
await _storeData(); await _storeData();
appLogger.i("Data loaded and state updated successfully."); logSafe("Data loaded and state updated successfully.");
} catch (e, stacktrace) { } catch (e, stacktrace) {
appLogger.e("Error loading data from API", error: e, stackTrace: stacktrace); logSafe("Error loading data from API", level: LogLevel.error, error: e, stackTrace: stacktrace);
} }
} }
@ -78,45 +75,48 @@ class PermissionController extends GetxController {
permissions.assignAll(userData['permissions']); permissions.assignAll(userData['permissions']);
employeeInfo.value = userData['employeeInfo']; employeeInfo.value = userData['employeeInfo'];
projectsInfo.assignAll(userData['projects']); projectsInfo.assignAll(userData['projects']);
appLogger.i("State updated with new user data.");
logSafe("State updated with new user data.", sensitive: true);
} catch (e, stacktrace) { } catch (e, stacktrace) {
appLogger.e("Error updating state", error: e, stackTrace: stacktrace); logSafe("Error updating state", level: LogLevel.error, error: e, stackTrace: stacktrace);
} }
} }
Future<String?> _getAuthToken() async { Future<String?> _getAuthToken() async {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
return prefs.getString('jwt_token'); final token = prefs.getString('jwt_token');
logSafe("Auth token retrieved successfully.", sensitive: true);
return token;
} catch (e, stacktrace) { } catch (e, stacktrace) {
appLogger.e("Error retrieving auth token", error: e, stackTrace: stacktrace); logSafe("Error retrieving auth token", level: LogLevel.error, error: e, stackTrace: stacktrace);
return null; return null;
} }
} }
void _startAutoRefresh() { void _startAutoRefresh() {
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async { _refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
appLogger.i("Auto-refresh triggered."); logSafe("Auto-refresh triggered.");
await _loadDataFromAPI(); await _loadDataFromAPI();
}); });
} }
bool hasPermission(String permissionId) { bool hasPermission(String permissionId) {
final hasPerm = permissions.any((p) => p.id == permissionId); final hasPerm = permissions.any((p) => p.id == permissionId);
appLogger.d("Checking permission $permissionId: $hasPerm"); logSafe("Checking permission $permissionId: $hasPerm", level: LogLevel.debug);
return hasPerm; return hasPerm;
} }
bool isUserAssignedToProject(String projectId) { bool isUserAssignedToProject(String projectId) {
final assigned = projectsInfo.any((project) => project.id == projectId); final assigned = projectsInfo.any((project) => project.id == projectId);
appLogger.d("Checking project assignment for $projectId: $assigned"); logSafe("Checking project assignment for $projectId: $assigned", level: LogLevel.debug);
return assigned; return assigned;
} }
@override @override
void onClose() { void onClose() {
_refreshTimer?.cancel(); _refreshTimer?.cancel();
appLogger.i("PermissionController disposed and timer cancelled."); logSafe("PermissionController disposed and timer cancelled.");
super.onClose(); super.onClose();
} }
} }

View File

@ -14,9 +14,9 @@ class ProjectController extends GetxController {
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
RxBool isLoadingProjects = true.obs; RxBool isLoadingProjects = true.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
GlobalProjectModel? get selectedProject { GlobalProjectModel? get selectedProject {
if (selectedProjectId.value.isEmpty) return null; if (selectedProjectId.value.isEmpty) return null;
return projects.firstWhereOrNull((p) => p.id == selectedProjectId.value); return projects.firstWhereOrNull((p) => p.id == selectedProjectId.value);
} }
@ -36,7 +36,10 @@ class ProjectController extends GetxController {
isLoadingProjects.value = false; isLoadingProjects.value = false;
isLoading.value = false; isLoading.value = false;
uploadingStates.clear(); uploadingStates.clear();
LocalStorage.saveString('selectedProjectId', ''); LocalStorage.saveString('selectedProjectId', '');
logSafe("Projects cleared and UI states reset.");
update(); update();
} }
@ -49,20 +52,21 @@ class ProjectController extends GetxController {
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
projects.assignAll( projects.assignAll(
response.map((json) => GlobalProjectModel.fromJson(json)).toList()); response.map((json) => GlobalProjectModel.fromJson(json)).toList(),
);
String? savedId = LocalStorage.getString('selectedProjectId'); String? savedId = LocalStorage.getString('selectedProjectId');
if (savedId != null && projects.any((p) => p.id == savedId)) { if (savedId != null && projects.any((p) => p.id == savedId)) {
selectedProjectId.value = savedId; // update value only selectedProjectId.value = savedId;
} else { } else {
selectedProjectId.value = projects.first.id.toString(); // selectedProjectId.value = projects.first.id.toString();
LocalStorage.saveString('selectedProjectId', selectedProjectId.value); LocalStorage.saveString('selectedProjectId', selectedProjectId.value);
} }
isProjectSelectionExpanded.value = false; isProjectSelectionExpanded.value = false;
appLogger.i("Projects fetched: ${projects.length}"); logSafe("Projects fetched: ${projects.length}");
} else { } else {
appLogger.w("No projects found or API call failed."); logSafe("No projects found or API call failed.", level: LogLevel.warning);
} }
isLoadingProjects.value = false; isLoadingProjects.value = false;
@ -72,8 +76,8 @@ class ProjectController extends GetxController {
Future<void> updateSelectedProject(String projectId) async { Future<void> updateSelectedProject(String projectId) async {
selectedProjectId.value = projectId; selectedProjectId.value = projectId;
await LocalStorage.saveString('selectedProjectId', projectId); await LocalStorage.saveString('selectedProjectId', projectId);
logSafe("Selected project updated to $projectId");
update(['selected_project']); update(['selected_project']);
} }
} }

View File

@ -1,11 +1,10 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlaning/master_work_category_model.dart'; import 'package:marco/model/dailyTaskPlaning/master_work_category_model.dart';
class AddTaskController extends GetxController { class AddTaskController extends GetxController {
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator(); MyFormValidator basicValidator = MyFormValidator();
@ -19,6 +18,7 @@ class AddTaskController extends GetxController {
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs; RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs;
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -29,15 +29,11 @@ class AddTaskController extends GetxController {
if (value == null || value.trim().isEmpty) { if (value == null || value.trim().isEmpty) {
return 'This field is required'; return 'This field is required';
} }
if (fieldType == "target") { if (fieldType == "target" && int.tryParse(value.trim()) == null) {
if (int.tryParse(value.trim()) == null) { return 'Please enter a valid number';
return 'Please enter a valid number';
}
} }
if (fieldType == "description") { if (fieldType == "description" && value.trim().length < 5) {
if (value.trim().length < 5) { return 'Description must be at least 5 characters';
return 'Description must be at least 5 characters';
}
} }
return null; return null;
} }
@ -49,7 +45,7 @@ class AddTaskController extends GetxController {
required List<String> taskTeam, required List<String> taskTeam,
DateTime? assignmentDate, DateTime? assignmentDate,
}) async { }) async {
appLogger.i("Starting assign task..."); logSafe("Starting task assignment...", level: LogLevel.info);
final response = await ApiService.assignDailyTask( final response = await ApiService.assignDailyTask(
workItemId: workItemId, workItemId: workItemId,
@ -60,7 +56,7 @@ class AddTaskController extends GetxController {
); );
if (response == true) { if (response == true) {
appLogger.i("Task assigned successfully."); logSafe("Task assigned successfully.", level: LogLevel.info);
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "Task assigned successfully!", message: "Task assigned successfully!",
@ -68,7 +64,7 @@ class AddTaskController extends GetxController {
); );
return true; return true;
} else { } else {
appLogger.e("Failed to assign task."); logSafe("Failed to assign task.", level: LogLevel.error);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to assign task.", message: "Failed to assign task.",
@ -87,7 +83,7 @@ class AddTaskController extends GetxController {
required String categoryId, required String categoryId,
DateTime? assignmentDate, DateTime? assignmentDate,
}) async { }) async {
appLogger.i("Creating new task..."); logSafe("Creating new task...", level: LogLevel.info);
final response = await ApiService.createTask( final response = await ApiService.createTask(
parentTaskId: parentTaskId, parentTaskId: parentTaskId,
@ -97,11 +93,10 @@ class AddTaskController extends GetxController {
activityId: activityId, activityId: activityId,
assignmentDate: assignmentDate, assignmentDate: assignmentDate,
categoryId: categoryId, categoryId: categoryId,
); );
if (response == true) { if (response == true) {
appLogger.i("Task created successfully."); logSafe("Task created successfully.", level: LogLevel.info);
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "Task created successfully!", message: "Task created successfully!",
@ -109,7 +104,7 @@ class AddTaskController extends GetxController {
); );
return true; return true;
} else { } else {
appLogger.e("Failed to create task."); logSafe("Failed to create task.", level: LogLevel.error);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to create task.", message: "Failed to create task.",
@ -122,9 +117,9 @@ class AddTaskController extends GetxController {
Future<void> fetchWorkMasterCategories() async { Future<void> fetchWorkMasterCategories() async {
isLoadingWorkMasterCategories.value = true; isLoadingWorkMasterCategories.value = true;
final response = await ApiService.getMasterWorkCategories(); try {
if (response != null) { final response = await ApiService.getMasterWorkCategories();
try { if (response != null) {
final dataList = response['data'] ?? []; final dataList = response['data'] ?? [];
final parsedList = List<WorkCategoryModel>.from( final parsedList = List<WorkCategoryModel>.from(
@ -132,19 +127,17 @@ class AddTaskController extends GetxController {
); );
workMasterCategories.assignAll(parsedList); workMasterCategories.assignAll(parsedList);
final Map<String, String> mapped = { final mapped = {for (var item in parsedList) item.id: item.name};
for (var item in parsedList) item.id: item.name,
};
categoryIdNameMap.assignAll(mapped); categoryIdNameMap.assignAll(mapped);
appLogger.i("Work categories fetched: ${dataList.length}"); logSafe("Work categories fetched: ${dataList.length}", level: LogLevel.info);
} catch (e) { } else {
appLogger.e("Error parsing work categories: $e"); logSafe("No work categories found or API call failed.", level: LogLevel.warning);
workMasterCategories.clear();
categoryIdNameMap.clear();
} }
} else { } catch (e, st) {
appLogger.w("No work categories found or API call failed."); logSafe("Error parsing work categories", level: LogLevel.error, error: e, stackTrace: st);
workMasterCategories.clear();
categoryIdNameMap.clear();
} }
isLoadingWorkMasterCategories.value = false; isLoadingWorkMasterCategories.value = false;
@ -154,5 +147,6 @@ class AddTaskController extends GetxController {
void selectCategory(String id) { void selectCategory(String id) {
selectedCategoryId.value = id; selectedCategoryId.value = id;
selectedCategoryName.value = categoryIdNameMap[id]; selectedCategoryName.value = categoryIdNameMap[id];
logSafe("Category selected", level: LogLevel.debug, sensitive: true);
} }
} }

View File

@ -1,31 +1,26 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/project_model.dart'; import 'package:marco/model/project_model.dart';
import 'package:marco/model/dailyTaskPlaning/daily_task_planing_model.dart'; import 'package:marco/model/dailyTaskPlaning/daily_task_planing_model.dart';
import 'package:marco/model/employee_model.dart'; import 'package:marco/model/employee_model.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class DailyTaskPlaningController extends GetxController { class DailyTaskPlaningController extends GetxController {
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
List<EmployeeModel> employees = []; List<EmployeeModel> employees = [];
List<TaskPlanningDetailsModel> dailyTasks = []; List<TaskPlanningDetailsModel> dailyTasks = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
List<Map<String, dynamic>> roles = []; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
RxnString selectedRoleId = RxnString();
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs; RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
void updateSelectedEmployees() { MyFormValidator basicValidator = MyFormValidator();
final selected = List<Map<String, dynamic>> roles = [];
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
selectedEmployees.value = selected;
}
RxnString selectedRoleId = RxnString();
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -41,114 +36,113 @@ class DailyTaskPlaningController extends GetxController {
if (value == null || value.trim().isEmpty) { if (value == null || value.trim().isEmpty) {
return 'This field is required'; return 'This field is required';
} }
if (fieldType == "target") { if (fieldType == "target" && int.tryParse(value.trim()) == null) {
if (int.tryParse(value.trim()) == null) { return 'Please enter a valid number';
return 'Please enter a valid number';
}
} }
if (fieldType == "description") { if (fieldType == "description" && value.trim().length < 5) {
if (value.trim().length < 5) { return 'Description must be at least 5 characters';
return 'Description must be at least 5 characters';
}
} }
return null; return null;
} }
Future<void> fetchRoles() async { void updateSelectedEmployees() {
appLogger.i("Fetching roles..."); final selected = employees
final result = await ApiService.getRoles(); .where((e) => uploadingStates[e.id]?.value == true)
if (result != null) { .toList();
roles = List<Map<String, dynamic>>.from(result); selectedEmployees.value = selected;
appLogger.i("Roles fetched successfully."); logSafe("Updated selected employees", level: LogLevel.debug, sensitive: true);
update();
} else {
appLogger.e("Failed to fetch roles.");
}
} }
void onRoleSelected(String? roleId) { void onRoleSelected(String? roleId) {
selectedRoleId.value = roleId; selectedRoleId.value = roleId;
appLogger.i("Role selected: $roleId"); logSafe("Role selected", level: LogLevel.info, sensitive: true);
}
Future<void> fetchRoles() async {
logSafe("Fetching roles...", level: LogLevel.info);
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logSafe("Roles fetched successfully", level: LogLevel.info);
update();
} else {
logSafe("Failed to fetch roles", level: LogLevel.error);
}
} }
Future<bool> assignDailyTask({ Future<bool> assignDailyTask({
required String workItemId, required String workItemId,
required int plannedTask, required int plannedTask,
required String description, required String description,
required List<String> taskTeam, required List<String> taskTeam,
DateTime? assignmentDate, DateTime? assignmentDate,
}) async { }) async {
appLogger.i("Starting assign task..."); logSafe("Starting assign task...", level: LogLevel.info);
final response = await ApiService.assignDailyTask( final response = await ApiService.assignDailyTask(
workItemId: workItemId, workItemId: workItemId,
plannedTask: plannedTask, plannedTask: plannedTask,
description: description, description: description,
taskTeam: taskTeam, taskTeam: taskTeam,
assignmentDate: assignmentDate, assignmentDate: assignmentDate,
); );
if (response == true) { if (response == true) {
appLogger.i("Task assigned successfully."); logSafe("Task assigned successfully", level: LogLevel.info);
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "Task assigned successfully!", message: "Task assigned successfully!",
type: SnackbarType.success, type: SnackbarType.success,
); );
return true; return true;
} else { } else {
appLogger.e("Failed to assign task."); logSafe("Failed to assign task", level: LogLevel.error);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to assign task.", message: "Failed to assign task.",
type: SnackbarType.error, type: SnackbarType.error,
); );
return false; return false;
}
} }
}
Future<void> fetchProjects() async { Future<void> fetchProjects() async {
isLoading.value = true;
try { try {
isLoading.value = true;
final response = await ApiService.getProjects(); final response = await ApiService.getProjects();
if (response?.isEmpty ?? true) { if (response?.isEmpty ?? true) {
appLogger.w("No project data found or API call failed."); logSafe("No project data found or API call failed", level: LogLevel.warning);
return; return;
} }
projects = response!.map((json) => ProjectModel.fromJson(json)).toList(); projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
appLogger.i("Projects fetched: ${projects.length} projects loaded."); logSafe("Projects fetched: ${projects.length} projects loaded", level: LogLevel.info);
update(); update();
} catch (e, stack) { } catch (e, stack) {
appLogger.e("Error fetching projects", error: e, stackTrace: stack); logSafe("Error fetching projects", level: LogLevel.error, error: e, stackTrace: stack);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
Future<void> fetchTaskData(String? projectId) async { Future<void> fetchTaskData(String? projectId) async {
if (projectId == null) return; if (projectId == null) {
logSafe("Project ID is null", level: LogLevel.warning);
return;
}
isLoading.value = true;
try { try {
isLoading.value = true;
final response = await ApiService.getDailyTasksDetails(projectId); final response = await ApiService.getDailyTasksDetails(projectId);
if (response != null) { final data = response?['data'];
final data = response['data']; if (data != null) {
if (data != null) { dailyTasks = [TaskPlanningDetailsModel.fromJson(data)];
dailyTasks = [TaskPlanningDetailsModel.fromJson(data)]; logSafe("Daily task Planning Details fetched", level: LogLevel.info, sensitive: true);
appLogger.i("Daily task Planning Details fetched.");
} else {
appLogger.e("Data field is null");
}
} else { } else {
appLogger.e( logSafe("Data field is null", level: LogLevel.warning);
"Failed to fetch daily task planning Details for project $projectId");
} }
} catch (e, stack) { } catch (e, stack) {
appLogger.e("Error fetching daily task data", error: e, stackTrace: stack); logSafe("Error fetching daily task data", level: LogLevel.error, error: e, stackTrace: stack);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
update(); update();
@ -157,7 +151,7 @@ class DailyTaskPlaningController extends GetxController {
Future<void> fetchEmployeesByProject(String? projectId) async { Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) { if (projectId == null || projectId.isEmpty) {
appLogger.e("Project ID is required but was null or empty."); logSafe("Project ID is required but was null or empty", level: LogLevel.error);
return; return;
} }
@ -165,21 +159,22 @@ class DailyTaskPlaningController extends GetxController {
try { try {
final response = await ApiService.getAllEmployeesByProject(projectId); final response = await ApiService.getAllEmployeesByProject(projectId);
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
employees = employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
response.map((json) => EmployeeModel.fromJson(json)).toList();
for (var emp in employees) { for (var emp in employees) {
uploadingStates[emp.id] = false.obs; uploadingStates[emp.id] = false.obs;
} }
appLogger.i("Employees fetched: ${employees.length} for project $projectId"); logSafe("Employees fetched: ${employees.length} for project $projectId",
level: LogLevel.info, sensitive: true);
} else { } else {
appLogger.w("No employees found for project $projectId.");
employees = []; employees = [];
logSafe("No employees found for project $projectId", level: LogLevel.warning, sensitive: true);
} }
} catch (e) { } catch (e, stack) {
appLogger.e("Error fetching employees for project $projectId: $e"); logSafe("Error fetching employees for project $projectId",
level: LogLevel.error, error: e, stackTrace: stack, sensitive: true);
} finally {
isLoading.value = false;
update();
} }
update();
isLoading.value = false;
} }
} }

View File

@ -4,7 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
@ -14,13 +14,9 @@ import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlaning/work_status_model.dart'; import 'package:marco/model/dailyTaskPlaning/work_status_model.dart';
enum ApiStatus { idle, loading, success, failure } enum ApiStatus { idle, loading, success, failure }
class ReportTaskActionController extends MyController { class ReportTaskActionController extends MyController {
//
// Reactive State
//
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs; final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs; final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
@ -37,9 +33,6 @@ class ReportTaskActionController extends MyController {
final RxString selectedWorkStatusName = ''.obs; final RxString selectedWorkStatusName = ''.obs;
//
// Controllers & Validators
//
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController()); final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
final ImagePicker _picker = ImagePicker(); final ImagePicker _picker = ImagePicker();
@ -72,13 +65,10 @@ class ReportTaskActionController extends MyController {
approvedTaskController, approvedTaskController,
]; ];
//
// Lifecycle Hooks
//
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
appLogger.i("Initializing ReportTaskController..."); logSafe("Initializing ReportTaskController...");
_initializeFormFields(); _initializeFormFields();
} }
@ -87,12 +77,10 @@ class ReportTaskActionController extends MyController {
for (final controller in _allControllers) { for (final controller in _allControllers) {
controller.dispose(); controller.dispose();
} }
logSafe("Disposed all text controllers in ReportTaskActionController.");
super.onClose(); super.onClose();
} }
//
// Form Field Setup
//
void _initializeFormFields() { void _initializeFormFields() {
basicValidator basicValidator
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController) ..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
@ -109,9 +97,6 @@ class ReportTaskActionController extends MyController {
..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController); ..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
} }
//
// Task Approval Logic
//
Future<bool> approveTask({ Future<bool> approveTask({
required String projectId, required String projectId,
required String comment, required String comment,
@ -119,14 +104,11 @@ class ReportTaskActionController extends MyController {
required String approvedTaskCount, required String approvedTaskCount,
List<File>? images, List<File>? images,
}) async { }) async {
appLogger.i("Starting task approval..."); logSafe("approveTask() started", sensitive: false);
appLogger.i("Project ID: $projectId");
appLogger.i("Comment: $comment");
appLogger.i("Report Action ID: $reportActionId");
appLogger.i("Approved Task Count: $approvedTaskCount");
if (projectId.isEmpty || reportActionId.isEmpty) { if (projectId.isEmpty || reportActionId.isEmpty) {
_showError("Project ID and Report Action ID are required."); _showError("Project ID and Report Action ID are required.");
logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
return false; return false;
} }
@ -135,25 +117,29 @@ class ReportTaskActionController extends MyController {
if (approvedTaskInt == null) { if (approvedTaskInt == null) {
_showError("Invalid approved task count."); _showError("Invalid approved task count.");
logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
return false; return false;
} }
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) { if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
_showError("Approved task count cannot exceed completed work."); _showError("Approved task count cannot exceed completed work.");
logSafe("Validation failed: approved > completed", level: LogLevel.warning);
return false; return false;
} }
if (comment.trim().isEmpty) { if (comment.trim().isEmpty) {
_showError("Comment is required."); _showError("Comment is required.");
logSafe("Comment field is empty", level: LogLevel.warning);
return false; return false;
} }
try { try {
reportStatus.value = ApiStatus.loading; reportStatus.value = ApiStatus.loading;
isLoading.value = true; isLoading.value = true;
logSafe("Calling _prepareImages() for approval...");
final imageData = await _prepareImages(images); final imageData = await _prepareImages(images);
logSafe("Calling ApiService.approveTask()");
final success = await ApiService.approveTask( final success = await ApiService.approveTask(
id: projectId, id: projectId,
workStatus: reportActionId, workStatus: reportActionId,
@ -163,15 +149,17 @@ class ReportTaskActionController extends MyController {
); );
if (success) { if (success) {
logSafe("Task approved successfully");
_showSuccess("Task approved successfully!"); _showSuccess("Task approved successfully!");
await taskController.fetchTaskData(projectId); await taskController.fetchTaskData(projectId);
return true; return true;
} else { } else {
logSafe("API returned failure on approveTask", level: LogLevel.error);
_showError("Failed to approve task."); _showError("Failed to approve task.");
return false; return false;
} }
} catch (e) { } catch (e, st) {
appLogger.e("Error approving task: $e"); logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred."); _showError("An error occurred.");
return false; return false;
} finally { } finally {
@ -182,26 +170,26 @@ class ReportTaskActionController extends MyController {
} }
} }
//
// Comment Task Logic
//
Future<void> commentTask({ Future<void> commentTask({
required String projectId, required String projectId,
required String comment, required String comment,
List<File>? images, List<File>? images,
}) async { }) async {
appLogger.i("Starting task comment..."); logSafe("commentTask() started", sensitive: false);
if (commentController.text.trim().isEmpty) { if (commentController.text.trim().isEmpty) {
_showError("Comment is required."); _showError("Comment is required.");
logSafe("Comment field is empty", level: LogLevel.warning);
return; return;
} }
try { try {
isLoading.value = true; isLoading.value = true;
logSafe("Calling _prepareImages() for comment...");
final imageData = await _prepareImages(images); final imageData = await _prepareImages(images);
logSafe("Calling ApiService.commentTask()");
final success = await ApiService.commentTask( final success = await ApiService.commentTask(
id: projectId, id: projectId,
comment: commentController.text.trim(), comment: commentController.text.trim(),
@ -211,31 +199,32 @@ class ReportTaskActionController extends MyController {
}); });
if (success) { if (success) {
logSafe("Comment added successfully");
_showSuccess("Task commented successfully!"); _showSuccess("Task commented successfully!");
await taskController.fetchTaskData(projectId); await taskController.fetchTaskData(projectId);
} else { } else {
logSafe("API returned failure on commentTask", level: LogLevel.error);
_showError("Failed to comment task."); _showError("Failed to comment task.");
} }
} catch (e) { } catch (e, st) {
appLogger.e("Error commenting task: $e"); logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred while commenting the task."); _showError("An error occurred while commenting the task.");
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
//
// API Helpers
//
Future<void> fetchWorkStatuses() async { Future<void> fetchWorkStatuses() async {
logSafe("Fetching work statuses...");
isLoadingWorkStatus.value = true; isLoadingWorkStatus.value = true;
final response = await ApiService.getWorkStatus(); final response = await ApiService.getWorkStatus();
if (response != null) { if (response != null) {
final model = WorkStatusResponseModel.fromJson(response); final model = WorkStatusResponseModel.fromJson(response);
workStatus.assignAll(model.data); workStatus.assignAll(model.data);
logSafe("Fetched ${model.data.length} work statuses");
} else { } else {
appLogger.w("No work statuses found or API call failed."); logSafe("No work statuses found or API call failed", level: LogLevel.warning);
} }
isLoadingWorkStatus.value = false; isLoadingWorkStatus.value = false;
@ -243,8 +232,12 @@ class ReportTaskActionController extends MyController {
} }
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async { Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async {
if (images == null || images.isEmpty) return null; if (images == null || images.isEmpty) {
logSafe("_prepareImages: No images selected.");
return null;
}
logSafe("_prepareImages: Compressing and encoding images...");
final results = await Future.wait(images.map((file) async { final results = await Future.wait(images.map((file) async {
final compressedBytes = await compressImageToUnder100KB(file); final compressedBytes = await compressImageToUnder100KB(file);
if (compressedBytes == null) return null; if (compressedBytes == null) return null;
@ -258,6 +251,7 @@ class ReportTaskActionController extends MyController {
}; };
})); }));
logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
return results.whereType<Map<String, dynamic>>().toList(); return results.whereType<Map<String, dynamic>>().toList();
} }
@ -272,33 +266,28 @@ class ReportTaskActionController extends MyController {
}; };
} }
//
// Image Picker Utils
//
Future<void> pickImages({required bool fromCamera}) async { Future<void> pickImages({required bool fromCamera}) async {
logSafe("Opening image picker...");
if (fromCamera) { if (fromCamera) {
final pickedFile = await _picker.pickImage( final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
source: ImageSource.camera,
imageQuality: 75,
);
if (pickedFile != null) { if (pickedFile != null) {
selectedImages.add(File(pickedFile.path)); selectedImages.add(File(pickedFile.path));
logSafe("Image added from camera: ${pickedFile.path}", sensitive: true);
} }
} else { } else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75); final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path))); selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
logSafe("${pickedFiles.length} images added from gallery.", sensitive: true);
} }
} }
void removeImageAt(int index) { void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) { if (index >= 0 && index < selectedImages.length) {
logSafe("Removing image at index $index", sensitive: true);
selectedImages.removeAt(index); selectedImages.removeAt(index);
} }
} }
//
// Snackbar Feedback
//
void _showError(String message) => showAppSnackbar( void _showError(String message) => showAppSnackbar(
title: "Error", message: message, type: SnackbarType.error); title: "Error", message: message, type: SnackbarType.error);

View File

@ -4,7 +4,7 @@ import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@ -12,11 +12,9 @@ import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'package:marco/helpers/widgets/my_image_compressor.dart'; import 'package:marco/helpers/widgets/my_image_compressor.dart';
enum ApiStatus { idle, loading, success, failure } enum ApiStatus { idle, loading, success, failure }
final DailyTaskPlaningController taskController = final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
Get.put(DailyTaskPlaningController());
final ImagePicker _picker = ImagePicker(); final ImagePicker _picker = ImagePicker();
class ReportTaskController extends MyController { class ReportTaskController extends MyController {
@ -28,7 +26,6 @@ class ReportTaskController extends MyController {
RxList<File> selectedImages = <File>[].obs; RxList<File> selectedImages = <File>[].obs;
// Controllers for each form field
final assignedDateController = TextEditingController(); final assignedDateController = TextEditingController();
final workAreaController = TextEditingController(); final workAreaController = TextEditingController();
final activityController = TextEditingController(); final activityController = TextEditingController();
@ -44,50 +41,37 @@ class ReportTaskController extends MyController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
appLogger.i("Initializing ReportTaskController..."); logSafe("Initializing ReportTaskController...");
basicValidator
basicValidator.addField('assigned_date', ..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
label: "Assigned Date", controller: assignedDateController); ..addField('work_area', label: "Work Area", controller: workAreaController)
basicValidator.addField('work_area', ..addField('activity', label: "Activity", controller: activityController)
label: "Work Area", controller: workAreaController); ..addField('team_size', label: "Team Size", controller: teamSizeController)
basicValidator.addField('activity', ..addField('task_id', label: "Task Id", controller: taskIdController)
label: "Activity", controller: activityController); ..addField('assigned', label: "Assigned", controller: assignedController)
basicValidator.addField('team_size', ..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
label: "Team Size", controller: teamSizeController); ..addField('comment', label: "Comment", required: true, controller: commentController)
basicValidator.addField('task_id', ..addField('assigned_by', label: "Assigned By", controller: assignedByController)
label: "Task Id", controller: taskIdController); ..addField('team_members', label: "Team Members", controller: teamMembersController)
basicValidator.addField('assigned', ..addField('planned_work', label: "Planned Work", controller: plannedWorkController);
label: "Assigned", controller: assignedController); logSafe("Form fields initialized.");
basicValidator.addField('completed_work',
label: "Completed Work",
required: true,
controller: completedWorkController);
basicValidator.addField('comment',
label: "Comment", required: true, controller: commentController);
basicValidator.addField('assigned_by',
label: "Assigned By", controller: assignedByController);
basicValidator.addField('team_members',
label: "Team Members", controller: teamMembersController);
basicValidator.addField('planned_work',
label: "Planned Work", controller: plannedWorkController);
appLogger.i(
"Fields initialized for assigned_date, work_area, activity, team_size, assigned, completed_work, and comment.");
} }
@override @override
void onClose() { void onClose() {
assignedDateController.dispose(); [
workAreaController.dispose(); assignedDateController,
activityController.dispose(); workAreaController,
teamSizeController.dispose(); activityController,
taskIdController.dispose(); teamSizeController,
assignedController.dispose(); taskIdController,
completedWorkController.dispose(); assignedController,
commentController.dispose(); completedWorkController,
assignedByController.dispose(); commentController,
teamMembersController.dispose(); assignedByController,
plannedWorkController.dispose(); teamMembersController,
plannedWorkController,
].forEach((controller) => controller.dispose());
super.onClose(); super.onClose();
} }
@ -99,36 +83,16 @@ class ReportTaskController extends MyController {
required DateTime reportedDate, required DateTime reportedDate,
List<File>? images, List<File>? images,
}) async { }) async {
appLogger.i("Starting task report..."); logSafe("Reporting task for projectId", sensitive: true);
final completedWork = completedWorkController.text.trim(); final completedWork = completedWorkController.text.trim();
if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) {
if (completedWork.isEmpty) { _showError("Completed work must be a positive number.");
showAppSnackbar(
title: "Error",
message: "Completed work is required.",
type: SnackbarType.error,
);
return false;
}
final completedWorkInt = int.tryParse(completedWork);
if (completedWorkInt == null || completedWorkInt < 0) {
showAppSnackbar(
title: "Error",
message: "Completed work must be a positive integer.",
type: SnackbarType.error,
);
return false; return false;
} }
final commentField = commentController.text.trim(); final commentField = commentController.text.trim();
if (commentField.isEmpty) { if (commentField.isEmpty) {
showAppSnackbar( _showError("Comment is required.");
title: "Error",
message: "Comment is required.",
type: SnackbarType.error,
);
return false; return false;
} }
@ -136,63 +100,30 @@ class ReportTaskController extends MyController {
reportStatus.value = ApiStatus.loading; reportStatus.value = ApiStatus.loading;
isLoading.value = true; isLoading.value = true;
List<Map<String, dynamic>>? imageData; final imageData = await _prepareImages(images, "task report");
if (images != null && images.isNotEmpty) {
final imageFutures = images.map((file) async {
final compressedBytes = await compressImageToUnder100KB(file);
if (compressedBytes == null) return null;
final base64Image = base64Encode(compressedBytes);
final fileName = file.path.split('/').last;
final contentType = _getContentTypeFromFileName(fileName);
return {
"fileName": fileName,
"base64Data": base64Image,
"contentType": contentType,
"fileSize": compressedBytes.lengthInBytes,
"description": "Image uploaded for task report",
};
}).toList();
final results = await Future.wait(imageFutures);
imageData = results.whereType<Map<String, dynamic>>().toList();
}
final success = await ApiService.reportTask( final success = await ApiService.reportTask(
id: projectId, id: projectId,
comment: commentField, comment: commentField,
completedTask: completedWorkInt, completedTask: int.parse(completedWork),
checkList: checklist, checkList: checklist,
images: imageData, images: imageData,
); );
if (success) { if (success) {
reportStatus.value = ApiStatus.success; reportStatus.value = ApiStatus.success;
showAppSnackbar( _showSuccess("Task reported successfully!");
title: "Success",
message: "Task reported successfully!",
type: SnackbarType.success,
);
await taskController.fetchTaskData(projectId); await taskController.fetchTaskData(projectId);
return true; return true;
} else { } else {
reportStatus.value = ApiStatus.failure; reportStatus.value = ApiStatus.failure;
showAppSnackbar( _showError("Failed to report task.");
title: "Error",
message: "Failed to report task.",
type: SnackbarType.error,
);
return false; return false;
} }
} catch (e) { } catch (e, s) {
appLogger.e("Error reporting task: $e"); logSafe("Exception while reporting task", level: LogLevel.error, error: e, stackTrace: s);
reportStatus.value = ApiStatus.failure; reportStatus.value = ApiStatus.failure;
showAppSnackbar( _showError("An error occurred while reporting the task.");
title: "Error",
message: "An error occurred while reporting the task.",
type: SnackbarType.error,
);
return false; return false;
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -202,121 +133,116 @@ class ReportTaskController extends MyController {
} }
} }
String _getContentTypeFromFileName(String fileName) {
final ext = fileName.split('.').last.toLowerCase();
switch (ext) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'webp':
return 'image/webp';
case 'gif':
return 'image/gif';
default:
return 'application/octet-stream';
}
}
Future<void> commentTask({ Future<void> commentTask({
required String projectId, required String projectId,
required String comment, required String comment,
List<File>? images, List<File>? images,
}) async { }) async {
appLogger.i("Starting task comment..."); logSafe("Submitting comment for project", sensitive: true);
final commentField = commentController.text.trim(); final commentField = commentController.text.trim();
if (commentField.isEmpty) { if (commentField.isEmpty) {
showAppSnackbar( _showError("Comment is required.");
title: "Error",
message: "Comment is required.",
type: SnackbarType.error,
);
return; return;
} }
try { try {
isLoading.value = true; isLoading.value = true;
List<Map<String, dynamic>>? imageData; final imageData = await _prepareImages(images, "task comment");
if (images != null && images.isNotEmpty) {
final imageFutures = images.map((file) async {
final compressedBytes = await compressImageToUnder100KB(file);
if (compressedBytes == null) return null;
final base64Image = base64Encode(compressedBytes);
final fileName = file.path.split('/').last;
final contentType = _getContentTypeFromFileName(fileName);
return {
"fileName": fileName,
"base64Data": base64Image,
"contentType": contentType,
"fileSize": compressedBytes.lengthInBytes,
"description": "Image uploaded for task comment",
};
}).toList();
final results = await Future.wait(imageFutures);
imageData = results.whereType<Map<String, dynamic>>().toList();
}
final success = await ApiService.commentTask( final success = await ApiService.commentTask(
id: projectId, id: projectId,
comment: commentField, comment: commentField,
images: imageData, images: imageData,
).timeout(const Duration(seconds: 30), onTimeout: () { ).timeout(const Duration(seconds: 30), onTimeout: () {
appLogger.e("Request timed out."); logSafe("Task comment request timed out.", level: LogLevel.error);
throw Exception("Request timed out."); throw Exception("Request timed out.");
}); });
if (success) { if (success) {
showAppSnackbar( _showSuccess("Task commented successfully!");
title: "Success",
message: "Task commented successfully!",
type: SnackbarType.success,
);
await taskController.fetchTaskData(projectId); await taskController.fetchTaskData(projectId);
} else { } else {
showAppSnackbar( _showError("Failed to comment task.");
title: "Error",
message: "Failed to comment task.",
type: SnackbarType.error,
);
} }
} catch (e) { } catch (e, s) {
appLogger.e("Error commenting task: $e"); logSafe("Exception while commenting task", level: LogLevel.error, error: e, stackTrace: s);
showAppSnackbar( _showError("An error occurred while commenting the task.");
title: "Error",
message: "An error occurred while commenting the task.",
type: SnackbarType.error,
);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
Future<void> pickImages({required bool fromCamera}) async { Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images, String context) async {
if (fromCamera) { if (images == null || images.isEmpty) return null;
final pickedFile = await _picker.pickImage(
source: ImageSource.camera, logSafe("Preparing images for $context upload...");
imageQuality: 75,
); final results = await Future.wait(images.map((file) async {
if (pickedFile != null) { try {
selectedImages.add(File(pickedFile.path)); final compressed = await compressImageToUnder100KB(file);
if (compressed == null) return null;
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(compressed),
"contentType": _getContentTypeFromFileName(file.path),
"fileSize": compressed.lengthInBytes,
"description": "Image uploaded for $context",
};
} catch (e) {
logSafe("Image processing failed: ${file.path}", level: LogLevel.warning, error: e);
return null;
} }
} else { }));
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
if (pickedFiles.isNotEmpty) { return results.whereType<Map<String, dynamic>>().toList();
}
String _getContentTypeFromFileName(String fileName) {
final ext = fileName.split('.').last.toLowerCase();
return switch (ext) {
'jpg' || 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'gif' => 'image/gif',
_ => 'application/octet-stream',
};
}
Future<void> pickImages({required bool fromCamera}) async {
try {
if (fromCamera) {
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
if (pickedFile != null) {
selectedImages.add(File(pickedFile.path));
}
} else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path))); selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
} }
logSafe("Images picked: ${selectedImages.length}", sensitive: true);
} catch (e) {
logSafe("Error picking images", level: LogLevel.warning, error: e);
} }
} }
void removeImageAt(int index) { void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) { if (index >= 0 && index < selectedImages.length) {
selectedImages.removeAt(index); selectedImages.removeAt(index);
logSafe("Removed image at index $index");
} }
} }
void _showError(String message) => showAppSnackbar(
title: "Error",
message: message,
type: SnackbarType.error,
);
void _showSuccess(String message) => showAppSnackbar(
title: "Success",
message: message,
type: SnackbarType.success,
);
} }

View File

@ -9,63 +9,60 @@ import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class ApiService { class ApiService {
static const Duration timeout = Duration(seconds: 30); static const Duration timeout = Duration(seconds: 30);
static const bool enableLogs = true; static const bool enableLogs = true;
static const Duration extendedTimeout = Duration(seconds: 60); static const Duration extendedTimeout = Duration(seconds: 60);
// === Helpers === static Future<String?> _getToken() async {
final token = await LocalStorage.getJwtToken();
static Future<String?> _getToken() async { if (token == null) {
final token = await LocalStorage.getJwtToken(); logSafe("No JWT token found.");
return null;
if (token == null) {
if (enableLogs) appLogger.w("No JWT token found.");
return null;
}
try {
// Check if the token is expired
if (JwtDecoder.isExpired(token)) {
_log("Access token is expired. Attempting refresh...");
final refreshed = await AuthService.refreshToken();
if (refreshed) {
return await LocalStorage.getJwtToken();
} else {
_log("Token refresh failed. Logging out...");
await LocalStorage.logout();
return null;
}
} }
// Check if token is about to expire in < 2 minutes try {
final expirationDate = JwtDecoder.getExpirationDate(token); if (JwtDecoder.isExpired(token)) {
final now = DateTime.now(); logSafe("Access token is expired. Attempting refresh...");
final difference = expirationDate.difference(now); final refreshed = await AuthService.refreshToken();
if (refreshed) {
if (difference.inMinutes < 2) { return await LocalStorage.getJwtToken();
_log("Access token is about to expire in ${difference.inSeconds}s. Refreshing..."); } else {
final refreshed = await AuthService.refreshToken(); logSafe("Token refresh failed. Logging out...");
if (refreshed) { await LocalStorage.logout();
return await LocalStorage.getJwtToken(); return null;
}
} }
final expirationDate = JwtDecoder.getExpirationDate(token);
final now = DateTime.now();
final difference = expirationDate.difference(now);
if (difference.inMinutes < 2) {
logSafe(
"Access token is about to expire in ${difference.inSeconds}s. Refreshing...");
final refreshed = await AuthService.refreshToken();
if (refreshed) {
return await LocalStorage.getJwtToken();
}
}
} catch (e) {
logSafe("Token decoding error: $e", level: LogLevel.error);
} }
} catch (e) { return token;
_log("Token decoding error: $e");
} }
return token;
}
static Map<String, String> _headers(String token) => { static Map<String, String> _headers(String token) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer $token', 'Authorization': 'Bearer $token',
}; };
static void _log(String message) { static void _log(String message) {
if (enableLogs) appLogger.i(message); if (enableLogs) logSafe(message);
} }
static dynamic _parseResponse(http.Response response, {String label = ''}) { static dynamic _parseResponse(http.Response response, {String label = ''}) {
@ -82,29 +79,27 @@ class ApiService {
return null; return null;
} }
static dynamic _parseResponseForAllData(http.Response response, static dynamic _parseResponseForAllData(http.Response response,
{String label = ''}) { {String label = ''}) {
_log("$label Response: ${response.body}"); _log("$label Response: ${response.body}");
try { try {
final body = response.body.trim(); final body = response.body.trim();
if (body.isEmpty) throw FormatException("Empty response body"); if (body.isEmpty) throw FormatException("Empty response body");
final json = jsonDecode(body); final json = jsonDecode(body);
if (response.statusCode == 200 && json['success'] == true) {
return json;
}
if (response.statusCode == 200 && json['success'] == true) { _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
return json; } catch (e) {
_log("Response parsing error [$label]: $e");
} }
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); return null;
} catch (e) {
_log("Response parsing error [$label]: $e");
} }
return null;
}
static Future<http.Response?> _getRequest( static Future<http.Response?> _getRequest(
String endpoint, { String endpoint, {
Map<String, String>? queryParams, Map<String, String>? queryParams,
@ -115,22 +110,22 @@ static dynamic _parseResponseForAllData(http.Response response,
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
.replace(queryParameters: queryParams); .replace(queryParameters: queryParams);
_log("GET $uri"); logSafe("GET $uri");
try { try {
final response = final response =
await http.get(uri, headers: _headers(token)).timeout(timeout); await http.get(uri, headers: _headers(token)).timeout(timeout);
if (response.statusCode == 401 && !hasRetried) { if (response.statusCode == 401 && !hasRetried) {
_log("Unauthorized. Attempting token refresh..."); logSafe("Unauthorized. Attempting token refresh...");
if (await AuthService.refreshToken()) { if (await AuthService.refreshToken()) {
return await _getRequest(endpoint, return await _getRequest(endpoint,
queryParams: queryParams, hasRetried: true); queryParams: queryParams, hasRetried: true);
} }
_log("Token refresh failed."); logSafe("Token refresh failed.");
} }
return response; return response;
} catch (e) { } catch (e) {
_log("HTTP GET Exception: $e"); logSafe("HTTP GET Exception: $e", level: LogLevel.error);
return null; return null;
} }
} }
@ -145,7 +140,8 @@ static dynamic _parseResponseForAllData(http.Response response,
if (token == null) return null; if (token == null) return null;
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
_log("POST $uri\nHeaders: ${_headers(token)}\nBody: $body"); logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
sensitive: true);
try { try {
final response = await http final response = await http
@ -153,7 +149,7 @@ static dynamic _parseResponseForAllData(http.Response response,
.timeout(customTimeout); .timeout(customTimeout);
if (response.statusCode == 401 && !hasRetried) { if (response.statusCode == 401 && !hasRetried) {
_log("Unauthorized POST. Attempting token refresh..."); logSafe("Unauthorized POST. Attempting token refresh...");
if (await AuthService.refreshToken()) { if (await AuthService.refreshToken()) {
return await _postRequest(endpoint, body, return await _postRequest(endpoint, body,
customTimeout: customTimeout, hasRetried: true); customTimeout: customTimeout, hasRetried: true);
@ -161,12 +157,12 @@ static dynamic _parseResponseForAllData(http.Response response,
} }
return response; return response;
} catch (e) { } catch (e) {
_log("HTTP POST Exception: $e"); logSafe("HTTP POST Exception: $e", level: LogLevel.error);
return null; return null;
} }
} }
// === Dashboard Endpoints === // === Dashboard Endpoints ===
static Future<List<dynamic>?> getDashboardAttendanceOverview( static Future<List<dynamic>?> getDashboardAttendanceOverview(
String projectId, int days) async { String projectId, int days) async {
@ -263,7 +259,7 @@ static dynamic _parseResponseForAllData(http.Response response,
"base64Data": base64Encode(bytes), "base64Data": base64Encode(bytes),
}; };
} catch (e) { } catch (e) {
_log("Image encoding error: $e"); logSafe("Image encoding error: $e", level: LogLevel.error);
return false; return false;
} }
} }
@ -278,7 +274,7 @@ static dynamic _parseResponseForAllData(http.Response response,
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) return true; if (response.statusCode == 200 && json['success'] == true) return true;
_log("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); logSafe("Failed to upload image: ${json['message'] ?? 'Unknown error'}");
return false; return false;
} }
@ -389,7 +385,7 @@ static dynamic _parseResponseForAllData(http.Response response,
Get.back(); Get.back();
return true; return true;
} }
_log("Failed to report task: ${json['message'] ?? 'Unknown error'}"); logSafe("Failed to report task: ${json['message'] ?? 'Unknown error'}");
return false; return false;
} }
@ -443,19 +439,19 @@ static dynamic _parseResponseForAllData(http.Response response,
Get.back(); Get.back();
return true; return true;
} }
_log("Failed to assign daily task: ${json['message'] ?? 'Unknown error'}"); logSafe(
"Failed to assign daily task: ${json['message'] ?? 'Unknown error'}");
return false; return false;
} }
static Future<Map<String, dynamic>?> getWorkStatus() async { static Future<Map<String, dynamic>?> getWorkStatus() async {
final res = await _getRequest(ApiEndpoints.getWorkStatus); final res = await _getRequest(ApiEndpoints.getWorkStatus);
if (res == null) { if (res == null) {
_log('Work Status API returned null'); logSafe('Work Status API returned null');
return null; return null;
} }
_log('Work Status raw response: ${res.body}'); logSafe('Work Status raw response: ${res.body}');
return _parseResponseForAllData(res, label: 'Work Status') return _parseResponseForAllData(res, label: 'Work Status')
as Map<String, dynamic>?; as Map<String, dynamic>?;
} }
@ -465,6 +461,7 @@ static dynamic _parseResponseForAllData(http.Response response,
res != null res != null
? _parseResponseForAllData(res, label: 'Master Work Categories') ? _parseResponseForAllData(res, label: 'Master Work Categories')
: null); : null);
static Future<bool> approveTask({ static Future<bool> approveTask({
required String id, required String id,
required String comment, required String comment,
@ -517,7 +514,7 @@ static dynamic _parseResponseForAllData(http.Response response,
return true; return true;
} }
_log("Failed to create task: ${json['message'] ?? 'Unknown error'}"); logSafe("Failed to create task: ${json['message'] ?? 'Unknown error'}");
return false; return false;
} }
} }

View File

@ -6,23 +6,43 @@ import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/theme/app_theme.dart'; import 'package:marco/helpers/theme/app_theme.dart';
import 'package:url_strategy/url_strategy.dart'; import 'package:url_strategy/url_strategy.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
Future<void> initializeApp() async { Future<void> initializeApp() async {
setPathUrlStrategy(); try {
logSafe("Starting app initialization...");
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( setPathUrlStrategy();
statusBarColor: Color.fromARGB(255, 255, 0, 0), logSafe("URL strategy set.");
statusBarIconBrightness: Brightness.light,
));
await LocalStorage.init(); SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
await ThemeCustomizer.init(); statusBarColor: Color.fromARGB(255, 255, 0, 0),
Get.put(PermissionController()); statusBarIconBrightness: Brightness.light,
Get.put(ProjectController(), permanent: true); ));
AppStyle.init(); logSafe("System UI overlay style set.");
appLogger.i("App initialization completed successfully."); await LocalStorage.init();
logSafe("Local storage initialized.");
await ThemeCustomizer.init();
logSafe("Theme customizer initialized.");
Get.put(PermissionController());
logSafe("PermissionController injected.");
Get.put(ProjectController(), permanent: true);
logSafe("ProjectController injected as permanent.");
AppStyle.init();
logSafe("AppStyle initialized.");
logSafe("App initialization completed successfully.");
} catch (e, stacktrace) {
logSafe("Error during app initialization",
level: LogLevel.error,
error: e,
stackTrace: stacktrace,
);
rethrow;
}
} }

View File

@ -12,10 +12,21 @@ late final FileLogOutput fileLogOutput;
/// Initialize logging (call once in `main()`) /// Initialize logging (call once in `main()`)
Future<void> initLogging() async { Future<void> initLogging() async {
await requestStoragePermission(); await requestStoragePermission();
fileLogOutput = FileLogOutput(); fileLogOutput = FileLogOutput();
appLogger = Logger( appLogger = Logger(
printer: SimpleFileLogPrinter(), printer: PrettyPrinter(
output: fileLogOutput, methodCount: 0,
printTime: true,
colors: true,
printEmojis: true,
),
output: MultiOutput([
ConsoleOutput(), // Console will use the top-level PrettyPrinter
fileLogOutput, // File will still use the SimpleFileLogPrinter
]),
level: Level.debug,
); );
} }
@ -27,6 +38,34 @@ Future<void> requestStoragePermission() async {
} }
} }
/// Safe logger wrapper
void logSafe(
String message, {
LogLevel level = LogLevel.info,
dynamic error,
StackTrace? stackTrace,
bool sensitive = false,
}) {
if (sensitive) return;
switch (level) {
case LogLevel.debug:
appLogger.d(message, error: error, stackTrace: stackTrace);
break;
case LogLevel.warning:
appLogger.w(message, error: error, stackTrace: stackTrace);
break;
case LogLevel.error:
appLogger.e(message, error: error, stackTrace: stackTrace);
break;
case LogLevel.verbose:
appLogger.v(message, error: error, stackTrace: stackTrace);
break;
default:
appLogger.i(message, error: error, stackTrace: stackTrace);
}
}
/// Custom log output that writes to a local `.txt` file /// Custom log output that writes to a local `.txt` file
class FileLogOutput extends LogOutput { class FileLogOutput extends LogOutput {
File? _logFile; File? _logFile;
@ -54,6 +93,9 @@ class FileLogOutput extends LogOutput {
@override @override
void output(OutputEvent event) async { void output(OutputEvent event) async {
await _init(); await _init();
if (event.lines.isEmpty) return;
final logMessage = event.lines.join('\n') + '\n'; final logMessage = event.lines.join('\n') + '\n';
await _logFile!.writeAsString( await _logFile!.writeAsString(
logMessage, logMessage,
@ -97,11 +139,18 @@ class FileLogOutput extends LogOutput {
class SimpleFileLogPrinter extends LogPrinter { class SimpleFileLogPrinter extends LogPrinter {
@override @override
List<String> log(LogEvent event) { List<String> log(LogEvent event) {
final message = event.message.toString();
if (message.contains('[SENSITIVE]')) return [];
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()); final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
final level = event.level.name.toUpperCase(); final level = event.level.name.toUpperCase();
final message = event.message;
final error = event.error != null ? ' | ERROR: ${event.error}' : ''; final error = event.error != null ? ' | ERROR: ${event.error}' : '';
final stack = event.stackTrace != null ? '\nSTACKTRACE:\n${event.stackTrace}' : ''; final stack =
event.stackTrace != null ? '\nSTACKTRACE:\n${event.stackTrace}' : '';
return ['[$timestamp] [$level] $message$error$stack']; return ['[$timestamp] [$level] $message$error$stack'];
} }
} }
/// Optional log level enum for better type safety
enum LogLevel { debug, info, warning, error, verbose }

View File

@ -6,7 +6,7 @@ import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class AuthService { class AuthService {
static const String _baseUrl = ApiEndpoints.baseUrl; static const String _baseUrl = ApiEndpoints.baseUrl;
@ -19,6 +19,7 @@ class AuthService {
/// Login with email and password /// Login with email and password
static Future<Map<String, String>?> loginUser(Map<String, dynamic> data) async { static Future<Map<String, String>?> loginUser(Map<String, dynamic> data) async {
try { try {
logSafe("Attempting login...");
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/login-mobile"), Uri.parse("$_baseUrl/auth/login-mobile"),
headers: _headers, headers: _headers,
@ -30,12 +31,14 @@ class AuthService {
await _handleLoginSuccess(responseData['data']); await _handleLoginSuccess(responseData['data']);
return null; return null;
} else if (response.statusCode == 401) { } else if (response.statusCode == 401) {
logSafe("Invalid login credentials.", level: LogLevel.warning);
return {"password": "Invalid email or password"}; return {"password": "Invalid email or password"};
} else { } else {
logSafe("Login error: ${responseData['message']}", level: LogLevel.warning);
return {"error": responseData['message'] ?? "Unexpected error occurred"}; return {"error": responseData['message'] ?? "Unexpected error occurred"};
} }
} catch (e) { } catch (e, stacktrace) {
appLogger.e("Login error: $e"); logSafe("Login exception", level: LogLevel.error, error: e, stackTrace: stacktrace);
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
@ -46,7 +49,7 @@ class AuthService {
final refreshToken = await LocalStorage.getRefreshToken(); final refreshToken = await LocalStorage.getRefreshToken();
if (accessToken == null || refreshToken == null || accessToken.isEmpty || refreshToken.isEmpty) { if (accessToken == null || refreshToken == null || accessToken.isEmpty || refreshToken.isEmpty) {
appLogger.w("Missing access/refresh token."); logSafe("Missing access or refresh token.", level: LogLevel.warning);
return false; return false;
} }
@ -56,6 +59,7 @@ class AuthService {
}; };
try { try {
logSafe("Refreshing token...");
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/refresh-token"), Uri.parse("$_baseUrl/auth/refresh-token"),
headers: _headers, headers: _headers,
@ -67,14 +71,14 @@ class AuthService {
await LocalStorage.setJwtToken(data['data']['token']); await LocalStorage.setJwtToken(data['data']['token']);
await LocalStorage.setRefreshToken(data['data']['refreshToken']); await LocalStorage.setRefreshToken(data['data']['refreshToken']);
await LocalStorage.setLoggedInUser(true); await LocalStorage.setLoggedInUser(true);
appLogger.i("Token refreshed."); logSafe("Token refreshed successfully.");
return true; return true;
} else { } else {
appLogger.w("Refresh token failed: ${data['message']}"); logSafe("Refresh token failed: ${data['message']}", level: LogLevel.warning);
return false; return false;
} }
} catch (e) { } catch (e, stacktrace) {
appLogger.e("Token refresh error: $e"); logSafe("Token refresh exception", level: LogLevel.error, error: e, stackTrace: stacktrace);
return false; return false;
} }
} }
@ -82,6 +86,7 @@ class AuthService {
/// Forgot password /// Forgot password
static Future<Map<String, String>?> forgotPassword(String email) async { static Future<Map<String, String>?> forgotPassword(String email) async {
try { try {
logSafe("Forgot password requested.");
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/forgot-password"), Uri.parse("$_baseUrl/auth/forgot-password"),
headers: _headers, headers: _headers,
@ -91,8 +96,8 @@ class AuthService {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
if (response.statusCode == 200 && data['success'] == true) return null; if (response.statusCode == 200 && data['success'] == true) return null;
return {"error": data['message'] ?? "Failed to send reset link."}; return {"error": data['message'] ?? "Failed to send reset link."};
} catch (e) { } catch (e, stacktrace) {
appLogger.e("Forgot password error: $e"); logSafe("Forgot password error", level: LogLevel.error, error: e, stackTrace: stacktrace);
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
@ -100,6 +105,7 @@ class AuthService {
/// Request demo /// Request demo
static Future<Map<String, String>?> requestDemo(Map<String, dynamic> demoData) async { static Future<Map<String, String>?> requestDemo(Map<String, dynamic> demoData) async {
try { try {
logSafe("Submitting demo request...");
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/market/inquiry"), Uri.parse("$_baseUrl/market/inquiry"),
headers: _headers, headers: _headers,
@ -109,8 +115,8 @@ class AuthService {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
if (response.statusCode == 200 && data['success'] == true) return null; if (response.statusCode == 200 && data['success'] == true) return null;
return {"error": data['message'] ?? "Failed to submit demo request."}; return {"error": data['message'] ?? "Failed to submit demo request."};
} catch (e) { } catch (e, stacktrace) {
appLogger.e("Request demo error: $e"); logSafe("Request demo error", level: LogLevel.error, error: e, stackTrace: stacktrace);
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
@ -118,6 +124,7 @@ class AuthService {
/// Get list of industries /// Get list of industries
static Future<List<Map<String, dynamic>>?> getIndustries() async { static Future<List<Map<String, dynamic>>?> getIndustries() async {
try { try {
logSafe("Fetching industries list...");
final response = await http.get( final response = await http.get(
Uri.parse("$_baseUrl/market/industries"), Uri.parse("$_baseUrl/market/industries"),
headers: _headers, headers: _headers,
@ -128,8 +135,8 @@ class AuthService {
return List<Map<String, dynamic>>.from(data['data']); return List<Map<String, dynamic>>.from(data['data']);
} }
return null; return null;
} catch (e) { } catch (e, stacktrace) {
appLogger.e("Get industries error: $e"); logSafe("Get industries error", level: LogLevel.error, error: e, stackTrace: stacktrace);
return null; return null;
} }
} }
@ -142,6 +149,7 @@ class AuthService {
final token = await LocalStorage.getJwtToken(); final token = await LocalStorage.getJwtToken();
try { try {
logSafe("Generating MPIN...");
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/generate-mpin"), Uri.parse("$_baseUrl/auth/generate-mpin"),
headers: { headers: {
@ -154,8 +162,8 @@ class AuthService {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
if (response.statusCode == 200 && data['success'] == true) return null; if (response.statusCode == 200 && data['success'] == true) return null;
return {"error": data['message'] ?? "Failed to generate MPIN."}; return {"error": data['message'] ?? "Failed to generate MPIN."};
} catch (e) { } catch (e, stacktrace) {
appLogger.e("Generate MPIN error: $e"); logSafe("Generate MPIN error", level: LogLevel.error, error: e, stackTrace: stacktrace);
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
@ -171,6 +179,7 @@ class AuthService {
final token = await LocalStorage.getJwtToken(); final token = await LocalStorage.getJwtToken();
try { try {
logSafe("Verifying MPIN...");
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/login-mpin"), Uri.parse("$_baseUrl/auth/login-mpin"),
headers: { headers: {
@ -187,8 +196,8 @@ class AuthService {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
if (response.statusCode == 200 && data['success'] == true) return null; if (response.statusCode == 200 && data['success'] == true) return null;
return {"error": data['message'] ?? "MPIN verification failed."}; return {"error": data['message'] ?? "MPIN verification failed."};
} catch (e) { } catch (e, stacktrace) {
appLogger.e("Verify MPIN error: $e"); logSafe("Verify MPIN error", level: LogLevel.error, error: e, stackTrace: stacktrace);
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
@ -196,6 +205,7 @@ class AuthService {
/// Generate OTP /// Generate OTP
static Future<Map<String, String>?> generateOtp(String email) async { static Future<Map<String, String>?> generateOtp(String email) async {
try { try {
logSafe("Generating OTP for email...");
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/send-otp"), Uri.parse("$_baseUrl/auth/send-otp"),
headers: _headers, headers: _headers,
@ -205,8 +215,8 @@ class AuthService {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
if (response.statusCode == 200 && data['success'] == true) return null; if (response.statusCode == 200 && data['success'] == true) return null;
return {"error": data['message'] ?? "Failed to generate OTP."}; return {"error": data['message'] ?? "Failed to generate OTP."};
} catch (e) { } catch (e, stacktrace) {
appLogger.e("Generate OTP error: $e"); logSafe("Generate OTP error", level: LogLevel.error, error: e, stackTrace: stacktrace);
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
@ -217,6 +227,7 @@ class AuthService {
required String otp, required String otp,
}) async { }) async {
try { try {
logSafe("Verifying OTP...");
final response = await http.post( final response = await http.post(
Uri.parse("$_baseUrl/auth/login-otp"), Uri.parse("$_baseUrl/auth/login-otp"),
headers: _headers, headers: _headers,
@ -229,14 +240,16 @@ class AuthService {
return null; return null;
} }
return {"error": data['message'] ?? "OTP verification failed."}; return {"error": data['message'] ?? "OTP verification failed."};
} catch (e) { } catch (e, stacktrace) {
appLogger.e("Verify OTP error: $e"); logSafe("Verify OTP error", level: LogLevel.error, error: e, stackTrace: stacktrace);
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
} }
} }
/// Handle login success flow /// Handle login success flow
static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async { static Future<void> _handleLoginSuccess(Map<String, dynamic> data) async {
logSafe("Processing login success...");
final jwtToken = data['token']; final jwtToken = data['token'];
final refreshToken = data['refreshToken']; final refreshToken = data['refreshToken'];
final mpinToken = data['mpinToken']; final mpinToken = data['mpinToken'];
@ -256,9 +269,10 @@ class AuthService {
final permissionController = Get.put(PermissionController()); final permissionController = Get.put(PermissionController());
await permissionController.loadData(jwtToken); await permissionController.loadData(jwtToken);
await Get.find<ProjectController>().fetchProjects(); await Get.find<ProjectController>().fetchProjects();
isLoggedIn = true; isLoggedIn = true;
appLogger.i("Login success initialized."); logSafe("Login flow completed.");
} }
} }

View File

@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/user_permission.dart'; import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employee_info.dart'; import 'package:marco/model/employee_info.dart';
import 'package:marco/model/projects_model.dart'; import 'package:marco/model/projects_model.dart';
@ -10,18 +10,19 @@ import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/api_endpoints.dart';
class PermissionService { class PermissionService {
static final Map<String, Map<String, dynamic>> _userDataCache = {}; static final Map<String, Map<String, dynamic>> _userDataCache = {};
static const String _baseUrl = ApiEndpoints.baseUrl; static const String _baseUrl = ApiEndpoints.baseUrl;
/// Fetches all user-related data (permissions, employee info, projects) /// Fetches all user-related data (permissions, employee info, projects)
static Future<Map<String, dynamic>> fetchAllUserData( static Future<Map<String, dynamic>> fetchAllUserData(
String token, { String token, {
bool hasRetried = false, bool hasRetried = false,
}) async { }) async {
// Return cached data if already available logSafe("Fetching user data...", sensitive: true);
if (_userDataCache.containsKey(token)) { if (_userDataCache.containsKey(token)) {
logSafe("User data cache hit.", sensitive: true);
return _userDataCache[token]!; return _userDataCache[token]!;
} }
@ -30,8 +31,10 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
try { try {
final response = await http.get(uri, headers: headers); final response = await http.get(uri, headers: headers);
final statusCode = response.statusCode;
if (response.statusCode == 200) { if (statusCode == 200) {
logSafe("User data fetched successfully.");
final data = json.decode(response.body)['data']; final data = json.decode(response.body)['data'];
final result = { final result = {
@ -44,8 +47,9 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
return result; return result;
} }
// Handle 401 by attempting a single retry with refreshed token if (statusCode == 401 && !hasRetried) {
if (response.statusCode == 401 && !hasRetried) { logSafe("Unauthorized. Attempting token refresh...", level: LogLevel.warning);
final refreshed = await AuthService.refreshToken(); final refreshed = await AuthService.refreshToken();
if (refreshed) { if (refreshed) {
final newToken = await LocalStorage.getJwtToken(); final newToken = await LocalStorage.getJwtToken();
@ -55,19 +59,23 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
} }
await _handleUnauthorized(); await _handleUnauthorized();
logSafe("Token refresh failed. Redirecting to login.", level: LogLevel.warning);
throw Exception('Unauthorized. Token refresh failed.'); throw Exception('Unauthorized. Token refresh failed.');
} }
final error = json.decode(response.body)['message'] ?? 'Unknown error'; final error = json.decode(response.body)['message'] ?? 'Unknown error';
logSafe("Failed to fetch user data: $error", level: LogLevel.warning);
throw Exception('Failed to fetch user data: $error'); throw Exception('Failed to fetch user data: $error');
} catch (e) { } catch (e, stacktrace) {
appLogger.e('Error fetching user data: $e'); logSafe("Exception while fetching user data", level: LogLevel.error, error: e, stackTrace: stacktrace);
rethrow; rethrow;
} }
} }
/// Clears auth data and redirects to login /// Clears auth data and redirects to login
static Future<void> _handleUnauthorized() async { static Future<void> _handleUnauthorized() async {
logSafe("Clearing tokens and redirecting to login due to unauthorized access.", level: LogLevel.warning);
await LocalStorage.removeToken('jwt_token'); await LocalStorage.removeToken('jwt_token');
await LocalStorage.removeToken('refresh_token'); await LocalStorage.removeToken('refresh_token');
await LocalStorage.setLoggedInUser(false); await LocalStorage.setLoggedInUser(false);
@ -76,6 +84,7 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
/// Converts raw permission data into list of `UserPermission` /// Converts raw permission data into list of `UserPermission`
static List<UserPermission> _parsePermissions(List<dynamic> permissions) { static List<UserPermission> _parsePermissions(List<dynamic> permissions) {
logSafe("Parsing user permissions...");
return permissions return permissions
.map((id) => UserPermission.fromJson({'id': id})) .map((id) => UserPermission.fromJson({'id': id}))
.toList(); .toList();
@ -83,11 +92,13 @@ static const String _baseUrl = ApiEndpoints.baseUrl;
/// Converts raw employee JSON into `EmployeeInfo` /// Converts raw employee JSON into `EmployeeInfo`
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> data) { static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> data) {
logSafe("Parsing employee info...");
return EmployeeInfo.fromJson(data); return EmployeeInfo.fromJson(data);
} }
/// Converts raw projects JSON into list of `ProjectInfo` /// Converts raw projects JSON into list of `ProjectInfo`
static List<ProjectInfo> _parseProjectsInfo(List<dynamic> projects) { static List<ProjectInfo> _parseProjectsInfo(List<dynamic> projects) {
logSafe("Parsing projects info...");
return projects.map((proj) => ProjectInfo.fromJson(proj)).toList(); return projects.map((proj) => ProjectInfo.fromJson(proj)).toList();
} }
} }

View File

@ -8,40 +8,61 @@ import 'package:marco/helpers/services/app_logger.dart';
Future<Uint8List?> compressImageToUnder100KB(File file) async { Future<Uint8List?> compressImageToUnder100KB(File file) async {
int quality = 40; int quality = 40;
Uint8List? result; Uint8List? result;
const int maxWidth = 800; const int maxWidth = 800;
const int maxHeight = 800; const int maxHeight = 800;
logSafe("Starting image compression...", sensitive: true);
while (quality >= 10) { while (quality >= 10) {
result = await FlutterImageCompress.compressWithFile( try {
file.absolute.path, result = await FlutterImageCompress.compressWithFile(
quality: quality, file.absolute.path,
minWidth: maxWidth, quality: quality,
minHeight: maxHeight, minWidth: maxWidth,
format: CompressFormat.jpeg, minHeight: maxHeight,
); format: CompressFormat.jpeg,
);
if (result != null) { if (result != null) {
appLogger.i('Quality: $quality, Size: ${(result.lengthInBytes / 1024).toStringAsFixed(2)} KB'); logSafe(
'Compression quality: $quality, size: ${(result.lengthInBytes / 1024).toStringAsFixed(2)} KB',
);
if (result.lengthInBytes <= 100 * 1024) { if (result.lengthInBytes <= 100 * 1024) {
return result; logSafe("Image compressed successfully under 100KB.");
return result;
}
} else {
logSafe("Compression returned null at quality $quality", level: LogLevel.warning);
} }
} catch (e, stacktrace) {
logSafe("Compression error at quality $quality", level: LogLevel.error, error: e, stackTrace: stacktrace);
} }
quality -= 10; quality -= 10;
} }
logSafe("Failed to compress image under 100KB. Returning best effort result.", level: LogLevel.warning);
return result; return result;
} }
Future<File> saveCompressedImageToFile(Uint8List bytes) async { Future<File> saveCompressedImageToFile(Uint8List bytes) async {
final tempDir = await getTemporaryDirectory(); try {
final filePath = path.join( final tempDir = await getTemporaryDirectory();
tempDir.path, final filePath = path.join(
'compressed_${DateTime.now().millisecondsSinceEpoch}.jpg', tempDir.path,
); 'compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
final file = File(filePath); );
return await file.writeAsBytes(bytes); final file = File(filePath);
final savedFile = await file.writeAsBytes(bytes);
logSafe("Compressed image saved to ${savedFile.path}", sensitive: true);
return savedFile;
} catch (e, stacktrace) {
logSafe("Error saving compressed image", level: LogLevel.error, error: e, stackTrace: stacktrace);
rethrow;
}
} }

View File

@ -8,12 +8,13 @@ import 'package:marco/helpers/services/app_logger.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await initLogging(); await initLogging();
logSafe("App starting...");
appLogger.i("App starting...");
try { try {
await initializeApp(); await initializeApp();
logSafe("App initialized successfully.");
runApp( runApp(
ChangeNotifierProvider<AppNotifier>( ChangeNotifierProvider<AppNotifier>(
create: (_) => AppNotifier(), create: (_) => AppNotifier(),
@ -21,12 +22,21 @@ Future<void> main() async {
), ),
); );
} catch (e, stacktrace) { } catch (e, stacktrace) {
appLogger.e('App failed to initialize:', error: e, stackTrace: stacktrace); logSafe('App failed to initialize.',
level: LogLevel.error,
error: e,
stackTrace: stacktrace,
);
runApp( runApp(
const MaterialApp( const MaterialApp(
home: Scaffold( home: Scaffold(
body: Center(child: Text("Failed to initialize the app.")), body: Center(
child: Text(
"Failed to initialize the app.",
style: TextStyle(color: Colors.red),
),
),
), ),
), ),
); );

View File

@ -3,7 +3,7 @@ import 'package:get/get.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/extensions/app_localization_delegate.dart'; import 'package:marco/helpers/extensions/app_localization_delegate.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/localizations/language.dart'; import 'package:marco/helpers/services/localizations/language.dart';
@ -14,33 +14,30 @@ import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/theme/app_notifier.dart'; import 'package:marco/helpers/theme/app_notifier.dart';
import 'package:marco/routes.dart'; import 'package:marco/routes.dart';
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
Future<String> _getInitialRoute() async { Future<String> _getInitialRoute() async {
try { try {
if (!AuthService.isLoggedIn) { if (!AuthService.isLoggedIn) {
appLogger.i("User not logged in. Routing to /auth/login-option"); logSafe("User not logged in. Routing to /auth/login-option");
return "/auth/login-option"; return "/auth/login-option";
} }
final bool hasMpin = LocalStorage.getIsMpin(); final bool hasMpin = LocalStorage.getIsMpin();
appLogger.i("MPIN enabled: $hasMpin"); logSafe("MPIN enabled: $hasMpin", sensitive: true);
if (hasMpin) { if (hasMpin) {
await LocalStorage.setBool("mpin_verified", false); await LocalStorage.setBool("mpin_verified", false);
appLogger logSafe("Routing to /auth/mpin-auth and setting mpin_verified to false");
.i("Routing to /auth/mpin-auth and setting mpin_verified to false");
return "/auth/mpin-auth"; return "/auth/mpin-auth";
} else { } else {
appLogger.i("MPIN not enabled. Routing to /home"); logSafe("MPIN not enabled. Routing to /dashboard");
return "/dashboard"; return "/dashboard";
} }
} catch (e, stacktrace) { } catch (e, stacktrace) {
appLogger.e("Error determining initial route", logSafe("Error determining initial route",
error: e, stackTrace: stacktrace); level: LogLevel.error, error: e, stackTrace: stacktrace);
return "/auth/login-option"; return "/auth/login-option";
} }
} }
@ -53,6 +50,8 @@ class MyApp extends StatelessWidget {
future: _getInitialRoute(), future: _getInitialRoute(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
logSafe("FutureBuilder snapshot error",
level: LogLevel.error, error: snapshot.error);
return const MaterialApp( return const MaterialApp(
home: Center(child: Text("Error determining route")), home: Center(child: Text("Error determining route")),
); );