Compare commits

..

38 Commits

Author SHA1 Message Date
03b82764ed Refactor attendance and document controllers to use reactive date ranges, implement reusable DateRangePickerWidget, and enhance filter functionality in attendance and expense screens.
- Updated AttendanceController to use Rx<DateTime> for date ranges.
- Introduced DateRangePickerWidget for selecting date ranges in attendance and expense filters.
- Refactored attendance filter bottom sheet to utilize the new DateRangePickerWidget.
- Enhanced user document filter bottom sheet with date range selection.
- Improved expense filter bottom sheet to include date range selection and refactored UI components for better readability.
- Cleaned up unused code and improved overall code structure for maintainability.
2025-11-04 14:15:45 +05:30
b33b3da6c0 refactor: Rename ProjectProgressChart to AttendanceDashboardChart and update data handling for attendance overview 2025-11-03 17:40:18 +05:30
817672c8b2 refactor: Improve Attendance and Project Progress Charts with enhanced styling and tooltip formatting 2025-11-03 16:51:51 +05:30
4f0261bf0b feat: Add Monthly Expense Dashboard Chart and related data models 2025-11-03 16:43:29 +05:30
b4be463da6 refactor: Adjust inner radius of ExpenseDonutChart for improved visual clarity 2025-11-01 17:14:44 +05:30
99f6c594b9 feat: Enhance tooltip behavior in ExpenseDonutChart to display formatted currency and percentage 2025-11-01 15:52:59 +05:30
9890fbaffe refactor: Update ExpenseDonutChart data labels to display formatted currency instead of percentage 2025-11-01 15:41:05 +05:30
4a5fd1c7cc feat: Update Dashboard with Expense Type Report Chart and enhance loading experience with skeleton loaders 2025-11-01 15:38:58 +05:30
68cac95908 chore: Update version number to 1.0.0+12 in pubspec.yaml 2025-11-01 11:34:50 +05:30
2f283765c1 refactor: Update FAQ and Support screens to use contentTheme for colors and improve UI consistency 2025-10-31 17:20:44 +05:30
d62f0d2c60 feat: Enhance ExpenseByStatusWidget with navigation and filter functionality; update ExpenseMainScreen to fetch expenses after UI initialization 2025-10-31 17:13:43 +05:30
1e39210a29 feat: Implement expense by status skeleton loader for improved loading experience 2025-10-31 16:05:43 +05:30
8dbd21df8b refactor: Update API base URL and change default color theme to green 2025-10-31 15:35:53 +05:30
6568dc70c8 refactor: Update package and bundle identifiers to reflect new naming convention 2025-10-31 15:22:05 +05:30
bc9fc4d6f1 Refactor UserDocumentsPage: Enhance UI with search bar, filter chips, and document cards; implement infinite scrolling and FAB for document upload; improve state management and permission checks. 2025-10-31 14:58:15 +05:30
62eb7b1d97 feat: Enhance theme customization with color theme persistence and toggle functionality 2025-10-31 10:55:12 +05:30
6d5137b103 feat: Add Expense Type Report feature with chart visualization and API integration 2025-10-30 15:50:55 +05:30
f01608e4e7 feat: Implement pending expenses feature with API integration and UI widget 2025-10-30 10:53:38 +05:30
c78231d0fd feat: Add theme customization feature with ThemeEditorWidget
- Introduced ThemeEditorWidget for user-friendly theme selection.
- Added ThemeOption class to manage theme properties.
- Implemented ThemeController to handle theme application logic.
- Updated ThemeCustomizer to allow external theme changes.
- Refactored wave background components to support dynamic colors.
- Updated various screens to utilize the new theme system.
- Enhanced UI elements with consistent styling and improved responsiveness.
2025-10-29 14:30:51 +05:30
cd21a3ac38 refactor: remove unused comment editor and related dependencies, update version 2025-10-27 14:47:56 +05:30
d26e7e3774 refactor: update package and bundle identifiers to com.onfieldwork.marcoaiot 2025-10-24 15:24:14 +05:30
90d1132e1c chnaged colour 2025-10-11 17:41:01 +05:30
8c3493b792 changed theme to green 2025-10-09 14:50:54 +05:30
f281eb5b50 chnaged date format 2025-10-06 10:48:31 +05:30
a1bd9a3108 resolved the email and has application acccess is not populated on edit 2025-10-01 16:59:15 +05:30
843f394ebe made chnages for name 2025-10-01 16:22:15 +05:30
7d9eb3fad2 modified bash file 2025-10-01 16:21:48 +05:30
90c76a1799 made chnage for date selection 2025-10-01 16:21:19 +05:30
e4165f2ee8 added add new project in projectselection 2025-10-01 15:47:21 +05:30
c9e6840161 added api and craeted add project bottomsheet 2025-10-01 15:06:23 +05:30
2b8196b216 made chnages in add contach for assignment bucket 2025-10-01 12:14:37 +05:30
a18c4dad45 removed assign employee icon 2025-09-30 20:50:42 +05:30
55695ef176 added faq 2025-09-30 19:45:25 +05:30
4feb2875f0 removed unwanted files 2025-09-30 17:00:20 +05:30
3515cab0d5 chnaged the login api 2025-09-30 16:51:01 +05:30
3f3185c2f4 removed login flow code 2025-09-30 15:46:14 +05:30
b1b5b52854 removed unwanted code 2025-09-30 15:17:03 +05:30
1993676470 removed the unwanted menus 2025-09-30 14:59:53 +05:30
317 changed files with 8375 additions and 36147 deletions

View File

@ -1,4 +1,4 @@
# On Field Work # marco
A new Flutter project. A new Flutter project.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -19,7 +19,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.6.0" apply false id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "2.2.21" apply false id "org.jetbrains.kotlin.android" version "1.8.22" apply false
id("com.google.gms.google-services") version "4.4.2" apply false id("com.google.gms.google-services") version "4.4.2" apply false
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 409 KiB

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# =============================== # ===============================
# Flutter APK Build Script (AAB Disabled) # Flutter APK & AAB Build Script
# =============================== # ===============================
# Exit immediately if a command exits with a non-zero status # Exit immediately if a command exits with a non-zero status
@ -14,7 +14,7 @@ YELLOW='\033[1;33m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# App info # App info
APP_NAME="On Field Work" APP_NAME="Marco"
BUILD_DIR="build/app/outputs" BUILD_DIR="build/app/outputs"
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}" echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"
@ -30,19 +30,19 @@ flutter pub get
# ============================== # ==============================
# Step 3: Build AAB (Commented) # Step 3: Build AAB (Commented)
# ============================== # ==============================
# echo -e "${CYAN}🏗 Building AAB file...${NC}" echo -e "${CYAN}🏗 Building AAB file...${NC}"
# flutter build appbundle --release flutter build appbundle --release
# Step 4: Build APK # Step 4: Build APK
echo -e "${CYAN}🏗 Building APK file...${NC}" echo -e "${CYAN}🏗 Building APK file...${NC}"
flutter build apk --release flutter build apk --release
# Step 5: Show output paths # Step 5: Show output paths
# AAB_PATH="$BUILD_DIR/bundle/release/app-release.aab" AAB_PATH="$BUILD_DIR/bundle/release/app-release.aab"
APK_PATH="$BUILD_DIR/apk/release/app-release.apk" APK_PATH="$BUILD_DIR/apk/release/app-release.apk"
echo -e "${GREEN}✅ Build completed successfully!${NC}" echo -e "${GREEN}✅ Build completed successfully!${NC}"
# echo -e "${YELLOW}📍 AAB file: ${CYAN}$AAB_PATH${NC}" echo -e "${YELLOW}📍 AAB file: ${CYAN}$AAB_PATH${NC}"
echo -e "${YELLOW}📍 APK file: ${CYAN}$APK_PATH${NC}" echo -e "${YELLOW}📍 APK file: ${CYAN}$APK_PATH${NC}"
# Optional: open the folder (Mac/Linux) # Optional: open the folder (Mac/Linux)

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>On Field Work</string> <string>Marco</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>on field work</string> <string>marco</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@ -8,5 +8,5 @@ class AppConstant {
static int iOSAppVersion = 1; static int iOSAppVersion = 1;
static String version = "1.0.0"; static String version = "1.0.0";
static String get appName => 'On Field Work'; static String get appName => 'Marco';
} }

View File

@ -1,41 +1,37 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:geolocator/geolocator.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/controller/project_controller.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart'; import 'package:marco/model/attendance/attendance_model.dart';
import 'package:on_field_work/model/attendance/attendance_log_model.dart'; import 'package:marco/model/project_model.dart';
import 'package:on_field_work/model/attendance/attendance_log_view_model.dart'; import 'package:marco/model/employees/employee_model.dart';
import 'package:on_field_work/model/attendance/attendance_model.dart'; import 'package:marco/model/attendance/attendance_log_model.dart';
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/regularization_log_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:marco/model/attendance/attendance_log_view_model.dart';
import 'package:on_field_work/model/project_model.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart';
import 'package:on_field_work/model/regularization_log_model.dart'; import 'package:marco/controller/project_controller.dart';
class AttendanceController extends GetxController { class AttendanceController extends GetxController {
// ------------------ Data Models ------------------ // ------------------ Data Models ------------------
final List<AttendanceModel> attendances = <AttendanceModel>[]; List<AttendanceModel> attendances = [];
final List<ProjectModel> projects = <ProjectModel>[]; List<ProjectModel> projects = [];
final List<EmployeeModel> employees = <EmployeeModel>[]; List<EmployeeModel> employees = [];
final List<AttendanceLogModel> attendanceLogs = <AttendanceLogModel>[]; List<AttendanceLogModel> attendanceLogs = [];
final List<RegularizationLogModel> regularizationLogs = List<RegularizationLogModel> regularizationLogs = [];
<RegularizationLogModel>[]; List<AttendanceLogViewModel> attendenceLogsView = [];
final List<AttendanceLogViewModel> attendenceLogsView =
<AttendanceLogViewModel>[];
// ------------------ Organizations ------------------ // ------------------ Organizations ------------------
final List<Organization> organizations = <Organization>[]; List<Organization> organizations = [];
Organization? selectedOrganization; Organization? selectedOrganization;
final RxBool isLoadingOrganizations = false.obs; final isLoadingOrganizations = false.obs;
// ------------------ States ------------------ // ------------------ States ------------------
String selectedTab = 'todaysAttendance'; String selectedTab = 'todaysAttendance';
@ -46,17 +42,16 @@ class AttendanceController extends GetxController {
final Rx<DateTime> endDateAttendance = final Rx<DateTime> endDateAttendance =
DateTime.now().subtract(const Duration(days: 1)).obs; DateTime.now().subtract(const Duration(days: 1)).obs;
final RxBool isLoading = true.obs; final isLoading = true.obs;
final RxBool isLoadingProjects = true.obs; final isLoadingProjects = true.obs;
final RxBool isLoadingEmployees = true.obs; final isLoadingEmployees = true.obs;
final RxBool isLoadingAttendanceLogs = true.obs; final isLoadingAttendanceLogs = true.obs;
final RxBool isLoadingRegularizationLogs = true.obs; final isLoadingRegularizationLogs = true.obs;
final RxBool isLoadingLogView = true.obs; final isLoadingLogView = true.obs;
final uploadingStates = <String, RxBool>{}.obs;
var showPendingOnly = false.obs;
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; final searchQuery = ''.obs;
final RxBool showPendingOnly = false.obs;
final RxString searchQuery = ''.obs;
@override @override
void onInit() { void onInit() {
@ -69,43 +64,35 @@ class AttendanceController extends GetxController {
} }
void _setDefaultDateRange() { void _setDefaultDateRange() {
final DateTime today = DateTime.now(); final today = DateTime.now();
startDateAttendance.value = today.subtract(const Duration(days: 7)); startDateAttendance.value = today.subtract(const Duration(days: 7));
endDateAttendance.value = today.subtract(const Duration(days: 1)); endDateAttendance.value = today.subtract(const Duration(days: 1));
logSafe( logSafe(
'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}', "Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}");
);
} }
// ------------------ Computed Filters ------------------ // ------------------ Computed Filters ------------------
List<EmployeeModel> get filteredEmployees { List<EmployeeModel> get filteredEmployees {
final String query = searchQuery.value.trim().toLowerCase(); if (searchQuery.value.isEmpty) return employees;
if (query.isEmpty) return employees;
return employees return employees
.where( .where((e) =>
(EmployeeModel e) => e.name.toLowerCase().contains(query), e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
)
.toList(); .toList();
} }
List<AttendanceLogModel> get filteredLogs { List<AttendanceLogModel> get filteredLogs {
final String query = searchQuery.value.trim().toLowerCase(); if (searchQuery.value.isEmpty) return attendanceLogs;
if (query.isEmpty) return attendanceLogs;
return attendanceLogs return attendanceLogs
.where( .where((log) =>
(AttendanceLogModel log) => log.name.toLowerCase().contains(query), log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
)
.toList(); .toList();
} }
List<RegularizationLogModel> get filteredRegularizationLogs { List<RegularizationLogModel> get filteredRegularizationLogs {
final String query = searchQuery.value.trim().toLowerCase(); if (searchQuery.value.isEmpty) return regularizationLogs;
if (query.isEmpty) return regularizationLogs;
return regularizationLogs return regularizationLogs
.where( .where((log) =>
(RegularizationLogModel log) => log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
log.name.toLowerCase().contains(query),
)
.toList(); .toList();
} }
@ -113,16 +100,13 @@ class AttendanceController extends GetxController {
Future<void> refreshDataFromNotification({String? projectId}) async { Future<void> refreshDataFromNotification({String? projectId}) async {
projectId ??= Get.find<ProjectController>().selectedProject?.id; projectId ??= Get.find<ProjectController>().selectedProject?.id;
if (projectId == null) { if (projectId == null) {
logSafe( logSafe("No project selected for attendance refresh from notification",
'No project selected for attendance refresh from notification', level: LogLevel.warning);
level: LogLevel.warning,
);
return; return;
} }
await fetchProjectData(projectId); await fetchProjectData(projectId);
logSafe( logSafe(
'Attendance data refreshed from notification for project $projectId', "Attendance data refreshed from notification for project $projectId");
);
} }
Future<void> fetchTodaysAttendance(String? projectId) async { Future<void> fetchTodaysAttendance(String? projectId) async {
@ -130,35 +114,19 @@ class AttendanceController extends GetxController {
isLoadingEmployees.value = true; isLoadingEmployees.value = true;
final List<dynamic>? response = await ApiService.getTodaysAttendance( final response = await ApiService.getTodaysAttendance(
projectId, projectId,
organizationId: selectedOrganization?.id, organizationId: selectedOrganization?.id,
); );
if (response != null) { if (response != null) {
employees employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
..clear() for (var emp in employees) {
..addAll(
response
.map<EmployeeModel>(
(dynamic e) => EmployeeModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
for (final EmployeeModel emp in employees) {
uploadingStates[emp.id] = false.obs; uploadingStates[emp.id] = false.obs;
} }
logSafe("Employees fetched: ${employees.length} for project $projectId");
logSafe(
'Employees fetched: ${employees.length} for project $projectId',
);
} else { } else {
logSafe( logSafe("Failed to fetch employees for project $projectId",
'Failed to fetch employees for project $projectId', level: LogLevel.error);
level: LogLevel.error,
);
} }
isLoadingEmployees.value = false; isLoadingEmployees.value = false;
@ -167,22 +135,14 @@ class AttendanceController extends GetxController {
Future<void> fetchOrganizations(String projectId) async { Future<void> fetchOrganizations(String projectId) async {
isLoadingOrganizations.value = true; isLoadingOrganizations.value = true;
// Keep original return type inference from your ApiService
final response = await ApiService.getAssignedOrganizations(projectId); final response = await ApiService.getAssignedOrganizations(projectId);
if (response != null) { if (response != null) {
organizations organizations = response.data;
..clear() logSafe("Organizations fetched: ${organizations.length}");
..addAll(response.data);
logSafe('Organizations fetched: ${organizations.length}');
} else { } else {
logSafe( logSafe("Failed to fetch organizations for project $projectId",
'Failed to fetch organizations for project $projectId', level: LogLevel.error);
level: LogLevel.error,
);
} }
isLoadingOrganizations.value = false; isLoadingOrganizations.value = false;
update(); update();
} }
@ -192,43 +152,61 @@ class AttendanceController extends GetxController {
String id, String id,
String employeeId, String employeeId,
String projectId, { String projectId, {
String comment = 'Marked via mobile app', String comment = "Marked via mobile app",
required int action, required int action,
bool imageCapture = true, bool imageCapture = true,
String? markTime, String? markTime,
String? date, String? date,
}) async { }) async {
try { try {
_setUploading(employeeId, true); uploadingStates[employeeId]?.value = true;
final XFile? image = await _captureAndPrepareImage( XFile? image;
employeeId: employeeId, if (imageCapture) {
imageCapture: imageCapture, image = await ImagePicker()
); .pickImage(source: ImageSource.camera, imageQuality: 80);
if (imageCapture && image == null) { if (image == null) {
return false; logSafe("Image capture cancelled.", level: LogLevel.warning);
return false;
}
final timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: File(image.path));
final compressedBytes =
await compressImageToUnder100KB(timestampedFile);
if (compressedBytes == null) {
logSafe("Image compression failed.", level: LogLevel.error);
return false;
}
final compressedFile = await saveCompressedImageToFile(compressedBytes);
image = XFile(compressedFile.path);
} }
final Position? position = await _getCurrentPositionSafely(); if (!await _handleLocationPermission()) return false;
if (position == null) return false; final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
final String imageName = imageCapture final imageName = imageCapture
? ApiService.generateImageName( ? ApiService.generateImageName(employeeId, employees.length + 1)
employeeId, : "";
employees.length + 1,
)
: '';
final DateTime effectiveDate = final now = DateTime.now();
_resolveEffectiveDateForAction(action, employeeId); DateTime effectiveDate = now;
final DateTime now = DateTime.now(); if (action == 1) {
final String formattedMarkTime = final log = attendanceLogs.firstWhereOrNull(
markTime ?? DateFormat('hh:mm a').format(now); (log) => log.employeeId == employeeId && log.checkOut == null,
final String formattedDate = );
if (log?.checkIn != null) effectiveDate = log!.checkIn!;
}
final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now);
final formattedDate =
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate); date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
final bool result = await ApiService.uploadAttendanceImage( final result = await ApiService.uploadAttendanceImage(
id, id,
employeeId, employeeId,
image, image,
@ -243,99 +221,15 @@ class AttendanceController extends GetxController {
date: formattedDate, date: formattedDate,
); );
if (result) { logSafe(
logSafe( "Attendance uploaded for $employeeId, action: $action, date: $formattedDate");
'Attendance uploaded for $employeeId, action: $action, date: $formattedDate',
);
if (Get.isRegistered<DashboardController>()) {
final DashboardController dashboardController =
Get.find<DashboardController>();
await dashboardController.fetchTodaysAttendance(projectId);
}
}
return result; return result;
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe( logSafe("Error uploading attendance",
'Error uploading attendance', level: LogLevel.error, error: e, stackTrace: stacktrace);
level: LogLevel.error,
error: e,
stackTrace: stacktrace,
);
return false; return false;
} finally { } finally {
_setUploading(employeeId, false); uploadingStates[employeeId]?.value = false;
}
}
Future<XFile?> _captureAndPrepareImage({
required String employeeId,
required bool imageCapture,
}) async {
if (!imageCapture) return null;
final XFile? rawImage = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (rawImage == null) {
logSafe(
'Image capture cancelled.',
level: LogLevel.warning,
);
return null;
}
final File timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: File(rawImage.path),
);
final List<int>? compressedBytes =
await compressImageToUnder100KB(timestampedFile);
if (compressedBytes == null) {
logSafe(
'Image compression failed.',
level: LogLevel.error,
);
return null;
}
// FIX: convert List<int> -> Uint8List
final Uint8List compressedUint8List = Uint8List.fromList(compressedBytes);
final File compressedFile =
await saveCompressedImageToFile(compressedUint8List);
return XFile(compressedFile.path);
}
Future<Position?> _getCurrentPositionSafely() async {
final bool permissionGranted = await _handleLocationPermission();
if (!permissionGranted) return null;
return Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
}
DateTime _resolveEffectiveDateForAction(int action, String employeeId) {
final DateTime now = DateTime.now();
if (action != 1) return now;
final AttendanceLogModel? log = attendanceLogs.firstWhereOrNull(
(AttendanceLogModel log) =>
log.employeeId == employeeId && log.checkOut == null,
);
return log?.checkIn ?? now;
}
void _setUploading(String employeeId, bool value) {
final RxBool? state = uploadingStates[employeeId];
if (state != null) {
state.value = value;
} else {
uploadingStates[employeeId] = value.obs;
} }
} }
@ -345,19 +239,14 @@ class AttendanceController extends GetxController {
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
logSafe( logSafe('Location permissions are denied', level: LogLevel.warning);
'Location permissions are denied',
level: LogLevel.warning,
);
return false; return false;
} }
} }
if (permission == LocationPermission.deniedForever) { if (permission == LocationPermission.deniedForever) {
logSafe( logSafe('Location permissions are permanently denied',
'Location permissions are permanently denied', level: LogLevel.error);
level: LogLevel.error,
);
return false; return false;
} }
@ -365,40 +254,25 @@ class AttendanceController extends GetxController {
} }
// ------------------ Attendance Logs ------------------ // ------------------ Attendance Logs ------------------
Future<void> fetchAttendanceLogs( Future<void> fetchAttendanceLogs(String? projectId,
String? projectId, { {DateTime? dateFrom, DateTime? dateTo}) async {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return; if (projectId == null) return;
isLoadingAttendanceLogs.value = true; isLoadingAttendanceLogs.value = true;
final List<dynamic>? response = await ApiService.getAttendanceLogs( final response = await ApiService.getAttendanceLogs(
projectId, projectId,
dateFrom: dateFrom, dateFrom: dateFrom,
dateTo: dateTo, dateTo: dateTo,
organizationId: selectedOrganization?.id, organizationId: selectedOrganization?.id,
); );
if (response != null) { if (response != null) {
attendanceLogs attendanceLogs =
..clear() response.map((e) => AttendanceLogModel.fromJson(e)).toList();
..addAll( logSafe("Attendance logs fetched: ${attendanceLogs.length}");
response
.map<AttendanceLogModel>(
(dynamic e) => AttendanceLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
logSafe('Attendance logs fetched: ${attendanceLogs.length}');
} else { } else {
logSafe( logSafe("Failed to fetch attendance logs for project $projectId",
'Failed to fetch attendance logs for project $projectId', level: LogLevel.error);
level: LogLevel.error,
);
} }
isLoadingAttendanceLogs.value = false; isLoadingAttendanceLogs.value = false;
@ -406,37 +280,25 @@ class AttendanceController extends GetxController {
} }
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() { Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final Map<String, List<AttendanceLogModel>> groupedLogs = final groupedLogs = <String, List<AttendanceLogModel>>{};
<String, List<AttendanceLogModel>>{};
for (final AttendanceLogModel logItem in attendanceLogs) { for (var logItem in attendanceLogs) {
final String checkInDate = logItem.checkIn != null final checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!) ? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown'; : 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
groupedLogs.putIfAbsent(
checkInDate,
() => <AttendanceLogModel>[],
)..add(logItem);
} }
final List<MapEntry<String, List<AttendanceLogModel>>> sortedEntries = final sortedEntries = groupedLogs.entries.toList()
groupedLogs.entries.toList() ..sort((a, b) {
..sort( if (a.key == 'Unknown') return 1;
(MapEntry<String, List<AttendanceLogModel>> a, if (b.key == 'Unknown') return -1;
MapEntry<String, List<AttendanceLogModel>> b) { final dateA = DateFormat('dd MMM yyyy').parse(a.key);
if (a.key == 'Unknown') return 1; final dateB = DateFormat('dd MMM yyyy').parse(b.key);
if (b.key == 'Unknown') return -1; return dateB.compareTo(dateA);
});
final DateTime dateA = DateFormat('dd MMM yyyy').parse(a.key); return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
final DateTime dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
},
);
return Map<String, List<AttendanceLogModel>>.fromEntries(
sortedEntries,
);
} }
// ------------------ Regularization Logs ------------------ // ------------------ Regularization Logs ------------------
@ -445,31 +307,17 @@ class AttendanceController extends GetxController {
isLoadingRegularizationLogs.value = true; isLoadingRegularizationLogs.value = true;
final List<dynamic>? response = await ApiService.getRegularizationLogs( final response = await ApiService.getRegularizationLogs(
projectId, projectId,
organizationId: selectedOrganization?.id, organizationId: selectedOrganization?.id,
); );
if (response != null) { if (response != null) {
regularizationLogs regularizationLogs =
..clear() response.map((e) => RegularizationLogModel.fromJson(e)).toList();
..addAll( logSafe("Regularization logs fetched: ${regularizationLogs.length}");
response
.map<RegularizationLogModel>(
(dynamic e) => RegularizationLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
logSafe(
'Regularization logs fetched: ${regularizationLogs.length}',
);
} else { } else {
logSafe( logSafe("Failed to fetch regularization logs for project $projectId",
'Failed to fetch regularization logs for project $projectId', level: LogLevel.error);
level: LogLevel.error,
);
} }
isLoadingRegularizationLogs.value = false; isLoadingRegularizationLogs.value = false;
@ -482,33 +330,16 @@ class AttendanceController extends GetxController {
isLoadingLogView.value = true; isLoadingLogView.value = true;
final List<dynamic>? response = await ApiService.getAttendanceLogView(id); final response = await ApiService.getAttendanceLogView(id);
if (response != null) { if (response != null) {
attendenceLogsView attendenceLogsView =
..clear() response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
..addAll( attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000))
response .compareTo(a.activityTime ?? DateTime(2000)));
.map<AttendanceLogViewModel>( logSafe("Attendance log view fetched for ID: $id");
(dynamic e) => AttendanceLogViewModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
attendenceLogsView.sort(
(AttendanceLogViewModel a, AttendanceLogViewModel b) =>
(b.activityTime ?? DateTime(2000))
.compareTo(a.activityTime ?? DateTime(2000)),
);
logSafe('Attendance log view fetched for ID: $id');
} else { } else {
logSafe( logSafe("Failed to fetch attendance log view for ID $id",
'Failed to fetch attendance log view for ID $id', level: LogLevel.error);
level: LogLevel.error,
);
} }
isLoadingLogView.value = false; isLoadingLogView.value = false;
@ -544,19 +375,16 @@ class AttendanceController extends GetxController {
} }
logSafe( logSafe(
'Project data fetched for project ID: $projectId, tab: $selectedTab', "Project data fetched for project ID: $projectId, tab: $selectedTab");
);
update(); update();
} }
// ------------------ UI Interaction ------------------ // ------------------ UI Interaction ------------------
Future<void> selectDateRangeForAttendance( Future<void> selectDateRangeForAttendance(
BuildContext context, BuildContext context, AttendanceController controller) async {
AttendanceController controller, final today = DateTime.now();
) async {
final DateTime today = DateTime.now();
final DateTimeRange? picked = await showDateRangePicker( final picked = await showDateRangePicker(
context: context, context: context,
firstDate: DateTime(2022), firstDate: DateTime(2022),
lastDate: today.subtract(const Duration(days: 1)), lastDate: today.subtract(const Duration(days: 1)),
@ -571,8 +399,7 @@ class AttendanceController extends GetxController {
endDateAttendance.value = picked.end; endDateAttendance.value = picked.end;
logSafe( logSafe(
'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}', "Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}");
);
await controller.fetchAttendanceLogs( await controller.fetchAttendanceLogs(
Get.find<ProjectController>().selectedProject?.id, Get.find<ProjectController>().selectedProject?.id,

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_validators.dart'; import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
class ForgotPasswordController extends MyController { class ForgotPasswordController extends MyController {
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();

View File

@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_validators.dart'; import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class LoginController extends MyController { class LoginController extends MyController {
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();
@ -14,7 +15,6 @@ class LoginController extends MyController {
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
final RxBool showPassword = false.obs; final RxBool showPassword = false.obs;
final RxBool isChecked = false.obs; final RxBool isChecked = false.obs;
final RxBool showSplash = false.obs;
@override @override
void onInit() { void onInit() {
@ -41,14 +41,18 @@ class LoginController extends MyController {
); );
} }
void onChangeCheckBox(bool? value) => isChecked.value = value ?? false; void onChangeCheckBox(bool? value) {
isChecked.value = value ?? false;
}
void onChangeShowPassword() => showPassword.toggle(); void onChangeShowPassword() {
showPassword.toggle();
}
Future<void> onLogin() async { Future<void> onLogin() async {
if (!basicValidator.validateForm()) return; if (!basicValidator.validateForm()) return;
showSplash.value = true; isLoading.value = true;
try { try {
final loginData = basicValidator.getData(); final loginData = basicValidator.getData();
@ -57,30 +61,50 @@ class LoginController extends MyController {
final errors = await AuthService.loginUser(loginData); final errors = await AuthService.loginUser(loginData);
if (errors != null) { if (errors != null) {
logSafe(
"Login failed for user: ${loginData['username']} with errors: $errors",
level: LogLevel.warning);
showAppSnackbar( showAppSnackbar(
title: "Login Failed", title: "Login Failed",
message: "Username or password is incorrect", message: "Username or password is incorrect",
type: SnackbarType.error, type: SnackbarType.error,
); );
basicValidator.addErrors(errors); basicValidator.addErrors(errors);
basicValidator.validateForm(); basicValidator.validateForm();
basicValidator.clearErrors(); basicValidator.clearErrors();
} else { } else {
await _handleRememberMe(); await _handleRememberMe();
// Enable remote logging after successful login
enableRemoteLogging(); enableRemoteLogging();
logSafe("✅ Remote logging enabled after login.");
final fcmToken = await LocalStorage.getFcmToken();
if (fcmToken?.isNotEmpty ?? false) {
final success = await AuthService.registerDeviceToken(fcmToken!);
logSafe(
success
? "✅ FCM token registered after login."
: "⚠️ Failed to register FCM token after login.",
level: LogLevel.warning);
}
logSafe("Login successful for user: ${loginData['username']}"); logSafe("Login successful for user: ${loginData['username']}");
Get.offNamed('/select-tenant'); Get.toNamed('/home');
} }
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Exception during login",
level: LogLevel.error, error: e, stackTrace: stacktrace);
showAppSnackbar( showAppSnackbar(
title: "Login Error", title: "Login Error",
message: "An unexpected error occurred", message: "An unexpected error occurred",
type: SnackbarType.error, type: SnackbarType.error,
); );
logSafe("Exception during login",
level: LogLevel.error, error: e, stackTrace: stacktrace);
} finally { } finally {
showSplash.value = false; isLoading.value = false;
} }
} }
@ -112,7 +136,11 @@ class LoginController extends MyController {
} }
} }
void goToForgotPassword() => Get.toNamed('/auth/forgot_password'); void goToForgotPassword() {
Get.toNamed('/auth/forgot_password');
}
void gotoRegister() => Get.offAndToNamed('/auth/register_account'); void gotoRegister() {
Get.offAndToNamed('/auth/register_account');
}
} }

View File

@ -1,13 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/view/dashboard/dashboard_screen.dart';
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/controller/permission_controller.dart'; import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:on_field_work/controller/project_controller.dart';
class MPINController extends GetxController { class MPINController extends GetxController {
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();
@ -139,17 +138,16 @@ class MPINController extends GetxController {
} }
/// Navigate to dashboard /// Navigate to dashboard
/// Navigate to tenant selection after MPIN verification void _navigateToDashboard({String? message}) {
void _navigateToTenantSelection({String? message}) {
if (message != null) { if (message != null) {
logSafe("Navigating to Tenant Selection with message: $message"); logSafe("Navigating to Dashboard with message: $message");
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: message, message: message,
type: SnackbarType.success, type: SnackbarType.success,
); );
} }
Get.offAllNamed('/select-tenant'); Get.offAll(() => const DashboardScreen());
} }
/// Clear the primary MPIN fields /// Clear the primary MPIN fields
@ -241,12 +239,15 @@ class MPINController extends GetxController {
logSafe("verifyMPIN triggered"); logSafe("verifyMPIN triggered");
final enteredMPIN = digitControllers.map((c) => c.text).join(); final enteredMPIN = digitControllers.map((c) => c.text).join();
logSafe("Entered MPIN: $enteredMPIN");
if (enteredMPIN.length < 4) { if (enteredMPIN.length < 4) {
_showError("Please enter all 4 digits."); _showError("Please enter all 4 digits.");
return; return;
} }
final mpinToken = await LocalStorage.getMpinToken(); final mpinToken = await LocalStorage.getMpinToken();
if (mpinToken == null || mpinToken.isEmpty) { if (mpinToken == null || mpinToken.isEmpty) {
_showError("Missing MPIN token. Please log in again."); _showError("Missing MPIN token. Please log in again.");
return; return;
@ -269,25 +270,12 @@ class MPINController extends GetxController {
logSafe("MPIN verified successfully"); logSafe("MPIN verified successfully");
await LocalStorage.setBool('mpin_verified', true); await LocalStorage.setBool('mpin_verified', true);
// 🔹 Ensure controllers are injected and loaded
final token = await LocalStorage.getJwtToken();
if (token != null && token.isNotEmpty) {
if (!Get.isRegistered<PermissionController>()) {
Get.put(PermissionController());
await Get.find<PermissionController>().loadData(token);
}
if (!Get.isRegistered<ProjectController>()) {
Get.put(ProjectController(), permanent: true);
await Get.find<ProjectController>().fetchProjects();
}
}
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "MPIN Verified Successfully", message: "MPIN Verified Successfully",
type: SnackbarType.success, type: SnackbarType.success,
); );
_navigateToTenantSelection(); _navigateToDashboard();
} else { } else {
final errorMessage = response["error"] ?? "Invalid MPIN"; final errorMessage = response["error"] ?? "Invalid MPIN";
logSafe("MPIN verification failed: $errorMessage", logSafe("MPIN verification failed: $errorMessage",
@ -303,7 +291,11 @@ class MPINController extends GetxController {
} catch (e) { } catch (e) {
isLoading.value = false; isLoading.value = false;
logSafe("Exception in verifyMPIN", level: LogLevel.error, error: e); logSafe("Exception in verifyMPIN", level: LogLevel.error, error: e);
_showError("Something went wrong. Please try again."); showAppSnackbar(
title: "Error",
message: "Something went wrong. Please try again.",
type: SnackbarType.error,
);
} }
} }

View File

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class OTPController extends GetxController { class OTPController extends GetxController {
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
@ -109,8 +109,7 @@ class OTPController extends GetxController {
} }
void onOTPChanged(String value, int index) { void onOTPChanged(String value, int index) {
logSafe("[OTPController] OTP field changed: index=$index", logSafe("[OTPController] OTP field changed: index=$index", level: LogLevel.debug);
level: LogLevel.debug);
if (value.isNotEmpty) { if (value.isNotEmpty) {
if (index < otpControllers.length - 1) { if (index < otpControllers.length - 1) {
focusNodes[index + 1].requestFocus(); focusNodes[index + 1].requestFocus();
@ -126,24 +125,30 @@ class OTPController extends GetxController {
Future<void> verifyOTP() async { Future<void> verifyOTP() async {
final enteredOTP = otpControllers.map((c) => c.text).join(); final enteredOTP = otpControllers.map((c) => c.text).join();
logSafe("[OTPController] Verifying OTP");
final result = await AuthService.verifyOtp( final result = await AuthService.verifyOtp(
email: email.value, email: email.value,
otp: enteredOTP, otp: enteredOTP,
); );
if (result == null) { if (result == null) {
// Handle remember-me like in LoginController logSafe("[OTPController] OTP verified successfully");
final remember = LocalStorage.getBool('remember_me') ?? false; showAppSnackbar(
if (remember) await LocalStorage.setToken('otp_email', email.value); title: "Success",
message: "OTP verified successfully",
type: SnackbarType.success,
);
final bool isMpinEnabled = LocalStorage.getIsMpin();
logSafe("[OTPController] MPIN Enabled: $isMpinEnabled");
// Enable remote logging Get.offAllNamed('/home');
enableRemoteLogging();
Get.offAllNamed('/select-tenant');
} else { } else {
final error = result['error'] ?? "Failed to verify OTP";
logSafe("[OTPController] OTP verification failed", level: LogLevel.warning, error: error);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: result['error']!, message: error,
type: SnackbarType.error, type: SnackbarType.error,
); );
} }
@ -210,8 +215,7 @@ class OTPController extends GetxController {
final savedEmail = LocalStorage.getToken('otp_email') ?? ''; final savedEmail = LocalStorage.getToken('otp_email') ?? '';
emailController.text = savedEmail; emailController.text = savedEmail;
email.value = savedEmail; email.value = savedEmail;
logSafe( logSafe("[OTPController] Loaded saved email from local storage: $savedEmail");
"[OTPController] Loaded saved email from local storage: $savedEmail");
} }
} }
} }

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_validators.dart'; import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class RegisterAccountController extends MyController { class RegisterAccountController extends MyController {
MyFormValidator basicValidator = MyFormValidator(); MyFormValidator basicValidator = MyFormValidator();

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_validators.dart'; import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class ResetPasswordController extends MyController { class ResetPasswordController extends MyController {
MyFormValidator basicValidator = MyFormValidator(); MyFormValidator basicValidator = MyFormValidator();
@ -49,8 +49,8 @@ class ResetPasswordController extends MyController {
basicValidator.clearErrors(); basicValidator.clearErrors();
} }
logSafe("[ResetPasswordController] Navigating to /dashboard"); logSafe("[ResetPasswordController] Navigating to /home");
Get.toNamed('/dashboard'); Get.toNamed('/home');
update(); update();
} else { } else {
logSafe("[ResetPasswordController] Form validation failed", level: LogLevel.warning); logSafe("[ResetPasswordController] Form validation failed", level: LogLevel.warning);

View File

@ -1,207 +1,197 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:on_field_work/model/dashboard/project_progress_model.dart'; import 'package:marco/model/dashboard/project_progress_model.dart';
import 'package:on_field_work/model/dashboard/pending_expenses_model.dart'; import 'package:marco/model/dashboard/pending_expenses_model.dart';
import 'package:on_field_work/model/dashboard/expense_type_report_model.dart'; import 'package:marco/model/dashboard/expense_type_report_model.dart';
import 'package:on_field_work/model/dashboard/monthly_expence_model.dart'; import 'package:marco/model/dashboard/monthly_expence_model.dart';
import 'package:on_field_work/model/expense/expense_type_model.dart'; import 'package:marco/model/expense/expense_type_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
class DashboardController extends GetxController { class DashboardController extends GetxController {
// Dependencies // =========================
final ProjectController projectController = Get.put(ProjectController()); // Attendance overview
// =========================
final RxList<Map<String, dynamic>> roleWiseData =
<Map<String, dynamic>>[].obs;
final RxString attendanceSelectedRange = '15D'.obs;
final RxBool attendanceIsChartView = true.obs;
final RxBool isAttendanceLoading = false.obs;
// ========================= // =========================
// 1. STATE VARIABLES // Project progress overview
// ========================= // =========================
final RxList<ChartTaskData> projectChartData = <ChartTaskData>[].obs;
final RxString projectSelectedRange = '15D'.obs;
final RxBool projectIsChartView = true.obs;
final RxBool isProjectLoading = false.obs;
// Attendance // =========================
final roleWiseData = <Map<String, dynamic>>[].obs; // Projects overview
final attendanceSelectedRange = '15D'.obs; // =========================
final attendanceIsChartView = true.obs; final RxInt totalProjects = 0.obs;
final isAttendanceLoading = false.obs; final RxInt ongoingProjects = 0.obs;
final RxBool isProjectsLoading = false.obs;
// Project Progress // =========================
final projectChartData = <ChartTaskData>[].obs; // Tasks overview
final projectSelectedRange = '15D'.obs; // =========================
final projectIsChartView = true.obs; final RxInt totalTasks = 0.obs;
final isProjectLoading = false.obs; final RxInt completedTasks = 0.obs;
final RxBool isTasksLoading = false.obs;
// Overview Counts // =========================
final totalProjects = 0.obs; // Teams overview
final ongoingProjects = 0.obs; // =========================
final isProjectsLoading = false.obs; final RxInt totalEmployees = 0.obs;
final RxInt inToday = 0.obs;
final RxBool isTeamsLoading = false.obs;
final totalTasks = 0.obs; // Common ranges
final completedTasks = 0.obs;
final isTasksLoading = false.obs;
final totalEmployees = 0.obs;
final inToday = 0.obs;
final isTeamsLoading = false.obs;
// Expenses & Reports
final isPendingExpensesLoading = false.obs;
final pendingExpensesData = Rx<PendingExpensesData?>(null);
final isExpenseTypeReportLoading = false.obs;
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
final expenseReportStartDate =
DateTime.now().subtract(const Duration(days: 15)).obs;
final expenseReportEndDate = DateTime.now().obs;
final isMonthlyExpenseLoading = false.obs;
final monthlyExpenseList = <MonthlyExpenseData>[].obs;
final selectedMonthlyExpenseDuration =
MonthlyExpenseDuration.twelveMonths.obs;
final selectedMonthsCount = 12.obs;
final expenseTypes = <ExpenseTypeModel>[].obs;
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
// Teams/Employees
final isLoadingEmployees = true.obs;
final employees = <EmployeeModel>[].obs;
final uploadingStates = <String, RxBool>{}.obs;
// Collection
final isCollectionOverviewLoading = true.obs;
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
// =========================
// Purchase Invoice Overview
// =========================
final isPurchaseInvoiceLoading = true.obs;
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
// Constants
final List<String> ranges = ['7D', '15D', '30D']; final List<String> ranges = ['7D', '15D', '30D'];
static const _rangeDaysMap = {
'7D': 7,
'15D': 15,
'30D': 30,
'3M': 90,
'6M': 180
};
// Inject ProjectController
final ProjectController projectController = Get.put(ProjectController());
// Pending Expenses overview
// =========================
final RxBool isPendingExpensesLoading = false.obs;
final Rx<PendingExpensesData?> pendingExpensesData =
Rx<PendingExpensesData?>(null);
// ========================= // =========================
// 2. COMPUTED PROPERTIES // Expense Type Report
// =========================
final RxBool isExpenseTypeReportLoading = false.obs;
final Rx<ExpenseTypeReportData?> expenseTypeReportData =
Rx<ExpenseTypeReportData?>(null);
final Rx<DateTime> expenseReportStartDate =
DateTime.now().subtract(const Duration(days: 15)).obs;
final Rx<DateTime> expenseReportEndDate = DateTime.now().obs;
// ========================= // =========================
// Monthly Expense Report
// =========================
final RxBool isMonthlyExpenseLoading = false.obs;
final RxList<MonthlyExpenseData> monthlyExpenseList =
<MonthlyExpenseData>[].obs;
// =========================
// Monthly Expense Report Filters
// =========================
final Rx<MonthlyExpenseDuration> selectedMonthlyExpenseDuration =
MonthlyExpenseDuration.twelveMonths.obs;
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7; final RxInt selectedMonthsCount = 12.obs;
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7; final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
final Rx<ExpenseTypeModel?> selectedExpenseType = Rx<ExpenseTypeModel?>(null);
// DSO Calculation Constants void updateSelectedExpenseType(ExpenseTypeModel? type) {
static const double _w0_30 = 15.0; selectedExpenseType.value = type;
static const double _w30_60 = 45.0;
static const double _w60_90 = 75.0;
static const double _w90_plus = 105.0;
double get calculatedDSO { // Debug print to verify
final data = collectionOverviewData.value; print('Selected: ${type?.name ?? "All Types"}');
if (data == null || data.totalDueAmount == 0) return 0.0;
final double weightedDue = (data.bucket0To30Amount * _w0_30) + if (type == null) {
(data.bucket30To60Amount * _w30_60) + fetchMonthlyExpenses();
(data.bucket60To90Amount * _w60_90) + } else {
(data.bucket90PlusAmount * _w90_plus); fetchMonthlyExpenses(categoryId: type.id);
}
return weightedDue / data.totalDueAmount;
} }
// =========================
// 3. LIFECYCLE
// =========================
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
logSafe('DashboardController initialized', level: LogLevel.info);
// Project Selection Listener logSafe(
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
level: LogLevel.info,
);
fetchAllDashboardData();
// React to project change
ever<String>(projectController.selectedProjectId, (id) { ever<String>(projectController.selectedProjectId, (id) {
if (id.isNotEmpty) { fetchAllDashboardData();
fetchAllDashboardData();
fetchTodaysAttendance(id);
}
}); });
// Expense Report Date Listener
everAll([expenseReportStartDate, expenseReportEndDate], (_) { everAll([expenseReportStartDate, expenseReportEndDate], (_) {
if (projectController.selectedProjectId.value.isNotEmpty) { fetchExpenseTypeReport(
fetchExpenseTypeReport( startDate: expenseReportStartDate.value,
startDate: expenseReportStartDate.value, endDate: expenseReportEndDate.value,
endDate: expenseReportEndDate.value, );
);
}
}); });
// React to range changes
// Chart Range Listeners
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
ever(projectSelectedRange, (_) => fetchProjectProgress()); ever(projectSelectedRange, (_) => fetchProjectProgress());
} }
// ========================= // =========================
// 4. USER ACTIONS // Helper Methods
// ========================= // =========================
int _getDaysFromRange(String range) {
void updateAttendanceRange(String range) => switch (range) {
attendanceSelectedRange.value = range; case '7D':
void updateProjectRange(String range) => projectSelectedRange.value = range; return 7;
void toggleAttendanceChartView(bool isChart) => case '15D':
attendanceIsChartView.value = isChart; return 15;
void toggleProjectChartView(bool isChart) => case '30D':
projectIsChartView.value = isChart; return 30;
case '3M':
void updateSelectedExpenseType(ExpenseTypeModel? type) { return 90;
selectedExpenseType.value = type; case '6M':
fetchMonthlyExpenses(categoryId: type?.id); return 180;
} default:
return 7;
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
selectedMonthlyExpenseDuration.value = duration;
// Efficient Map lookup instead of Switch
const durationMap = {
MonthlyExpenseDuration.oneMonth: 1,
MonthlyExpenseDuration.threeMonths: 3,
MonthlyExpenseDuration.sixMonths: 6,
MonthlyExpenseDuration.twelveMonths: 12,
MonthlyExpenseDuration.all: 0,
};
selectedMonthsCount.value = durationMap[duration] ?? 12;
fetchMonthlyExpenses();
}
Future<void> refreshDashboard() => fetchAllDashboardData();
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
Future<void> refreshProjects() => fetchProjectProgress();
Future<void> refreshTasks() async {
final id = projectController.selectedProjectId.value;
if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
}
// =========================
// 5. DATA FETCHING (API)
// =========================
/// Wrapper to reduce try-finally boilerplate for loading states
Future<void> _executeApiCall(
RxBool loader, Future<void> Function() apiLogic) async {
loader.value = true;
try {
await apiLogic();
} finally {
loader.value = false;
} }
} }
int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value);
int getProjectDays() => _getDaysFromRange(projectSelectedRange.value);
void updateAttendanceRange(String range) {
attendanceSelectedRange.value = range;
logSafe('Attendance range updated to $range', level: LogLevel.debug);
}
void updateProjectRange(String range) {
projectSelectedRange.value = range;
logSafe('Project range updated to $range', level: LogLevel.debug);
}
void toggleAttendanceChartView(bool isChart) {
attendanceIsChartView.value = isChart;
logSafe('Attendance chart view toggled to: $isChart',
level: LogLevel.debug);
}
void toggleProjectChartView(bool isChart) {
projectIsChartView.value = isChart;
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
}
// =========================
// Manual Refresh Methods
// =========================
Future<void> refreshDashboard() async {
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
await fetchAllDashboardData();
}
Future<void> refreshAttendance() async => fetchRoleWiseAttendance();
Future<void> refreshTasks() async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) await fetchDashboardTasks(projectId: projectId);
}
Future<void> refreshProjects() async => fetchProjectProgress();
// =========================
// Fetch All Dashboard Data
// =========================
Future<void> fetchAllDashboardData() async { Future<void> fetchAllDashboardData() async {
final String projectId = projectController.selectedProjectId.value; final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
if (projectId.isEmpty) {
logSafe('No project selected. Skipping dashboard API calls.',
level: LogLevel.warning);
return;
}
await Future.wait([ await Future.wait([
fetchRoleWiseAttendance(), fetchRoleWiseAttendance(),
@ -214,150 +204,248 @@ class DashboardController extends GetxController {
endDate: expenseReportEndDate.value, endDate: expenseReportEndDate.value,
), ),
fetchMonthlyExpenses(), fetchMonthlyExpenses(),
fetchMasterData(), fetchMasterData()
fetchCollectionOverview(),
fetchPurchaseInvoiceOverview(),
]); ]);
} }
Future<void> fetchCollectionOverview() async { void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
final projectId = projectController.selectedProjectId.value; selectedMonthlyExpenseDuration.value = duration;
if (projectId.isEmpty) return;
await _executeApiCall(isCollectionOverviewLoading, () async { // Set months count based on selection
final response = switch (duration) {
await ApiService.getCollectionOverview(projectId: projectId); case MonthlyExpenseDuration.oneMonth:
collectionOverviewData.value = selectedMonthsCount.value = 1;
(response?.success == true) ? response!.data : null; break;
}); case MonthlyExpenseDuration.threeMonths:
} selectedMonthsCount.value = 3;
break;
case MonthlyExpenseDuration.sixMonths:
selectedMonthsCount.value = 6;
break;
case MonthlyExpenseDuration.twelveMonths:
selectedMonthsCount.value = 12;
break;
case MonthlyExpenseDuration.all:
selectedMonthsCount.value = 0; // 0 = All months in your API
break;
}
Future<void> fetchTodaysAttendance(String projectId) async { // Re-fetch updated data
await _executeApiCall(isLoadingEmployees, () async { fetchMonthlyExpenses();
final response = await ApiService.getAttendanceForDashboard(projectId);
if (response != null) {
employees.value = response;
for (var emp in employees) {
uploadingStates.putIfAbsent(emp.id, () => false.obs);
}
}
});
} }
Future<void> fetchMasterData() async { Future<void> fetchMasterData() async {
try { try {
final data = await ApiService.getMasterExpenseTypes(); final expenseTypesData = await ApiService.getMasterExpenseTypes();
if (data is List) { if (expenseTypesData is List) {
expenseTypes.value = expenseTypes.value =
data.map((e) => ExpenseTypeModel.fromJson(e)).toList(); expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
} }
} catch (_) {} } catch (e) {
logSafe('Error fetching master data', level: LogLevel.error, error: e);
}
} }
Future<void> fetchMonthlyExpenses({String? categoryId}) async { Future<void> fetchMonthlyExpenses({String? categoryId}) async {
await _executeApiCall(isMonthlyExpenseLoading, () async { try {
isMonthlyExpenseLoading.value = true;
int months = selectedMonthsCount.value;
logSafe(
'Fetching Monthly Expense Report for last $months months'
'${categoryId != null ? ' (categoryId: $categoryId)' : ''}',
level: LogLevel.info,
);
final response = await ApiService.getDashboardMonthlyExpensesApi( final response = await ApiService.getDashboardMonthlyExpensesApi(
categoryId: categoryId, categoryId: categoryId,
months: selectedMonthsCount.value, months: months,
); );
monthlyExpenseList.value =
(response?.success == true) ? response!.data : [];
});
}
Future<void> fetchPurchaseInvoiceOverview() async { if (response != null && response.success) {
final projectId = projectController.selectedProjectId.value; monthlyExpenseList.value = response.data;
if (projectId.isEmpty) return; logSafe('Monthly Expense Report fetched successfully.',
level: LogLevel.info);
await _executeApiCall(isPurchaseInvoiceLoading, () async { } else {
final response = await ApiService.getPurchaseInvoiceOverview( monthlyExpenseList.clear();
projectId: projectId, logSafe('Failed to fetch Monthly Expense Report.',
); level: LogLevel.error);
purchaseInvoiceOverviewData.value = }
(response?.success == true) ? response!.data : null; } catch (e, st) {
}); monthlyExpenseList.clear();
logSafe('Error fetching Monthly Expense Report',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isMonthlyExpenseLoading.value = false;
}
} }
Future<void> fetchPendingExpenses() async { Future<void> fetchPendingExpenses() async {
final id = projectController.selectedProjectId.value; final String projectId = projectController.selectedProjectId.value;
if (id.isEmpty) return; if (projectId.isEmpty) return;
await _executeApiCall(isPendingExpensesLoading, () async { try {
final response = await ApiService.getPendingExpensesApi(projectId: id); isPendingExpensesLoading.value = true;
pendingExpensesData.value = final response =
(response?.success == true) ? response!.data : null; await ApiService.getPendingExpensesApi(projectId: projectId);
});
if (response != null && response.success) {
pendingExpensesData.value = response.data;
logSafe('Pending expenses fetched successfully.', level: LogLevel.info);
} else {
pendingExpensesData.value = null;
logSafe('Failed to fetch pending expenses.', level: LogLevel.error);
}
} catch (e, st) {
pendingExpensesData.value = null;
logSafe('Error fetching pending expenses',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isPendingExpensesLoading.value = false;
}
} }
// =========================
// API Calls
// =========================
Future<void> fetchRoleWiseAttendance() async { Future<void> fetchRoleWiseAttendance() async {
final id = projectController.selectedProjectId.value; final String projectId = projectController.selectedProjectId.value;
if (id.isEmpty) return; if (projectId.isEmpty) return;
await _executeApiCall(isAttendanceLoading, () async { try {
final response = await ApiService.getDashboardAttendanceOverview( isAttendanceLoading.value = true;
id, getAttendanceDays()); final List<dynamic>? response =
roleWiseData.value = await ApiService.getDashboardAttendanceOverview(
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? []; projectId, getAttendanceDays());
});
if (response != null) {
roleWiseData.value =
response.map((e) => Map<String, dynamic>.from(e)).toList();
logSafe('Attendance overview fetched successfully.',
level: LogLevel.info);
} else {
roleWiseData.clear();
logSafe('Failed to fetch attendance overview: response is null.',
level: LogLevel.error);
}
} catch (e, st) {
roleWiseData.clear();
logSafe('Error fetching attendance overview',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isAttendanceLoading.value = false;
}
} }
Future<void> fetchExpenseTypeReport( Future<void> fetchExpenseTypeReport({
{required DateTime startDate, required DateTime endDate}) async { required DateTime startDate,
final id = projectController.selectedProjectId.value; required DateTime endDate,
if (id.isEmpty) return; }) async {
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
try {
isExpenseTypeReportLoading.value = true;
await _executeApiCall(isExpenseTypeReportLoading, () async {
final response = await ApiService.getExpenseTypeReportApi( final response = await ApiService.getExpenseTypeReportApi(
projectId: id, projectId: projectId,
startDate: startDate, startDate: startDate,
endDate: endDate, endDate: endDate,
); );
expenseTypeReportData.value =
(response?.success == true) ? response!.data : null; if (response != null && response.success) {
}); expenseTypeReportData.value = response.data;
logSafe('Expense Type Report fetched successfully.',
level: LogLevel.info);
} else {
expenseTypeReportData.value = null;
logSafe('Failed to fetch Expense Type Report.', level: LogLevel.error);
}
} catch (e, st) {
expenseTypeReportData.value = null;
logSafe('Error fetching Expense Type Report',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isExpenseTypeReportLoading.value = false;
}
} }
Future<void> fetchProjectProgress() async { Future<void> fetchProjectProgress() async {
final id = projectController.selectedProjectId.value; final String projectId = projectController.selectedProjectId.value;
if (id.isEmpty) return; if (projectId.isEmpty) return;
await _executeApiCall(isProjectLoading, () async { try {
isProjectLoading.value = true;
final response = await ApiService.getProjectProgress( final response = await ApiService.getProjectProgress(
projectId: id, days: getProjectDays()); projectId: projectId, days: getProjectDays());
if (response?.success == true) {
projectChartData.value = response!.data if (response != null && response.success) {
.map((d) => ChartTaskData.fromProjectData(d)) projectChartData.value =
.toList(); response.data.map((d) => ChartTaskData.fromProjectData(d)).toList();
logSafe('Project progress data mapped for chart', level: LogLevel.info);
} else { } else {
projectChartData.clear(); projectChartData.clear();
logSafe('Failed to fetch project progress', level: LogLevel.error);
} }
}); } catch (e, st) {
projectChartData.clear();
logSafe('Error fetching project progress',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isProjectLoading.value = false;
}
} }
Future<void> fetchDashboardTasks({required String projectId}) async { Future<void> fetchDashboardTasks({required String projectId}) async {
await _executeApiCall(isTasksLoading, () async { if (projectId.isEmpty) return;
try {
isTasksLoading.value = true;
final response = await ApiService.getDashboardTasks(projectId: projectId); final response = await ApiService.getDashboardTasks(projectId: projectId);
if (response?.success == true) {
totalTasks.value = response!.data?.totalTasks ?? 0; if (response != null && response.success) {
totalTasks.value = response.data?.totalTasks ?? 0;
completedTasks.value = response.data?.completedTasks ?? 0; completedTasks.value = response.data?.completedTasks ?? 0;
logSafe('Dashboard tasks fetched', level: LogLevel.info);
} else { } else {
totalTasks.value = 0; totalTasks.value = 0;
completedTasks.value = 0; completedTasks.value = 0;
logSafe('Failed to fetch tasks', level: LogLevel.error);
} }
}); } catch (e, st) {
totalTasks.value = 0;
completedTasks.value = 0;
logSafe('Error fetching tasks',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isTasksLoading.value = false;
}
} }
Future<void> fetchDashboardTeams({required String projectId}) async { Future<void> fetchDashboardTeams({required String projectId}) async {
await _executeApiCall(isTeamsLoading, () async { if (projectId.isEmpty) return;
try {
isTeamsLoading.value = true;
final response = await ApiService.getDashboardTeams(projectId: projectId); final response = await ApiService.getDashboardTeams(projectId: projectId);
if (response?.success == true) {
totalEmployees.value = response!.data?.totalEmployees ?? 0; if (response != null && response.success) {
totalEmployees.value = response.data?.totalEmployees ?? 0;
inToday.value = response.data?.inToday ?? 0; inToday.value = response.data?.inToday ?? 0;
logSafe('Dashboard teams fetched', level: LogLevel.info);
} else { } else {
totalEmployees.value = 0; totalEmployees.value = 0;
inToday.value = 0; inToday.value = 0;
logSafe('Failed to fetch teams', level: LogLevel.error);
} }
}); } catch (e, st) {
totalEmployees.value = 0;
inToday.value = 0;
logSafe('Error fetching teams',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isTeamsLoading.value = false;
}
} }
} }

View File

@ -1,9 +1,9 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/controller/directory/directory_controller.dart'; import 'package:marco/controller/directory/directory_controller.dart';
import 'package:on_field_work/controller/directory/notes_controller.dart'; import 'package:marco/controller/directory/notes_controller.dart';
class AddCommentController extends GetxController { class AddCommentController extends GetxController {
final String contactId; final String contactId;

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
class AddContactController extends GetxController { class AddContactController extends GetxController {
final RxList<String> categories = <String>[].obs; final RxList<String> categories = <String>[].obs;

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
class BucketController extends GetxController { class BucketController extends GetxController {
RxBool isCreating = false.obs; RxBool isCreating = false.obs;

View File

@ -1,13 +1,12 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/directory/contact_model.dart';
import 'package:on_field_work/model/directory/contact_model.dart'; import 'package:marco/model/directory/contact_bucket_list_model.dart';
import 'package:on_field_work/model/directory/contact_bucket_list_model.dart'; import 'package:marco/model/directory/directory_comment_model.dart';
import 'package:on_field_work/model/directory/directory_comment_model.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
class DirectoryController extends GetxController { class DirectoryController extends GetxController {
// -------------------- CONTACTS --------------------
RxList<ContactModel> allContacts = <ContactModel>[].obs; RxList<ContactModel> allContacts = <ContactModel>[].obs;
RxList<ContactModel> filteredContacts = <ContactModel>[].obs; RxList<ContactModel> filteredContacts = <ContactModel>[].obs;
RxList<ContactCategory> contactCategories = <ContactCategory>[].obs; RxList<ContactCategory> contactCategories = <ContactCategory>[].obs;
@ -17,10 +16,16 @@ class DirectoryController extends GetxController {
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxList<ContactBucket> contactBuckets = <ContactBucket>[].obs; RxList<ContactBucket> contactBuckets = <ContactBucket>[].obs;
RxString searchQuery = ''.obs; RxString searchQuery = ''.obs;
RxBool showFabMenu = false.obs;
final RxBool showFullEditorToolbar = false.obs;
final RxBool isEditorFocused = false.obs;
RxBool isNotesView = false.obs;
final Map<String, RxList<DirectoryComment>> contactCommentsMap = {};
RxList<DirectoryComment> getCommentsForContact(String contactId) {
return contactCommentsMap[contactId] ?? <DirectoryComment>[].obs;
}
// -------------------- COMMENTS --------------------
final Map<String, RxList<DirectoryComment>> activeCommentsMap = {};
final Map<String, RxList<DirectoryComment>> inactiveCommentsMap = {};
final editingCommentId = Rxn<String>(); final editingCommentId = Rxn<String>();
@override @override
@ -29,75 +34,26 @@ class DirectoryController extends GetxController {
fetchContacts(); fetchContacts();
fetchBuckets(); fetchBuckets();
} }
// inside DirectoryController
// -------------------- COMMENTS HANDLING --------------------
RxList<DirectoryComment> getCommentsForContact(String contactId,
{bool active = true}) {
return active
? activeCommentsMap[contactId] ?? <DirectoryComment>[].obs
: inactiveCommentsMap[contactId] ?? <DirectoryComment>[].obs;
}
Future<void> fetchCommentsForContact(String contactId,
{bool active = true}) async {
try {
final data =
await ApiService.getDirectoryComments(contactId, active: active);
var comments =
data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? [];
// Deduplicate by ID before storing
final Map<String, DirectoryComment> uniqueMap = {
for (var c in comments) c.id: c,
};
comments = uniqueMap.values.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
if (active) {
activeCommentsMap[contactId] = <DirectoryComment>[].obs
..assignAll(comments);
} else {
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs
..assignAll(comments);
}
} catch (e, stack) {
logSafe("Error fetching ${active ? 'active' : 'inactive'} comments: $e",
level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
if (active) {
activeCommentsMap[contactId] = <DirectoryComment>[].obs;
} else {
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs;
}
}
}
List<DirectoryComment> combinedComments(String contactId) {
final activeList = getCommentsForContact(contactId, active: true);
final inactiveList = getCommentsForContact(contactId, active: false);
// Deduplicate by ID (active wins)
final Map<String, DirectoryComment> byId = {};
for (final c in inactiveList) {
byId[c.id] = c;
}
for (final c in activeList) {
byId[c.id] = c;
}
final combined = byId.values.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return combined;
}
Future<void> updateComment(DirectoryComment comment) async { Future<void> updateComment(DirectoryComment comment) async {
try { try {
final existing = getCommentsForContact(comment.contactId) logSafe(
.firstWhereOrNull((c) => c.id == comment.id); "Attempting to update comment. id: ${comment.id}, contactId: ${comment.contactId}");
if (existing != null && existing.note.trim() == comment.note.trim()) { final commentList = contactCommentsMap[comment.contactId];
final oldComment =
commentList?.firstWhereOrNull((c) => c.id == comment.id);
if (oldComment == null) {
logSafe("Old comment not found. id: ${comment.id}");
} else {
logSafe("Old comment note: ${oldComment.note}");
logSafe("New comment note: ${comment.note}");
}
if (oldComment != null && oldComment.note.trim() == comment.note.trim()) {
logSafe("No changes detected in comment. id: ${comment.id}");
showAppSnackbar( showAppSnackbar(
title: "No Changes", title: "No Changes",
message: "No changes were made to the comment.", message: "No changes were made to the comment.",
@ -107,26 +63,32 @@ class DirectoryController extends GetxController {
} }
final success = await ApiService.updateContactComment( final success = await ApiService.updateContactComment(
comment.id, comment.note, comment.contactId); comment.id,
comment.note,
comment.contactId,
);
if (success) { if (success) {
await fetchCommentsForContact(comment.contactId, active: true); logSafe("Comment updated successfully. id: ${comment.id}");
await fetchCommentsForContact(comment.contactId, active: false); await fetchCommentsForContact(comment.contactId);
// Show success message
showAppSnackbar( showAppSnackbar(
title: "Success", title: "Success",
message: "Comment updated successfully.", message: "Comment updated successfully.",
type: SnackbarType.success, type: SnackbarType.success,
); );
} else { } else {
logSafe("Failed to update comment via API. id: ${comment.id}");
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to update comment.", message: "Failed to update comment.",
type: SnackbarType.error, type: SnackbarType.error,
); );
} }
} catch (e, stack) { } catch (e, stackTrace) {
logSafe("Update comment failed: $e", level: LogLevel.error); logSafe("Update comment failed: ${e.toString()}");
logSafe(stack.toString(), level: LogLevel.debug); logSafe("StackTrace: ${stackTrace.toString()}");
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to update comment.", message: "Failed to update comment.",
@ -135,20 +97,53 @@ class DirectoryController extends GetxController {
} }
} }
Future<void> fetchCommentsForContact(String contactId,
{bool active = true}) async {
try {
final data =
await ApiService.getDirectoryComments(contactId, active: active);
logSafe(
"Fetched ${active ? 'active' : 'inactive'} comments for contact $contactId: $data");
final comments =
data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? [];
if (!contactCommentsMap.containsKey(contactId)) {
contactCommentsMap[contactId] = <DirectoryComment>[].obs;
}
contactCommentsMap[contactId]!.assignAll(comments);
contactCommentsMap[contactId]?.refresh();
} catch (e) {
logSafe(
"Error fetching ${active ? 'active' : 'inactive'} comments for contact $contactId: $e",
level: LogLevel.error);
contactCommentsMap[contactId] ??= <DirectoryComment>[].obs;
contactCommentsMap[contactId]!.clear();
}
}
/// 🗑 Delete a comment (soft delete)
Future<void> deleteComment(String commentId, String contactId) async { Future<void> deleteComment(String commentId, String contactId) async {
try { try {
logSafe("Deleting comment. id: $commentId");
final success = await ApiService.restoreContactComment(commentId, false); final success = await ApiService.restoreContactComment(commentId, false);
if (success) { if (success) {
if (editingCommentId.value == commentId) editingCommentId.value = null; logSafe("Comment deleted successfully. id: $commentId");
await fetchCommentsForContact(contactId, active: true);
await fetchCommentsForContact(contactId, active: false); // Refresh comments after deletion
await fetchCommentsForContact(contactId);
showAppSnackbar( showAppSnackbar(
title: "Deleted", title: "Deleted",
message: "Comment deleted successfully.", message: "Comment deleted successfully.",
type: SnackbarType.success, type: SnackbarType.success,
); );
} else { } else {
logSafe("Failed to delete comment via API. id: $commentId");
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to delete comment.", message: "Failed to delete comment.",
@ -156,8 +151,8 @@ class DirectoryController extends GetxController {
); );
} }
} catch (e, stack) { } catch (e, stack) {
logSafe("Delete comment failed: $e", level: LogLevel.error); logSafe("Delete comment failed: ${e.toString()}", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug); logSafe("StackTrace: $stack", level: LogLevel.debug);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Something went wrong while deleting comment.", message: "Something went wrong while deleting comment.",
@ -166,19 +161,26 @@ class DirectoryController extends GetxController {
} }
} }
/// Restore a previously deleted comment
Future<void> restoreComment(String commentId, String contactId) async { Future<void> restoreComment(String commentId, String contactId) async {
try { try {
logSafe("Restoring comment. id: $commentId");
final success = await ApiService.restoreContactComment(commentId, true); final success = await ApiService.restoreContactComment(commentId, true);
if (success) { if (success) {
await fetchCommentsForContact(contactId, active: true); logSafe("Comment restored successfully. id: $commentId");
await fetchCommentsForContact(contactId, active: false);
// Refresh comments after restore
await fetchCommentsForContact(contactId);
showAppSnackbar( showAppSnackbar(
title: "Restored", title: "Restored",
message: "Comment restored successfully.", message: "Comment restored successfully.",
type: SnackbarType.success, type: SnackbarType.success,
); );
} else { } else {
logSafe("Failed to restore comment via API. id: $commentId");
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to restore comment.", message: "Failed to restore comment.",
@ -186,8 +188,8 @@ class DirectoryController extends GetxController {
); );
} }
} catch (e, stack) { } catch (e, stack) {
logSafe("Restore comment failed: $e", level: LogLevel.error); logSafe("Restore comment failed: ${e.toString()}", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug); logSafe("StackTrace: $stack", level: LogLevel.debug);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Something went wrong while restoring comment.", message: "Something went wrong while restoring comment.",
@ -196,8 +198,6 @@ class DirectoryController extends GetxController {
} }
} }
// -------------------- CONTACTS HANDLING --------------------
Future<void> fetchBuckets() async { Future<void> fetchBuckets() async {
try { try {
final response = await ApiService.getContactBucketList(); final response = await ApiService.getContactBucketList();
@ -213,71 +213,11 @@ class DirectoryController extends GetxController {
logSafe("Bucket fetch error: $e", level: LogLevel.error); logSafe("Bucket fetch error: $e", level: LogLevel.error);
} }
} }
// -------------------- CONTACT DELETION / RESTORE --------------------
Future<void> deleteContact(String contactId) async {
try {
final success = await ApiService.deleteDirectoryContact(contactId);
if (success) {
// Refresh contacts after deletion
await fetchContacts(active: true);
await fetchContacts(active: false);
showAppSnackbar(
title: "Deleted",
message: "Contact deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to delete contact.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Delete contact failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while deleting contact.",
type: SnackbarType.error,
);
}
}
Future<void> restoreContact(String contactId) async {
try {
final success = await ApiService.restoreDirectoryContact(contactId);
if (success) {
// Refresh contacts after restore
await fetchContacts(active: true);
await fetchContacts(active: false);
showAppSnackbar(
title: "Restored",
message: "Contact restored successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to restore contact.",
type: SnackbarType.error,
);
}
} catch (e, stack) {
logSafe("Restore contact failed: $e", level: LogLevel.error);
logSafe(stack.toString(), level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "Something went wrong while restoring contact.",
type: SnackbarType.error,
);
}
}
Future<void> fetchContacts({bool active = true}) async { Future<void> fetchContacts({bool active = true}) async {
try { try {
isLoading.value = true; isLoading.value = true;
final response = await ApiService.getDirectoryData(isActive: active); final response = await ApiService.getDirectoryData(isActive: active);
if (response != null) { if (response != null) {
@ -298,12 +238,14 @@ class DirectoryController extends GetxController {
void extractCategoriesFromContacts() { void extractCategoriesFromContacts() {
final uniqueCategories = <String, ContactCategory>{}; final uniqueCategories = <String, ContactCategory>{};
for (final contact in allContacts) { for (final contact in allContacts) {
final category = contact.contactCategory; final category = contact.contactCategory;
if (category != null) { if (category != null && !uniqueCategories.containsKey(category.id)) {
uniqueCategories.putIfAbsent(category.id, () => category); uniqueCategories[category.id] = category;
} }
} }
contactCategories.value = uniqueCategories.values.toList(); contactCategories.value = uniqueCategories.values.toList();
} }
@ -328,7 +270,6 @@ class DirectoryController extends GetxController {
contact.tags.any((tag) => tag.name.toLowerCase().contains(query)); contact.tags.any((tag) => tag.name.toLowerCase().contains(query));
final categoryNameMatch = final categoryNameMatch =
contact.contactCategory?.name.toLowerCase().contains(query) ?? false; contact.contactCategory?.name.toLowerCase().contains(query) ?? false;
final bucketNameMatch = contact.bucketIds.any((id) { final bucketNameMatch = contact.bucketIds.any((id) {
final bucketName = contactBuckets final bucketName = contactBuckets
.firstWhereOrNull((b) => b.id == id) .firstWhereOrNull((b) => b.id == id)
@ -350,6 +291,7 @@ class DirectoryController extends GetxController {
return categoryMatch && bucketMatch && searchMatch; return categoryMatch && bucketMatch && searchMatch;
}).toList(); }).toList();
// 🔑 Ensure results are always alphabetically sorted
filteredContacts filteredContacts
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
} }

View File

@ -1,9 +1,9 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:marco/model/employees/employee_model.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/controller/directory/directory_controller.dart'; import 'package:marco/controller/directory/directory_controller.dart';
class ManageBucketController extends GetxController { class ManageBucketController extends GetxController {
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs; RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;

View File

@ -1,8 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/directory/note_list_response_model.dart'; import 'package:marco/model/directory/note_list_response_model.dart';
class NotesController extends GetxController { class NotesController extends GetxController {
RxList<NoteModel> notesList = <NoteModel>[].obs; RxList<NoteModel> notesList = <NoteModel>[].obs;

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/model/document/document_details_model.dart'; import 'package:marco/model/document/document_details_model.dart';
import 'package:on_field_work/model/document/document_version_model.dart'; import 'package:marco/model/document/document_version_model.dart';
class DocumentDetailsController extends GetxController { class DocumentDetailsController extends GetxController {
/// Observables /// Observables

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/model/document/master_document_type_model.dart'; import 'package:marco/model/document/master_document_type_model.dart';
import 'package:on_field_work/model/document/master_document_tags.dart'; import 'package:marco/model/document/master_document_tags.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
class DocumentUploadController extends GetxController { class DocumentUploadController extends GetxController {
// Observables // Observables

View File

@ -1,9 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/model/document/document_filter_model.dart'; import 'package:marco/model/document/document_filter_model.dart';
import 'package:on_field_work/model/document/documents_list_model.dart'; import 'package:marco/model/document/documents_list_model.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
class DocumentController extends GetxController { class DocumentController extends GetxController {
// ==================== Observables ==================== // ==================== Observables ====================
@ -39,6 +38,7 @@ class DocumentController extends GetxController {
final endDate = Rxn<DateTime>(); final endDate = Rxn<DateTime>();
// ==================== Lifecycle ==================== // ==================== Lifecycle ====================
@override @override
void onClose() { void onClose() {
// Don't dispose searchController here - it's managed by the page // Don't dispose searchController here - it's managed by the page
@ -87,23 +87,13 @@ class DocumentController extends GetxController {
entityId: entityId, entityId: entityId,
reset: true, reset: true,
); );
// Show success snackbar
showAppSnackbar(
title: 'Success',
message: isActive ? 'Document deactivated' : 'Document activated',
type: SnackbarType.success,
);
return true; return true;
} else { } else {
errorMessage.value = 'Failed to update document state'; errorMessage.value = 'Failed to update document state';
_showError('Failed to update document state');
return false; return false;
} }
} catch (e) { } catch (e) {
errorMessage.value = 'Error updating document: $e'; errorMessage.value = 'Error updating document: $e';
_showError('Error updating document: $e');
debugPrint('❌ Error toggling document state: $e'); debugPrint('❌ Error toggling document state: $e');
return false; return false;
} finally { } finally {
@ -120,13 +110,17 @@ class DocumentController extends GetxController {
bool reset = false, bool reset = false,
}) async { }) async {
try { try {
// Reset pagination if needed
if (reset) { if (reset) {
pageNumber.value = 1; pageNumber.value = 1;
documents.clear(); documents.clear();
hasMore.value = true; hasMore.value = true;
} }
// Don't fetch if no more data
if (!hasMore.value && !reset) return; if (!hasMore.value && !reset) return;
// Prevent duplicate requests
if (isLoading.value) return; if (isLoading.value) return;
isLoading.value = true; isLoading.value = true;
@ -142,8 +136,8 @@ class DocumentController extends GetxController {
); );
if (response != null && response.success) { if (response != null && response.success) {
if (response.data?.data.isNotEmpty ?? false) { if (response.data.data.isNotEmpty) {
documents.addAll(response.data!.data); documents.addAll(response.data.data);
pageNumber.value++; pageNumber.value++;
} else { } else {
hasMore.value = false; hasMore.value = false;
@ -153,24 +147,12 @@ class DocumentController extends GetxController {
errorMessage.value = response?.message ?? 'Failed to fetch documents'; errorMessage.value = response?.message ?? 'Failed to fetch documents';
if (documents.isEmpty) { if (documents.isEmpty) {
_showError('Failed to load documents'); _showError('Failed to load documents');
} else {
showAppSnackbar(
title: 'Warning',
message: 'No more documents to load',
type: SnackbarType.warning,
);
} }
} }
} catch (e) { } catch (e) {
errorMessage.value = 'Error fetching documents: $e'; errorMessage.value = 'Error fetching documents: $e';
if (documents.isEmpty) { if (documents.isEmpty) {
_showError('Error loading documents'); _showError('Error loading documents');
} else {
showAppSnackbar(
title: 'Error',
message: 'Error fetching additional documents',
type: SnackbarType.error,
);
} }
debugPrint('❌ Error fetching documents: $e'); debugPrint('❌ Error fetching documents: $e');
} finally { } finally {
@ -203,12 +185,17 @@ class DocumentController extends GetxController {
isVerified.value != null; isVerified.value != null;
} }
/// Show error message via snackbar /// Show error message
void _showError(String message) { void _showError(String message) {
showAppSnackbar( Get.snackbar(
title: 'Error', 'Error',
message: message, message,
type: SnackbarType.error, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade100,
colorText: Colors.red.shade900,
margin: const EdgeInsets.all(16),
borderRadius: 8,
duration: const Duration(seconds: 3),
); );
} }

View File

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart'; import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class DynamicMenuController extends GetxController { class DynamicMenuController extends GetxController {
// UI reactive states // UI reactive states

View File

@ -1,11 +1,11 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -73,7 +73,8 @@ class AddEmployeeController extends MyController {
controller: TextEditingController(), controller: TextEditingController(),
); );
logSafe('Fields initialized for first_name, phone_number, last_name, email.'); logSafe(
'Fields initialized for first_name, phone_number, last_name, email.');
} }
// Prefill fields in edit mode // Prefill fields in edit mode
@ -87,7 +88,8 @@ class AddEmployeeController extends MyController {
editingEmployeeData?['phone_number'] ?? ''; editingEmployeeData?['phone_number'] ?? '';
selectedGender = editingEmployeeData?['gender'] != null selectedGender = editingEmployeeData?['gender'] != null
? Gender.values.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender']) ? Gender.values
.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
: null; : null;
basicValidator.getController('email')?.text = basicValidator.getController('email')?.text =
@ -121,12 +123,24 @@ class AddEmployeeController extends MyController {
if (result != null) { if (result != null) {
roles = List<Map<String, dynamic>>.from(result); roles = List<Map<String, dynamic>>.from(result);
logSafe('Roles fetched successfully.'); logSafe('Roles fetched successfully.');
// If editing, and role already selected, update the role controller text here
if (editingEmployeeData != null && selectedRoleId != null) {
final selectedRole = roles.firstWhereOrNull(
(r) => r['id'] == selectedRoleId,
);
if (selectedRole != null) {
update();
}
}
update(); update();
} else { } else {
logSafe('Failed to fetch roles: null result', level: LogLevel.error); logSafe('Failed to fetch roles: null result', level: LogLevel.error);
} }
} catch (e, st) { } catch (e, st) {
logSafe('Error fetching roles', level: LogLevel.error, error: e, stackTrace: st); logSafe('Error fetching roles',
level: LogLevel.error, error: e, stackTrace: st);
} }
} }
@ -156,7 +170,8 @@ class AddEmployeeController extends MyController {
final firstName = basicValidator.getController('first_name')?.text.trim(); final firstName = basicValidator.getController('first_name')?.text.trim();
final lastName = basicValidator.getController('last_name')?.text.trim(); final lastName = basicValidator.getController('last_name')?.text.trim();
final phoneNumber = basicValidator.getController('phone_number')?.text.trim(); final phoneNumber =
basicValidator.getController('phone_number')?.text.trim();
try { try {
// sanitize orgId before sending // sanitize orgId before sending
@ -216,7 +231,8 @@ class AddEmployeeController extends MyController {
showAppSnackbar( showAppSnackbar(
title: 'Permission Required', 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, type: SnackbarType.warning,
); );
return false; return false;

View File

@ -1,10 +1,10 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/model/global_project_model.dart'; import 'package:marco/model/global_project_model.dart';
import 'package:on_field_work/model/employees/assigned_projects_model.dart'; import 'package:marco/model/employees/assigned_projects_model.dart';
import 'package:on_field_work/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
class AssignProjectController extends GetxController { class AssignProjectController extends GetxController {
final String employeeId; final String employeeId;

View File

@ -1,60 +1,91 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:marco/model/attendance/attendance_model.dart';
import 'package:on_field_work/model/employees/employee_details_model.dart'; import 'package:marco/model/project_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/employees/employee_details_model.dart';
import 'package:marco/controller/project_controller.dart';
class EmployeesScreenController extends GetxController { class EmployeesScreenController extends GetxController {
/// Data lists List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeDetailsModel> employeeDetails = [];
RxBool isAllEmployeeSelected = false.obs;
RxList<EmployeeModel> employees = <EmployeeModel>[].obs; RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
Rxn<EmployeeDetailsModel> selectedEmployeeDetails = Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
Rxn<EmployeeDetailsModel>(); Rxn<EmployeeDetailsModel>();
/// Loading states
RxBool isLoading = false.obs;
RxBool isLoadingEmployeeDetails = false.obs; RxBool isLoadingEmployeeDetails = false.obs;
/// Selection state
RxBool isAllEmployeeSelected = false.obs;
RxSet<String> selectedEmployeeIds = <String>{}.obs;
/// Upload state tracking (if needed later)
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
RxList<EmployeeModel> selectedEmployeePrimaryManagers = <EmployeeModel>[].obs;
RxList<EmployeeModel> selectedEmployeeSecondaryManagers =
<EmployeeModel>[].obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
fetchAllEmployees(); isLoading.value = true;
fetchAllProjects().then((_) {
final projectId = Get.find<ProjectController>().selectedProject?.id;
if (projectId != null) {
selectedProjectId = projectId;
fetchEmployeesByProject(projectId);
} else if (isAllEmployeeSelected.value) {
fetchAllEmployees();
} else {
clearEmployees();
}
});
}
Future<void> fetchAllProjects() async {
isLoading.value = true;
await _handleApiCall(
ApiService.getProjects,
onSuccess: (data) {
projects = data.map((json) => ProjectModel.fromJson(json)).toList();
logSafe(
"Projects fetched: ${projects.length} projects loaded.",
level: LogLevel.info,
);
},
onEmpty: () {
logSafe("No project data found or API call failed.",
level: LogLevel.warning);
},
);
isLoading.value = false;
update();
}
void clearEmployees() {
employees.clear();
logSafe("Employees cleared", level: LogLevel.info);
update(['employee_screen_controller']);
} }
/// 🔹 Fetch all employees (no project filter)
Future<void> fetchAllEmployees({String? organizationId}) async { Future<void> fetchAllEmployees({String? organizationId}) async {
isLoading.value = true; isLoading.value = true;
update(['employee_screen_controller']); update(['employee_screen_controller']);
await _handleApiCall( await _handleApiCall(
() => ApiService.getAllEmployees(organizationId: organizationId), () => ApiService.getAllEmployees(
organizationId: organizationId), // pass orgId to API
onSuccess: (data) { onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
logSafe( logSafe(
"All Employees fetched: ${employees.length} employees loaded.", "All Employees fetched: ${employees.length} employees loaded.",
level: LogLevel.info, level: LogLevel.info,
); );
// Reset selection states when new data arrives
selectedEmployeeIds.clear();
isAllEmployeeSelected.value = false;
}, },
onEmpty: () { onEmpty: () {
employees.clear(); employees.clear();
selectedEmployeeIds.clear(); logSafe(
isAllEmployeeSelected.value = false; "No Employee data found or API call failed",
logSafe("No Employee data found or API call failed", level: LogLevel.warning,
level: LogLevel.warning); );
}, },
); );
@ -62,7 +93,28 @@ class EmployeesScreenController extends GetxController {
update(['employee_screen_controller']); update(['employee_screen_controller']);
} }
/// 🔹 Fetch details for a specific employee Future<void> fetchEmployeesByProject(String projectId,
{String? organizationId}) async {
if (projectId.isEmpty) return;
isLoading.value = true;
await _handleApiCall(
() => ApiService.getAllEmployeesByProject(projectId,
organizationId: organizationId),
onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
},
onEmpty: () => employees.clear(),
);
isLoading.value = false;
update(['employee_screen_controller']);
}
Future<void> fetchEmployeeDetails(String? employeeId) async { Future<void> fetchEmployeeDetails(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return; if (employeeId == null || employeeId.isEmpty) return;
@ -72,80 +124,31 @@ class EmployeesScreenController extends GetxController {
() => ApiService.getEmployeeDetails(employeeId), () => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) { onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data); selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
logSafe("Employee details loaded for $employeeId", logSafe(
level: LogLevel.info); "Employee details loaded for $employeeId",
level: LogLevel.info,
);
}, },
onEmpty: () { onEmpty: () {
selectedEmployeeDetails.value = null; selectedEmployeeDetails.value = null;
logSafe("No employee details found for $employeeId", logSafe(
level: LogLevel.warning); "No employee details found for $employeeId",
level: LogLevel.warning,
);
}, },
onError: (e) { onError: (e) {
selectedEmployeeDetails.value = null; selectedEmployeeDetails.value = null;
logSafe("Error fetching employee details for $employeeId", logSafe(
level: LogLevel.error, error: e); "Error fetching employee details for $employeeId",
level: LogLevel.error,
error: e,
);
}, },
); );
isLoadingEmployeeDetails.value = false; isLoadingEmployeeDetails.value = false;
} }
/// Fetch reporting managers for a specific employee from /organization/hierarchy/list/:employeeId
Future<void> fetchReportingManagers(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return;
try {
// Always clear before new fetch (to avoid mixing old data)
selectedEmployeePrimaryManagers.clear();
selectedEmployeeSecondaryManagers.clear();
// Fetch from existing API helper
final data = await ApiService.getOrganizationHierarchyList(employeeId);
if (data == null || data.isEmpty) {
update(['employee_screen_controller']);
return;
}
for (final item in data) {
try {
final reportTo = item['reportTo'];
if (reportTo == null) continue;
final emp = EmployeeModel.fromJson(reportTo);
final isPrimary = item['isPrimary'] == true;
if (isPrimary) {
if (!selectedEmployeePrimaryManagers.any((e) => e.id == emp.id)) {
selectedEmployeePrimaryManagers.add(emp);
}
} else {
if (!selectedEmployeeSecondaryManagers.any((e) => e.id == emp.id)) {
selectedEmployeeSecondaryManagers.add(emp);
}
}
} catch (_) {
// ignore malformed items
}
}
update(['employee_screen_controller']);
} catch (e) {
logSafe("Error fetching reporting managers for $employeeId",
level: LogLevel.error, error: e);
}
}
/// 🔹 Clear all employee data
void clearEmployees() {
employees.clear();
selectedEmployeeIds.clear();
isAllEmployeeSelected.value = false;
logSafe("Employees cleared", level: LogLevel.info);
update(['employee_screen_controller']);
}
/// 🔹 Generic handler for list API responses
Future<void> _handleApiCall( Future<void> _handleApiCall(
Future<List<dynamic>?> Function() apiCall, { Future<List<dynamic>?> Function() apiCall, {
required Function(List<dynamic>) onSuccess, required Function(List<dynamic>) onSuccess,
@ -168,7 +171,6 @@ class EmployeesScreenController extends GetxController {
} }
} }
/// 🔹 Generic handler for single-object API responses
Future<void> _handleSingleApiCall( Future<void> _handleSingleApiCall(
Future<Map<String, dynamic>?> Function() apiCall, { Future<Map<String, dynamic>?> Function() apiCall, {
required Function(Map<String, dynamic>) onSuccess, required Function(Map<String, dynamic>) onSuccess,

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
class ComingSoonController extends MyController { class ComingSoonController extends MyController {
Timer? countdownTimer; Timer? countdownTimer;

View File

@ -1,5 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
class Error404Controller extends MyController { class Error404Controller extends MyController {
void goToDashboardScreen() { void goToDashboardScreen() {

View File

@ -1,5 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
class Error500Controller extends MyController { class Error500Controller extends MyController {
void goToDashboardScreen() { void goToDashboardScreen() {

View File

@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:async';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -11,14 +10,14 @@ import 'package:intl/intl.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:on_field_work/controller/expense/expense_screen_controller.dart'; import 'package:marco/controller/expense/expense_screen_controller.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:marco/model/employees/employee_model.dart';
import 'package:on_field_work/model/expense/expense_type_model.dart'; import 'package:marco/model/expense/expense_type_model.dart';
import 'package:on_field_work/model/expense/payment_types_model.dart'; import 'package:marco/model/expense/payment_types_model.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart'; import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
class AddExpenseController extends GetxController { class AddExpenseController extends GetxController {
// --- Text Controllers --- // --- Text Controllers ---
@ -51,22 +50,10 @@ class AddExpenseController extends GetxController {
final isEditMode = false.obs; final isEditMode = false.obs;
final isSearchingEmployees = false.obs; final isSearchingEmployees = false.obs;
// --- Paid By (Single + Multi Selection Support) ---
// single selection
final selectedPaidBy = Rxn<EmployeeModel>();
// helper setters
void setSelectedPaidBy(EmployeeModel? emp) {
selectedPaidBy.value = emp;
}
// --- Dropdown Selections & Data --- // --- Dropdown Selections & Data ---
final selectedPaymentMode = Rxn<PaymentModeModel>(); final selectedPaymentMode = Rxn<PaymentModeModel>();
final selectedExpenseType = Rxn<ExpenseTypeModel>(); final selectedExpenseType = Rxn<ExpenseTypeModel>();
// final selectedPaidBy = Rxn<EmployeeModel>(); final selectedPaidBy = Rxn<EmployeeModel>();
final selectedProject = ''.obs; final selectedProject = ''.obs;
final selectedTransactionDate = Rxn<DateTime>(); final selectedTransactionDate = Rxn<DateTime>();
@ -209,7 +196,7 @@ class AddExpenseController extends GetxController {
'Location: ${locationController.text}', 'Location: ${locationController.text}',
'Transaction Date: ${transactionDateController.text}', 'Transaction Date: ${transactionDateController.text}',
'No. of Persons: ${noOfPersonsController.text}', 'No. of Persons: ${noOfPersonsController.text}',
'Expense Category: ${selectedExpenseType.value?.name}', 'Expense Type: ${selectedExpenseType.value?.name}',
'Payment Mode: ${selectedPaymentMode.value?.name}', 'Payment Mode: ${selectedPaymentMode.value?.name}',
'Paid By: ${selectedPaidBy.value?.name}', 'Paid By: ${selectedPaidBy.value?.name}',
'Attachments: ${attachments.length}', 'Attachments: ${attachments.length}',
@ -407,86 +394,47 @@ class AddExpenseController extends GetxController {
} }
} }
Future<bool> _submitToApi(Map<String, dynamic>? payload) async { Future<bool> _submitToApi(Map<String, dynamic> payload) async {
if (payload == null) { if (isEditMode.value && editingExpenseId != null) {
_errorSnackbar("Payload is empty. Cannot submit."); return ApiService.editExpenseApi(
return false; expenseId: editingExpenseId!,
} payload: payload,
);
try {
if (isEditMode.value && editingExpenseId != null) {
// Edit existing expense
return await ApiService.editExpenseApi(
expenseId: editingExpenseId!,
payload: payload,
);
} else {
// Create new expense
return await ApiService.createExpenseApi(
projectId: payload['projectId'],
expensesTypeId: payload['expenseCategoryId'],
paymentModeId: payload['paymentModeId'],
paidById: payload['paidById'],
transactionDate: DateTime.parse(payload['transactionDate']),
transactionId: payload['transactionId'],
description: payload['description'],
location: payload['location'],
supplerName: payload['supplerName'],
amount: payload['amount'],
noOfPersons: payload['noOfPersons'],
billAttachments: payload['billAttachments'],
);
}
} catch (e) {
_errorSnackbar("Failed to submit expense: $e");
return false;
} }
return ApiService.createExpenseApi(
projectId: payload['projectId'],
expensesTypeId: payload['expensesTypeId'],
paymentModeId: payload['paymentModeId'],
paidById: payload['paidById'],
transactionDate: DateTime.parse(payload['transactionDate']),
transactionId: payload['transactionId'],
description: payload['description'],
location: payload['location'],
supplerName: payload['supplerName'],
amount: payload['amount'],
noOfPersons: payload['noOfPersons'],
billAttachments: payload['billAttachments'],
);
} }
Future<Map<String, dynamic>?> _buildExpensePayload() async { Future<Map<String, dynamic>> _buildExpensePayload() async {
final now = DateTime.now(); final now = DateTime.now();
// --- Get IDs safely ---
final projectId = projectsMap[selectedProject.value];
final expenseType = selectedExpenseType.value;
final paymentMode = selectedPaymentMode.value;
final paidBy = selectedPaidBy.value;
// --- Validate essential fields ---
if (projectId == null) {
_errorSnackbar("Project not selected or invalid");
return null;
}
if (expenseType == null) {
_errorSnackbar("Expense Category not selected");
return null;
}
if (paymentMode == null) {
_errorSnackbar("Payment mode not selected");
return null;
}
if (paidBy == null) {
_errorSnackbar("Paid By not selected");
return null;
}
// --- Process existing attachments (for edit mode) ---
final existingPayload = isEditMode.value final existingPayload = isEditMode.value
? existingAttachments ? existingAttachments
.map((e) => { .map((e) => {
"documentId": e['documentId'], "documentId": e['documentId'],
"fileName": e['fileName'] ?? "", "fileName": e['fileName'],
"contentType": e['contentType'] ?? "", "contentType": e['contentType'],
"fileSize": 0, "fileSize": 0,
"description": "", "description": "",
"url": e['url'] ?? "", "url": e['url'],
"isActive": e['isActive'] ?? true, "isActive": e['isActive'] ?? true,
"base64Data": "", "base64Data": "",
}) })
.toList() .toList()
: <Map<String, dynamic>>[]; : <Map<String, dynamic>>[];
// --- Process new attachments ---
final newPayload = await Future.wait( final newPayload = await Future.wait(
attachments.map((file) async { attachments.map((file) async {
final bytes = await file.readAsBytes(); final bytes = await file.readAsBytes();
@ -501,36 +449,38 @@ class AddExpenseController extends GetxController {
}), }),
); );
// --- Build final payload --- final type = selectedExpenseType.value!;
final payload = {
return {
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId, if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
"projectId": projectId, "projectId": projectsMap[selectedProject.value]!,
"expenseCategoryId": expenseType.id, "expensesTypeId": type.id,
"paymentModeId": paymentMode.id, "paymentModeId": selectedPaymentMode.value!.id,
"paidById": paidBy.id, "paidById": selectedPaidBy.value!.id,
"transactionDate": "transactionDate":
(selectedTransactionDate.value ?? now).toUtc().toIso8601String(), (selectedTransactionDate.value ?? now).toUtc().toIso8601String(),
"transactionId": transactionIdController.text.trim(), "transactionId": transactionIdController.text,
"description": descriptionController.text.trim(), "description": descriptionController.text,
"location": locationController.text.trim(), "location": locationController.text,
"supplerName": supplierController.text.trim(), "supplerName": supplierController.text,
"amount": double.tryParse(amountController.text.trim()) ?? 0, "amount": double.parse(amountController.text.trim()),
"noOfPersons": expenseType.noOfPersonsRequired == true "noOfPersons": type.noOfPersonsRequired == true
? int.tryParse(noOfPersonsController.text.trim()) ?? 0 ? int.tryParse(noOfPersonsController.text.trim()) ?? 0
: 0, : 0,
"billAttachments": [...existingPayload, ...newPayload].isEmpty "billAttachments": [
...existingPayload,
...newPayload,
].isEmpty
? null ? null
: [...existingPayload, ...newPayload], : [...existingPayload, ...newPayload],
}; };
return payload;
} }
String validateForm() { String validateForm() {
final missing = <String>[]; final missing = <String>[];
if (selectedProject.value.isEmpty) missing.add("Project"); if (selectedProject.value.isEmpty) missing.add("Project");
if (selectedExpenseType.value == null) missing.add("Expense Category"); if (selectedExpenseType.value == null) missing.add("Expense Type");
if (selectedPaymentMode.value == null) missing.add("Payment Mode"); if (selectedPaymentMode.value == null) missing.add("Payment Mode");
if (selectedPaidBy.value == null) missing.add("Paid By"); if (selectedPaidBy.value == null) missing.add("Paid By");
if (amountController.text.trim().isEmpty) missing.add("Amount"); if (amountController.text.trim().isEmpty) missing.add("Amount");

View File

@ -1,8 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/model/expense/expense_detail_model.dart'; import 'package:marco/model/expense/expense_detail_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:marco/model/employees/employee_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ExpenseDetailController extends GetxController { class ExpenseDetailController extends GetxController {
@ -142,10 +142,6 @@ class ExpenseDetailController extends GetxController {
required String reimburseDate, required String reimburseDate,
required String reimburseById, required String reimburseById,
required String statusId, required String statusId,
double? baseAmount,
double? taxAmount,
double? tdsPercent,
double? netPayable,
}) async { }) async {
final success = await _apiCallWrapper( final success = await _apiCallWrapper(
() => ApiService.updateExpenseStatusApi( () => ApiService.updateExpenseStatusApi(
@ -155,16 +151,13 @@ class ExpenseDetailController extends GetxController {
reimburseTransactionId: reimburseTransactionId, reimburseTransactionId: reimburseTransactionId,
reimburseDate: reimburseDate, reimburseDate: reimburseDate,
reimbursedById: reimburseById, reimbursedById: reimburseById,
baseAmount: baseAmount,
taxAmount: taxAmount,
tdsPercent: tdsPercent,
netPayable: netPayable,
), ),
"submit reimbursement", "submit reimbursement",
); );
if (success == true) { if (success == true) {
await fetchExpenseDetails(); // Explicitly check for true as _apiCallWrapper returns T?
await fetchExpenseDetails(); // Refresh details after successful update
return true; return true;
} else { } else {
errorMessage.value = "Failed to submit reimbursement."; errorMessage.value = "Failed to submit reimbursement.";

View File

@ -1,13 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/model/expense/expense_list_model.dart'; import 'package:marco/model/expense/expense_list_model.dart';
import 'package:on_field_work/model/expense/payment_types_model.dart'; import 'package:marco/model/expense/payment_types_model.dart';
import 'package:on_field_work/model/expense/expense_type_model.dart'; import 'package:marco/model/expense/expense_type_model.dart';
import 'package:on_field_work/model/expense/expense_status_model.dart'; import 'package:marco/model/expense/expense_status_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:marco/model/employees/employee_model.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ExpenseController extends GetxController { class ExpenseController extends GetxController {
@ -213,7 +213,7 @@ class ExpenseController extends GetxController {
selectedCreatedByEmployees.clear(); selectedCreatedByEmployees.clear();
} }
/// Fetch master data: Expense Categorys, payment modes, and expense status /// Fetch master data: expense types, payment modes, and expense status
Future<void> fetchMasterData() async { Future<void> fetchMasterData() async {
try { try {
final expenseTypesData = await ApiService.getMasterExpenseTypes(); final expenseTypesData = await ApiService.getMasterExpenseTypes();

View File

@ -1,5 +1,5 @@
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/widgets/my_text_utils.dart'; import 'package:marco/helpers/widgets/my_text_utils.dart';
class FaqsController extends MyController { class FaqsController extends MyController {
final List<bool> dataExpansionPanel = [true, false, false, false, false, false]; final List<bool> dataExpansionPanel = [true, false, false, false, false, false];

View File

@ -1,4 +1,4 @@
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
class PricingController extends MyController { class PricingController extends MyController {
bool isMonth = false; bool isMonth = false;

View File

@ -1,419 +0,0 @@
// payment_request_controller.dart
import 'dart:io';
import 'dart:convert';
import 'package:get/get.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'package:intl/intl.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
import 'package:on_field_work/model/finance/expense_category_model.dart';
import 'package:on_field_work/model/finance/currency_list_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
class AddPaymentRequestController extends GetxController {
// Loading States
final isLoadingPayees = false.obs;
final isLoadingCategories = false.obs;
final isLoadingCurrencies = false.obs;
final isProcessingAttachment = false.obs;
final isSubmitting = false.obs;
// Data Lists
final payees = <String>[].obs;
final categories = <ExpenseCategory>[].obs;
final currencies = <Currency>[].obs;
final globalProjects = <Map<String, dynamic>>[].obs;
// Selected Values
final selectedProject = Rx<Map<String, dynamic>?>(null);
final selectedCategory = Rx<ExpenseCategory?>(null);
final selectedPayee = Rx<EmployeeModel?>(null);
final selectedCurrency = Rx<Currency?>(null);
final isAdvancePayment = false.obs;
final selectedDueDate = Rx<DateTime?>(null);
// Text Controllers
final titleController = TextEditingController();
final dueDateController = TextEditingController();
final amountController = TextEditingController();
final descriptionController = TextEditingController();
final removedAttachments = <Map<String, dynamic>>[].obs;
// Attachments
final attachments = <File>[].obs;
final existingAttachments = <Map<String, dynamic>>[].obs;
final ImagePicker _picker = ImagePicker();
@override
void onInit() {
super.onInit();
fetchAllMasterData();
fetchGlobalProjects();
}
@override
void onClose() {
titleController.dispose();
dueDateController.dispose();
amountController.dispose();
descriptionController.dispose();
super.onClose();
}
/// Fetch all master data concurrently
Future<void> fetchAllMasterData() async {
await Future.wait([
_fetchData(
payees, ApiService.getExpensePaymentRequestPayeeApi, isLoadingPayees),
_fetchData(categories, ApiService.getMasterExpenseCategoriesApi,
isLoadingCategories),
_fetchData(
currencies, ApiService.getMasterCurrenciesApi, isLoadingCurrencies),
]);
}
/// Generic fetch handler
Future<void> _fetchData<T>(
RxList<T> list, Future<dynamic> Function() apiCall, RxBool loader) async {
try {
loader.value = true;
final response = await apiCall();
if (response != null && response.data.isNotEmpty) {
list.value = response.data;
} else {
list.clear();
}
} catch (e) {
logSafe("Error fetching data: $e", level: LogLevel.error);
list.clear();
} finally {
loader.value = false;
}
}
/// Fetch projects
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
globalProjects.value = (response ?? [])
.map<Map<String, dynamic>>((e) => {
'id': e['id']?.toString() ?? '',
'name': e['name']?.toString().trim() ?? '',
})
.where((p) => p['id']!.isNotEmpty && p['name']!.isNotEmpty)
.toList();
} catch (e) {
logSafe("Error fetching projects: $e", level: LogLevel.error);
globalProjects.clear();
}
}
/// Pick due date
Future<void> pickDueDate(BuildContext context) async {
final pickedDate = await showDatePicker(
context: context,
initialDate: selectedDueDate.value ?? DateTime.now(),
firstDate: DateTime(DateTime.now().year - 5),
lastDate: DateTime(DateTime.now().year + 5),
);
if (pickedDate != null) {
selectedDueDate.value = pickedDate;
dueDateController.text = DateFormat('dd MMM yyyy').format(pickedDate);
}
}
/// Generic file picker for multiple sources
Future<void> pickAttachments(
{bool fromGallery = false, bool fromCamera = false}) async {
try {
if (fromCamera) {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
isProcessingAttachment.value = true;
final timestamped = await TimestampImageHelper.addTimestamp(
imageFile: File(pickedFile.path));
attachments.add(timestamped);
}
} else if (fromGallery) {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) attachments.add(File(pickedFile.path));
} else {
final result = await FilePicker.platform
.pickFiles(type: FileType.any, allowMultiple: true);
if (result != null && result.paths.isNotEmpty)
attachments.addAll(result.paths.whereType<String>().map(File.new));
}
attachments.refresh();
} catch (e) {
_errorSnackbar("Attachment error: $e");
} finally {
isProcessingAttachment.value = false;
}
}
Future<void> pickFromCamera() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
isProcessingAttachment.value = true;
File imageFile = File(pickedFile.path);
// Add timestamp to the captured image
File timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: imageFile,
);
attachments.add(timestampedFile);
attachments.refresh(); // refresh UI
}
} catch (e) {
_errorSnackbar("Camera error: $e");
} finally {
isProcessingAttachment.value = false; // stop loading
}
}
/// Selection handlers
void selectProject(Map<String, dynamic> project) =>
selectedProject.value = project;
void selectCategory(ExpenseCategory category) =>
selectedCategory.value = category;
void selectPayee(EmployeeModel payee) => selectedPayee.value = payee;
void selectCurrency(Currency currency) => selectedCurrency.value = currency;
void addAttachment(File file) => attachments.add(file);
void removeAttachment(File file) {
if (attachments.contains(file)) {
attachments.remove(file);
}
}
void removeExistingAttachment(Map<String, dynamic> existingAttachment) {
final index = existingAttachments.indexWhere(
(e) => e['id'] == existingAttachment['id']); // match by normalized id
if (index != -1) {
// Mark as inactive
existingAttachments[index]['isActive'] = false;
existingAttachments.refresh();
// Add to removedAttachments to inform API
removedAttachments.add({
"documentId": existingAttachment['id'], // ensure API receives id
"isActive": false,
});
// Show snackbar feedback
showAppSnackbar(
title: 'Removed',
message: 'Attachment has been removed.',
type: SnackbarType.success,
);
}
}
/// Build attachment payload
Future<List<Map<String, dynamic>>> buildAttachmentPayload() async {
final existingPayload = existingAttachments
.map((e) => {
"documentId": e['id'], // use the normalized id
"fileName": e['fileName'],
"contentType": e['contentType'] ?? 'application/octet-stream',
"fileSize": e['fileSize'] ?? 0,
"description": "",
"url": e['url'],
"isActive": e['isActive'] ?? true,
})
.toList();
final newPayload = await Future.wait(attachments.map((file) async {
final bytes = await file.readAsBytes();
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(bytes),
"contentType": lookupMimeType(file.path) ?? 'application/octet-stream',
"fileSize": await file.length(),
"description": "",
};
}));
// Combine active + removed attachments
return [...existingPayload, ...newPayload, ...removedAttachments];
}
/// Submit edited payment request
Future<bool> submitEditedPaymentRequest({required String requestId}) async {
if (isSubmitting.value) return false;
try {
isSubmitting.value = true;
// Validate form
if (!_validateForm()) return false;
// Build attachment payload
final billAttachments = await buildAttachmentPayload();
final payload = {
"id": requestId,
"title": titleController.text.trim(),
"projectId": selectedProject.value?['id'] ?? '',
"expenseCategoryId": selectedCategory.value?.id ?? '',
"amount": double.tryParse(amountController.text.trim()) ?? 0,
"currencyId": selectedCurrency.value?.id ?? '',
"description": descriptionController.text.trim(),
"payee": selectedPayee.value?.id ?? "",
"dueDate": selectedDueDate.value?.toIso8601String(),
"isAdvancePayment": isAdvancePayment.value,
"billAttachments": billAttachments.map((a) {
return {
"documentId": a['documentId'],
"fileName": a['fileName'],
"base64Data": a['base64Data'] ?? "",
"contentType": a['contentType'],
"fileSize": a['fileSize'],
"description": a['description'] ?? "",
"isActive": a['isActive'] ?? true,
};
}).toList(),
};
logSafe("💡 Submitting Edited Payment Request: ${jsonEncode(payload)}");
final success = await ApiService.editExpensePaymentRequestApi(
id: payload['id'],
title: payload['title'],
projectId: payload['projectId'],
expenseCategoryId: payload['expenseCategoryId'],
amount: payload['amount'],
currencyId: payload['currencyId'],
description: payload['description'],
payee: payload['payee'],
dueDate: payload['dueDate'] ?? '',
isAdvancePayment: payload['isAdvancePayment'],
billAttachments: payload['billAttachments'],
);
logSafe("💡 Edit Payment Request API Response: $success");
if (success == true) {
logSafe("✅ Payment request edited successfully.");
return true;
} else {
return _errorSnackbar("Failed to edit payment request.");
}
} catch (e, st) {
logSafe("💥 Submit Edited Payment Request Error: $e\n$st",
level: LogLevel.error);
return _errorSnackbar("Something went wrong. Please try again later.");
} finally {
isSubmitting.value = false;
}
}
/// Submit payment request (Project API style)
Future<bool> submitPaymentRequest() async {
if (isSubmitting.value) return false;
try {
isSubmitting.value = true;
// Validate form
if (!_validateForm()) return false;
// Build attachment payload
final billAttachments = await buildAttachmentPayload();
final payload = {
"title": titleController.text.trim(),
"projectId": selectedProject.value?['id'] ?? '',
"expenseCategoryId": selectedCategory.value?.id ?? '',
"amount": double.tryParse(amountController.text.trim()) ?? 0,
"currencyId": selectedCurrency.value?.id ?? '',
"description": descriptionController.text.trim(),
"payee": selectedPayee.value?.id ?? "",
"dueDate": selectedDueDate.value?.toIso8601String(),
"isAdvancePayment": isAdvancePayment.value,
"billAttachments": billAttachments.map((a) {
return {
"fileName": a['fileName'],
"fileSize": a['fileSize'],
"contentType": a['contentType'],
};
}).toList(),
};
logSafe("💡 Submitting Payment Request: ${jsonEncode(payload)}");
final success = await ApiService.createExpensePaymentRequestApi(
title: payload['title'],
projectId: payload['projectId'],
expenseCategoryId: payload['expenseCategoryId'],
amount: payload['amount'],
currencyId: payload['currencyId'],
description: payload['description'],
payee: payload['payee'],
dueDate: selectedDueDate.value,
isAdvancePayment: payload['isAdvancePayment'],
billAttachments: billAttachments,
);
logSafe("💡 Payment Request API Response: $success");
if (success == true) {
logSafe("✅ Payment request created successfully.");
return true;
} else {
return _errorSnackbar("Failed to create payment request.");
}
} catch (e, st) {
logSafe("💥 Submit Payment Request Error: $e\n$st",
level: LogLevel.error);
return _errorSnackbar("Something went wrong. Please try again later.");
} finally {
isSubmitting.value = false;
}
}
/// Form validation
bool _validateForm() {
if (selectedProject.value == null ||
selectedProject.value!['id'].toString().isEmpty)
return _errorSnackbar("Please select a project");
if (selectedCategory.value == null)
return _errorSnackbar("Please select a category");
if (selectedPayee.value == null)
return _errorSnackbar("Please select a payee");
if (selectedCurrency.value == null)
return _errorSnackbar("Please select currency");
return true;
}
bool _errorSnackbar(String msg, [String title = "Error"]) {
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
return false;
}
/// Clear form
void clearForm() {
titleController.clear();
dueDateController.clear();
amountController.clear();
descriptionController.clear();
selectedProject.value = null;
selectedCategory.value = null;
selectedPayee.value = null;
selectedCurrency.value = null;
isAdvancePayment.value = false;
attachments.clear();
existingAttachments.clear();
removedAttachments.clear();
}
}

View File

@ -1,149 +0,0 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:on_field_work/model/finance/advance_payment_model.dart';
import 'package:on_field_work/model/finance/get_employee_model.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
class AdvancePaymentController extends GetxController {
/// Advance payments list
var payments = <AdvancePayment>[].obs;
var isLoading = false.obs;
/// Employees for dropdown search
var employees = <Employee>[].obs;
var allEmployees = <Employee>[]; // cache of last API response
var employeesLoading = false.obs;
var searchQuery = ''.obs;
var selectedEmployee = Rxn<Employee>();
/// Prevents unwanted API calls while programmatically updating search
var _suppressSearch = false.obs;
Timer? _debounceTimer;
@override
void onInit() {
super.onInit();
ever<String>(searchQuery, (q) {
if (_suppressSearch.value) return; // Skip while selecting employee
// 🔹 When user types new text, clear previous employee + payments instantly
if (selectedEmployee.value != null) {
selectedEmployee.value = null;
payments.clear();
}
// 🔹 Show fresh dropdown results for new query
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 400), () {
if (q.isNotEmpty) {
fetchEmployees(q); // repopulate dropdown
} else {
employees.clear(); // hide dropdown when search cleared
}
});
});
}
@override
void onClose() {
_debounceTimer?.cancel();
super.onClose();
}
/// Fetch employees by query
Future<void> fetchEmployees(String q) async {
if (q.isEmpty) {
employees.clear();
return;
}
if (employeesLoading.value) return;
try {
employeesLoading.value = true;
// Build query params
final queryParams = {
'allEmployee': 'true',
if (q.isNotEmpty) 'q': q, // only include search query if not empty
};
final list = await ApiService.getEmployees(queryParams: queryParams);
final parsed = Employee.listFromJson(list);
logSafe(
"✅ Employees fetched from API: ${parsed.map((e) => e.name).toList()}");
allEmployees = parsed;
_filterEmployees(q);
} catch (e, s) {
logSafe("❌ fetchEmployees error: $e\n$s", level: LogLevel.error);
employees.clear();
} finally {
employeesLoading.value = false;
}
}
/// Local filter to update list based on search text
void _filterEmployees(String query) {
final q = query.toLowerCase();
employees
..clear()
..addAll(allEmployees.where((e) {
return e.name.toLowerCase().contains(q) ||
e.email.toLowerCase().contains(q);
}));
}
/// When user selects employee
void selectEmployee(Employee emp) {
_suppressSearch.value = true;
selectedEmployee.value = emp;
employees.clear(); // hide dropdown
searchQuery.value = emp.name;
fetchAdvancePayments(emp.id);
// Re-enable search after a short delay
Future.delayed(const Duration(milliseconds: 300), () {
_suppressSearch.value = false;
});
}
/// Fetch advance payments for the selected employee
Future<void> fetchAdvancePayments(String employeeId) async {
if (employeeId.isEmpty) {
payments.clear();
return;
}
try {
isLoading.value = true;
final list = await ApiService.getAdvancePayments(employeeId);
payments.assignAll(list);
} catch (e, s) {
logSafe("❌ fetchAdvancePayments error: $e\n$s", level: LogLevel.error);
payments.clear();
} finally {
isLoading.value = false;
}
}
/// Clear employee selection
void clearSelection() {
selectedEmployee.value = null;
payments.clear();
employees.clear();
searchQuery.value = '';
}
void resetSelectionOnNewSearch() {
if (selectedEmployee.value != null) {
selectedEmployee.value = null;
payments.clear();
}
}
}

View File

@ -1,134 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/finance/payment_request_list_model.dart';
import 'package:on_field_work/model/finance/payment_request_filter.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
class PaymentRequestController extends GetxController {
// ---------------- Observables ----------------
final RxList<PaymentRequest> paymentRequests = <PaymentRequest>[].obs;
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
final RxBool isFilterApplied = false.obs;
// ---------------- Pagination ----------------
int _pageSize = 20;
int _pageNumber = 1;
bool _hasMoreData = true;
// ---------------- Filters ----------------
RxMap<String, dynamic> appliedFilter = <String, dynamic>{}.obs;
RxString searchString = ''.obs;
// ---------------- Filter Options ----------------
RxList<IdNameModel> projects = <IdNameModel>[].obs;
RxList<IdNameModel> payees = <IdNameModel>[].obs;
RxList<IdNameModel> categories = <IdNameModel>[].obs;
RxList<IdNameModel> currencies = <IdNameModel>[].obs;
RxList<IdNameModel> statuses = <IdNameModel>[].obs;
RxList<IdNameModel> createdBy = <IdNameModel>[].obs;
// ---------------- Fetch Filter Options ----------------
Future<void> fetchPaymentRequestFilterOptions() async {
try {
final response = await ApiService.getExpensePaymentRequestFilterApi();
if (response != null && response.data != null) {
projects.assignAll(response.data!.projects ?? []);
payees.assignAll(response.data!.payees ?? []);
categories.assignAll(response.data!.expenseCategory ?? []);
currencies.assignAll(response.data!.currency ?? []);
statuses.assignAll(response.data!.status ?? []);
createdBy.assignAll(response.data!.createdBy ?? []);
} else {
logSafe("Payment request filter API returned null",
level: LogLevel.warning);
}
} catch (e, stack) {
logSafe("Exception in fetchPaymentRequestFilterOptions: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
}
// ---------------- Fetch Payment Requests ----------------
Future<void> fetchPaymentRequests({int pageSize = 20}) async {
isLoading.value = true;
errorMessage.value = '';
_pageNumber = 1;
_pageSize = pageSize;
_hasMoreData = true;
paymentRequests.clear();
await _fetchPaymentRequestsFromApi();
isLoading.value = false;
}
// ---------------- Load More ----------------
Future<void> loadMorePaymentRequests() async {
if (isLoading.value || !_hasMoreData) return;
_pageNumber += 1;
isLoading.value = true;
await _fetchPaymentRequestsFromApi();
isLoading.value = false;
}
// ---------------- Internal API Call ----------------
Future<void> _fetchPaymentRequestsFromApi() async {
try {
final response = await ApiService.getExpensePaymentRequestListApi(
pageSize: _pageSize,
pageNumber: _pageNumber,
filter: appliedFilter,
searchString: searchString.value,
);
final data = response?.data;
final reqList = data?.data ?? [];
if (response != null && data != null && reqList.isNotEmpty) {
if (_pageNumber == 1) {
paymentRequests.assignAll(reqList);
} else {
paymentRequests.addAll(reqList);
}
if (reqList.length < _pageSize) {
_hasMoreData = false;
}
} else {
if (_pageNumber == 1) {
errorMessage.value = 'No payment requests found.';
}
_hasMoreData = false;
}
} catch (e, stack) {
errorMessage.value = 'Failed to fetch payment requests.';
logSafe("Exception in _fetchPaymentRequestsFromApi: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
_hasMoreData = false;
}
}
// ---------------- Filter Management ----------------
void setFilterApplied(bool applied) {
isFilterApplied.value = applied;
}
void applyFilter(Map<String, dynamic> filter, {String search = ''}) {
appliedFilter.assignAll(filter);
searchString.value = search;
isFilterApplied.value = filter.isNotEmpty || search.isNotEmpty;
fetchPaymentRequests();
}
void clearFilter() {
appliedFilter.clear();
searchString.value = '';
isFilterApplied.value = false;
fetchPaymentRequests();
}
}

View File

@ -1,363 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/finance/payment_request_details_model.dart';
import 'package:on_field_work/model/expense/payment_types_model.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:mime/mime.dart';
import 'package:on_field_work/controller/finance/payment_request_controller.dart';
class PaymentRequestDetailController extends GetxController {
final Rx<PaymentRequestData?> paymentRequest = Rx<PaymentRequestData?>(null);
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
// Employee selection
final Rx<EmployeeModel?> selectedReimbursedBy = Rx<EmployeeModel?>(null);
final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
final TextEditingController employeeSearchController =
TextEditingController();
PaymentRequestController get paymentRequestController =>
Get.find<PaymentRequestController>();
final RxBool isSearchingEmployees = false.obs;
// Attachments
final RxList<File> attachments = <File>[].obs;
final RxList<Map<String, dynamic>> existingAttachments =
<Map<String, dynamic>>[].obs;
final isProcessingAttachment = false.obs;
// Payment mode
final selectedPaymentMode = Rxn<PaymentModeModel>();
// Text controllers for form
final TextEditingController locationController = TextEditingController();
final TextEditingController gstNumberController = TextEditingController();
// Form submission state
final RxBool isSubmitting = false.obs;
late String _requestId;
bool _isInitialized = false;
RxBool paymentSheetOpened = false.obs;
final ImagePicker _picker = ImagePicker();
/// Initialize controller
void init(String requestId) {
if (_isInitialized) return;
_isInitialized = true;
_requestId = requestId;
// Fetch payment request details + employees concurrently
Future.wait([
fetchPaymentRequestDetail(),
fetchAllEmployees(),
fetchPaymentModes(),
]);
}
/// Generic API wrapper for error handling
Future<T?> _apiCallWrapper<T>(
Future<T?> Function() apiCall, String operationName) async {
isLoading.value = true;
errorMessage.value = '';
try {
final result = await apiCall();
return result;
} catch (e) {
errorMessage.value = 'Error during $operationName: $e';
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error);
return null;
} finally {
isLoading.value = false;
}
}
/// Fetch payment request details
Future<void> fetchPaymentRequestDetail() async {
isLoading.value = true;
try {
final response =
await ApiService.getExpensePaymentRequestDetailApi(_requestId);
if (response != null) {
paymentRequest.value = response.data;
} else {
errorMessage.value = "Failed to fetch payment request details";
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error,
);
}
} catch (e) {
errorMessage.value = "Error fetching payment request details: $e";
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
/// Pick files from gallery or file picker
Future<void> pickAttachments() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
allowMultiple: true,
);
if (result != null) {
attachments.addAll(
result.paths.whereType<String>().map(File.new),
);
}
} catch (e) {
_errorSnackbar("Attachment error: $e");
}
}
void removeAttachment(File file) => attachments.remove(file);
Future<void> pickFromCamera() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
isProcessingAttachment.value = true;
File imageFile = File(pickedFile.path);
File timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: imageFile,
);
attachments.add(timestampedFile);
attachments.refresh();
}
} catch (e) {
_errorSnackbar("Camera error: $e");
} finally {
isProcessingAttachment.value = false;
}
}
// --- Location ---
final RxBool isFetchingLocation = false.obs;
Future<void> fetchCurrentLocation() async {
isFetchingLocation.value = true;
try {
if (!await _ensureLocationPermission()) return;
final position = await Geolocator.getCurrentPosition();
final placemarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
locationController.text = placemarks.isNotEmpty
? [
placemarks.first.name,
placemarks.first.street,
placemarks.first.locality,
placemarks.first.administrativeArea,
placemarks.first.country,
].where((e) => e?.isNotEmpty == true).join(", ")
: "${position.latitude}, ${position.longitude}";
} catch (e) {
_errorSnackbar("Location error: $e");
} finally {
isFetchingLocation.value = false;
}
}
Future<bool> _ensureLocationPermission() async {
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
_errorSnackbar("Location permission denied.");
return false;
}
}
if (!await Geolocator.isLocationServiceEnabled()) {
_errorSnackbar("Location service disabled.");
return false;
}
return true;
}
/// Fetch all employees
Future<void> fetchAllEmployees() async {
final response = await _apiCallWrapper(
() => ApiService.getAllEmployees(), "fetch all employees");
if (response != null && response.isNotEmpty) {
try {
allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e)));
} catch (e) {
errorMessage.value = 'Failed to parse employee data: $e';
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error);
}
} else {
allEmployees.clear();
}
}
/// Fetch payment modes
Future<void> fetchPaymentModes() async {
isLoading.value = true;
try {
final paymentModesData = await ApiService.getMasterPaymentModes();
if (paymentModesData is List) {
paymentModes.value =
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
} else {
paymentModes.clear();
showAppSnackbar(
title: 'Error',
message: 'Failed to fetch payment modes',
type: SnackbarType.error);
}
} catch (e) {
paymentModes.clear();
showAppSnackbar(
title: 'Error',
message: 'Error fetching payment modes: $e',
type: SnackbarType.error);
} finally {
isLoading.value = false;
}
}
/// Search employees
Future<void> searchEmployees(String query) async {
if (query.trim().isEmpty) {
employeeSearchResults.clear();
return;
}
isSearchingEmployees.value = true;
try {
final data =
await ApiService.searchEmployeesBasic(searchString: query.trim());
employeeSearchResults.assignAll(
(data ?? []).map((e) => EmployeeModel.fromJson(e)),
);
} catch (e) {
employeeSearchResults.clear();
} finally {
isSearchingEmployees.value = false;
}
}
/// Update payment request status
Future<bool> updatePaymentRequestStatus({
required String statusId,
required String comment,
String? paidTransactionId,
String? paidById,
DateTime? paidAt,
double? baseAmount,
double? taxAmount,
String? tdsPercentage,
}) async {
isLoading.value = true;
try {
final success = await ApiService.updateExpensePaymentRequestStatusApi(
paymentRequestId: _requestId,
statusId: statusId,
comment: comment,
paidTransactionId: paidTransactionId,
paidById: paidById,
paidAt: paidAt,
baseAmount: baseAmount,
taxAmount: taxAmount,
tdsPercentage: tdsPercentage,
);
if (success) {
// Controller refreshes the data but does not show snackbars.
await fetchPaymentRequestDetail();
paymentRequestController.fetchPaymentRequests();
}
return success;
} catch (e) {
// Controller returns false on error; UI will show the snackbar.
return false;
} finally {
isLoading.value = false;
}
}
// --- Snackbar Helper ---
void _errorSnackbar(String msg, [String title = "Error"]) {
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
}
// --- Payment Mode Selection ---
void selectPaymentMode(PaymentModeModel mode) {
selectedPaymentMode.value = mode;
}
// --- Submit Expense ---
Future<bool> submitExpense(
{required String statusId, String? comment}) async {
if (selectedPaymentMode.value == null) return false;
isSubmitting.value = true;
try {
// prepare attachments
final success = await ApiService.createExpenseForPRApi(
paymentModeId: selectedPaymentMode.value!.id,
location: locationController.text,
gstNumber: gstNumberController.text,
paymentRequestId: _requestId,
billAttachments: attachments.map((file) {
final bytes = file.readAsBytesSync();
final mimeType =
lookupMimeType(file.path) ?? 'application/octet-stream';
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(bytes),
"contentType": mimeType,
"description": "",
"fileSize": bytes.length,
"isActive": true,
};
}).toList(),
statusId: statusId,
comment: comment ?? '',
);
if (success) {
// Refresh the payment request details so the UI updates
await fetchPaymentRequestDetail();
}
return success;
} finally {
isSubmitting.value = false;
}
}
}

View File

@ -1,48 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/infra_project/infra_project_list.dart';
class InfraProjectController extends GetxController {
final projects = <ProjectData>[].obs;
final isLoading = false.obs;
final searchQuery = ''.obs;
// Filtered list
List<ProjectData> get filteredProjects {
final q = searchQuery.value.trim().toLowerCase();
if (q.isEmpty) return projects;
return projects.where((p) {
return (p.name?.toLowerCase().contains(q) ?? false) ||
(p.shortName?.toLowerCase().contains(q) ?? false) ||
(p.projectAddress?.toLowerCase().contains(q) ?? false) ||
(p.contactPerson?.toLowerCase().contains(q) ?? false);
}).toList();
}
// Fetch Projects
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
try {
isLoading.value = true;
final response = await ApiService.getInfraProjectsList(
pageNumber: pageNumber,
pageSize: pageSize,
);
if (response != null && response.data != null) {
projects.assignAll(response.data!.data ?? []);
} else {
projects.clear();
}
} catch (e) {
rethrow;
} finally {
isLoading.value = false;
}
}
void updateSearch(String query) {
searchQuery.value = query;
}
}

View File

@ -1,38 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/infra_project/infra_project_details.dart';
class InfraProjectDetailsController extends GetxController {
final String projectId;
InfraProjectDetailsController({required this.projectId});
var isLoading = true.obs;
var projectDetails = Rxn<ProjectData>();
var errorMessage = ''.obs;
@override
void onInit() {
super.onInit();
fetchProjectDetails();
}
Future<void> fetchProjectDetails() async {
try {
isLoading.value = true;
final response = await ApiService.getInfraProjectDetails(projectId: projectId);
if (response != null && response.success == true && response.data != null) {
projectDetails.value = response.data;
isLoading.value = false;
} else {
errorMessage.value = response?.message ?? "Failed to load project details";
}
} catch (e) {
errorMessage.value = "Error fetching project details: $e";
} finally {
isLoading.value = false;
}
}
}

View File

@ -1,3 +1,3 @@
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
class AuthLayout2Controller extends MyController {} class AuthLayout2Controller extends MyController {}

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/widgets/my_text_utils.dart'; import 'package:marco/helpers/widgets/my_text_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AuthLayoutController extends MyController { class AuthLayoutController extends MyController {

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:on_field_work/model/project_model.dart'; import 'package:marco/model/project_model.dart';
class LayoutController extends GetxController { class LayoutController extends GetxController {
// Theme Customization // Theme Customization
@ -55,7 +55,7 @@ class LayoutController extends GetxController {
isLoadingProjects.value = true; isLoadingProjects.value = true;
try { try {
final response = await ApiService.getGlobalProjects(); final response = await ApiService.getProjects();
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList(); final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();

View File

@ -1,5 +1,5 @@
import 'package:get/get_state_manager/get_state_manager.dart'; import 'package:get/get_state_manager/get_state_manager.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
abstract class MyController extends GetxController { abstract class MyController extends GetxController {
@override @override

View File

@ -2,21 +2,17 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/permission_service.dart'; import 'package:marco/helpers/services/permission_service.dart';
import 'package:on_field_work/model/user_permission.dart'; import 'package:marco/model/user_permission.dart';
import 'package:on_field_work/model/employees/employee_info.dart'; import 'package:marco/model/employees/employee_info.dart';
import 'package:on_field_work/model/projects_model.dart'; import 'package:marco/model/projects_model.dart';
class PermissionController extends GetxController { class PermissionController extends GetxController {
var permissions = <UserPermission>[].obs; var permissions = <UserPermission>[].obs;
var employeeInfo = Rxn<EmployeeInfo>(); var employeeInfo = Rxn<EmployeeInfo>();
var projectsInfo = <ProjectInfo>[].obs; var projectsInfo = <ProjectInfo>[].obs;
Timer? _refreshTimer; Timer? _refreshTimer;
var isLoading = true.obs;
/// NEW: reactive flag to signal permissions are loaded
var permissionsLoaded = false.obs;
@override @override
void onInit() { void onInit() {
@ -30,8 +26,7 @@ class PermissionController extends GetxController {
await loadData(token!); await loadData(token!);
_startAutoRefresh(); _startAutoRefresh();
} else { } else {
logSafe("Token is null or empty. Skipping API load and auto-refresh.", logSafe("Token is null or empty. Skipping API load and auto-refresh.", level: LogLevel.warning);
level: LogLevel.warning);
} }
} }
@ -42,28 +37,19 @@ class PermissionController extends GetxController {
logSafe("Auth token retrieved: $token", level: LogLevel.debug); logSafe("Auth token retrieved: $token", level: LogLevel.debug);
return token; return token;
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error retrieving auth token", logSafe("Error retrieving auth token", level: LogLevel.error, error: e, stackTrace: stacktrace);
level: LogLevel.error, error: e, stackTrace: stacktrace);
return null; return null;
} }
} }
Future<void> loadData(String token) async { Future<void> loadData(String token) async {
try { try {
isLoading.value = true;
final userData = await PermissionService.fetchAllUserData(token); final userData = await PermissionService.fetchAllUserData(token);
_updateState(userData); _updateState(userData);
await _storeData(); await _storeData();
logSafe("Data loaded and state updated successfully."); logSafe("Data loaded and state updated successfully.");
// NEW: mark permissions as loaded
permissionsLoaded.value = true;
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error loading data from API", logSafe("Error loading data from API", level: LogLevel.error, error: e, stackTrace: stacktrace);
level: LogLevel.error, error: e, stackTrace: stacktrace);
} finally {
isLoading.value = false;
} }
} }
@ -74,8 +60,7 @@ class PermissionController extends GetxController {
projectsInfo.assignAll(userData['projects']); projectsInfo.assignAll(userData['projects']);
logSafe("State updated with user data."); logSafe("State updated with user data.");
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error updating state", logSafe("Error updating state", level: LogLevel.error, error: e, stackTrace: stacktrace);
level: LogLevel.error, error: e, stackTrace: stacktrace);
} }
} }
@ -104,33 +89,31 @@ class PermissionController extends GetxController {
logSafe("User data successfully stored in SharedPreferences."); logSafe("User data successfully stored in SharedPreferences.");
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error storing data", logSafe("Error storing data", level: LogLevel.error, error: e, stackTrace: stacktrace);
level: LogLevel.error, error: e, stackTrace: stacktrace);
} }
} }
void _startAutoRefresh() { void _startAutoRefresh() {
_refreshTimer = Timer.periodic(const Duration(minutes: 30), (timer) async { _refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
logSafe("Auto-refresh triggered."); logSafe("Auto-refresh triggered.");
final token = await _getAuthToken(); final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) { if (token?.isNotEmpty ?? false) {
await loadData(token!); await loadData(token!);
} else { } else {
logSafe("Token missing during auto-refresh. Skipping.", logSafe("Token missing during auto-refresh. Skipping.", level: LogLevel.warning);
level: LogLevel.warning);
} }
}); });
} }
bool hasPermission(String permissionId) { bool hasPermission(String permissionId) {
final hasPerm = permissions.any((p) => p.id == permissionId); final hasPerm = permissions.any((p) => p.id == permissionId);
logSafe("Checking permission $permissionId: $hasPerm", level: LogLevel.debug);
return hasPerm; return hasPerm;
} }
bool isUserAssignedToProject(String projectId) { bool isUserAssignedToProject(String projectId) {
final assigned = projectsInfo.any((project) => project.id == projectId); final assigned = projectsInfo.any((project) => project.id == projectId);
logSafe("Checking project assignment for $projectId: $assigned", logSafe("Checking project assignment for $projectId: $assigned", level: LogLevel.debug);
level: LogLevel.debug);
return assigned; return assigned;
} }

View File

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
class ProjectStatus {
final String id;
final String name;
ProjectStatus({required this.id, required this.name});
}
class CreateProjectController extends GetxController {
// Observables
var isSubmitting = false.obs;
var statusList = <ProjectStatus>[].obs;
ProjectStatus? selectedStatus;
/// Text controllers for form fields
final nameCtrl = TextEditingController();
final shortNameCtrl = TextEditingController();
final addressCtrl = TextEditingController();
final contactCtrl = TextEditingController();
final startDateCtrl = TextEditingController();
final endDateCtrl = TextEditingController();
@override
void onInit() {
super.onInit();
loadHardcodedStatuses();
}
/// Hardcoded project statuses
void loadHardcodedStatuses() {
final List<ProjectStatus> statuses = [
ProjectStatus(
id: "b74da4c2-d07e-46f2-9919-e75e49b12731", name: "Active"),
ProjectStatus(
id: "cdad86aa-8a56-4ff4-b633-9c629057dfef", name: "In Progress"),
ProjectStatus(
id: "603e994b-a27f-4e5d-a251-f3d69b0498ba", name: "On Hold"),
ProjectStatus(
id: "ef1c356e-0fe0-42df-a5d3-8daee355492d", name: "In Active"),
ProjectStatus(
id: "33deaef9-9af1-4f2a-b443-681ea0d04f81", name: "Completed"),
];
statusList.assignAll(statuses);
}
/// Create project API call using ApiService
Future<bool> createProject({
required String name,
required String projectAddress,
required String shortName,
required String contactPerson,
required DateTime startDate,
required DateTime endDate,
required String projectStatusId,
}) async {
try {
isSubmitting.value = true;
final success = await ApiService.createProjectApi(
name: name,
projectAddress: projectAddress,
shortName: shortName,
contactPerson: contactPerson,
startDate: startDate,
endDate: endDate,
projectStatusId: projectStatusId,
);
if (success) {
showAppSnackbar(
title: "Success",
message: "Project created successfully",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to create project",
type: SnackbarType.error,
);
}
return success;
} catch (e, stack) {
logSafe("Create project error: $e", level: LogLevel.error);
logSafe("Stacktrace: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred",
type: SnackbarType.error,
);
return false;
} finally {
isSubmitting.value = false;
}
}
@override
void onClose() {
nameCtrl.dispose();
shortNameCtrl.dispose();
addressCtrl.dispose();
contactCtrl.dispose();
startDateCtrl.dispose();
endDateCtrl.dispose();
super.onClose();
}
}

View File

@ -1,8 +1,8 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:on_field_work/model/global_project_model.dart'; import 'package:marco/model/global_project_model.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
class ProjectController extends GetxController { class ProjectController extends GetxController {
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs; RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;

View File

@ -1,110 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/controller/service_project/service_project_details_screen_controller.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/service_project/service_project_branches_model.dart';
class AddServiceProjectJobController extends GetxController {
// FORM CONTROLLERS
final titleCtrl = TextEditingController();
final descCtrl = TextEditingController();
final tagCtrl = TextEditingController();
final searchFocusNode = FocusNode();
// OBSERVABLES
final startDate = Rx<DateTime?>(DateTime.now());
final dueDate = Rx<DateTime?>(DateTime.now().add(const Duration(days: 1)));
final enteredTags = <String>[].obs;
final selectedAssignees = <EmployeeModel>[].obs;
// Branches
final branches = <Branch>[].obs;
final selectedBranch = Rxn<Branch>();
final isBranchLoading = false.obs;
// Loading
final isLoading = false.obs;
@override
void onClose() {
titleCtrl.dispose();
descCtrl.dispose();
tagCtrl.dispose();
searchFocusNode.dispose();
super.onClose();
}
// FETCH BRANCHES
Future<void> fetchBranches(String projectId) async {
isBranchLoading.value = true;
final response = await ApiService.getServiceProjectBranchesFull(
projectId: projectId,
);
if (response != null && response.success) {
branches.assignAll(response.data?.data ?? []);
}
isBranchLoading.value = false;
}
// CREATE JOB
Future<void> createJob(String projectId) async {
if (titleCtrl.text.trim().isEmpty || descCtrl.text.trim().isEmpty) {
showAppSnackbar(
title: "Validation",
message: "Title and Description are required",
type: SnackbarType.warning,
);
return;
}
isLoading.value = true;
final jobId = await ApiService.createServiceProjectJobApi(
title: titleCtrl.text.trim(),
description: descCtrl.text.trim(),
projectId: projectId,
branchId: selectedBranch.value?.id,
assignees: selectedAssignees // payload mapping
.map((e) => {"employeeId": e.id, "isActive": true})
.toList(),
startDate: startDate.value!,
dueDate: dueDate.value!,
tags: enteredTags
.map((tag) => {"id": null, "name": tag, "isActive": true})
.toList(),
);
isLoading.value = false;
if (jobId != null) {
if (Get.isRegistered<ServiceProjectDetailsController>()) {
final detailsCtrl = Get.find<ServiceProjectDetailsController>();
// 🔥 1. Refresh job LIST
detailsCtrl.refreshJobsAfterAdd();
// 🔥 2. Refresh job DETAILS (FULL DATA - including tags and assignees)
await detailsCtrl.fetchJobDetail(jobId);
}
Get.back();
showAppSnackbar(
title: "Success",
message: "Job created successfully",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to create job",
type: SnackbarType.error,
);
}
}
}

View File

@ -1,80 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
class ServiceProjectAllocationController extends GetxController {
final projectId = ''.obs;
// Roles
var roles = <TeamRole>[].obs;
var selectedRole = Rxn<TeamRole>();
// Employees
var roleEmployees = <Employee>[].obs;
var selectedEmployees = <Employee>[].obs;
final displayController = TextEditingController();
// Loading
var isLoading = false.obs;
@override
void onInit() {
super.onInit();
ever(selectedEmployees, (_) {
displayController.text = selectedEmployees.isEmpty
? ''
: selectedEmployees
.map((e) => '${e.firstName} ${e.lastName}')
.join(', ');
});
}
// Fetch all roles
Future<void> fetchRoles() async {
isLoading.value = true;
final result = await ApiService.getTeamRoles();
if (result != null) {
roles.assignAll(result);
}
isLoading.value = false;
}
// Fetch employees by role
Future<void> fetchEmployeesByRole(String roleId) async {
isLoading.value = true;
final allocations = await ApiService.getServiceProjectAllocationList(
projectId: projectId.value);
if (allocations != null) {
roleEmployees.assignAll(
allocations
.where((a) => a.teamRole.id == roleId)
.map((a) => a.employee)
.toList(),
);
}
isLoading.value = false;
}
void toggleEmployee(Employee emp) {
if (selectedEmployees.contains(emp)) {
selectedEmployees.remove(emp);
} else {
selectedEmployees.add(emp);
}
}
Future<bool> submitAllocation() async {
final payload = selectedEmployees
.map((e) => {
"projectId": projectId.value,
"employeeId": e.id,
"teamRoleId": selectedRole.value?.id,
"isActive": true,
})
.toList();
return await ApiService.manageServiceProjectAllocation(payload: payload);
}
}

View File

@ -1,479 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/service_project/service_projects_details_model.dart';
import 'package:on_field_work/model/service_project/job_list_model.dart';
import 'package:on_field_work/model/service_project/service_project_job_detail_model.dart';
import 'package:geolocator/geolocator.dart';
import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart';
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
import 'package:on_field_work/model/service_project/job_status_response.dart';
import 'package:on_field_work/model/service_project/job_comments.dart';
import 'dart:convert';
import 'dart:io';
import 'package:mime/mime.dart';
import 'package:image_picker/image_picker.dart';
class ServiceProjectDetailsController extends GetxController {
// -------------------- Observables --------------------
var projectId = ''.obs;
var projectDetail = Rxn<ProjectDetail>();
var jobList = <JobEntity>[].obs;
var jobDetail = Rxn<JobDetailsResponse>();
var showArchivedJobs = false.obs; // true = archived, false = active
// Loading states
var isLoading = false.obs;
var isJobLoading = false.obs;
var isJobDetailLoading = false.obs;
// Error messages
var errorMessage = ''.obs;
var jobErrorMessage = ''.obs;
var jobDetailErrorMessage = ''.obs;
final ImagePicker picker = ImagePicker();
var isProcessingAttachment = false.obs;
// Pagination
var pageNumber = 1;
final int pageSize = 20;
var hasMoreJobs = true.obs;
var isTagging = false.obs;
var attendanceMessage = ''.obs;
var attendanceLog = Rxn<JobAttendanceResponse>();
var teamList = <ServiceProjectAllocation>[].obs;
var isTeamLoading = false.obs;
var teamErrorMessage = ''.obs;
var filteredJobList = <JobEntity>[].obs;
// -------------------- Job Status --------------------
// With this:
var jobStatusList = <JobStatus>[].obs;
var selectedJobStatus = Rx<JobStatus?>(null);
var isJobStatusLoading = false.obs;
var jobStatusErrorMessage = ''.obs;
// -------------------- Job Comments --------------------
var jobComments = <CommentItem>[].obs;
var isCommentsLoading = false.obs;
var commentsErrorMessage = ''.obs;
// -------------------- Lifecycle --------------------
@override
void onInit() {
super.onInit();
fetchProjectJobs();
filteredJobList.value = jobList;
}
// -------------------- Project --------------------
void setProjectId(String id) {
if (projectId.value == id) return;
projectId.value = id;
// Reset pagination and list
pageNumber = 1;
hasMoreJobs.value = true;
jobList.clear();
filteredJobList.clear();
// Fetch project detail
fetchProjectDetail();
// Always fetch jobs for this project
fetchProjectJobs(refresh: true);
}
void updateJobSearch(String searchText) {
if (searchText.isEmpty) {
filteredJobList.value = jobList;
} else {
filteredJobList.value = jobList.where((job) {
final lowerSearch = searchText.toLowerCase();
return job.title.toLowerCase().contains(lowerSearch) ||
(job.description.toLowerCase().contains(lowerSearch)) ||
(job.tags?.any(
(tag) => tag.name.toLowerCase().contains(lowerSearch)) ??
false);
}).toList();
}
}
Future<void> fetchProjectTeams() async {
if (projectId.value.isEmpty) {
teamErrorMessage.value = "Invalid project ID";
return;
}
isTeamLoading.value = true;
teamErrorMessage.value = '';
try {
final result = await ApiService.getServiceProjectAllocationList(
projectId: projectId.value,
isActive: true,
);
if (result != null) {
teamList.value = result;
} else {
teamErrorMessage.value = "No teams found";
}
} catch (e) {
teamErrorMessage.value = "Error fetching teams: $e";
} finally {
isTeamLoading.value = false;
}
}
Future<void> fetchJobStatus({required String statusId}) async {
if (projectId.value.isEmpty) {
jobStatusErrorMessage.value = "Invalid project ID";
return;
}
isJobStatusLoading.value = true;
jobStatusErrorMessage.value = '';
try {
final statuses = await ApiService.getMasterJobStatus(
projectId: projectId.value,
statusId: statusId,
);
if (statuses != null && statuses.isNotEmpty) {
jobStatusList.value = statuses;
// Keep previously selected if exists, else pick first
selectedJobStatus.value = statuses.firstWhere(
(status) => status.id == selectedJobStatus.value?.id,
orElse: () => statuses.first,
);
print("Job Status List: ${jobStatusList.map((e) => e.name).toList()}");
} else {
jobStatusErrorMessage.value = "No job statuses found";
}
} catch (e) {
jobStatusErrorMessage.value = "Error fetching job status: $e";
} finally {
isJobStatusLoading.value = false;
}
}
Future<void> fetchProjectDetail() async {
if (projectId.value.isEmpty) {
errorMessage.value = "Invalid project ID";
return;
}
isLoading.value = true;
errorMessage.value = '';
try {
final result =
await ApiService.getServiceProjectDetailApi(projectId.value);
if (result != null && result.data != null) {
projectDetail.value = result.data!;
} else {
errorMessage.value =
result?.message ?? "Failed to fetch project details";
}
} catch (e) {
errorMessage.value = "Error: $e";
} finally {
isLoading.value = false;
}
}
Future<void> fetchJobAttendanceLog(String attendanceId) async {
if (attendanceId.isEmpty) {
attendanceMessage.value = "Invalid attendance ID";
return;
}
isJobDetailLoading.value = true;
attendanceMessage.value = '';
try {
final result =
await ApiService.getJobAttendanceLog(attendanceId: attendanceId);
if (result != null) {
attendanceLog.value = result;
} else {
attendanceMessage.value = "Attendance log not found or empty";
}
} catch (e) {
attendanceMessage.value = "Error fetching attendance log: $e";
} finally {
isJobDetailLoading.value = false;
}
}
// -------------------- Job List (modified to always load) --------------------
Future<void> fetchProjectJobs({bool refresh = false}) async {
if (projectId.value.isEmpty) return;
if (refresh) pageNumber = 1;
if (!hasMoreJobs.value && !refresh) return;
isJobLoading.value = true;
jobErrorMessage.value = '';
try {
final result = await ApiService.getServiceProjectJobListApi(
projectId: projectId.value,
pageNumber: pageNumber,
pageSize: pageSize,
isActive: true,
isArchive: showArchivedJobs.value,
);
if (result != null && result.data != null) {
final newJobs = result.data?.data ?? [];
if (refresh || pageNumber == 1) {
jobList.value = newJobs;
} else {
jobList.addAll(newJobs);
}
filteredJobList.value = jobList;
hasMoreJobs.value = newJobs.length == pageSize;
if (hasMoreJobs.value) pageNumber++;
} else {
jobErrorMessage.value = result?.message ?? "Failed to fetch jobs";
}
} catch (e) {
jobErrorMessage.value = "Error fetching jobs: $e";
} finally {
isJobLoading.value = false;
}
}
Future<void> fetchMoreJobs() async => fetchProjectJobs();
// -------------------- Manual Refresh --------------------
Future<void> refresh() async {
pageNumber = 1;
hasMoreJobs.value = true;
await Future.wait([
fetchProjectDetail(),
fetchProjectJobs(),
]);
}
// -------------------- Job Detail --------------------
Future<void> fetchJobDetail(String jobId) async {
if (jobId.isEmpty) {
jobDetailErrorMessage.value = "Invalid job ID";
return;
}
isJobDetailLoading.value = true;
jobDetailErrorMessage.value = '';
try {
final result = await ApiService.getServiceProjectJobDetailApi(jobId);
if (result != null) {
jobDetail.value = result;
} else {
jobDetailErrorMessage.value = "Failed to fetch job details";
}
} catch (e) {
jobDetailErrorMessage.value = "Error fetching job details: $e";
} finally {
isJobDetailLoading.value = false;
}
}
Future<Position?> _getCurrentLocation() async {
try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
attendanceMessage.value = "Location services are disabled.";
return null;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
attendanceMessage.value = "Location permission denied";
return null;
}
}
if (permission == LocationPermission.deniedForever) {
attendanceMessage.value =
"Location permission permanently denied. Enable it from settings.";
return null;
}
return await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
} catch (e) {
attendanceMessage.value = "Failed to get location: $e";
return null;
}
}
Future<void> fetchJobComments({bool refresh = false}) async {
if (jobDetail.value?.data?.id == null) {
commentsErrorMessage.value = "Invalid job ID";
return;
}
if (refresh) pageNumber = 1;
isCommentsLoading.value = true;
commentsErrorMessage.value = '';
try {
final response = await ApiService.getJobCommentList(
jobTicketId: jobDetail.value!.data!.id!,
pageNumber: pageNumber,
pageSize: pageSize,
);
if (response != null && response.data != null) {
final newComments = response.data?.data ?? [];
if (refresh || pageNumber == 1) {
jobComments.value = newComments;
} else {
jobComments.addAll(newComments);
}
hasMoreJobs.value =
(response.data?.totalEntities ?? 0) > (pageNumber * pageSize);
if (hasMoreJobs.value) pageNumber++;
} else {
commentsErrorMessage.value =
response?.message ?? "Failed to fetch comments";
}
} catch (e) {
commentsErrorMessage.value = "Error fetching comments: $e";
} finally {
isCommentsLoading.value = false;
}
}
Future<bool> addJobComment({
required String jobId,
required String comment,
List<File>? files,
}) async {
try {
List<Map<String, dynamic>> attachments = [];
if (files != null && files.isNotEmpty) {
for (final file in files) {
final bytes = await file.readAsBytes();
final base64Data = base64Encode(bytes);
final mimeType =
lookupMimeType(file.path) ?? "application/octet-stream";
attachments.add({
"fileName": file.path.split('/').last,
"base64Data": base64Data,
"contentType": mimeType,
"fileSize": bytes.length,
"description": "",
"isActive": true,
});
}
}
final success = await ApiService.addJobComment(
jobTicketId: jobId,
comment: comment,
attachments: attachments,
);
if (success) {
await fetchJobDetail(jobId);
refresh();
}
return success;
} catch (e) {
print("Error adding comment: $e");
return false;
}
}
/// Tag In / Tag Out for a job with proper payload
Future<void> updateJobAttendance({
required String jobId,
required int action,
String comment = "Updated via app",
File? attachment,
}) async {
if (jobId.isEmpty) return;
isTagging.value = true;
attendanceMessage.value = '';
try {
final position = await _getCurrentLocation();
if (position == null) {
isTagging.value = false;
return;
}
Map<String, dynamic>? attachmentPayload;
if (attachment != null) {
final bytes = await attachment.readAsBytes();
final base64Data = base64Encode(bytes);
final mimeType =
lookupMimeType(attachment.path) ?? 'application/octet-stream';
attachmentPayload = {
"documentId": jobId,
"fileName": attachment.path.split('/').last,
"base64Data": base64Data,
"contentType": mimeType,
"fileSize": bytes.length,
"description": "Attached via app",
"isActive": true,
};
}
final payload = {
"jobTcketId": jobId,
"action": action,
"latitude": position.latitude.toString(),
"longitude": position.longitude.toString(),
"comment": comment,
"attachment": attachmentPayload,
};
final success = await ApiService.updateServiceProjectJobAttendance(
payload: payload,
);
if (success) {
attendanceMessage.value =
action == 0 ? "Tagged In successfully" : "Tagged Out successfully";
await fetchJobDetail(jobId);
} else {
attendanceMessage.value = "Failed to update attendance";
}
} catch (e) {
attendanceMessage.value = "Error updating attendance: $e";
} finally {
isTagging.value = false;
}
}
// ------------------------------------------------------------
// 🔥 AUTO REFRESH JOB LIST AFTER ADDING A JOB
// ------------------------------------------------------------
Future<void> refreshJobsAfterAdd() async {
pageNumber = 1;
hasMoreJobs.value = true;
await fetchProjectJobs();
}
}

View File

@ -1,59 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/service_project/service_projects_list_model.dart';
class ServiceProjectController extends GetxController {
final projects = <ProjectItem>[].obs;
final isLoading = false.obs;
final searchQuery = ''.obs;
/// Computed filtered project list
List<ProjectItem> get filteredProjects {
final query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return projects;
return projects.where((p) {
final nameMatch = p.name.toLowerCase().contains(query);
final shortNameMatch = p.shortName.toLowerCase().contains(query);
final addressMatch = p.address.toLowerCase().contains(query);
final contactMatch = p.contactName.toLowerCase().contains(query);
final clientMatch = p.client != null &&
(p.client!.name.toLowerCase().contains(query) ||
p.client!.contactPerson.toLowerCase().contains(query));
return nameMatch ||
shortNameMatch ||
addressMatch ||
contactMatch ||
clientMatch;
}).toList();
}
/// Fetch projects from API
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
try {
isLoading.value = true;
final result = await ApiService.getServiceProjectsListApi(
pageNumber: pageNumber,
pageSize: pageSize,
);
if (result != null && result.data != null) {
projects.assignAll(result.data!.data);
} else {
projects.clear();
}
} catch (e) {
// Optional: log or show error
rethrow;
} finally {
isLoading.value = false;
}
}
/// Update search
void updateSearch(String query) {
searchQuery.value = query;
}
}

View File

@ -1,152 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/dailyTaskPlanning/master_work_category_model.dart';
class AddTaskController extends GetxController {
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
RxnString selectedCategoryId = RxnString();
RxnString selectedCategoryName = RxnString();
var categoryIdNameMap = <String, String>{}.obs;
List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString();
RxBool isLoadingWorkMasterCategories = false.obs;
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs;
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetchWorkMasterCategories();
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
if (fieldType == "description" && value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
return null;
}
Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
logSafe("Starting task assignment...", level: LogLevel.info);
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
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<bool> createTask({
required String parentTaskId,
required String workAreaId,
required String activityId,
required int plannedTask,
required String comment,
required String categoryId,
DateTime? assignmentDate,
}) async {
logSafe("Creating new task...", level: LogLevel.info);
final response = await ApiService.createTask(
parentTaskId: parentTaskId,
plannedTask: plannedTask,
comment: comment,
workAreaId: workAreaId,
activityId: activityId,
assignmentDate: assignmentDate,
categoryId: categoryId,
);
if (response == true) {
logSafe("Task created successfully.", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task created successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to create task.", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to create task.",
type: SnackbarType.error,
);
return false;
}
}
Future<void> fetchWorkMasterCategories() async {
isLoadingWorkMasterCategories.value = true;
try {
final response = await ApiService.getMasterWorkCategories();
if (response != null) {
final dataList = response['data'] ?? [];
final parsedList = List<WorkCategoryModel>.from(
dataList.map((e) => WorkCategoryModel.fromJson(e)),
);
workMasterCategories.assignAll(parsedList);
final mapped = {for (var item in parsedList) item.id: item.name};
categoryIdNameMap.assignAll(mapped);
logSafe("Work categories fetched: ${dataList.length}", level: LogLevel.info);
} else {
logSafe("No work categories found or API call failed.", level: LogLevel.warning);
}
} catch (e, st) {
logSafe("Error parsing work categories", level: LogLevel.error, error: e, stackTrace: st);
workMasterCategories.clear();
categoryIdNameMap.clear();
}
isLoadingWorkMasterCategories.value = false;
update();
}
void selectCategory(String id) {
selectedCategoryId.value = id;
selectedCategoryName.value = categoryIdNameMap[id];
logSafe("Category selected", level: LogLevel.debug, );
}
}

View File

@ -1,241 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/project_model.dart';
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_model.dart';
import 'package:on_field_work/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
class DailyTaskController extends GetxController {
List<ProjectModel> projects = [];
String? selectedProjectId;
DateTime? startDateTask;
DateTime? endDateTask;
// Rx fields for DateRangePickerWidget
Rx<DateTime> startDateTaskRx = DateTime.now().obs;
Rx<DateTime> endDateTaskRx = DateTime.now().obs;
List<TaskModel> dailyTasks = [];
final RxSet<String> expandedDates = <String>{}.obs;
void toggleDate(String dateKey) {
if (expandedDates.contains(dateKey)) {
expandedDates.remove(dateKey);
} else {
expandedDates.add(dateKey);
}
}
RxSet<String> selectedBuildings = <String>{}.obs;
RxSet<String> selectedFloors = <String>{}.obs;
RxSet<String> selectedActivities = <String>{}.obs;
RxSet<String> selectedServices = <String>{}.obs;
RxBool isFilterLoading = false.obs;
RxBool isLoading = true.obs;
RxBool isLoadingMore = false.obs;
Map<String, List<TaskModel>> groupedDailyTasks = {};
// Pagination
int currentPage = 1;
int pageSize = 20;
bool hasMore = true;
FilterData? taskFilterData;
@override
void onInit() {
super.onInit();
_initializeDefaults();
_initializeRxDates();
}
void _initializeDefaults() {
_setDefaultDateRange();
}
void _setDefaultDateRange() {
final today = DateTime.now();
startDateTask = today.subtract(const Duration(days: 7));
endDateTask = today;
logSafe(
"Default date range set: $startDateTask to $endDateTask",
level: LogLevel.info,
);
}
void _initializeRxDates() {
startDateTaskRx.value =
startDateTask ?? DateTime.now().subtract(const Duration(days: 7));
endDateTaskRx.value = endDateTask ?? DateTime.now();
}
void clearTaskFilters() {
selectedBuildings.clear();
selectedFloors.clear();
selectedActivities.clear();
selectedServices.clear();
startDateTask = null;
endDateTask = null;
// reset Rx dates as well
startDateTaskRx.value = DateTime.now().subtract(const Duration(days: 7));
endDateTaskRx.value = DateTime.now();
update();
}
void updateDateRange(DateTime? start, DateTime? end) {
if (start != null && end != null) {
startDateTask = start;
endDateTask = end;
startDateTaskRx.value = start;
endDateTaskRx.value = end;
update();
}
}
Future<void> fetchTaskData(
String projectId, {
int pageNumber = 1,
int pageSize = 20,
bool isLoadMore = false,
}) async {
if (!isLoadMore) {
isLoading.value = true;
currentPage = 1;
hasMore = true;
groupedDailyTasks.clear();
dailyTasks.clear();
} else {
isLoadingMore.value = true;
}
// Create the filter object
final filter = {
"buildingIds": selectedBuildings.toList(),
"floorIds": selectedFloors.toList(),
"activityIds": selectedActivities.toList(),
"serviceIds": selectedServices.toList(),
"dateFrom": startDateTask?.toIso8601String(),
"dateTo": endDateTask?.toIso8601String(),
};
final response = await ApiService.getDailyTasks(
projectId,
filter: filter,
pageNumber: pageNumber,
pageSize: pageSize,
);
if (response != null && response.isNotEmpty) {
if (!isLoadMore) {
groupedDailyTasks.clear();
}
for (var task in response) {
final assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0];
// Initialize list if not present
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []);
// Only add task if it doesn't already exist (avoid duplicates)
if (!groupedDailyTasks[assignmentDateKey]!
.any((t) => t.id == task.id)) {
groupedDailyTasks[assignmentDateKey]!.add(task);
}
}
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
currentPage = pageNumber;
} else {
hasMore = false;
}
isLoading.value = false;
isLoadingMore.value = false;
update();
}
Future<void> fetchTaskFilter(String projectId) async {
isFilterLoading.value = true;
try {
final filterResponse = await ApiService.getDailyTaskFilter(projectId);
if (filterResponse != null && filterResponse.success) {
taskFilterData = filterResponse.data;
logSafe(
"Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}",
level: LogLevel.info,
);
} else {
logSafe(
"Failed to fetch task filter for projectId: $projectId",
level: LogLevel.warning,
);
}
} catch (e, stack) {
logSafe("Exception in fetchTaskFilter: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
} finally {
isFilterLoading.value = false;
update();
}
}
Future<void> selectDateRangeForTaskData(
BuildContext context,
DailyTaskController controller,
) async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start:
startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
end: endDateTask ?? DateTime.now(),
),
);
if (picked == null) {
logSafe("Date range picker cancelled by user.", level: LogLevel.debug);
return;
}
startDateTask = picked.start;
endDateTask = picked.end;
// update Rx fields as well
startDateTaskRx.value = picked.start;
endDateTaskRx.value = picked.end;
logSafe(
"Date range selected: $startDateTask to $endDateTask",
level: LogLevel.info,
);
final projectId = controller.selectedProjectId;
if (projectId != null && projectId.isNotEmpty) {
await controller.fetchTaskData(projectId);
} else {
logSafe("Project ID is null or empty, skipping fetchTaskData",
level: LogLevel.warning);
}
}
void refreshTasksFromNotification({
required String projectId,
required String taskAllocationId,
}) async {
await fetchTaskData(projectId);
update();
}
}

View File

@ -1,367 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/project_model.dart';
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_planning_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
class DailyTaskPlanningController extends GetxController {
List<ProjectModel> projects = [];
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
List<EmployeeModel> allEmployeesCache = [];
List<TaskPlanningDetailsModel> dailyTasks = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
List<Map<String, dynamic>> roles = [];
RxBool isAssigningTask = false.obs;
RxnString selectedRoleId = RxnString();
RxBool isFetchingTasks = true.obs;
RxBool isFetchingProjects = true.obs;
RxBool isFetchingEmployees = true.obs;
/// New: track per-building loading and loaded state for lazy infra loading
RxMap<String, RxBool> buildingLoadingStates = <String, RxBool>{}.obs;
final Set<String> buildingsWithDetails = <String>{};
@override
void onInit() {
super.onInit();
fetchRoles();
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) return 'This field is required';
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
if (fieldType == "description" && value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
return null;
}
void updateSelectedEmployees() {
selectedEmployees.value =
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
logSafe("Updated selected employees", level: LogLevel.debug);
}
void onRoleSelected(String? roleId) {
selectedRoleId.value = roleId;
logSafe("Role selected", level: LogLevel.info);
}
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,
String? organizationId,
String? serviceId,
}) async {
isAssigningTask.value = true;
logSafe("Starting assign task...", level: LogLevel.info);
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
organizationId: organizationId,
serviceId: serviceId,
);
isAssigningTask.value = 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;
}
}
/// Fetch buildings list only (no deep area/workItem calls) for initial load.
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
if (projectId == null) return;
isFetchingTasks.value = true;
try {
final infraResponse = await ApiService.getInfraDetails(
projectId,
serviceId: serviceId,
);
final infraData = infraResponse?['data'] as List<dynamic>?;
if (infraData == null || infraData.isEmpty) {
dailyTasks = [];
return;
}
// Filter buildings with 0 planned & completed work
final filteredBuildings = infraData.where((b) {
final planned = (b['plannedWork'] as num?)?.toDouble() ?? 0;
final completed = (b['completedWork'] as num?)?.toDouble() ?? 0;
return planned > 0 || completed > 0;
}).toList();
dailyTasks = filteredBuildings.map((buildingJson) {
final building = Building(
id: buildingJson['id'],
name: buildingJson['buildingName'],
description: buildingJson['description'],
floors: [],
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
completedWork:
(buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
);
return TaskPlanningDetailsModel(
id: building.id,
name: building.name,
projectAddress: "",
contactPerson: "",
startDate: DateTime.now(),
endDate: DateTime.now(),
projectStatusId: "",
buildings: [building],
);
}).toList();
buildingLoadingStates.clear();
buildingsWithDetails.clear();
} catch (e, stack) {
logSafe("Error fetching daily task data",
level: LogLevel.error, error: e, stackTrace: stack);
} finally {
isFetchingTasks.value = false;
update();
}
}
/// Fetch full infra for a single building (floors, workAreas, workItems).
/// Called lazily when user expands a building in the UI.
Future<void> fetchBuildingInfra(
String buildingId, String projectId, String? serviceId) async {
if (buildingId.isEmpty) return;
// mark loading
buildingLoadingStates.putIfAbsent(buildingId, () => true.obs);
buildingLoadingStates[buildingId]!.value = true;
update();
try {
// Re-use getInfraDetails and find the building entry for the requested buildingId
final infraResponse =
await ApiService.getInfraDetails(projectId, serviceId: serviceId);
final infraData = infraResponse?['data'] as List<dynamic>? ?? [];
final buildingJson = infraData.firstWhere(
(b) => b['id'].toString() == buildingId.toString(),
orElse: () => null,
);
if (buildingJson == null) {
logSafe("Building $buildingId not found in infra response",
level: LogLevel.warning);
return;
}
// Build floors & workAreas for this building
final building = Building(
id: buildingJson['id'],
name: buildingJson['buildingName'],
description: buildingJson['description'],
floors:
(buildingJson['floors'] as List<dynamic>? ?? []).map((floorJson) {
return Floor(
id: floorJson['id'],
floorName: floorJson['floorName'],
workAreas: (floorJson['workAreas'] as List<dynamic>? ?? [])
.map((areaJson) {
return WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [], // will populate later
);
}).toList(),
);
}).toList(),
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
completedWork: (buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
);
// For each workArea, fetch its work items and populate
await Future.wait(
building.floors.expand((f) => f.workAreas).map((area) async {
try {
final taskResponse = await ApiService.getWorkItemsByWorkArea(area.id,
serviceId: serviceId);
final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper(
workItemId: taskJson['id'],
workItem: WorkItem(
id: taskJson['id'],
activityMaster: taskJson['activityMaster'] != null
? ActivityMaster.fromJson(taskJson['activityMaster'])
: null,
workCategoryMaster: taskJson['workCategoryMaster'] != null
? WorkCategoryMaster.fromJson(
taskJson['workCategoryMaster'])
: null,
plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(),
completedWork:
(taskJson['completedWork'] as num?)?.toDouble(),
todaysAssigned:
(taskJson['todaysAssigned'] as num?)?.toDouble(),
description: taskJson['description'] as String?,
taskDate: taskJson['taskDate'] != null
? DateTime.tryParse(taskJson['taskDate'])
: null,
),
)));
} catch (e, stack) {
logSafe("Error fetching tasks for work area ${area.id}",
level: LogLevel.error, error: e, stackTrace: stack);
}
}));
// Merge/replace the building into dailyTasks
bool merged = false;
for (var t in dailyTasks) {
final idx = t.buildings
.indexWhere((b) => b.id.toString() == building.id.toString());
if (idx != -1) {
t.buildings[idx] = building;
merged = true;
break;
}
}
if (!merged) {
// If not present, add a new TaskPlanningDetailsModel wrapper (fallback)
dailyTasks.add(TaskPlanningDetailsModel(
id: building.id,
name: building.name,
projectAddress: "",
contactPerson: "",
startDate: DateTime.now(),
endDate: DateTime.now(),
projectStatusId: "",
buildings: [building],
));
}
// Mark as loaded
buildingsWithDetails.add(buildingId.toString());
} catch (e, stack) {
logSafe("Error fetching infra for building $buildingId",
level: LogLevel.error, error: e, stackTrace: stack);
} finally {
buildingLoadingStates.putIfAbsent(buildingId, () => false.obs);
buildingLoadingStates[buildingId]!.value = false;
update();
}
}
Future<void> fetchEmployeesByProjectService({
required String projectId,
String? serviceId,
String? organizationId,
}) async {
isFetchingEmployees.value = true;
try {
final response = await ApiService.getEmployeesByProjectService(
projectId,
serviceId: serviceId ?? '',
organizationId: organizationId ?? '',
);
if (response != null && response.isNotEmpty) {
employees
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
if (serviceId == null && organizationId == null) {
allEmployeesCache = List.from(employees);
}
final currentEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates
.removeWhere((key, _) => !currentEmployeeIds.contains(key));
employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs);
});
selectedEmployees
.removeWhere((e) => !currentEmployeeIds.contains(e.id));
logSafe("Employees fetched: ${employees.length}", level: LogLevel.info);
} else {
employees.clear();
uploadingStates.clear();
selectedEmployees.clear();
logSafe(
serviceId != null || organizationId != null
? "Filtered employees empty"
: "No employees found",
level: LogLevel.warning,
);
}
} catch (e, stack) {
logSafe("Error fetching employees",
level: LogLevel.error, error: e, stackTrace: stack);
if (serviceId == null &&
organizationId == null &&
allEmployeesCache.isNotEmpty) {
employees.assignAll(allEmployeesCache);
final cachedEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates
.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs);
});
selectedEmployees.removeWhere((e) => !cachedEmployeeIds.contains(e.id));
} else {
employees.clear();
uploadingStates.clear();
selectedEmployees.clear();
}
} finally {
isFetchingEmployees.value = false;
update();
}
}
}

View File

@ -1,336 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/controller/my_controller.dart';
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/dailyTaskPlanning/work_status_model.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
enum ApiStatus { idle, loading, success, failure }
class ReportTaskActionController extends MyController {
final RxBool isLoading = false.obs;
final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
final RxList<File> selectedImages = <File>[].obs;
final RxList<WorkStatus> workStatus = <WorkStatus>[].obs;
final RxList<WorkStatus> workStatuses = <WorkStatus>[].obs;
final RxBool showAddTaskCheckbox = false.obs;
final RxBool isAddTaskChecked = false.obs;
final RxBool isLoadingWorkStatus = false.obs;
final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
final RxString selectedWorkStatusName = ''.obs;
final RxBool isPickingImage = false.obs;
final MyFormValidator basicValidator = MyFormValidator();
final DailyTaskPlanningController taskController =
Get.put(DailyTaskPlanningController());
final ImagePicker _picker = ImagePicker();
final assignedDateController = TextEditingController();
final workAreaController = TextEditingController();
final activityController = TextEditingController();
final teamSizeController = TextEditingController();
final taskIdController = TextEditingController();
final assignedController = TextEditingController();
final completedWorkController = TextEditingController();
final commentController = TextEditingController();
final assignedByController = TextEditingController();
final teamMembersController = TextEditingController();
final plannedWorkController = TextEditingController();
final approvedTaskController = TextEditingController();
List<TextEditingController> get _allControllers => [
assignedDateController,
workAreaController,
activityController,
teamSizeController,
taskIdController,
assignedController,
completedWorkController,
commentController,
assignedByController,
teamMembersController,
plannedWorkController,
approvedTaskController,
];
@override
void onInit() {
super.onInit();
logSafe("Initializing ReportTaskController...");
_initializeFormFields();
}
@override
void onClose() {
for (final controller in _allControllers) {
controller.dispose();
}
logSafe("Disposed all text controllers in ReportTaskActionController.");
super.onClose();
}
void _initializeFormFields() {
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)
..addField('approved_task',
label: "Approved Task",
required: true,
controller: approvedTaskController);
}
Future<bool> approveTask({
required String projectId,
required String comment,
required String reportActionId,
required String approvedTaskCount,
List<File>? images,
}) async {
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;
}
final approvedTaskInt = int.tryParse(approvedTaskCount);
final completedWorkInt = int.tryParse(completedWorkController.text.trim());
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,
approvedTask: approvedTaskInt,
comment: comment,
images: imageData,
);
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, st) {
logSafe("Error in approveTask: $e",
level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred.");
return false;
} finally {
isLoading.value = false;
Future.delayed(const Duration(milliseconds: 500), () {
reportStatus.value = ApiStatus.idle;
});
}
}
Future<void> commentTask({
required String projectId,
required String comment,
List<File>? images,
}) async {
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(),
images: imageData,
).timeout(const Duration(seconds: 30), onTimeout: () {
throw Exception("Request timed out.");
});
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, 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;
}
}
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 {
logSafe("No work statuses found or API call failed",
level: LogLevel.warning);
}
isLoadingWorkStatus.value = false;
update(['dashboard_controller']);
}
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async {
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;
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(compressedBytes),
"contentType": _getContentTypeFromFileName(file.path),
"fileSize": compressedBytes.lengthInBytes,
"description": "Image uploaded for task",
};
}));
logSafe(
"_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
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 {
isPickingImage.value = true; // start loading
logSafe("Opening image picker...");
if (fromCamera) {
final pickedFile = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 75,
);
if (pickedFile != null) {
final timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: File(pickedFile.path),
);
selectedImages.add(timestampedFile);
logSafe("Image added from camera with timestamp: ${pickedFile.path}");
}
} else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
logSafe("${pickedFiles.length} images added from gallery.");
}
} catch (e, st) {
logSafe("Error picking images: $e",
level: LogLevel.error, stackTrace: st);
} finally {
isPickingImage.value = false; // stop loading
}
}
void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) {
logSafe(
"Removing image at index $index",
);
selectedImages.removeAt(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

@ -1,284 +0,0 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:on_field_work/controller/my_controller.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'dart:convert';
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
enum ApiStatus { idle, loading, success, failure }
final DailyTaskPlanningController taskController =
Get.put(DailyTaskPlanningController());
final ImagePicker _picker = ImagePicker();
class ReportTaskController extends MyController {
List<PlatformFile> files = [];
MyFormValidator basicValidator = MyFormValidator();
RxBool isLoading = false.obs;
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
final RxBool isPickingImage = false.obs;
RxList<File> selectedImages = <File>[].obs;
final assignedDateController = TextEditingController();
final workAreaController = TextEditingController();
final activityController = TextEditingController();
final teamSizeController = TextEditingController();
final taskIdController = TextEditingController();
final assignedController = TextEditingController();
final completedWorkController = TextEditingController();
final commentController = TextEditingController();
final assignedByController = TextEditingController();
final teamMembersController = TextEditingController();
final plannedWorkController = TextEditingController();
@override
void onInit() {
super.onInit();
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,
workAreaController,
activityController,
teamSizeController,
taskIdController,
assignedController,
completedWorkController,
commentController,
assignedByController,
teamMembersController,
plannedWorkController,
].forEach((controller) => controller.dispose());
super.onClose();
}
Future<bool> reportTask({
required String projectId,
required String comment,
required int completedTask,
required List<Map<String, dynamic>> checklist,
required DateTime reportedDate,
List<File>? images,
}) async {
logSafe(
"Reporting task for projectId",
);
final completedWork = completedWorkController.text.trim();
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) {
_showError("Comment is required.");
return false;
}
try {
reportStatus.value = ApiStatus.loading;
isLoading.value = true;
final imageData = await _prepareImages(images, "task report");
final success = await ApiService.reportTask(
id: projectId,
comment: commentField,
completedTask: int.parse(completedWork),
checkList: checklist,
images: imageData,
);
if (success) {
reportStatus.value = ApiStatus.success;
_showSuccess("Task reported successfully!");
await taskController.fetchTaskData(projectId);
return true;
} else {
reportStatus.value = ApiStatus.failure;
_showError("Failed to report task.");
return false;
}
} catch (e, s) {
logSafe("Exception while reporting task",
level: LogLevel.error, error: e, stackTrace: s);
reportStatus.value = ApiStatus.failure;
_showError("An error occurred while reporting the task.");
return false;
} finally {
isLoading.value = false;
Future.delayed(const Duration(milliseconds: 500), () {
reportStatus.value = ApiStatus.idle;
});
}
}
Future<void> commentTask({
required String projectId,
required String comment,
List<File>? images,
}) async {
logSafe(
"Submitting comment for project",
);
final commentField = commentController.text.trim();
if (commentField.isEmpty) {
_showError("Comment is required.");
return;
}
try {
isLoading.value = true;
final imageData = await _prepareImages(images, "task comment");
final success = await ApiService.commentTask(
id: projectId,
comment: commentField,
images: imageData,
).timeout(const Duration(seconds: 30), onTimeout: () {
logSafe("Task comment request timed out.", level: LogLevel.error);
throw Exception("Request timed out.");
});
if (success) {
_showSuccess("Task commented successfully!");
await taskController.fetchTaskData(projectId);
} else {
_showError("Failed to comment task.");
}
} catch (e, s) {
logSafe("Exception while commenting task",
level: LogLevel.error, error: e, stackTrace: s);
_showError("An error occurred while commenting the task.");
} finally {
isLoading.value = false;
}
}
Future<List<Map<String, dynamic>>?> _prepareImages(
List<File>? images, String context) async {
if (images == null || images.isEmpty) return null;
logSafe("Preparing images for $context upload...");
final results = await Future.wait(images.map((file) async {
try {
final compressed = await compressImageToUnder100KB(file);
if (compressed == null) return null;
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(compressed),
"contentType": _getContentTypeFromFileName(file.path),
"fileSize": compressed.lengthInBytes,
"description": "Image uploaded for $context",
};
} catch (e) {
logSafe("Image processing failed: ${file.path}",
level: LogLevel.warning, error: e);
return null;
}
}));
return results.whereType<Map<String, dynamic>>().toList();
}
String _getContentTypeFromFileName(String fileName) {
final ext = fileName.split('.').last.toLowerCase();
return switch (ext) {
'jpg' || 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'gif' => 'image/gif',
_ => 'application/octet-stream',
};
}
Future<void> pickImages({required bool fromCamera}) async {
try {
isPickingImage.value = true; // Start loading
if (fromCamera) {
final pickedFile = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 75,
);
if (pickedFile != null) {
// Only camera images get timestamp
final timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: File(pickedFile.path),
);
selectedImages.add(timestampedFile);
}
} else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
// Gallery images added as-is without timestamp
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
}
logSafe("Images picked: ${selectedImages.length}");
} catch (e) {
logSafe("Error picking images", level: LogLevel.warning, error: e);
} finally {
isPickingImage.value = false; // Stop loading
}
}
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

@ -1,66 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/all_organization_model.dart';
class AllOrganizationController extends GetxController {
RxList<AllOrganization> organizations = <AllOrganization>[].obs;
Rxn<AllOrganization> selectedOrganization = Rxn<AllOrganization>();
final isLoadingOrganizations = false.obs;
String? passedOrgId;
AllOrganizationController({this.passedOrgId});
@override
void onInit() {
super.onInit();
fetchAllOrganizations();
}
Future<void> fetchAllOrganizations() async {
try {
isLoadingOrganizations.value = true;
final response = await ApiService.getAllOrganizations();
if (response != null && response.data.data.isNotEmpty) {
organizations.value = response.data.data;
// Select organization based on passed ID, or fallback to first
if (passedOrgId != null) {
selectedOrganization.value =
organizations.firstWhere(
(org) => org.id == passedOrgId,
orElse: () => organizations.first,
);
} else {
selectedOrganization.value ??= organizations.first;
}
} else {
organizations.clear();
selectedOrganization.value = null;
}
} catch (e, stackTrace) {
logSafe(
"Failed to fetch organizations: $e",
level: LogLevel.error,
error: e,
stackTrace: stackTrace,
);
organizations.clear();
selectedOrganization.value = null;
} finally {
isLoadingOrganizations.value = false;
}
}
void selectOrganization(AllOrganization? org) {
selectedOrganization.value = org;
}
void clearSelection() {
selectedOrganization.value = null;
}
String get currentSelection => selectedOrganization.value?.name ?? "All Organizations";
}

View File

@ -1,52 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
class OrganizationController extends GetxController {
/// List of organizations assigned to the selected project
List<Organization> organizations = [];
/// Currently selected organization (reactive)
Rxn<Organization> selectedOrganization = Rxn<Organization>();
/// Loading state for fetching organizations
final isLoadingOrganizations = false.obs;
/// Fetch organizations assigned to a given project
Future<void> fetchOrganizations(String projectId) async {
try {
isLoadingOrganizations.value = true;
final response = await ApiService.getAssignedOrganizations(projectId);
if (response != null && response.data.isNotEmpty) {
organizations = response.data;
logSafe("Organizations fetched: ${organizations.length}");
} else {
organizations = [];
logSafe("No organizations found for project $projectId",
level: LogLevel.warning);
}
} catch (e, stackTrace) {
logSafe("Failed to fetch organizations: $e",
level: LogLevel.error, error: e, stackTrace: stackTrace);
organizations = [];
} finally {
isLoadingOrganizations.value = false;
}
}
/// Select an organization
void selectOrganization(Organization? org) {
selectedOrganization.value = org;
}
/// Clear the selection (set to "All Organizations")
void clearSelection() {
selectedOrganization.value = null;
}
/// Current selection name for UI
String get currentSelection =>
selectedOrganization.value?.name ?? "All Organizations";
}

View File

@ -1,43 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/model/tenant/tenant_services_model.dart';
class ServiceController extends GetxController {
List<Service> services = [];
Service? selectedService;
final isLoadingServices = false.obs;
/// Fetch services assigned to a project
Future<void> fetchServices(String projectId) async {
try {
isLoadingServices.value = true;
final response = await ApiService.getAssignedServices(projectId);
if (response != null) {
services = response.data;
logSafe("Services fetched: ${services.length}");
} else {
logSafe("Failed to fetch services for project $projectId",
level: LogLevel.error);
}
} finally {
isLoadingServices.value = false;
update();
}
}
/// Select a service
void selectService(Service? service) {
selectedService = service;
update();
}
/// Clear selection
void clearSelection() {
selectedService = null;
update();
}
/// Current selected name
String get currentSelection => selectedService?.name ?? "All Services";
}

View File

@ -1,136 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/tenant_service.dart';
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/controller/permission_controller.dart';
class TenantSelectionController extends GetxController {
final TenantService _tenantService = TenantService();
// Tenant list
final tenants = <Tenant>[].obs;
// Loading state
final isLoading = false.obs;
// Selected tenant ID
final selectedTenantId = RxnString();
// Flag to indicate auto-selection (for splash screen)
final isAutoSelecting = false.obs;
@override
void onInit() {
super.onInit();
loadTenants();
}
/// Load tenants and handle auto-selection
Future<void> loadTenants() async {
isLoading.value = true;
isAutoSelecting.value = true; // show splash during auto-selection
try {
final data = await _tenantService.getTenants();
if (data == null || data.isEmpty) {
tenants.clear();
logSafe("⚠️ No tenants found for the user.", level: LogLevel.warning);
return;
}
tenants.value = data.map((e) => Tenant.fromJson(e)).toList();
final recentTenantId = LocalStorage.getRecentTenantId();
// Auto-select if only one tenant
if (tenants.length == 1) {
await _selectTenant(tenants.first.id);
}
// Auto-select recent tenant if available
else if (recentTenantId != null) {
final recentTenant =
tenants.firstWhereOrNull((t) => t.id == recentTenantId);
if (recentTenant != null) {
await _selectTenant(recentTenant.id);
} else {
_clearSelection();
}
}
// No auto-selection
else {
_clearSelection();
}
} catch (e, st) {
logSafe("❌ Exception in loadTenants",
level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "Failed to load organizations. Please try again.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
isAutoSelecting.value = false; // hide splash
}
}
/// User manually selects a tenant
Future<void> onTenantSelected(String tenantId) async {
isAutoSelecting.value = true;
await _selectTenant(tenantId);
isAutoSelecting.value = false;
}
/// Internal tenant selection logic
Future<void> _selectTenant(String tenantId) async {
try {
isLoading.value = true;
final success = await _tenantService.selectTenant(tenantId);
if (!success) {
showAppSnackbar(
title: "Error",
message: "Unable to select organization. Please try again.",
type: SnackbarType.error,
);
return;
}
// Update tenant & persist
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
TenantService.setSelectedTenant(selectedTenant);
selectedTenantId.value = tenantId;
await LocalStorage.setRecentTenantId(tenantId);
// Load permissions if token exists
final token = LocalStorage.getJwtToken();
if (token != null && token.isNotEmpty) {
if (!Get.isRegistered<PermissionController>()) {
Get.put(PermissionController());
}
await Get.find<PermissionController>().loadData(token);
}
// Navigate **before changing isAutoSelecting**
await Get.offAllNamed('/dashboard');
// Then hide splash
isAutoSelecting.value = false;
} catch (e) {
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred while selecting organization.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
/// Clear tenant selection
void _clearSelection() {
selectedTenantId.value = null;
TenantService.currentTenant = null;
}
}

View File

@ -1,106 +0,0 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/tenant_service.dart';
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/controller/permission_controller.dart';
class TenantSwitchController extends GetxController {
final TenantService _tenantService = TenantService();
final tenants = <Tenant>[].obs;
final isLoading = false.obs;
final selectedTenantId = RxnString();
@override
void onInit() {
super.onInit();
loadTenants();
}
/// Load all tenants for switching (does not auto-select)
Future<void> loadTenants() async {
isLoading.value = true;
try {
final data = await _tenantService.getTenants();
if (data == null || data.isEmpty) {
tenants.clear();
logSafe("⚠️ No tenants available for switching.", level: LogLevel.warning);
return;
}
tenants.value = data.map((e) => Tenant.fromJson(e)).toList();
// Keep current tenant as selected
selectedTenantId.value = TenantService.currentTenant?.id;
} catch (e, st) {
logSafe("❌ Exception in loadTenants", level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "Failed to load organizations for switching.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
/// Switch to a different tenant and navigate fully
Future<void> switchTenant(String tenantId) async {
if (TenantService.currentTenant?.id == tenantId) return;
isLoading.value = true;
try {
final success = await _tenantService.selectTenant(tenantId);
if (!success) {
logSafe("❌ Tenant switch failed: $tenantId", level: LogLevel.warning);
showAppSnackbar(
title: "Error",
message: "Unable to switch organization. Try again.",
type: SnackbarType.error,
);
return;
}
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
TenantService.setSelectedTenant(selectedTenant);
selectedTenantId.value = tenantId;
// Persist recent tenant
await LocalStorage.setRecentTenantId(tenantId);
logSafe("✅ Tenant switched successfully: $tenantId");
// 🔹 Load permissions after tenant switch (null-safe)
final token = await LocalStorage.getJwtToken();
if (token != null && token.isNotEmpty) {
if (!Get.isRegistered<PermissionController>()) {
Get.put(PermissionController());
logSafe("✅ PermissionController injected after tenant switch.");
}
await Get.find<PermissionController>().loadData(token);
} else {
logSafe("⚠️ JWT token is null. Cannot load permissions.", level: LogLevel.warning);
}
// FULL NAVIGATION: reload app/dashboard
Get.offAllNamed('/dashboard');
showAppSnackbar(
title: "Success",
message: "Switched to organization: ${selectedTenant.name}",
type: SnackbarType.success,
);
} catch (e, st) {
logSafe("❌ Exception in switchTenant", level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred while switching organization.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
}

View File

@ -1,4 +1,4 @@
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
class ButtonsController extends MyController { class ButtonsController extends MyController {
List<bool> selected = List.filled(3, false); List<bool> selected = List.filled(3, false);

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:carousel_slider/carousel_controller.dart'; import 'package:carousel_slider/carousel_controller.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CarouselsController extends MyController { class CarouselsController extends MyController {

View File

@ -1,5 +1,5 @@
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/widgets/my_text_utils.dart'; import 'package:marco/helpers/widgets/my_text_utils.dart';
class DialogsController extends MyController { class DialogsController extends MyController {
List<String> dummyTexts = List<String> dummyTexts =

View File

@ -1,3 +1,3 @@
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
class LoadersController extends MyController {} class LoadersController extends MyController {}

View File

@ -1,5 +1,5 @@
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/widgets/my_text_utils.dart'; import 'package:marco/helpers/widgets/my_text_utils.dart';
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/extensions/string.dart'; import 'package:marco/helpers/extensions/string.dart';
import 'package:on_field_work/helpers/theme/admin_theme.dart'; import 'package:marco/helpers/theme/admin_theme.dart';
import 'package:on_field_work/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_button.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:flutter_lucide/flutter_lucide.dart';

View File

@ -1,5 +1,5 @@
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/helpers/widgets/my_text_utils.dart'; import 'package:marco/helpers/widgets/my_text_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class TabsController extends MyController { class TabsController extends MyController {

View File

@ -1,4 +1,4 @@
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ToastMessageController extends MyController { class ToastMessageController extends MyController {

View File

@ -1,5 +1,5 @@
import 'package:on_field_work/helpers/services/localizations/language.dart'; import 'package:marco/helpers/services/localizations/language.dart';
import 'package:on_field_work/helpers/theme/app_notifier.dart'; import 'package:marco/helpers/theme/app_notifier.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@ -1,5 +1,5 @@
import 'dart:ui'; import 'dart:ui';
import 'package:on_field_work/helpers/services/localizations/translator.dart'; import 'package:marco/helpers/services/localizations/translator.dart';
extension StringUtil on String { extension StringUtil on String {
Color get toColor { Color get toColor {

View File

@ -1,33 +1,13 @@
class ApiEndpoints { class ApiEndpoints {
static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api";
// static const String baseUrl = "https://ofwapi.marcoaiot.com/api";
static const String baseUrl = "https://api.onfieldwork.com/api";
// static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api";
// static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api";
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
// static const String baseUrl = "https://api.onfieldwork.com/api";
static const String getMasterCurrencies = "/Master/currencies/list";
static const String getMasterExpensesCategories =
"/Master/expenses-categories";
static const String getExpensePaymentRequestPayee =
"/Expense/payment-request/payee";
// Dashboard Module API Endpoints // Dashboard Module API Endpoints
static const String getDashboardAttendanceOverview = static const String getDashboardAttendanceOverview =
"/dashboard/attendance-overview"; "/dashboard/attendance-overview";
static const String createExpensePaymentRequest =
"/expense/payment-request/create";
static const String getExpensePaymentRequestList =
"/Expense/get/payment-requests/list";
static const String getExpensePaymentRequestDetails =
"/Expense/get/payment-request/details";
static const String getExpensePaymentRequestFilter =
"/Expense/payment-request/filter";
static const String updateExpensePaymentRequestStatus =
"/Expense/payment-request/action";
static const String createExpenseforPR = "/expense/payment-request/action";
static const String getExpensePaymentRequestEdit =
"/expense/payment-request/edit";
static const String getDashboardProjectProgress = "/dashboard/progression"; static const String getDashboardProjectProgress = "/dashboard/progression";
static const String getDashboardTasks = "/dashboard/tasks"; static const String getDashboardTasks = "/dashboard/tasks";
static const String getDashboardTeams = "/dashboard/teams"; static const String getDashboardTeams = "/dashboard/teams";
@ -36,10 +16,6 @@ class ApiEndpoints {
"/Dashboard/expense/monthly"; "/Dashboard/expense/monthly";
static const String getExpenseTypeReport = "/Dashboard/expense/type"; static const String getExpenseTypeReport = "/Dashboard/expense/type";
static const String getPendingExpenses = "/Dashboard/expense/pendings"; static const String getPendingExpenses = "/Dashboard/expense/pendings";
static const String getCollectionOverview = "/dashboard/collection-overview";
static const String getPurchaseInvoiceOverview =
"/dashboard/purchase-invoice-overview";
///// Projects Module API Endpoints ///// Projects Module API Endpoints
static const String createProject = "/project"; static const String createProject = "/project";
@ -48,7 +24,6 @@ class ApiEndpoints {
static const String getProjects = "/project/list"; static const String getProjects = "/project/list";
static const String getGlobalProjects = "/project/list/basic"; static const String getGlobalProjects = "/project/list/basic";
static const String getTodaysAttendance = "/attendance/project/team"; static const String getTodaysAttendance = "/attendance/project/team";
static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId";
static const String getAttendanceLogs = "/attendance/project/log"; static const String getAttendanceLogs = "/attendance/project/log";
static const String getAttendanceLogView = "/attendance/log/attendance"; static const String getAttendanceLogView = "/attendance/log/attendance";
static const String getRegularizationLogs = "/attendance/regularize"; static const String getRegularizationLogs = "/attendance/regularize";
@ -56,7 +31,6 @@ class ApiEndpoints {
// Employee Screen API Endpoints // Employee Screen API Endpoints
static const String getAllEmployeesByProject = "/employee/list"; static const String getAllEmployeesByProject = "/employee/list";
static const String getAllEmployeesByOrganization = "/project/get/task/team";
static const String getAllEmployees = "/employee/list"; static const String getAllEmployees = "/employee/list";
static const String getEmployeesWithoutPermission = "/employee/basic"; static const String getEmployeesWithoutPermission = "/employee/basic";
static const String getRoles = "/roles/jobrole"; static const String getRoles = "/roles/jobrole";
@ -76,7 +50,6 @@ class ApiEndpoints {
static const String approveReportAction = "/task/approve"; static const String approveReportAction = "/task/approve";
static const String assignTask = "/project/task"; static const String assignTask = "/project/task";
static const String getmasterWorkCategories = "/Master/work-categories"; static const String getmasterWorkCategories = "/Master/work-categories";
static const String getDailyTaskProjectProgressFilter = "/task/filter";
////// Directory Module API Endpoints /////// ////// Directory Module API Endpoints ///////
static const String getDirectoryContacts = "/directory"; static const String getDirectoryContacts = "/directory";
@ -88,8 +61,6 @@ class ApiEndpoints {
static const String getDirectoryOrganization = "/directory/organization"; static const String getDirectoryOrganization = "/directory/organization";
static const String createContact = "/directory"; static const String createContact = "/directory";
static const String updateContact = "/directory"; static const String updateContact = "/directory";
static const String deleteContact = "/directory";
static const String restoreContact = "/directory/note";
static const String getDirectoryNotes = "/directory/notes"; static const String getDirectoryNotes = "/directory/notes";
static const String updateDirectoryNotes = "/directory/note"; static const String updateDirectoryNotes = "/directory/note";
static const String createBucket = "/directory/bucket"; static const String createBucket = "/directory/bucket";
@ -104,7 +75,7 @@ class ApiEndpoints {
static const String editExpense = "/Expense/edit"; static const String editExpense = "/Expense/edit";
static const String getMasterPaymentModes = "/master/payment-modes"; static const String getMasterPaymentModes = "/master/payment-modes";
static const String getMasterExpenseStatus = "/master/expenses-status"; static const String getMasterExpenseStatus = "/master/expenses-status";
static const String getMasterExpenseCategory = "/master/expenses-categories"; static const String getMasterExpenseTypes = "/master/expenses-types";
static const String updateExpenseStatus = "/expense/action"; static const String updateExpenseStatus = "/expense/action";
static const String deleteExpense = "/expense/delete"; static const String deleteExpense = "/expense/delete";
@ -131,40 +102,5 @@ class ApiEndpoints {
static const String getAssignedOrganizations = static const String getAssignedOrganizations =
"/project/get/assigned/organization"; "/project/get/assigned/organization";
static const getAllOrganizations = "/organization/list";
static const String getAssignedServices = "/Project/get/assigned/services"; static const String getAssignedServices = "/Project/get/assigned/services";
static const String getAdvancePayments = '/Expense/get/transactions';
// Organization Hierarchy endpoints
static const String getOrganizationHierarchyList =
"/organization/hierarchy/list";
static const String manageOrganizationHierarchy =
"/organization/hierarchy/manage";
// Service Project Module API Endpoints
static const String getServiceProjectsList = "/serviceproject/list";
static const String getServiceProjectDetail = "/serviceproject/details";
static const String getServiceProjectJobList = "/serviceproject/job/list";
static const String getServiceProjectJobDetail =
"/serviceproject/job/details";
static const String editServiceProjectJob = "/serviceproject/job/edit";
static const String createServiceProjectJob = "/serviceproject/job/create";
static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance";
static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log";
static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list";
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation";
static const String getTeamRoles = "/master/team-roles/list";
static const String getServiceProjectBranches = "/serviceproject/branch/list";
static const String getMasterJobStatus = "/Master/job-status/list";
static const String addJobComment = "/ServiceProject/job/add/comment";
static const String getJobCommentList = "/ServiceProject/job/comment/list";
// Infra Project Module API Endpoints
static const String getInfraProjectsList = "/project/list";
static const String getInfraProjectDetail = "/project/details";
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,17 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:url_strategy/url_strategy.dart'; import 'package:url_strategy/url_strategy.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/device_info_service.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/theme/app_theme.dart'; import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:marco/helpers/services/device_info_service.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/theme/app_theme.dart';
Future<void> initializeApp() async { Future<void> initializeApp() async {
try { try {
@ -22,6 +26,7 @@ Future<void> initializeApp() async {
await _setupDeviceInfo(); await _setupDeviceInfo();
await _handleAuthTokens(); await _handleAuthTokens();
await _setupTheme(); await _setupTheme();
await _setupControllers();
await _setupFirebaseMessaging(); await _setupFirebaseMessaging();
_finalizeAppStyle(); _finalizeAppStyle();
@ -38,19 +43,6 @@ Future<void> initializeApp() async {
} }
} }
Future<void> _handleAuthTokens() async {
final refreshToken = await LocalStorage.getRefreshToken();
if (refreshToken?.isNotEmpty ?? false) {
logSafe("🔁 Refresh token found. Attempting to refresh JWT...");
final success = await AuthService.refreshToken();
if (!success) {
logSafe("⚠️ Refresh token invalid or expired. User must login again.");
}
} else {
logSafe("❌ No refresh token found. Skipping refresh.");
}
}
Future<void> _setupUI() async { Future<void> _setupUI() async {
setPathUrlStrategy(); setPathUrlStrategy();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
@ -77,11 +69,50 @@ Future<void> _setupDeviceInfo() async {
logSafe("📱 Device Info: ${deviceInfoService.deviceData}"); logSafe("📱 Device Info: ${deviceInfoService.deviceData}");
} }
Future<void> _handleAuthTokens() async {
final refreshToken = await LocalStorage.getRefreshToken();
if (refreshToken?.isNotEmpty ?? false) {
logSafe("🔁 Refresh token found. Attempting to refresh JWT...");
final success = await AuthService.refreshToken();
if (!success) {
logSafe(
"⚠️ Refresh token invalid or expired. Skipping controller injection.");
}
} else {
logSafe("❌ No refresh token found. Skipping refresh.");
}
}
Future<void> _setupTheme() async { Future<void> _setupTheme() async {
await ThemeCustomizer.init(); await ThemeCustomizer.init();
logSafe("💡 Theme customizer initialized."); logSafe("💡 Theme customizer initialized.");
} }
Future<void> _setupControllers() async {
final token = LocalStorage.getString('jwt_token');
if (token?.isEmpty ?? true) {
logSafe("⚠️ No valid JWT token found. Skipping controller initialization.");
return;
}
if (!Get.isRegistered<PermissionController>()) {
Get.put(PermissionController());
logSafe("💡 PermissionController injected.");
}
if (!Get.isRegistered<ProjectController>()) {
Get.put(ProjectController(), permanent: true);
logSafe("💡 ProjectController injected as permanent.");
}
await Future.wait([
Get.find<PermissionController>().loadData(token!),
Get.find<ProjectController>().fetchProjects(),
]);
}
// Commented out Firebase Messaging setup
Future<void> _setupFirebaseMessaging() async { Future<void> _setupFirebaseMessaging() async {
await FirebaseNotificationService().initialize(); await FirebaseNotificationService().initialize();
logSafe("💡 Firebase Messaging initialized."); logSafe("💡 Firebase Messaging initialized.");

View File

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
/// Global logger instance /// Global logger instance
Logger? _appLogger; Logger? _appLogger;

View File

@ -1,8 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:on_field_work/helpers/services/api_endpoints.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:on_field_work/helpers/services/app_logger.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';
import 'package:marco/helpers/services/app_logger.dart';
class AuthService { class AuthService {
static const String _baseUrl = ApiEndpoints.baseUrl; static const String _baseUrl = ApiEndpoints.baseUrl;
@ -79,7 +83,7 @@ class AuthService {
logSafe("Login payload (raw): $data"); logSafe("Login payload (raw): $data");
logSafe("Login payload (JSON): ${jsonEncode(data)}"); logSafe("Login payload (JSON): ${jsonEncode(data)}");
final responseData = await _post("/auth/app/login", data); final responseData = await _post("/auth/login-mobile", data);
if (responseData == null) if (responseData == null)
return {"error": "Network error. Please check your connection."}; return {"error": "Network error. Please check your connection."};
@ -94,8 +98,8 @@ class AuthService {
} }
static Future<bool> refreshToken() async { static Future<bool> refreshToken() async {
final accessToken = LocalStorage.getJwtToken(); final accessToken = await LocalStorage.getJwtToken();
final refreshToken = LocalStorage.getRefreshToken(); final refreshToken = await LocalStorage.getRefreshToken();
if ([accessToken, refreshToken].any((t) => t == null || t.isEmpty)) { if ([accessToken, refreshToken].any((t) => t == null || t.isEmpty)) {
logSafe("Missing access or refresh token.", level: LogLevel.warning); logSafe("Missing access or refresh token.", level: LogLevel.warning);
@ -111,7 +115,7 @@ class AuthService {
logSafe("Token refreshed successfully."); logSafe("Token refreshed successfully.");
// 🔹 Retry FCM token registration after token refresh // 🔹 Retry FCM token registration after token refresh
final newFcmToken = LocalStorage.getFcmToken(); final newFcmToken = await LocalStorage.getFcmToken();
if (newFcmToken?.isNotEmpty ?? false) { if (newFcmToken?.isNotEmpty ?? false) {
final success = await registerDeviceToken(newFcmToken!); final success = await registerDeviceToken(newFcmToken!);
logSafe( logSafe(
@ -153,7 +157,7 @@ class AuthService {
}) => }) =>
_wrapErrorHandling( _wrapErrorHandling(
() async { () async {
final token = LocalStorage.getJwtToken(); final token = await LocalStorage.getJwtToken();
return _post( return _post(
"/auth/generate-mpin", "/auth/generate-mpin",
{"employeeId": employeeId, "mpin": mpin}, {"employeeId": employeeId, "mpin": mpin},
@ -175,7 +179,7 @@ class AuthService {
if (employeeInfo == null) return null; if (employeeInfo == null) return null;
final token = await LocalStorage.getJwtToken(); final token = await LocalStorage.getJwtToken();
return _post( return _post(
"/auth/login-mpin", "/auth/login-mpin/v1",
{ {
"employeeId": employeeInfo.id, "employeeId": employeeInfo.id,
"mpin": mpin, "mpin": mpin,
@ -198,7 +202,7 @@ class AuthService {
required String email, required String email,
required String otp, required String otp,
}) async { }) async {
final data = await _post("/auth/login-otp", {"email": email, "otp": otp}); final data = await _post("/auth/login-otp/v1", {"email": email, "otp": otp});
if (data != null && data['data'] != null) { if (data != null && data['data'] != null) {
await _handleLoginSuccess(data['data']); await _handleLoginSuccess(data['data']);
return null; return null;
@ -286,6 +290,30 @@ class AuthService {
await LocalStorage.setIsMpin(false); await LocalStorage.setIsMpin(false);
await LocalStorage.removeMpinToken(); await LocalStorage.removeMpinToken();
} }
if (!Get.isRegistered<PermissionController>()) {
Get.put(PermissionController());
logSafe("✅ PermissionController injected after login.");
}
if (!Get.isRegistered<ProjectController>()) {
Get.put(ProjectController(), permanent: true);
logSafe("✅ ProjectController injected after login.");
}
await Get.find<PermissionController>().loadData(data['token']);
await Get.find<ProjectController>().fetchProjects();
// 🔹 Always try to register FCM token after login
final fcmToken = await LocalStorage.getFcmToken();
if (fcmToken?.isNotEmpty ?? false) {
final success = await registerDeviceToken(fcmToken!);
logSafe(
success
? "✅ FCM token registered after login."
: "⚠️ Failed to register FCM token after login.",
level: success ? LogLevel.info : LogLevel.warning);
}
isLoggedIn = true; isLoggedIn = true;
logSafe("✅ Login flow completed and controllers initialized."); logSafe("✅ Login flow completed and controllers initialized.");
} }

View File

@ -1,10 +1,10 @@
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:on_field_work/helpers/services/local_notification_service.dart'; import 'package:marco/helpers/services/local_notification_service.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/notification_action_handler.dart'; import 'package:marco/helpers/services/notification_action_handler.dart';
/// Firebase Notification Service /// Firebase Notification Service
class FirebaseNotificationService { class FirebaseNotificationService {
@ -19,7 +19,7 @@ class FirebaseNotificationService {
_registerMessageListeners(); _registerMessageListeners();
_registerTokenRefreshListener(); _registerTokenRefreshListener();
// Fetch token on app start (and register with server if JWT available) // Fetch token on app start (but only register with server if JWT available)
await getFcmToken(registerOnServer: true); await getFcmToken(registerOnServer: true);
} }
@ -49,7 +49,6 @@ class FirebaseNotificationService {
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap); FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap);
// Background messages
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
} }
@ -112,6 +111,8 @@ class FirebaseNotificationService {
} }
} }
/// Handle tap on notification /// Handle tap on notification
void _handleNotificationTap(RemoteMessage message) { void _handleNotificationTap(RemoteMessage message) {
_logger.i('📌 Notification tapped: ${message.data}'); _logger.i('📌 Notification tapped: ${message.data}');
@ -128,9 +129,7 @@ class FirebaseNotificationService {
} }
} }
/// 🔹 Background handler (required by Firebase) /// Background handler (required by Firebase)
/// Must be a top-level function and annotated for AOT
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
final logger = Logger(); final logger = Logger();
logger logger

View File

@ -1,5 +1,5 @@
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Language { class Language {

View File

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:on_field_work/helpers/services/localizations/language.dart'; import 'package:marco/helpers/services/localizations/language.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get_utils/src/extensions/string_extensions.dart'; import 'package:get/get_utils/src/extensions/string_extensions.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';

View File

@ -1,17 +1,14 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/controller/expense/expense_screen_controller.dart';
import 'package:on_field_work/controller/task_planning/daily_task_controller.dart'; import 'package:marco/controller/expense/expense_detail_controller.dart';
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/controller/directory/directory_controller.dart';
import 'package:on_field_work/controller/expense/expense_screen_controller.dart'; import 'package:marco/controller/directory/notes_controller.dart';
import 'package:on_field_work/controller/expense/expense_detail_controller.dart'; import 'package:marco/controller/document/user_document_controller.dart';
import 'package:on_field_work/controller/directory/directory_controller.dart'; import 'package:marco/controller/document/document_details_controller.dart';
import 'package:on_field_work/controller/directory/notes_controller.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/controller/document/user_document_controller.dart'; import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:on_field_work/controller/document/document_details_controller.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
/// Handles incoming FCM notification actions and updates UI/controllers. /// Handles incoming FCM notification actions and updates UI/controllers.
class NotificationActionHandler { class NotificationActionHandler {
@ -71,26 +68,6 @@ class NotificationActionHandler {
case 'Team_Modified': case 'Team_Modified':
_handleDashboardUpdate(data); _handleDashboardUpdate(data);
break; break;
/// 🔹 Tasks
case 'Report_Task':
_handleTaskUpdated(data, isComment: false);
_handleDashboardUpdate(data);
break;
case 'Task_Comment':
_handleTaskUpdated(data, isComment: true);
_handleDashboardUpdate(data);
break;
case 'Task_Modified':
case 'WorkArea_Modified':
case 'Floor_Modified':
case 'Building_Modified':
_handleTaskPlanningUpdated(data);
_handleDashboardUpdate(data);
break;
/// 🔹 Expenses /// 🔹 Expenses
case 'Expenses_Modified': case 'Expenses_Modified':
_handleExpenseUpdated(data); _handleExpenseUpdated(data);
@ -127,28 +104,6 @@ class NotificationActionHandler {
/// ---------------------- HANDLERS ---------------------- /// ---------------------- HANDLERS ----------------------
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored task planning update from another project.");
return;
}
final projectId = data['ProjectId'];
if (projectId == null) {
_logger.w("⚠️ TaskPlanning update received without ProjectId: $data");
return;
}
_safeControllerUpdate<DailyTaskPlanningController>(
onFound: (controller) {
controller.fetchTaskData(projectId);
},
notFoundMessage:
'⚠️ DailyTaskPlanningController not found, cannot refresh.',
successMessage:
'✅ DailyTaskPlanningController refreshed from notification.',
);
}
static bool _isAttendanceAction(String? action) { static bool _isAttendanceAction(String? action) {
const validActions = { const validActions = {
@ -211,22 +166,6 @@ class NotificationActionHandler {
); );
} }
static void _handleTaskUpdated(Map<String, dynamic> data,
{required bool isComment}) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored task update from another project.");
return;
}
_safeControllerUpdate<DailyTaskController>(
onFound: (controller) => controller.refreshTasksFromNotification(
projectId: data['ProjectId'],
taskAllocationId: data['TaskAllocationId'],
),
notFoundMessage: '⚠️ DailyTaskController not found, cannot update.',
successMessage: '✅ DailyTaskController refreshed from notification.',
);
}
/// ---------------------- DOCUMENT HANDLER ---------------------- /// ---------------------- DOCUMENT HANDLER ----------------------
static void _handleDocumentModified(Map<String, dynamic> data) { static void _handleDocumentModified(Map<String, dynamic> data) {
@ -414,17 +353,12 @@ class NotificationActionHandler {
required String notFoundMessage, required String notFoundMessage,
required String successMessage, required String successMessage,
}) { }) {
if (!Get.isRegistered<T>()) {
_logger.w(notFoundMessage);
return;
}
try { try {
final controller = Get.find<T>(); final controller = Get.find<T>();
onFound(controller); onFound(controller);
_logger.i(successMessage); _logger.i(successMessage);
} catch (e) { } catch (e) {
_logger.w('⚠️ Error updating controller: $e'); _logger.w(notFoundMessage);
} }
} }
} }

View File

@ -2,13 +2,13 @@ import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/model/user_permission.dart'; import 'package:marco/model/user_permission.dart';
import 'package:on_field_work/model/employees/employee_info.dart'; import 'package:marco/model/employees/employee_info.dart';
import 'package:on_field_work/model/projects_model.dart'; import 'package:marco/model/projects_model.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/api_endpoints.dart';
class PermissionService { class PermissionService {
// In-memory cache keyed by user token // In-memory cache keyed by user token

View File

@ -2,13 +2,13 @@ import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:on_field_work/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/localizations/language.dart'; import 'package:marco/helpers/services/localizations/language.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:on_field_work/model/employees/employee_info.dart'; import 'package:marco/model/employees/employee_info.dart';
import 'package:on_field_work/model/user_permission.dart'; import 'package:marco/model/user_permission.dart';
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart'; import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
class LocalStorage { class LocalStorage {
static const String _loggedInUserKey = "user"; static const String _loggedInUserKey = "user";

View File

@ -1,173 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:get/get.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/helpers/services/api_endpoints.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
/// Abstract interface for tenant service functionality
abstract class ITenantService {
Future<List<Map<String, dynamic>>?> getTenants({bool hasRetried = false});
Future<bool> selectTenant(String tenantId, {bool hasRetried = false});
}
/// Tenant API service
class TenantService implements ITenantService {
static const String _baseUrl = ApiEndpoints.baseUrl;
static const Map<String, String> _headers = {
'Content-Type': 'application/json',
};
/// Currently selected tenant
static Tenant? currentTenant;
/// Set the selected tenant
static void setSelectedTenant(Tenant tenant) {
currentTenant = tenant;
}
/// Check if tenant is selected
static bool get isTenantSelected => currentTenant != null;
/// Build authorized headers
static Future<Map<String, String>> _authorizedHeaders() async {
final token = await LocalStorage.getJwtToken();
if (token == null || token.isEmpty) {
throw Exception('Missing JWT token');
}
return {..._headers, 'Authorization': 'Bearer $token'};
}
/// Handle API errors
static void _handleApiError(
http.Response response, dynamic data, String context) {
final message = data['message'] ?? 'Unknown error';
final level =
response.statusCode >= 500 ? LogLevel.error : LogLevel.warning;
logSafe("$context failed: $message [Status: ${response.statusCode}]",
level: level);
}
/// Log exceptions
static void _logException(dynamic e, dynamic st, String context) {
logSafe("$context exception",
level: LogLevel.error, error: e, stackTrace: st);
}
@override
Future<List<Map<String, dynamic>>?> getTenants(
{bool hasRetried = false}) async {
try {
final headers = await _authorizedHeaders();
final response = await http.get(
Uri.parse("$_baseUrl/auth/get/user/tenants"),
headers: headers,
);
// Handle empty response BEFORE decoding
if (response.body.isEmpty || response.body.trim().isEmpty) {
logSafe("❌ Empty tenant response — auto logout");
await LocalStorage.logout();
return null;
}
Map<String, dynamic> data;
try {
data = jsonDecode(response.body);
} catch (e) {
logSafe("❌ Invalid JSON in tenant response — auto logout");
await LocalStorage.logout();
return null;
}
// SUCCESS CASE
if (response.statusCode == 200 && data['success'] == true) {
final list = data['data'];
if (list is! List) return null;
return List<Map<String, dynamic>>.from(list);
}
// TOKEN EXPIRED
if (response.statusCode == 401 && !hasRetried) {
final refreshed = await AuthService.refreshToken();
if (refreshed) return getTenants(hasRetried: true);
return null;
}
_handleApiError(response, data, "Fetching tenants");
return null;
} catch (e, st) {
_logException(e, st, "Get Tenants API");
return null;
}
}
@override
Future<bool> selectTenant(String tenantId, {bool hasRetried = false}) async {
try {
final headers = await _authorizedHeaders();
logSafe(
"➡️ POST $_baseUrl/auth/select-tenant/$tenantId\nHeaders: $headers",
level: LogLevel.info);
final response = await http.post(
Uri.parse("$_baseUrl/auth/select-tenant/$tenantId"),
headers: headers,
);
final data = jsonDecode(response.body);
logSafe(
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
level: LogLevel.info);
if (response.statusCode == 200 && data['success'] == true) {
await LocalStorage.setJwtToken(data['data']['token']);
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
logSafe("✅ Tenant selected successfully. Tokens updated.");
// 🔥 Refresh projects when tenant changes
try {
final projectController = Get.find<ProjectController>();
projectController.clearProjects();
projectController.fetchProjects();
} catch (_) {
logSafe("⚠️ ProjectController not found while refreshing projects");
}
// 🔹 Register FCM token after tenant selection
final fcmToken = LocalStorage.getFcmToken();
if (fcmToken?.isNotEmpty ?? false) {
final success = await AuthService.registerDeviceToken(fcmToken!);
logSafe(
success
? "✅ FCM token registered after tenant selection."
: "⚠️ Failed to register FCM token after tenant selection.",
level: success ? LogLevel.info : LogLevel.warning);
}
return true;
}
if (response.statusCode == 401 && !hasRetried) {
logSafe("⚠️ Unauthorized while selecting tenant. Refreshing token...",
level: LogLevel.warning);
final refreshed = await AuthService.refreshToken();
if (refreshed) return selectTenant(tenantId, hasRetried: true);
logSafe("❌ Token refresh failed while selecting tenant.",
level: LogLevel.error);
return false;
}
_handleApiError(response, data, "Selecting tenant");
return false;
} catch (e, st) {
_logException(e, st, "Select Tenant API");
return false;
}
}
}

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
enum LeftBarThemeType { light, dark } enum LeftBarThemeType { light, dark }
enum ContentThemeType { light, dark } enum ContentThemeType { light, dark }
@ -266,7 +266,7 @@ class AdminTheme {
leftBarTheme: LeftBarTheme.lightLeftBarTheme, leftBarTheme: LeftBarTheme.lightLeftBarTheme,
topBarTheme: TopBarTheme.lightTopBarTheme, topBarTheme: TopBarTheme.lightTopBarTheme,
rightBarTheme: RightBarTheme.lightRightBarTheme, rightBarTheme: RightBarTheme.lightRightBarTheme,
contentTheme: ContentTheme.withColorTheme(ColorThemeType.purple, mode: ThemeMode.light), contentTheme: ContentTheme.withColorTheme(ColorThemeType.green, mode: ThemeMode.light),
); );
static void setTheme() { static void setTheme() {

View File

@ -1,8 +1,8 @@
import 'package:on_field_work/helpers/services/localizations/language.dart'; import 'package:marco/helpers/services/localizations/language.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/theme/app_theme.dart'; import 'package:marco/helpers/theme/app_theme.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:on_field_work/helpers/widgets/my.dart'; import 'package:marco/helpers/widgets/my.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';

View File

@ -6,13 +6,13 @@
* */ * */
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:on_field_work/helpers/theme/admin_theme.dart'; import 'package:marco/helpers/theme/admin_theme.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:on_field_work/helpers/widgets/my.dart'; import 'package:marco/helpers/widgets/my.dart';
import 'package:on_field_work/helpers/widgets/my_breadcrumb_item.dart'; import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:on_field_work/helpers/widgets/my_constant.dart'; import 'package:marco/helpers/widgets/my_constant.dart';
import 'package:on_field_work/helpers/widgets/my_screen_media.dart'; import 'package:marco/helpers/widgets/my_screen_media.dart';
import 'package:on_field_work/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@ -230,7 +230,7 @@ class AppStyle {
containerRadius: AppStyle.containerRadius.medium, containerRadius: AppStyle.containerRadius.medium,
cardRadius: AppStyle.cardRadius.medium, cardRadius: AppStyle.cardRadius.medium,
buttonRadius: AppStyle.buttonRadius.medium, buttonRadius: AppStyle.buttonRadius.medium,
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'On Field Work', route: '/client/dashboard'), defaultBreadCrumbItem: MyBreadcrumbItem(name: 'Marco', route: '/client/home'),
)); ));
bool isMobile = true; bool isMobile = true;
try { try {

View File

@ -1,12 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:on_field_work/helpers/services/json_decoder.dart'; import 'package:marco/helpers/services/json_decoder.dart';
import 'package:on_field_work/helpers/services/localizations/language.dart'; import 'package:marco/helpers/services/localizations/language.dart';
import 'package:on_field_work/helpers/services/localizations/translator.dart'; import 'package:marco/helpers/services/localizations/translator.dart';
import 'package:on_field_work/helpers/services/navigation_services.dart'; import 'package:marco/helpers/services/navigation_services.dart';
import 'package:on_field_work/helpers/theme/admin_theme.dart'; import 'package:marco/helpers/theme/admin_theme.dart';
import 'package:on_field_work/helpers/theme/app_notifier.dart'; import 'package:marco/helpers/theme/app_notifier.dart';
import 'package:on_field_work/helpers/theme/app_theme.dart'; import 'package:marco/helpers/theme/app_theme.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -24,7 +24,7 @@ class ThemeCustomizer {
ThemeMode leftBarTheme = ThemeMode.light; ThemeMode leftBarTheme = ThemeMode.light;
ThemeMode rightBarTheme = ThemeMode.light; ThemeMode rightBarTheme = ThemeMode.light;
ThemeMode topBarTheme = ThemeMode.light; ThemeMode topBarTheme = ThemeMode.light;
ColorThemeType colorTheme = ColorThemeType.red; ColorThemeType colorTheme = ColorThemeType.green;
bool rightBarOpen = false; bool rightBarOpen = false;
bool leftBarCondensed = false; bool leftBarCondensed = false;
@ -34,7 +34,7 @@ class ThemeCustomizer {
static Future<void> init() async { static Future<void> init() async {
await initLanguage(); await initLanguage();
await _loadColorTheme(); await _loadColorTheme();
_notify(); _notify();
} }
static initLanguage() async { static initLanguage() async {

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/widgets/wave_background.dart'; import 'package:marco/helpers/widgets/wave_background.dart';
import 'package:on_field_work/helpers/theme/admin_theme.dart'; import 'package:marco/helpers/theme/admin_theme.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
class ThemeOption { class ThemeOption {
final String label; final String label;
@ -63,9 +63,6 @@ class ThemeController extends GetxController {
await Future.delayed(const Duration(milliseconds: 600)); await Future.delayed(const Duration(milliseconds: 600));
showApplied.value = false; showApplied.value = false;
// Navigate to dashboard after applying theme
Get.offAllNamed('/dashboard');
} }
} }

View File

@ -95,7 +95,7 @@ class AttendanceButtonHelper {
} }
} }
static Color getprimary({ static Color getButtonColor({
required bool isYesterday, required bool isYesterday,
required bool isTodayApproved, required bool isTodayApproved,
required int activity, required int activity,

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class BaseBottomSheet extends StatefulWidget { class BaseBottomSheet extends StatefulWidget {
final String title; final String title;

Some files were not shown because too many files have changed in this diff Show More