Vaibhav_Feature-Logger #51

Merged
vaibhav.surve merged 2 commits from Vaibhav_Feature-Logger into main 2025-06-25 12:10:37 +00:00
29 changed files with 1142 additions and 969 deletions

View File

@ -3,6 +3,9 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

View File

@ -5,12 +5,16 @@ import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
class ForgotPasswordController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false;
final MyFormValidator basicValidator = MyFormValidator();
final RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
basicValidator.addField(
'email',
required: true,
@ -18,49 +22,49 @@ class ForgotPasswordController extends MyController {
validators: [MyEmailValidator()],
controller: TextEditingController(text: "demo@example.com"),
);
super.onInit();
}
Future<void> onLogin() async {
if (basicValidator.validateForm()) {
update();
var errors = await AuthService.loginUser(basicValidator.getData());
if (errors != null) {
basicValidator.validateForm();
basicValidator.clearErrors();
}
Get.toNamed('/auth/reset_password');
update();
}
}
/// New: Forgot password function
Future<void> onForgotPassword() async {
if (basicValidator.validateForm()) {
update();
final data = basicValidator.getData();
final email = data['email']?.toString() ?? '';
if (!basicValidator.validateForm()) return;
isLoading.value = true;
final data = basicValidator.getData();
final email = data['email']?.toString() ?? '';
try {
logSafe("Forgot password requested for: $email", sensitive: true);
final result = await AuthService.forgotPassword(email);
if (result != null) {
if (result == null) {
showAppSnackbar(
title: "Success",
message: "Your password reset link was sent.",
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: "Success",
message: "Your password reset link was sent.",
type: SnackbarType.success,
title: "Failed",
message: errorMessage,
type: SnackbarType.error,
);
logSafe("Failed to send reset password email for $email: $errorMessage", level: LogLevel.warning, sensitive: true);
}
update();
} catch (e, stacktrace) {
logSafe("Error during forgot password", level: LogLevel.error, error: e, stackTrace: stacktrace);
showAppSnackbar(
title: "Error",
message: "Something went wrong. Please try again later.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
void gotoLogIn() {
Get.toNamed('/auth/login-option');
Get.offAllNamed('/auth/login-option');
}
}

View File

@ -6,6 +6,8 @@ import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart'; // <-- logging
class LoginController extends MyController {
final MyFormValidator basicValidator = MyFormValidator();
@ -51,54 +53,51 @@ class LoginController extends MyController {
isLoading.value = true;
final errors = await AuthService.loginUser(basicValidator.getData());
try {
final loginData = basicValidator.getData();
logSafe("Attempting login for user: ${loginData['username']}", sensitive: true);
if (errors != null) {
showAppSnackbar(
title: "Login Failed",
message: "Username or password is incorrect",
type: SnackbarType.error,
);
final errors = await AuthService.loginUser(loginData);
basicValidator.addErrors(errors);
basicValidator.validateForm();
basicValidator.clearErrors();
} else {
await _handleRememberMe();
final bool isMpinEnabled = LocalStorage.getIsMpin();
print('MPIN Enabled? $isMpinEnabled');
if (errors != null) {
logSafe("Login failed for user: ${loginData['username']} with errors: $errors", level: LogLevel.warning, sensitive: true);
if (isMpinEnabled) {
Get.toNamed('/home');
showAppSnackbar(
title: "Login Failed",
message: "Username or password is incorrect",
type: SnackbarType.error,
);
basicValidator.addErrors(errors);
basicValidator.validateForm();
basicValidator.clearErrors();
} else {
await _handleRememberMe();
logSafe("Login successful for user: ${loginData['username']}", sensitive: true);
Get.toNamed('/home');
}
}
isLoading.value = false;
}
void handlePostLoginNavigation() {
final bool isMpinEnabled = LocalStorage.getIsMpin();
if (isMpinEnabled) {
Get.offAllNamed('/home');
} else {
Get.offAllNamed('/home');
} catch (e, stacktrace) {
logSafe("Exception during login", level: LogLevel.error, error: e, stackTrace: stacktrace);
showAppSnackbar(
title: "Login Error",
message: "An unexpected error occurred",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
Future<void> _handleRememberMe() async {
if (isChecked.value) {
await LocalStorage.setToken(
'username', basicValidator.getController('username')!.text);
await LocalStorage.setToken(
'password', basicValidator.getController('password')!.text);
await LocalStorage.setToken('username', basicValidator.getController('username')!.text);
await LocalStorage.setToken('password', basicValidator.getController('password')!.text);
await LocalStorage.setBool('remember_me', true);
} else {
await LocalStorage.removeToken('username');
await LocalStorage.removeToken('password');
await LocalStorage.setBool('remember_me', false);
basicValidator.clearErrors();
}
}
@ -123,4 +122,3 @@ class LoginController extends MyController {
Get.offAndToNamed('/auth/register_account');
}
}

View File

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

View File

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

View File

@ -3,16 +3,17 @@ import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
class RegisterAccountController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false;
@override
void onInit() {
logSafe("[RegisterAccountController] onInit called");
basicValidator.addField(
'email',
required: true,
@ -38,29 +39,40 @@ class RegisterAccountController extends MyController {
validators: [MyLengthValidator(min: 6, max: 10)],
controller: TextEditingController(),
);
super.onInit();
}
Future<void> onLogin() async {
if (basicValidator.validateForm()) {
update();
var errors = await AuthService.loginUser(basicValidator.getData());
final data = basicValidator.getData();
logSafe("[RegisterAccountController] Submitting registration data");
final errors = await AuthService.loginUser(data);
if (errors != null) {
logSafe("[RegisterAccountController] Login errors: $errors", level: LogLevel.warning);
basicValidator.addErrors(errors);
basicValidator.validateForm();
basicValidator.clearErrors();
}
logSafe("[RegisterAccountController] Redirecting to /starter");
Get.toNamed('/starter');
update();
} else {
logSafe("[RegisterAccountController] Validation failed", level: LogLevel.warning);
}
}
void onChangeShowPassword() {
showPassword = !showPassword;
logSafe("[RegisterAccountController] showPassword toggled: $showPassword");
update();
}
void gotoLogin() {
logSafe("[RegisterAccountController] Navigating to /auth/login-option");
Get.toNamed('/auth/login-option');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +1,26 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/dailyTaskPlaning/daily_task_planing_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
final Logger log = Logger();
class DailyTaskPlaningController extends GetxController {
List<ProjectModel> projects = [];
List<EmployeeModel> employees = [];
List<TaskPlanningDetailsModel> dailyTasks = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString();
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
void updateSelectedEmployees() {
final selected =
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
selectedEmployees.value = selected;
}
MyFormValidator basicValidator = MyFormValidator();
List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString();
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
@ -42,114 +36,113 @@ class DailyTaskPlaningController extends GetxController {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
if (fieldType == "target") {
if (int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
if (fieldType == "description") {
if (value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
if (fieldType == "description" && value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
return null;
}
Future<void> fetchRoles() async {
logger.i("Fetching roles...");
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logger.i("Roles fetched successfully.");
update();
} else {
logger.e("Failed to fetch roles.");
}
void updateSelectedEmployees() {
final selected = employees
.where((e) => uploadingStates[e.id]?.value == true)
.toList();
selectedEmployees.value = selected;
logSafe("Updated selected employees", level: LogLevel.debug, sensitive: true);
}
void onRoleSelected(String? roleId) {
selectedRoleId.value = roleId;
logger.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({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
logger.i("Starting assign task...");
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
logSafe("Starting assign task...", level: LogLevel.info);
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
if (response == true) {
logger.i("Task assigned successfully.");
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logger.e("Failed to assign task.");
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
if (response == true) {
logSafe("Task assigned successfully", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to assign task", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
}
}
}
Future<void> fetchProjects() async {
isLoading.value = true;
try {
isLoading.value = true;
final response = await ApiService.getProjects();
if (response?.isEmpty ?? true) {
log.w("No project data found or API call failed.");
logSafe("No project data found or API call failed", level: LogLevel.warning);
return;
}
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
log.i("Projects fetched: ${projects.length} projects loaded.");
logSafe("Projects fetched: ${projects.length} projects loaded", level: LogLevel.info);
update();
} catch (e, stack) {
log.e("Error fetching projects", error: e, stackTrace: stack);
logSafe("Error fetching projects", level: LogLevel.error, error: e, stackTrace: stack);
} finally {
isLoading.value = false;
}
}
Future<void> fetchTaskData(String? projectId) async {
if (projectId == null) return;
if (projectId == null) {
logSafe("Project ID is null", level: LogLevel.warning);
return;
}
isLoading.value = true;
try {
isLoading.value = true;
final response = await ApiService.getDailyTasksDetails(projectId);
if (response != null) {
final data = response['data'];
if (data != null) {
dailyTasks = [TaskPlanningDetailsModel.fromJson(data)];
log.i("Daily task Planning Details fetched.");
} else {
log.e("Data field is null");
}
final data = response?['data'];
if (data != null) {
dailyTasks = [TaskPlanningDetailsModel.fromJson(data)];
logSafe("Daily task Planning Details fetched", level: LogLevel.info, sensitive: true);
} else {
log.e(
"Failed to fetch daily task planning Details for project $projectId");
logSafe("Data field is null", level: LogLevel.warning);
}
} catch (e, stack) {
log.e("Error fetching daily task data", error: e, stackTrace: stack);
logSafe("Error fetching daily task data", level: LogLevel.error, error: e, stackTrace: stack);
} finally {
isLoading.value = false;
update();
@ -158,7 +151,7 @@ class DailyTaskPlaningController extends GetxController {
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) {
log.e("Project ID is required but was null or empty.");
logSafe("Project ID is required but was null or empty", level: LogLevel.error);
return;
}
@ -166,21 +159,22 @@ class DailyTaskPlaningController extends GetxController {
try {
final response = await ApiService.getAllEmployeesByProject(projectId);
if (response != null && response.isNotEmpty) {
employees =
response.map((json) => EmployeeModel.fromJson(json)).toList();
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
log.i("Employees fetched: ${employees.length} for project $projectId");
logSafe("Employees fetched: ${employees.length} for project $projectId",
level: LogLevel.info, sensitive: true);
} else {
log.w("No employees found for project $projectId.");
employees = [];
logSafe("No employees found for project $projectId", level: LogLevel.warning, sensitive: true);
}
} catch (e) {
log.e("Error fetching employees for project $projectId: $e");
} catch (e, stack) {
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:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
@ -14,14 +14,9 @@ import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlaning/work_status_model.dart';
final Logger logger = Logger();
enum ApiStatus { idle, loading, success, failure }
class ReportTaskActionController extends MyController {
//
// Reactive State
//
final RxBool isLoading = false.obs;
final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
@ -38,9 +33,6 @@ class ReportTaskActionController extends MyController {
final RxString selectedWorkStatusName = ''.obs;
//
// Controllers & Validators
//
final MyFormValidator basicValidator = MyFormValidator();
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
final ImagePicker _picker = ImagePicker();
@ -73,13 +65,10 @@ class ReportTaskActionController extends MyController {
approvedTaskController,
];
//
// Lifecycle Hooks
//
@override
void onInit() {
super.onInit();
logger.i("Initializing ReportTaskController...");
logSafe("Initializing ReportTaskController...");
_initializeFormFields();
}
@ -88,12 +77,10 @@ class ReportTaskActionController extends MyController {
for (final controller in _allControllers) {
controller.dispose();
}
logSafe("Disposed all text controllers in ReportTaskActionController.");
super.onClose();
}
//
// Form Field Setup
//
void _initializeFormFields() {
basicValidator
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
@ -110,9 +97,6 @@ class ReportTaskActionController extends MyController {
..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
}
//
// Task Approval Logic
//
Future<bool> approveTask({
required String projectId,
required String comment,
@ -120,14 +104,11 @@ class ReportTaskActionController extends MyController {
required String approvedTaskCount,
List<File>? images,
}) async {
logger.i("Starting task approval...");
logger.i("Project ID: $projectId");
logger.i("Comment: $comment");
logger.i("Report Action ID: $reportActionId");
logger.i("Approved Task Count: $approvedTaskCount");
logSafe("approveTask() started", sensitive: false);
if (projectId.isEmpty || reportActionId.isEmpty) {
_showError("Project ID and Report Action ID are required.");
logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
return false;
}
@ -136,25 +117,29 @@ class ReportTaskActionController extends MyController {
if (approvedTaskInt == null) {
_showError("Invalid approved task count.");
logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
return false;
}
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
_showError("Approved task count cannot exceed completed work.");
logSafe("Validation failed: approved > completed", level: LogLevel.warning);
return false;
}
if (comment.trim().isEmpty) {
_showError("Comment is required.");
logSafe("Comment field is empty", level: LogLevel.warning);
return false;
}
try {
reportStatus.value = ApiStatus.loading;
isLoading.value = true;
logSafe("Calling _prepareImages() for approval...");
final imageData = await _prepareImages(images);
logSafe("Calling ApiService.approveTask()");
final success = await ApiService.approveTask(
id: projectId,
workStatus: reportActionId,
@ -164,15 +149,17 @@ class ReportTaskActionController extends MyController {
);
if (success) {
logSafe("Task approved successfully");
_showSuccess("Task approved successfully!");
await taskController.fetchTaskData(projectId);
return true;
} else {
logSafe("API returned failure on approveTask", level: LogLevel.error);
_showError("Failed to approve task.");
return false;
}
} catch (e) {
logger.e("Error approving task: $e");
} catch (e, st) {
logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred.");
return false;
} finally {
@ -183,26 +170,26 @@ class ReportTaskActionController extends MyController {
}
}
//
// Comment Task Logic
//
Future<void> commentTask({
required String projectId,
required String comment,
List<File>? images,
}) async {
logger.i("Starting task comment...");
logSafe("commentTask() started", sensitive: false);
if (commentController.text.trim().isEmpty) {
_showError("Comment is required.");
logSafe("Comment field is empty", level: LogLevel.warning);
return;
}
try {
isLoading.value = true;
logSafe("Calling _prepareImages() for comment...");
final imageData = await _prepareImages(images);
logSafe("Calling ApiService.commentTask()");
final success = await ApiService.commentTask(
id: projectId,
comment: commentController.text.trim(),
@ -212,31 +199,32 @@ class ReportTaskActionController extends MyController {
});
if (success) {
logSafe("Comment added successfully");
_showSuccess("Task commented successfully!");
await taskController.fetchTaskData(projectId);
} else {
logSafe("API returned failure on commentTask", level: LogLevel.error);
_showError("Failed to comment task.");
}
} catch (e) {
logger.e("Error commenting task: $e");
} catch (e, st) {
logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred while commenting the task.");
} finally {
isLoading.value = false;
}
}
//
// API Helpers
//
Future<void> fetchWorkStatuses() async {
logSafe("Fetching work statuses...");
isLoadingWorkStatus.value = true;
final response = await ApiService.getWorkStatus();
if (response != null) {
final model = WorkStatusResponseModel.fromJson(response);
workStatus.assignAll(model.data);
logSafe("Fetched ${model.data.length} work statuses");
} else {
logger.w("No work statuses found or API call failed.");
logSafe("No work statuses found or API call failed", level: LogLevel.warning);
}
isLoadingWorkStatus.value = false;
@ -244,8 +232,12 @@ class ReportTaskActionController extends MyController {
}
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async {
if (images == null || images.isEmpty) return null;
if (images == null || images.isEmpty) {
logSafe("_prepareImages: No images selected.");
return null;
}
logSafe("_prepareImages: Compressing and encoding images...");
final results = await Future.wait(images.map((file) async {
final compressedBytes = await compressImageToUnder100KB(file);
if (compressedBytes == null) return null;
@ -259,6 +251,7 @@ class ReportTaskActionController extends MyController {
};
}));
logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
return results.whereType<Map<String, dynamic>>().toList();
}
@ -273,33 +266,28 @@ class ReportTaskActionController extends MyController {
};
}
//
// Image Picker Utils
//
Future<void> pickImages({required bool fromCamera}) async {
logSafe("Opening image picker...");
if (fromCamera) {
final pickedFile = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 75,
);
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
if (pickedFile != null) {
selectedImages.add(File(pickedFile.path));
logSafe("Image added from camera: ${pickedFile.path}", sensitive: true);
}
} else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
logSafe("${pickedFiles.length} images added from gallery.", sensitive: true);
}
}
void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) {
logSafe("Removing image at index $index", sensitive: true);
selectedImages.removeAt(index);
}
}
//
// Snackbar Feedback
//
void _showError(String message) => showAppSnackbar(
title: "Error", message: message, type: SnackbarType.error);

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

View File

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

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/app_theme.dart';
import 'package:url_strategy/url_strategy.dart';
import 'package:logger/logger.dart';
final Logger logger = Logger();
import 'package:marco/helpers/services/app_logger.dart';
Future<void> initializeApp() async {
setPathUrlStrategy();
try {
logSafe("Starting app initialization...");
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Color.fromARGB(255, 255, 0, 0),
statusBarIconBrightness: Brightness.light,
));
setPathUrlStrategy();
logSafe("URL strategy set.");
await LocalStorage.init();
await ThemeCustomizer.init();
Get.put(PermissionController());
Get.put(ProjectController(), permanent: true);
AppStyle.init();
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Color.fromARGB(255, 255, 0, 0),
statusBarIconBrightness: Brightness.light,
));
logSafe("System UI overlay style set.");
logger.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

@ -0,0 +1,156 @@
import 'dart:io';
import 'package:logger/logger.dart';
import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart';
/// Global logger instance
late final Logger appLogger;
/// Log file output handler
late final FileLogOutput fileLogOutput;
/// Initialize logging (call once in `main()`)
Future<void> initLogging() async {
await requestStoragePermission();
fileLogOutput = FileLogOutput();
appLogger = Logger(
printer: PrettyPrinter(
methodCount: 0,
printTime: true,
colors: true,
printEmojis: true,
),
output: MultiOutput([
ConsoleOutput(), // Console will use the top-level PrettyPrinter
fileLogOutput, // File will still use the SimpleFileLogPrinter
]),
level: Level.debug,
);
}
/// Request storage permission (for Android 11+)
Future<void> requestStoragePermission() async {
final status = await Permission.manageExternalStorage.status;
if (!status.isGranted) {
await Permission.manageExternalStorage.request();
}
}
/// Safe logger wrapper
void logSafe(
String message, {
LogLevel level = LogLevel.info,
dynamic error,
StackTrace? stackTrace,
bool sensitive = false,
}) {
if (sensitive) return;
switch (level) {
case LogLevel.debug:
appLogger.d(message, error: error, stackTrace: stackTrace);
break;
case LogLevel.warning:
appLogger.w(message, error: error, stackTrace: stackTrace);
break;
case LogLevel.error:
appLogger.e(message, error: error, stackTrace: stackTrace);
break;
case LogLevel.verbose:
appLogger.v(message, error: error, stackTrace: stackTrace);
break;
default:
appLogger.i(message, error: error, stackTrace: stackTrace);
}
}
/// Custom log output that writes to a local `.txt` file
class FileLogOutput extends LogOutput {
File? _logFile;
/// Initialize log file in Downloads/marco_logs/log_YYYY-MM-DD.txt
Future<void> _init() async {
if (_logFile != null) return;
final directory = Directory('/storage/emulated/0/Download/marco_logs');
if (!await directory.exists()) {
await directory.create(recursive: true);
}
final date = DateFormat('yyyy-MM-dd').format(DateTime.now());
final filePath = '${directory.path}/log_$date.txt';
_logFile = File(filePath);
if (!await _logFile!.exists()) {
await _logFile!.create();
}
await _cleanOldLogs(directory);
}
@override
void output(OutputEvent event) async {
await _init();
if (event.lines.isEmpty) return;
final logMessage = event.lines.join('\n') + '\n';
await _logFile!.writeAsString(
logMessage,
mode: FileMode.append,
flush: true,
);
}
Future<String> getLogFilePath() async {
await _init();
return _logFile!.path;
}
Future<void> clearLogs() async {
await _init();
await _logFile!.writeAsString('');
}
Future<String> readLogs() async {
await _init();
return _logFile!.readAsString();
}
/// Delete logs older than 3 days
Future<void> _cleanOldLogs(Directory directory) async {
final files = directory.listSync();
final now = DateTime.now();
for (var file in files) {
if (file is File && file.path.endsWith('.txt')) {
final stat = await file.stat();
if (now.difference(stat.modified).inDays > 3) {
await file.delete();
}
}
}
}
}
/// A simple, readable log printer for file output
class SimpleFileLogPrinter extends LogPrinter {
@override
List<String> log(LogEvent event) {
final message = event.message.toString();
if (message.contains('[SENSITIVE]')) return [];
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
final level = event.level.name.toUpperCase();
final error = event.error != null ? ' | ERROR: ${event.error}' : '';
final stack =
event.stackTrace != null ? '\nSTACKTRACE:\n${event.stackTrace}' : '';
return ['[$timestamp] [$level] $message$error$stack'];
}
}
/// Optional log level enum for better type safety
enum LogLevel { debug, info, warning, error, verbose }

View File

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

View File

@ -0,0 +1,79 @@
import 'dart:io';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:intl/intl.dart';
class FileLogOutput extends LogOutput {
late final Directory _logDirectory;
late final String _todayLogFileName;
FileLogOutput() {
_init();
}
Future<void> _init() async {
_logDirectory = await getApplicationDocumentsDirectory();
final today = DateFormat('yyyy-MM-dd').format(DateTime.now());
_todayLogFileName = 'log_$today.txt';
await _cleanupOldLogs();
}
@override
void output(OutputEvent event) async {
final file = await _getTodayLogFile();
final logMessage = event.lines.join('\n') + '\n';
await file.writeAsString(logMessage, mode: FileMode.append, flush: true);
}
Future<File> _getTodayLogFile() async {
final path = '${_logDirectory.path}/$_todayLogFileName';
final file = File(path);
if (!await file.exists()) {
await file.create(recursive: true);
}
return file;
}
/// Keep only the most recent 3 days of logs
Future<void> _cleanupOldLogs() async {
final files = _logDirectory
.listSync()
.whereType<File>()
.where((f) => f.path.contains(RegExp(r'log_\d{4}-\d{2}-\d{2}\.txt')))
.toList();
files.sort((a, b) => b.path.compareTo(a.path));
if (files.length > 3) {
final oldFiles = files.sublist(3);
for (final f in oldFiles) {
try {
await f.delete();
} catch (e) {
}
}
}
}
/// For reading today's log
Future<String> readTodayLogs() async {
final file = await _getTodayLogFile();
return file.readAsString();
}
/// Read all log files (optional utility)
Future<Map<String, String>> readAllLogs() async {
final files = _logDirectory
.listSync()
.whereType<File>()
.where((f) => f.path.contains('log_'))
.toList();
Map<String, String> logs = {};
for (final f in files) {
logs[f.path.split('/').last] = await f.readAsString();
}
return logs;
}
}

View File

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

View File

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

@ -3,15 +3,18 @@ import 'package:marco/helpers/services/app_initializer.dart';
import 'package:marco/view/my_app.dart';
import 'package:provider/provider.dart';
import 'package:marco/helpers/theme/app_notifier.dart';
import 'package:logger/logger.dart';
final Logger logger = Logger();
import 'package:marco/helpers/services/app_logger.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initLogging();
logSafe("App starting...");
try {
await initializeApp();
logSafe("App initialized successfully.");
runApp(
ChangeNotifierProvider<AppNotifier>(
create: (_) => AppNotifier(),
@ -19,11 +22,21 @@ Future<void> main() async {
),
);
} catch (e, stacktrace) {
logger.e('App failed to initialize:', error: e, stackTrace: stacktrace);
logSafe('App failed to initialize.',
level: LogLevel.error,
error: e,
stackTrace: stacktrace,
);
runApp(
const MaterialApp(
home: Scaffold(
body: Center(child: Text("Failed to initialize the app.")),
body: Center(
child: Text(
"Failed to initialize the app.",
style: TextStyle(color: Colors.red),
),
),
),
),
);

View File

@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/task_planing/add_task_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
final Logger log = Logger();
void showCreateTaskBottomSheet({
required String workArea,

View File

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