Compare commits

..

1 Commits

Author SHA1 Message Date
878e9a82ea Added Material Requisition form and related updates 2025-10-14 15:53:39 +05:30
313 changed files with 11199 additions and 33223 deletions

View File

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

View File

@ -15,7 +15,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
// Define the namespace for your Android application // Define the namespace for your Android application
namespace = "com.marcoonfieldwork.aiot" namespace = "com.marco.aiot"
// Set the compile SDK version based on Flutter's configuration // Set the compile SDK version based on Flutter's configuration
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
// Set the NDK version based on Flutter's configuration // Set the NDK version based on Flutter's configuration
@ -37,7 +37,7 @@ android {
// Default configuration for your application // Default configuration for your application
defaultConfig { defaultConfig {
// Specify your unique Application ID. This identifies your app on Google Play. // Specify your unique Application ID. This identifies your app on Google Play.
applicationId = "com.marcoonfieldwork.aiot" applicationId = "com.marco.aiot"
// Set minimum and target SDK versions based on Flutter's configuration // Set minimum and target SDK versions based on Flutter's configuration
minSdk = 23 minSdk = 23
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion

View File

@ -9,7 +9,7 @@
"client_info": { "client_info": {
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024", "mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
"android_client_info": { "android_client_info": {
"package_name": "com.marcoonfieldwork.aiot" "package_name": "com.marco.aiot"
} }
}, },
"oauth_client": [], "oauth_client": [],

View File

@ -8,7 +8,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application <application
android:label="On Field Work" android:label="Marco"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

View File

@ -1,4 +1,4 @@
package com.marcoonfieldwork.aiot package com.marco.aiot
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

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
} }

View File

@ -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}"

View File

@ -368,7 +368,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot; PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -547,7 +547,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot; PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -569,7 +569,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot; PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

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,67 +1,60 @@
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:on_field_work/helpers/widgets/my_image_compressor.dart'; import 'package:marco/model/attendance/attendance_model.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart'; import 'package:marco/model/project_model.dart';
import 'package:on_field_work/model/attendance/attendance_log_model.dart'; import 'package:marco/model/employees/employee_model.dart';
import 'package:on_field_work/model/attendance/attendance_log_view_model.dart'; import 'package:marco/model/attendance/attendance_log_model.dart';
import 'package:on_field_work/model/attendance/attendance_model.dart'; import 'package:marco/model/regularization_log_model.dart';
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/attendance/attendance_log_view_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart';
import 'package:on_field_work/model/project_model.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:on_field_work/model/regularization_log_model.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';
DateTime? startDateAttendance;
DateTime? endDateAttendance;
// Reactive date range final isLoading = true.obs;
final Rx<DateTime> startDateAttendance = final isLoadingProjects = true.obs;
DateTime.now().subtract(const Duration(days: 7)).obs; final isLoadingEmployees = true.obs;
final Rx<DateTime> endDateAttendance = final isLoadingAttendanceLogs = true.obs;
DateTime.now().subtract(const Duration(days: 1)).obs; final isLoadingRegularizationLogs = true.obs;
final isLoadingLogView = true.obs;
final RxBool isLoading = true.obs; final uploadingStates = <String, RxBool>{}.obs;
final RxBool isLoadingProjects = true.obs; var showPendingOnly = false.obs;
final RxBool isLoadingEmployees = true.obs;
final RxBool isLoadingAttendanceLogs = true.obs;
final RxBool isLoadingRegularizationLogs = true.obs;
final RxBool isLoadingLogView = true.obs;
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
final RxBool showPendingOnly = false.obs;
final RxString searchQuery = ''.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initializeDefaults(); _initializeDefaults();
// 🔹 Fetch organizations for the selected project
final projectId = Get.find<ProjectController>().selectedProject?.id;
if (projectId != null) {
fetchOrganizations(projectId);
}
} }
void _initializeDefaults() { void _initializeDefaults() {
@ -69,60 +62,55 @@ 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 = today.subtract(const Duration(days: 7));
endDateAttendance.value = today.subtract(const Duration(days: 1)); endDateAttendance = today.subtract(const Duration(days: 1));
logSafe( logSafe(
'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}', "Default date range set: $startDateAttendance to $endDateAttendance");
);
} }
// ------------------ Computed Filters ------------------ // ------------------ Project & Employee ------------------
List<EmployeeModel> get filteredEmployees { /// Called when a notification says attendance has been updated
final String query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return employees;
return employees
.where(
(EmployeeModel e) => e.name.toLowerCase().contains(query),
)
.toList();
}
List<AttendanceLogModel> get filteredLogs {
final String query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return attendanceLogs;
return attendanceLogs
.where(
(AttendanceLogModel log) => log.name.toLowerCase().contains(query),
)
.toList();
}
List<RegularizationLogModel> get filteredRegularizationLogs {
final String query = searchQuery.value.trim().toLowerCase();
if (query.isEmpty) return regularizationLogs;
return regularizationLogs
.where(
(RegularizationLogModel log) =>
log.name.toLowerCase().contains(query),
)
.toList();
}
// ------------------ Project & Employee APIs ------------------
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");
); }
// 🔍 Search query
final searchQuery = ''.obs;
// Computed filtered employees
List<EmployeeModel> get filteredEmployees {
if (searchQuery.value.isEmpty) return employees;
return employees
.where((e) =>
e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
// Computed filtered logs
List<AttendanceLogModel> get filteredLogs {
if (searchQuery.value.isEmpty) return attendanceLogs;
return attendanceLogs
.where((log) =>
(log.name).toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
// Computed filtered regularization logs
List<RegularizationLogModel> get filteredRegularizationLogs {
if (searchQuery.value.isEmpty) return regularizationLogs;
return regularizationLogs
.where((log) =>
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
} }
Future<void> fetchTodaysAttendance(String? projectId) async { Future<void> fetchTodaysAttendance(String? projectId) async {
@ -130,105 +118,105 @@ 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;
update(); update();
} }
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();
} }
// ------------------ Attendance Capture ------------------ // ------------------ Attendance Capture ------------------
Future<bool> captureAndUploadAttendance( Future<bool> captureAndUploadAttendance(
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, // still optional in controller
String? date, String? date, // new optional param
}) 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 compressedBytes =
await compressImageToUnder100KB(File(image.path));
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 = // ---------------- DATE / TIME LOGIC ----------------
_resolveEffectiveDateForAction(action, employeeId); final now = DateTime.now();
final DateTime now = DateTime.now(); // Default effectiveDate = now
final String formattedMarkTime = DateTime effectiveDate = now;
markTime ?? DateFormat('hh:mm a').format(now);
final String formattedDate = if (action == 1) {
// Checkout
// Try to find today's open log for this employee
final log = attendanceLogs.firstWhereOrNull(
(log) => log.employeeId == employeeId && log.checkOut == null,
);
if (log?.checkIn != null) {
effectiveDate = log!.checkIn!; // use check-in date
}
}
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( // ---------------- API CALL ----------------
final result = await ApiService.uploadAttendanceImage(
id, id,
employeeId, employeeId,
image, image,
@ -243,99 +231,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 +249,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 +264,26 @@ class AttendanceController extends GetxController {
} }
// ------------------ Attendance Logs ------------------ // ------------------ Attendance Logs ------------------
Future<void> fetchAttendanceLogs(
String? projectId, { Future<void> fetchAttendanceLogs(String? projectId,
DateTime? dateFrom, {DateTime? dateFrom, DateTime? dateTo}) async {
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,70 +291,45 @@ 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 ------------------
Future<void> fetchRegularizationLogs(String? projectId) async { Future<void> fetchRegularizationLogs(String? projectId) async {
if (projectId == null) return; if (projectId == null) return;
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;
@ -477,38 +337,22 @@ class AttendanceController extends GetxController {
} }
// ------------------ Attendance Log View ------------------ // ------------------ Attendance Log View ------------------
Future<void> fetchLogsView(String? id) async { Future<void> fetchLogsView(String? id) async {
if (id == null) return; if (id == null) return;
isLoadingLogView.value = true; isLoadingLogView.value = true;
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;
@ -516,6 +360,7 @@ class AttendanceController extends GetxController {
} }
// ------------------ Combined Load ------------------ // ------------------ Combined Load ------------------
Future<void> loadAttendanceData(String projectId) async { Future<void> loadAttendanceData(String projectId) async {
isLoading.value = true; isLoading.value = true;
await fetchProjectData(projectId); await fetchProjectData(projectId);
@ -527,6 +372,7 @@ class AttendanceController extends GetxController {
await fetchOrganizations(projectId); await fetchOrganizations(projectId);
// Call APIs depending on the selected tab only
switch (selectedTab) { switch (selectedTab) {
case 'todaysAttendance': case 'todaysAttendance':
await fetchTodaysAttendance(projectId); await fetchTodaysAttendance(projectId);
@ -534,8 +380,8 @@ class AttendanceController extends GetxController {
case 'attendanceLogs': case 'attendanceLogs':
await fetchAttendanceLogs( await fetchAttendanceLogs(
projectId, projectId,
dateFrom: startDateAttendance.value, dateFrom: startDateAttendance,
dateTo: endDateAttendance.value, dateTo: endDateAttendance,
); );
break; break;
case 'regularizationRequests': case 'regularizationRequests':
@ -544,35 +390,31 @@ 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(
BuildContext context,
AttendanceController controller,
) async {
final DateTime today = DateTime.now();
final DateTimeRange? picked = await showDateRangePicker( Future<void> selectDateRangeForAttendance(
BuildContext context, AttendanceController controller) async {
final today = DateTime.now();
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)),
initialDateRange: DateTimeRange( initialDateRange: DateTimeRange(
start: startDateAttendance.value, start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
end: endDateAttendance.value, end: endDateAttendance ?? today.subtract(const Duration(days: 1)),
), ),
); );
if (picked != null) { if (picked != null) {
startDateAttendance.value = picked.start; startDateAttendance = picked.start;
endDateAttendance.value = picked.end; endDateAttendance = picked.end;
logSafe( logSafe(
'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}', "Date range selected: $startDateAttendance to $endDateAttendance");
);
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,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/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();

View File

@ -1,13 +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/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/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart'; import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:on_field_work/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
import 'package:on_field_work/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
class MPINController extends GetxController { class MPINController extends GetxController {
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();

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>();

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();

View File

@ -1,370 +1,263 @@
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:on_field_work/model/dashboard/expense_type_report_model.dart';
import 'package:on_field_work/model/dashboard/monthly_expence_model.dart';
import 'package:on_field_work/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
};
// ========================= // Inside your DashboardController
// 2. COMPUTED PROPERTIES final ProjectController projectController =
// ========================= Get.put(ProjectController(), permanent: true);
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
// DSO Calculation Constants
static const double _w0_30 = 15.0;
static const double _w30_60 = 45.0;
static const double _w60_90 = 75.0;
static const double _w90_plus = 105.0;
double get calculatedDSO {
final data = collectionOverviewData.value;
if (data == null || data.totalDueAmount == 0) return 0.0;
final double weightedDue = (data.bucket0To30Amount * _w0_30) +
(data.bucket30To60Amount * _w30_60) +
(data.bucket60To90Amount * _w60_90) +
(data.bucket90PlusAmount * _w90_plus);
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 // React to range changes
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
if (projectController.selectedProjectId.value.isNotEmpty) {
fetchExpenseTypeReport(
startDate: expenseReportStartDate.value,
endDate: expenseReportEndDate.value,
);
}
});
// 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(),
fetchProjectProgress(), fetchProjectProgress(),
fetchDashboardTasks(projectId: projectId), fetchDashboardTasks(projectId: projectId),
fetchDashboardTeams(projectId: projectId), fetchDashboardTeams(projectId: projectId),
fetchPendingExpenses(),
fetchExpenseTypeReport(
startDate: expenseReportStartDate.value,
endDate: expenseReportEndDate.value,
),
fetchMonthlyExpenses(),
fetchMasterData(),
fetchCollectionOverview(),
fetchPurchaseInvoiceOverview(),
]); ]);
} }
Future<void> fetchCollectionOverview() async { // =========================
final projectId = projectController.selectedProjectId.value; // API Calls
if (projectId.isEmpty) return; // =========================
await _executeApiCall(isCollectionOverviewLoading, () async {
final response =
await ApiService.getCollectionOverview(projectId: projectId);
collectionOverviewData.value =
(response?.success == true) ? response!.data : null;
});
}
Future<void> fetchTodaysAttendance(String projectId) async {
await _executeApiCall(isLoadingEmployees, () async {
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 {
try {
final data = await ApiService.getMasterExpenseTypes();
if (data is List) {
expenseTypes.value =
data.map((e) => ExpenseTypeModel.fromJson(e)).toList();
}
} catch (_) {}
}
Future<void> fetchMonthlyExpenses({String? categoryId}) async {
await _executeApiCall(isMonthlyExpenseLoading, () async {
final response = await ApiService.getDashboardMonthlyExpensesApi(
categoryId: categoryId,
months: selectedMonthsCount.value,
);
monthlyExpenseList.value =
(response?.success == true) ? response!.data : [];
});
}
Future<void> fetchPurchaseInvoiceOverview() async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
await _executeApiCall(isPurchaseInvoiceLoading, () async {
final response = await ApiService.getPurchaseInvoiceOverview(
projectId: projectId,
);
purchaseInvoiceOverviewData.value =
(response?.success == true) ? response!.data : null;
});
}
Future<void> fetchPendingExpenses() async {
final id = projectController.selectedProjectId.value;
if (id.isEmpty) return;
await _executeApiCall(isPendingExpensesLoading, () async {
final response = await ApiService.getPendingExpensesApi(projectId: id);
pendingExpensesData.value =
(response?.success == true) ? response!.data : null;
});
}
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());
});
}
Future<void> fetchExpenseTypeReport( if (response != null) {
{required DateTime startDate, required DateTime endDate}) async { roleWiseData.value =
final id = projectController.selectedProjectId.value; response.map((e) => Map<String, dynamic>.from(e)).toList();
if (id.isEmpty) return; logSafe('Attendance overview fetched successfully.',
level: LogLevel.info);
await _executeApiCall(isExpenseTypeReportLoading, () async { } else {
final response = await ApiService.getExpenseTypeReportApi( roleWiseData.clear();
projectId: id, logSafe('Failed to fetch attendance overview: response is null.',
startDate: startDate, level: LogLevel.error);
endDate: endDate, }
); } catch (e, st) {
expenseTypeReportData.value = roleWiseData.clear();
(response?.success == true) ? response!.data : null; logSafe('Error fetching attendance overview',
}); level: LogLevel.error, error: e, stackTrace: st);
} finally {
isAttendanceLoading.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;
}
} }
} }
enum MonthlyExpenseDuration {
oneMonth,
threeMonths,
sixMonths,
twelveMonths,
all,
}

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,10 +1,10 @@
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/contact_model.dart'; import 'package:marco/model/directory/contact_model.dart';
import 'package:on_field_work/model/directory/contact_bucket_list_model.dart'; import 'package:marco/model/directory/contact_bucket_list_model.dart';
import 'package:on_field_work/model/directory/directory_comment_model.dart'; import 'package:marco/model/directory/directory_comment_model.dart';
class DirectoryController extends GetxController { class DirectoryController extends GetxController {
// -------------------- CONTACTS -------------------- // -------------------- CONTACTS --------------------

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,67 +1,58 @@
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 ---------------------
final isLoading = false.obs; var isLoading = false.obs;
final documents = <DocumentItem>[].obs; var documents = <DocumentItem>[].obs;
final filters = Rxn<DocumentFiltersData>(); var filters = Rxn<DocumentFiltersData>();
// Selected filters (multi-select) // Selected filters (multi-select support)
final selectedUploadedBy = <String>[].obs; var selectedUploadedBy = <String>[].obs;
final selectedCategory = <String>[].obs; var selectedCategory = <String>[].obs;
final selectedType = <String>[].obs; var selectedType = <String>[].obs;
final selectedTag = <String>[].obs; var selectedTag = <String>[].obs;
// Pagination // Pagination state
final pageNumber = 1.obs; var pageNumber = 1.obs;
final pageSize = 20; final int pageSize = 20;
final hasMore = true.obs; var hasMore = true.obs;
// Error handling // Error message
final errorMessage = ''.obs; var errorMessage = "".obs;
// Preferences // NEW: show inactive toggle
final showInactive = false.obs; var showInactive = false.obs;
// Search // NEW: search
final searchQuery = ''.obs; var searchQuery = ''.obs;
final searchController = TextEditingController(); var searchController = TextEditingController();
// New filter fields
var isUploadedAt = true.obs;
var isVerified = RxnBool();
var startDate = Rxn<String>();
var endDate = Rxn<String>();
// Additional filters // ------------------ API Calls -----------------------
final isUploadedAt = true.obs;
final isVerified = RxnBool();
final startDate = Rxn<DateTime>();
final endDate = Rxn<DateTime>();
// ==================== Lifecycle ==================== /// Fetch Document Filters for an Entity
@override
void onClose() {
// Don't dispose searchController here - it's managed by the page
super.onClose();
}
// ==================== API Methods ====================
/// Fetch document filters for entity
Future<void> fetchFilters(String entityTypeId) async { Future<void> fetchFilters(String entityTypeId) async {
try { try {
isLoading.value = true;
final response = await ApiService.getDocumentFilters(entityTypeId); final response = await ApiService.getDocumentFilters(entityTypeId);
if (response != null && response.success) { if (response != null && response.success) {
filters.value = response.data; filters.value = response.data;
} else { } else {
errorMessage.value = response?.message ?? 'Failed to fetch filters'; errorMessage.value = response?.message ?? "Failed to fetch filters";
_showError('Failed to load filters');
} }
} catch (e) { } catch (e) {
errorMessage.value = 'Error fetching filters: $e'; errorMessage.value = "Error fetching filters: $e";
_showError('Error loading filters'); } finally {
debugPrint('❌ Error fetching filters: $e'); isLoading.value = false;
} }
} }
@ -74,44 +65,53 @@ class DocumentController extends GetxController {
}) async { }) async {
try { try {
isLoading.value = true; isLoading.value = true;
final success =
final success = await ApiService.deleteDocumentApi( await ApiService.deleteDocumentApi(id: id, isActive: isActive);
id: id,
isActive: isActive,
);
if (success) { if (success) {
// Refresh list after state change // 🔥 Always fetch fresh list after toggle
await fetchDocuments( await fetchDocuments(
entityTypeId: entityTypeId, entityTypeId: entityTypeId,
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');
return false; return false;
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
/// Fetch documents for entity with pagination /// Permanently delete a document (or deactivate depending on API)
Future<bool> deleteDocument(String id, {bool isActive = false}) async {
try {
isLoading.value = true;
final success =
await ApiService.deleteDocumentApi(id: id, isActive: isActive);
if (success) {
// remove from local list immediately for better UX
documents.removeWhere((doc) => doc.id == id);
return true;
} else {
errorMessage.value = "Failed to delete document";
return false;
}
} catch (e) {
errorMessage.value = "Error deleting document: $e";
return false;
} finally {
isLoading.value = false;
}
}
/// Fetch Documents for an entity
Future<void> fetchDocuments({ Future<void> fetchDocuments({
required String entityTypeId, required String entityTypeId,
required String entityId, required String entityId,
@ -126,15 +126,14 @@ class DocumentController extends GetxController {
hasMore.value = true; hasMore.value = true;
} }
if (!hasMore.value && !reset) return; if (!hasMore.value) return;
if (isLoading.value) return;
isLoading.value = true; isLoading.value = true;
final response = await ApiService.getDocumentListApi( final response = await ApiService.getDocumentListApi(
entityTypeId: entityTypeId, entityTypeId: entityTypeId,
entityId: entityId, entityId: entityId,
filter: filter ?? '', filter: filter ?? "",
searchString: searchString ?? searchQuery.value, searchString: searchString ?? searchQuery.value,
pageNumber: pageNumber.value, pageNumber: pageNumber.value,
pageSize: pageSize, pageSize: pageSize,
@ -142,45 +141,25 @@ 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;
} }
errorMessage.value = '';
} else { } else {
errorMessage.value = response?.message ?? 'Failed to fetch documents'; errorMessage.value = response?.message ?? "Failed to fetch documents";
if (documents.isEmpty) {
_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) {
_showError('Error loading documents');
} else {
showAppSnackbar(
title: 'Error',
message: 'Error fetching additional documents',
type: SnackbarType.error,
);
}
debugPrint('❌ Error fetching documents: $e');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
// ==================== Helper Methods ==================== // ------------------ Helpers -----------------------
/// Clear all selected filters /// Clear selected filters
void clearFilters() { void clearFilters() {
selectedUploadedBy.clear(); selectedUploadedBy.clear();
selectedCategory.clear(); selectedCategory.clear();
@ -192,35 +171,11 @@ class DocumentController extends GetxController {
endDate.value = null; endDate.value = null;
} }
/// Check if any filters are active /// Check if any filters are active (for red dot indicator)
bool hasActiveFilters() { bool hasActiveFilters() {
return selectedUploadedBy.isNotEmpty || return selectedUploadedBy.isNotEmpty ||
selectedCategory.isNotEmpty || selectedCategory.isNotEmpty ||
selectedType.isNotEmpty || selectedType.isNotEmpty ||
selectedTag.isNotEmpty || selectedTag.isNotEmpty;
startDate.value != null ||
endDate.value != null ||
isVerified.value != null;
}
/// Show error message via snackbar
void _showError(String message) {
showAppSnackbar(
title: 'Error',
message: message,
type: SnackbarType.error,
);
}
/// Reset controller state
void reset() {
documents.clear();
clearFilters();
searchController.clear();
searchQuery.value = '';
pageNumber.value = 1;
hasMore.value = true;
showInactive.value = false;
errorMessage.value = '';
} }
} }

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
@ -30,6 +30,15 @@ class DynamicMenuController extends GetxController {
final menuResponse = MenuResponse.fromJson(responseData); final menuResponse = MenuResponse.fromJson(responseData);
menuItems.assignAll(menuResponse.data); menuItems.assignAll(menuResponse.data);
// TEMP: Add Material Requisition menu manually for local testing
menuItems.add(MenuItem(
id: '999',
name: "Material Requisition",
icon: "inventory_2",
route: "/dashboard/material-requisition-list",
available: true,
));
logSafe("✅ Menu loaded from API with ${menuItems.length} items"); logSafe("✅ Menu loaded from API with ${menuItems.length} items");
} else { } else {
_handleApiFailure("Menu API returned null response"); _handleApiFailure("Menu API returned null response");

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';

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,13 @@ 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';
class AddExpenseController extends GetxController { class AddExpenseController extends GetxController {
// --- Text Controllers --- // --- Text Controllers ---
@ -51,22 +49,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>();
@ -79,7 +65,6 @@ class AddExpenseController extends GetxController {
final paymentModes = <PaymentModeModel>[].obs; final paymentModes = <PaymentModeModel>[].obs;
final allEmployees = <EmployeeModel>[].obs; final allEmployees = <EmployeeModel>[].obs;
final employeeSearchResults = <EmployeeModel>[].obs; final employeeSearchResults = <EmployeeModel>[].obs;
final isProcessingAttachment = false.obs;
String? editingExpenseId; String? editingExpenseId;
@ -209,7 +194,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}',
@ -267,22 +252,9 @@ class AddExpenseController extends GetxController {
Future<void> pickFromCamera() async { Future<void> pickFromCamera() async {
try { try {
final pickedFile = await _picker.pickImage(source: ImageSource.camera); final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) { if (pickedFile != null) attachments.add(File(pickedFile.path));
isProcessingAttachment.value = true; // start loading
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) { } catch (e) {
_errorSnackbar("Camera error: $e"); _errorSnackbar("Camera error: $e");
} finally {
isProcessingAttachment.value = false; // stop loading
} }
} }
@ -407,86 +379,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 +434,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

@ -0,0 +1,208 @@
// import 'package:get/get.dart';
// import 'package:marco/helpers/services/api_service.dart';
// import 'package:marco/model/inventory/material_requisition_model.dart';
// import 'package:marco/model/inventory/requisition_item_model.dart';
// import 'package:marco/helpers/services/app_logger.dart';
// class MaterialRequisitionController extends GetxController {
// final isLoading = false.obs;
// final requisitions = <MaterialRequisition>[].obs;
// final selectedMR = MaterialRequisition().obs;
// // Dropdown master lists
// final projects = <Project>[].obs;
// final materials = <Map<String, dynamic>>[].obs;
// // Form state
// // 🔧 FIX: use integer type since project.id is int
// final selectedProjectId = RxnInt();
// final status = 'Draft'.obs;
// @override
// void onInit() {
// super.onInit();
// fetchProjects();
// fetchAllRequisitions();
// }
// /// === Fetch all Projects ===
// Future<void> fetchProjects() async {
// try {
// isLoading(true);
// final res = await ApiService.getProjects();
// if (res != null) {
// projects.assignAll(
// (res as List).map((e) => Project.fromJson(e)).toList(),
// );
// }
// } catch (e) {
// logSafe("fetchProjects() error: $e", level: LogLevel.error);
// } finally {
// isLoading(false);
// }
// }
// /// === Fetch materials for a selected project ===
// /// 🔧 FIX: change argument from String int
// Future<void> fetchMaterialsByProject(int projectId) async {
// try {
// isLoading(true);
// final res = await ApiService.getMaterialsByProject(projectId.toString());
// if (res != null) {
// materials.assignAll(List<Map<String, dynamic>>.from(res));
// }
// } catch (e) {
// logSafe("fetchMaterialsByProject() error: $e", level: LogLevel.error);
// } finally {
// isLoading(false);
// }
// }
// /// === Fetch all Material Requisitions ===
// Future<void> fetchAllRequisitions() async {
// try {
// isLoading(true);
// final res = await ApiService.getMaterialRequisitions();
// if (res != null) {
// requisitions.assignAll(
// (res as List)
// .map((e) => MaterialRequisition.fromJson(e))
// .toList(),
// );
// }
// } catch (e) {
// logSafe("fetchAllRequisitions() error: $e", level: LogLevel.error);
// } finally {
// isLoading(false);
// }
// }
// /// === Save Draft ===
// Future<void> saveDraft(MaterialRequisition mr) async {
// try {
// isLoading(true);
// final res = await ApiService.createMaterialRequisition(mr.toJson());
// if (res != null) {
// Get.snackbar('Success', 'Saved as Draft');
// await fetchAllRequisitions(); // refresh list
// }
// } catch (e) {
// logSafe("saveDraft() error: $e", level: LogLevel.error);
// } finally {
// isLoading(false);
// }
// }
// /// === Submit Material Requisition ===
// /// 🔧 FIX: id type int for consistency
// Future<void> submitMR(int mrId) async {
// try {
// isLoading(true);
// final res =
// await ApiService.updateMaterialRequisitionStatus(mrId.toString(), 'Submitted');
// if (res != null) {
// Get.snackbar('Submitted', 'MR submitted for review');
// await fetchAllRequisitions();
// }
// } catch (e) {
// logSafe("submitMR() error: $e", level: LogLevel.error);
// } finally {
// isLoading(false);
// }
// }
// /// === Approve / Reject / Comment actions ===
// /// 🔧 FIX: id type int for consistency
// Future<void> performAction(int mrId, String action, String? comment) async {
// try {
// final body = {
// "mrId": mrId,
// "action": action,
// "comments": comment,
// };
// final res = await ApiService.postMaterialRequisitionAction(body);
// if (res != null) {
// Get.snackbar('Success', 'Action: $action');
// await fetchAllRequisitions();
// }
// } catch (e) {
// logSafe("performAction() error: $e", level: LogLevel.error);
// }
// }
// }
import 'package:get/get.dart';
class MaterialRequisitionController extends GetxController {
// --- Mock UI State ---
var isLoading = false.obs;
// Mock project dropdown
var projects = [
{'id': 1, 'name': 'Project Alpha'},
{'id': 2, 'name': 'Project Beta'},
].obs;
var selectedProjectId = RxnInt();
// Mock MR list
var materialRequisitions = <Map<String, dynamic>>[].obs;
// Mock selected MR object
var selectedMR = Rxn<Map<String, dynamic>>();
// --- Methods ---
void fetchMaterialRequisitions() {
isLoading.value = true;
Future.delayed(const Duration(seconds: 1), () {
materialRequisitions.value = [
{
'id': 1,
'projectName': 'Project Alpha',
'status': 'Draft',
'items': [
{'material': 'Cement', 'qty': 50},
{'material': 'Steel', 'qty': 100},
]
},
{
'id': 2,
'projectName': 'Project Beta',
'status': 'Approved',
'items': [
{'material': 'Bricks', 'qty': 500},
{'material': 'Sand', 'qty': 200},
]
},
];
isLoading.value = false;
});
}
void fetchMaterialsByProject(int? projectId) {
if (projectId == null) return;
selectedMR.value = {
'id': projectId,
'projectName': projects.firstWhere(
(p) => p['id'] == projectId,
orElse: () => {'name': 'Unknown'})['name'],
'items': [
{'material': 'Cement', 'qty': 25},
{'material': 'Steel', 'qty': 80},
]
};
}
void saveDraft(Map<String, dynamic> mr) {
print("📝 Saved draft for ${mr['projectName']}");
}
void submitMR(int? mrId) {
print("🚀 Submitted MR ID: $mrId");
}
}

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,11 +2,11 @@ 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;
@ -15,9 +15,6 @@ class PermissionController extends GetxController {
Timer? _refreshTimer; Timer? _refreshTimer;
var isLoading = true.obs; var isLoading = true.obs;
/// NEW: reactive flag to signal permissions are loaded
var permissionsLoaded = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -55,10 +52,6 @@ class PermissionController extends GetxController {
_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);
@ -110,7 +103,7 @@ class PermissionController extends GetxController {
} }
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) {
@ -124,6 +117,8 @@ class PermissionController extends GetxController {
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;
} }

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,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/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/model/dailyTaskPlanning/master_work_category_model.dart'; import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart';
class AddTaskController extends GetxController { class AddTaskController extends GetxController {
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;

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/project_model.dart'; import 'package:marco/model/project_model.dart';
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_model.dart'; import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
import 'package:on_field_work/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart'; import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
class DailyTaskController extends GetxController { class DailyTaskController extends GetxController {
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
@ -13,10 +13,6 @@ class DailyTaskController extends GetxController {
DateTime? startDateTask; DateTime? startDateTask;
DateTime? endDateTask; DateTime? endDateTask;
// Rx fields for DateRangePickerWidget
Rx<DateTime> startDateTaskRx = DateTime.now().obs;
Rx<DateTime> endDateTaskRx = DateTime.now().obs;
List<TaskModel> dailyTasks = []; List<TaskModel> dailyTasks = [];
final RxSet<String> expandedDates = <String>{}.obs; final RxSet<String> expandedDates = <String>{}.obs;
@ -37,19 +33,14 @@ class DailyTaskController extends GetxController {
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
RxBool isLoadingMore = false.obs; RxBool isLoadingMore = false.obs;
Map<String, List<TaskModel>> groupedDailyTasks = {}; Map<String, List<TaskModel>> groupedDailyTasks = {};
// Pagination // Pagination
int currentPage = 1; int currentPage = 1;
int pageSize = 20; int pageSize = 20;
bool hasMore = true; bool hasMore = true;
FilterData? taskFilterData;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initializeDefaults(); _initializeDefaults();
_initializeRxDates();
} }
void _initializeDefaults() { void _initializeDefaults() {
@ -67,12 +58,6 @@ class DailyTaskController extends GetxController {
); );
} }
void _initializeRxDates() {
startDateTaskRx.value =
startDateTask ?? DateTime.now().subtract(const Duration(days: 7));
endDateTaskRx.value = endDateTask ?? DateTime.now();
}
void clearTaskFilters() { void clearTaskFilters() {
selectedBuildings.clear(); selectedBuildings.clear();
selectedFloors.clear(); selectedFloors.clear();
@ -80,26 +65,9 @@ class DailyTaskController extends GetxController {
selectedServices.clear(); selectedServices.clear();
startDateTask = null; startDateTask = null;
endDateTask = null; endDateTask = null;
// reset Rx dates as well
startDateTaskRx.value = DateTime.now().subtract(const Duration(days: 7));
endDateTaskRx.value = DateTime.now();
update(); 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( Future<void> fetchTaskData(
String projectId, { String projectId, {
int pageNumber = 1, int pageNumber = 1,
@ -128,30 +96,17 @@ class DailyTaskController extends GetxController {
final response = await ApiService.getDailyTasks( final response = await ApiService.getDailyTasks(
projectId, projectId,
filter: filter, filter: filter,
pageNumber: pageNumber, pageNumber: pageNumber,
pageSize: pageSize, pageSize: pageSize,
); );
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
if (!isLoadMore) {
groupedDailyTasks.clear();
}
for (var task in response) { for (var task in response) {
final assignmentDateKey = final assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0]; task.assignmentDate.toIso8601String().split('T')[0];
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
// 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(); dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
currentPage = pageNumber; currentPage = pageNumber;
} else { } else {
@ -164,13 +119,16 @@ class DailyTaskController extends GetxController {
update(); update();
} }
FilterData? taskFilterData;
Future<void> fetchTaskFilter(String projectId) async { Future<void> fetchTaskFilter(String projectId) async {
isFilterLoading.value = true; isFilterLoading.value = true;
try { try {
final filterResponse = await ApiService.getDailyTaskFilter(projectId); final filterResponse = await ApiService.getDailyTaskFilter(projectId);
if (filterResponse != null && filterResponse.success) { if (filterResponse != null && filterResponse.success) {
taskFilterData = filterResponse.data; taskFilterData =
filterResponse.data; // now taskFilterData is FilterData?
logSafe( logSafe(
"Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}", "Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}",
level: LogLevel.info, level: LogLevel.info,
@ -213,15 +171,12 @@ class DailyTaskController extends GetxController {
startDateTask = picked.start; startDateTask = picked.start;
endDateTask = picked.end; endDateTask = picked.end;
// update Rx fields as well
startDateTaskRx.value = picked.start;
endDateTaskRx.value = picked.end;
logSafe( logSafe(
"Date range selected: $startDateTask to $endDateTask", "Date range selected: $startDateTask to $endDateTask",
level: LogLevel.info, level: LogLevel.info,
); );
// Add null check before calling fetchTaskData
final projectId = controller.selectedProjectId; final projectId = controller.selectedProjectId;
if (projectId != null && projectId.isNotEmpty) { if (projectId != null && projectId.isNotEmpty) {
await controller.fetchTaskData(projectId); await controller.fetchTaskData(projectId);
@ -235,7 +190,9 @@ class DailyTaskController extends GetxController {
required String projectId, required String projectId,
required String taskAllocationId, required String taskAllocationId,
}) async { }) async {
// re-fetch tasks
await fetchTaskData(projectId); await fetchTaskData(projectId);
update();
update(); // rebuilds UI
} }
} }

View File

@ -1,11 +1,11 @@
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/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/model/project_model.dart'; import 'package:marco/model/project_model.dart';
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_planning_model.dart'; import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:marco/model/employees/employee_model.dart';
class DailyTaskPlanningController extends GetxController { class DailyTaskPlanningController extends GetxController {
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
@ -23,10 +23,6 @@ class DailyTaskPlanningController extends GetxController {
RxBool isFetchingProjects = true.obs; RxBool isFetchingProjects = true.obs;
RxBool isFetchingEmployees = 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 @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -110,7 +106,7 @@ class DailyTaskPlanningController extends GetxController {
} }
} }
/// Fetch buildings list only (no deep area/workItem calls) for initial load. /// Fetch Infra details and then tasks per work area
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async { Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
if (projectId == null) return; if (projectId == null) return;
@ -127,24 +123,25 @@ class DailyTaskPlanningController extends GetxController {
return; return;
} }
// Filter buildings with 0 planned & completed work dailyTasks = infraData.map((buildingJson) {
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( final building = Building(
id: buildingJson['id'], id: buildingJson['id'],
name: buildingJson['buildingName'], name: buildingJson['buildingName'],
description: buildingJson['description'], description: buildingJson['description'],
floors: [], floors: (buildingJson['floors'] as List<dynamic>)
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0, .map((floorJson) => Floor(
completedWork: id: floorJson['id'],
(buildingJson['completedWork'] as num?)?.toDouble() ?? 0, floorName: floorJson['floorName'],
workAreas: (floorJson['workAreas'] as List<dynamic>)
.map((areaJson) => WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [],
))
.toList(),
))
.toList(),
); );
return TaskPlanningDetailsModel( return TaskPlanningDetailsModel(
id: building.id, id: building.id,
name: building.name, name: building.name,
@ -157,75 +154,14 @@ class DailyTaskPlanningController extends GetxController {
); );
}).toList(); }).toList();
buildingLoadingStates.clear(); await Future.wait(dailyTasks
buildingsWithDetails.clear(); .expand((task) => task.buildings)
} catch (e, stack) { .expand((b) => b.floors)
logSafe("Error fetching daily task data", .expand((f) => f.workAreas)
level: LogLevel.error, error: e, stackTrace: stack); .map((area) async {
} 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 { try {
final taskResponse = await ApiService.getWorkItemsByWorkArea(area.id, final taskResponse =
serviceId: serviceId); await ApiService.getWorkItemsByWorkArea(area.id, serviceId: serviceId);
final taskData = taskResponse?['data'] as List<dynamic>? ?? []; final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper( area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper(
workItemId: taskJson['id'], workItemId: taskJson['id'],
@ -235,14 +171,11 @@ class DailyTaskPlanningController extends GetxController {
? ActivityMaster.fromJson(taskJson['activityMaster']) ? ActivityMaster.fromJson(taskJson['activityMaster'])
: null, : null,
workCategoryMaster: taskJson['workCategoryMaster'] != null workCategoryMaster: taskJson['workCategoryMaster'] != null
? WorkCategoryMaster.fromJson( ? WorkCategoryMaster.fromJson(taskJson['workCategoryMaster'])
taskJson['workCategoryMaster'])
: null, : null,
plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(), plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(),
completedWork: completedWork: (taskJson['completedWork'] as num?)?.toDouble(),
(taskJson['completedWork'] as num?)?.toDouble(), todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(),
todaysAssigned:
(taskJson['todaysAssigned'] as num?)?.toDouble(),
description: taskJson['description'] as String?, description: taskJson['description'] as String?,
taskDate: taskJson['taskDate'] != null taskDate: taskJson['taskDate'] != null
? DateTime.tryParse(taskJson['taskDate']) ? DateTime.tryParse(taskJson['taskDate'])
@ -254,40 +187,11 @@ class DailyTaskPlanningController extends GetxController {
level: LogLevel.error, error: e, stackTrace: stack); 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) { } catch (e, stack) {
logSafe("Error fetching infra for building $buildingId", logSafe("Error fetching daily task data",
level: LogLevel.error, error: e, stackTrace: stack); level: LogLevel.error, error: e, stackTrace: stack);
} finally { } finally {
buildingLoadingStates.putIfAbsent(buildingId, () => false.obs); isFetchingTasks.value = false;
buildingLoadingStates[buildingId]!.value = false;
update(); update();
} }
} }
@ -307,8 +211,7 @@ class DailyTaskPlanningController extends GetxController {
); );
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
employees employees.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
if (serviceId == null && organizationId == null) { if (serviceId == null && organizationId == null) {
allEmployeesCache = List.from(employees); allEmployeesCache = List.from(employees);
@ -316,14 +219,12 @@ class DailyTaskPlanningController extends GetxController {
final currentEmployeeIds = employees.map((e) => e.id).toSet(); final currentEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates uploadingStates.removeWhere((key, _) => !currentEmployeeIds.contains(key));
.removeWhere((key, _) => !currentEmployeeIds.contains(key));
employees.forEach((emp) { employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs); uploadingStates.putIfAbsent(emp.id, () => false.obs);
}); });
selectedEmployees selectedEmployees.removeWhere((e) => !currentEmployeeIds.contains(e.id));
.removeWhere((e) => !currentEmployeeIds.contains(e.id));
logSafe("Employees fetched: ${employees.length}", level: LogLevel.info); logSafe("Employees fetched: ${employees.length}", level: LogLevel.info);
} else { } else {
@ -338,17 +239,13 @@ class DailyTaskPlanningController extends GetxController {
); );
} }
} catch (e, stack) { } catch (e, stack) {
logSafe("Error fetching employees", logSafe("Error fetching employees", level: LogLevel.error, error: e, stackTrace: stack);
level: LogLevel.error, error: e, stackTrace: stack);
if (serviceId == null && if (serviceId == null && organizationId == null && allEmployeesCache.isNotEmpty) {
organizationId == null &&
allEmployeesCache.isNotEmpty) {
employees.assignAll(allEmployeesCache); employees.assignAll(allEmployeesCache);
final cachedEmployeeIds = employees.map((e) => e.id).toSet(); final cachedEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates uploadingStates.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
employees.forEach((emp) { employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs); uploadingStates.putIfAbsent(emp.id, () => false.obs);
}); });

View File

@ -4,16 +4,15 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:on_field_work/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/controller/task_Planning/daily_task_Planning_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/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart'; import 'package:marco/helpers/widgets/my_image_compressor.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/dailyTaskPlanning/work_status_model.dart'; import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
enum ApiStatus { idle, loading, success, failure } enum ApiStatus { idle, loading, success, failure }
@ -33,11 +32,9 @@ class ReportTaskActionController extends MyController {
final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>(); final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
final RxString selectedWorkStatusName = ''.obs; final RxString selectedWorkStatusName = ''.obs;
final RxBool isPickingImage = false.obs;
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();
final DailyTaskPlanningController taskController = final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
Get.put(DailyTaskPlanningController());
final ImagePicker _picker = ImagePicker(); final ImagePicker _picker = ImagePicker();
final assignedDateController = TextEditingController(); final assignedDateController = TextEditingController();
@ -86,31 +83,18 @@ class ReportTaskActionController extends MyController {
void _initializeFormFields() { void _initializeFormFields() {
basicValidator basicValidator
..addField('assigned_date', ..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
label: "Assigned Date", controller: assignedDateController) ..addField('work_area', label: "Work Area", controller: workAreaController)
..addField('work_area',
label: "Work Area", controller: workAreaController)
..addField('activity', label: "Activity", controller: activityController) ..addField('activity', label: "Activity", controller: activityController)
..addField('team_size', ..addField('team_size', label: "Team Size", controller: teamSizeController)
label: "Team Size", controller: teamSizeController)
..addField('task_id', label: "Task Id", controller: taskIdController) ..addField('task_id', label: "Task Id", controller: taskIdController)
..addField('assigned', label: "Assigned", controller: assignedController) ..addField('assigned', label: "Assigned", controller: assignedController)
..addField('completed_work', ..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
label: "Completed Work", ..addField('comment', label: "Comment", required: true, controller: commentController)
required: true, ..addField('assigned_by', label: "Assigned By", controller: assignedByController)
controller: completedWorkController) ..addField('team_members', label: "Team Members", controller: teamMembersController)
..addField('comment', ..addField('planned_work', label: "Planned Work", controller: plannedWorkController)
label: "Comment", required: true, controller: commentController) ..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
..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({ Future<bool> approveTask({
@ -124,8 +108,7 @@ class ReportTaskActionController extends MyController {
if (projectId.isEmpty || reportActionId.isEmpty) { if (projectId.isEmpty || reportActionId.isEmpty) {
_showError("Project ID and Report Action ID are required."); _showError("Project ID and Report Action ID are required.");
logSafe("Missing required projectId or reportActionId", logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
level: LogLevel.warning);
return false; return false;
} }
@ -134,15 +117,13 @@ class ReportTaskActionController extends MyController {
if (approvedTaskInt == null) { if (approvedTaskInt == null) {
_showError("Invalid approved task count."); _showError("Invalid approved task count.");
logSafe("Invalid approvedTaskCount: $approvedTaskCount", logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
level: LogLevel.warning);
return false; return false;
} }
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) { if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
_showError("Approved task count cannot exceed completed work."); _showError("Approved task count cannot exceed completed work.");
logSafe("Validation failed: approved > completed", logSafe("Validation failed: approved > completed", level: LogLevel.warning);
level: LogLevel.warning);
return false; return false;
} }
@ -178,8 +159,7 @@ class ReportTaskActionController extends MyController {
return false; return false;
} }
} catch (e, st) { } catch (e, st) {
logSafe("Error in approveTask: $e", logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred."); _showError("An error occurred.");
return false; return false;
} finally { } finally {
@ -227,8 +207,7 @@ class ReportTaskActionController extends MyController {
_showError("Failed to comment task."); _showError("Failed to comment task.");
} }
} catch (e, st) { } catch (e, st) {
logSafe("Error in commentTask: $e", logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred while commenting the task."); _showError("An error occurred while commenting the task.");
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -245,8 +224,7 @@ class ReportTaskActionController extends MyController {
workStatus.assignAll(model.data); workStatus.assignAll(model.data);
logSafe("Fetched ${model.data.length} work statuses"); logSafe("Fetched ${model.data.length} work statuses");
} else { } else {
logSafe("No work statuses found or API call failed", logSafe("No work statuses found or API call failed", level: LogLevel.warning);
level: LogLevel.warning);
} }
isLoadingWorkStatus.value = false; isLoadingWorkStatus.value = false;
@ -273,8 +251,7 @@ class ReportTaskActionController extends MyController {
}; };
})); }));
logSafe( logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
"_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
return results.whereType<Map<String, dynamic>>().toList(); return results.whereType<Map<String, dynamic>>().toList();
} }
@ -290,40 +267,23 @@ class ReportTaskActionController extends MyController {
} }
Future<void> pickImages({required bool fromCamera}) async { Future<void> pickImages({required bool fromCamera}) async {
try { logSafe("Opening image picker...");
isPickingImage.value = true; // start loading if (fromCamera) {
logSafe("Opening image picker..."); final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
if (pickedFile != null) {
if (fromCamera) { selectedImages.add(File(pickedFile.path));
final pickedFile = await _picker.pickImage( logSafe("Image added from camera: ${pickedFile.path}", );
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) { } else {
logSafe("Error picking images: $e", final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
level: LogLevel.error, stackTrace: st); selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
} finally { logSafe("${pickedFiles.length} images added from gallery.", );
isPickingImage.value = false; // stop loading
} }
} }
void removeImageAt(int index) { void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) { if (index >= 0 && index < selectedImages.length) {
logSafe( logSafe("Removing image at index $index", );
"Removing image at index $index",
);
selectedImages.removeAt(index); selectedImages.removeAt(index);
} }
} }

View File

@ -1,22 +1,20 @@
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: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/services/api_service.dart'; import 'package:marco/helpers/services/api_service.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/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart'; import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
enum ApiStatus { idle, loading, success, failure } enum ApiStatus { idle, loading, success, failure }
final DailyTaskPlanningController taskController = final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
Get.put(DailyTaskPlanningController());
final ImagePicker _picker = ImagePicker(); final ImagePicker _picker = ImagePicker();
class ReportTaskController extends MyController { class ReportTaskController extends MyController {
@ -25,7 +23,6 @@ class ReportTaskController extends MyController {
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs; Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs; Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
final RxBool isPickingImage = false.obs;
RxList<File> selectedImages = <File>[].obs; RxList<File> selectedImages = <File>[].obs;
@ -46,27 +43,17 @@ class ReportTaskController extends MyController {
super.onInit(); super.onInit();
logSafe("Initializing ReportTaskController..."); logSafe("Initializing ReportTaskController...");
basicValidator basicValidator
..addField('assigned_date', ..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
label: "Assigned Date", controller: assignedDateController) ..addField('work_area', label: "Work Area", controller: workAreaController)
..addField('work_area',
label: "Work Area", controller: workAreaController)
..addField('activity', label: "Activity", controller: activityController) ..addField('activity', label: "Activity", controller: activityController)
..addField('team_size', ..addField('team_size', label: "Team Size", controller: teamSizeController)
label: "Team Size", controller: teamSizeController)
..addField('task_id', label: "Task Id", controller: taskIdController) ..addField('task_id', label: "Task Id", controller: taskIdController)
..addField('assigned', label: "Assigned", controller: assignedController) ..addField('assigned', label: "Assigned", controller: assignedController)
..addField('completed_work', ..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
label: "Completed Work", ..addField('comment', label: "Comment", required: true, controller: commentController)
required: true, ..addField('assigned_by', label: "Assigned By", controller: assignedByController)
controller: completedWorkController) ..addField('team_members', label: "Team Members", controller: teamMembersController)
..addField('comment', ..addField('planned_work', label: "Planned Work", controller: plannedWorkController);
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."); logSafe("Form fields initialized.");
} }
@ -96,13 +83,9 @@ class ReportTaskController extends MyController {
required DateTime reportedDate, required DateTime reportedDate,
List<File>? images, List<File>? images,
}) async { }) async {
logSafe( logSafe("Reporting task for projectId", );
"Reporting task for projectId",
);
final completedWork = completedWorkController.text.trim(); final completedWork = completedWorkController.text.trim();
if (completedWork.isEmpty || if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) {
int.tryParse(completedWork) == null ||
int.parse(completedWork) < 0) {
_showError("Completed work must be a positive number."); _showError("Completed work must be a positive number.");
return false; return false;
} }
@ -138,8 +121,7 @@ class ReportTaskController extends MyController {
return false; return false;
} }
} catch (e, s) { } catch (e, s) {
logSafe("Exception while reporting task", logSafe("Exception while reporting task", level: LogLevel.error, error: e, stackTrace: s);
level: LogLevel.error, error: e, stackTrace: s);
reportStatus.value = ApiStatus.failure; reportStatus.value = ApiStatus.failure;
_showError("An error occurred while reporting the task."); _showError("An error occurred while reporting the task.");
return false; return false;
@ -156,9 +138,7 @@ class ReportTaskController extends MyController {
required String comment, required String comment,
List<File>? images, List<File>? images,
}) async { }) async {
logSafe( logSafe("Submitting comment for project", );
"Submitting comment for project",
);
final commentField = commentController.text.trim(); final commentField = commentController.text.trim();
if (commentField.isEmpty) { if (commentField.isEmpty) {
@ -186,16 +166,14 @@ class ReportTaskController extends MyController {
_showError("Failed to comment task."); _showError("Failed to comment task.");
} }
} catch (e, s) { } catch (e, s) {
logSafe("Exception while commenting task", logSafe("Exception while commenting task", level: LogLevel.error, error: e, stackTrace: s);
level: LogLevel.error, error: e, stackTrace: s);
_showError("An error occurred while commenting the task."); _showError("An error occurred while commenting the task.");
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
Future<List<Map<String, dynamic>>?> _prepareImages( Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images, String context) async {
List<File>? images, String context) async {
if (images == null || images.isEmpty) return null; if (images == null || images.isEmpty) return null;
logSafe("Preparing images for $context upload..."); logSafe("Preparing images for $context upload...");
@ -213,8 +191,7 @@ class ReportTaskController extends MyController {
"description": "Image uploaded for $context", "description": "Image uploaded for $context",
}; };
} catch (e) { } catch (e) {
logSafe("Image processing failed: ${file.path}", logSafe("Image processing failed: ${file.path}", level: LogLevel.warning, error: e);
level: LogLevel.warning, error: e);
return null; return null;
} }
})); }));
@ -235,31 +212,18 @@ class ReportTaskController extends MyController {
Future<void> pickImages({required bool fromCamera}) async { Future<void> pickImages({required bool fromCamera}) async {
try { try {
isPickingImage.value = true; // Start loading
if (fromCamera) { if (fromCamera) {
final pickedFile = await _picker.pickImage( final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
source: ImageSource.camera,
imageQuality: 75,
);
if (pickedFile != null) { if (pickedFile != null) {
// Only camera images get timestamp selectedImages.add(File(pickedFile.path));
final timestampedFile = await TimestampImageHelper.addTimestamp(
imageFile: File(pickedFile.path),
);
selectedImages.add(timestampedFile);
} }
} else { } else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75); final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
// Gallery images added as-is without timestamp
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path))); selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
} }
logSafe("Images picked: ${selectedImages.length}", );
logSafe("Images picked: ${selectedImages.length}");
} catch (e) { } catch (e) {
logSafe("Error picking images", level: LogLevel.warning, error: e); logSafe("Error picking images", level: LogLevel.warning, error: e);
} finally {
isPickingImage.value = false; // Stop loading
} }
} }

View File

@ -1,7 +1,7 @@
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/all_organization_model.dart'; import 'package:marco/model/all_organization_model.dart';
class AllOrganizationController extends GetxController { class AllOrganizationController extends GetxController {
RxList<AllOrganization> organizations = <AllOrganization>[].obs; RxList<AllOrganization> organizations = <AllOrganization>[].obs;

View File

@ -1,7 +1,7 @@
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/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart';
class OrganizationController extends GetxController { class OrganizationController extends GetxController {
/// List of organizations assigned to the selected project /// List of organizations assigned to the selected project

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/model/tenant/tenant_services_model.dart'; import 'package:marco/model/tenant/tenant_services_model.dart';
class ServiceController extends GetxController { class ServiceController extends GetxController {
List<Service> services = []; List<Service> services = [];

View File

@ -1,10 +1,10 @@
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/tenant_service.dart'; import 'package:marco/helpers/services/tenant_service.dart';
import 'package:on_field_work/model/tenant/tenant_list_model.dart'; import 'package:marco/model/tenant/tenant_list_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/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
class TenantSelectionController extends GetxController { class TenantSelectionController extends GetxController {
final TenantService _tenantService = TenantService(); final TenantService _tenantService = TenantService();

View File

@ -1,10 +1,10 @@
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/tenant_service.dart'; import 'package:marco/helpers/services/tenant_service.dart';
import 'package:on_field_work/model/tenant/tenant_list_model.dart'; import 'package:marco/model/tenant/tenant_list_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/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/controller/permission_controller.dart'; import 'package:marco/controller/permission_controller.dart';
class TenantSwitchController extends GetxController { class TenantSwitchController extends GetxController {
final TenantService _tenantService = TenantService(); final TenantService _tenantService = TenantService();

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

@ -2,53 +2,19 @@ class ApiEndpoints {
static const String baseUrl = "https://stageapi.marcoaiot.com/api"; static const String baseUrl = "https://stageapi.marcoaiot.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";
static const String getDashboardProjects = "/dashboard/projects"; static const String getDashboardProjects = "/dashboard/projects";
static const String getDashboardMonthlyExpenses =
"/Dashboard/expense/monthly";
static const String getExpenseTypeReport = "/Dashboard/expense/type";
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
static const String createProject = "/project";
// Attendance Module API Endpoints // Attendance Module API Endpoints
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";
@ -104,7 +70,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";
@ -134,37 +100,4 @@ class ApiEndpoints {
static const getAllOrganizations = "/organization/list"; 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,13 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.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: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';
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/firebase/firebase_messaging_service.dart'; import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:on_field_work/helpers/services/device_info_service.dart'; import 'package:marco/helpers/services/device_info_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/helpers/theme/app_theme.dart'; import 'package:marco/helpers/theme/app_theme.dart';
Future<void> initializeApp() async { Future<void> initializeApp() async {
try { try {

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,8 @@
import 'dart:convert'; import 'dart:convert';
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:marco/helpers/services/api_endpoints.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 AuthService { class AuthService {
static const String _baseUrl = ApiEndpoints.baseUrl; static const String _baseUrl = ApiEndpoints.baseUrl;

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 {

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,17 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:on_field_work/controller/task_planning/daily_task_controller.dart'; import 'package:marco/controller/task_planning/daily_task_controller.dart';
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/controller/task_Planning/daily_task_Planning_controller.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/controller/expense/expense_detail_controller.dart'; import 'package:marco/controller/expense/expense_detail_controller.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';
import 'package:on_field_work/controller/document/user_document_controller.dart'; import 'package:marco/controller/document/user_document_controller.dart';
import 'package:on_field_work/controller/document/document_details_controller.dart'; import 'package:marco/controller/document/document_details_controller.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart'; import 'package:marco/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 {
@ -69,6 +69,7 @@ class NotificationActionHandler {
} }
break; break;
case 'Team_Modified': case 'Team_Modified':
// Call method to handle team modifications and dashboard update
_handleDashboardUpdate(data); _handleDashboardUpdate(data);
break; break;
@ -128,11 +129,6 @@ class NotificationActionHandler {
/// ---------------------- HANDLERS ---------------------- /// ---------------------- HANDLERS ----------------------
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) { static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored task planning update from another project.");
return;
}
final projectId = data['ProjectId']; final projectId = data['ProjectId'];
if (projectId == null) { if (projectId == null) {
_logger.w("⚠️ TaskPlanning update received without ProjectId: $data"); _logger.w("⚠️ TaskPlanning update received without ProjectId: $data");
@ -163,17 +159,13 @@ class NotificationActionHandler {
} }
static void _handleExpenseUpdated(Map<String, dynamic> data) { static void _handleExpenseUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored expense update from another project.");
return;
}
final expenseId = data['ExpenseId']; final expenseId = data['ExpenseId'];
if (expenseId == null) { if (expenseId == null) {
_logger.w("⚠️ Expense update received without ExpenseId: $data"); _logger.w("⚠️ Expense update received without ExpenseId: $data");
return; return;
} }
// Update Expense List
_safeControllerUpdate<ExpenseController>( _safeControllerUpdate<ExpenseController>(
onFound: (controller) async { onFound: (controller) async {
await controller.fetchExpenses(); await controller.fetchExpenses();
@ -183,6 +175,7 @@ class NotificationActionHandler {
'✅ ExpenseController refreshed from expense notification.', '✅ ExpenseController refreshed from expense notification.',
); );
// Update Expense Detail (if open and matches this expenseId)
_safeControllerUpdate<ExpenseDetailController>( _safeControllerUpdate<ExpenseDetailController>(
onFound: (controller) async { onFound: (controller) async {
if (controller.expense.value?.id == expenseId) { if (controller.expense.value?.id == expenseId) {
@ -197,11 +190,6 @@ class NotificationActionHandler {
} }
static void _handleAttendanceUpdated(Map<String, dynamic> data) { static void _handleAttendanceUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored attendance update from another project.");
return;
}
_safeControllerUpdate<AttendanceController>( _safeControllerUpdate<AttendanceController>(
onFound: (controller) => controller.refreshDataFromNotification( onFound: (controller) => controller.refreshDataFromNotification(
projectId: data['ProjectId'], projectId: data['ProjectId'],
@ -213,11 +201,6 @@ class NotificationActionHandler {
static void _handleTaskUpdated(Map<String, dynamic> data, static void _handleTaskUpdated(Map<String, dynamic> data,
{required bool isComment}) { {required bool isComment}) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored task update from another project.");
return;
}
_safeControllerUpdate<DailyTaskController>( _safeControllerUpdate<DailyTaskController>(
onFound: (controller) => controller.refreshTasksFromNotification( onFound: (controller) => controller.refreshTasksFromNotification(
projectId: data['ProjectId'], projectId: data['ProjectId'],
@ -230,15 +213,11 @@ class NotificationActionHandler {
/// ---------------------- DOCUMENT HANDLER ---------------------- /// ---------------------- DOCUMENT HANDLER ----------------------
static void _handleDocumentModified(Map<String, dynamic> data) { static void _handleDocumentModified(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored document update from another project.");
return;
}
String entityTypeId; String entityTypeId;
String entityId; String entityId;
String? documentId = data['DocumentId']; String? documentId = data['DocumentId'];
// Determine entity type and ID
if (data['Keyword'] == 'Employee_Document_Modified') { if (data['Keyword'] == 'Employee_Document_Modified') {
entityTypeId = Permissions.employeeEntity; entityTypeId = Permissions.employeeEntity;
entityId = data['EmployeeId'] ?? ''; entityId = data['EmployeeId'] ?? '';
@ -258,6 +237,7 @@ class NotificationActionHandler {
_logger.i( _logger.i(
"🔔 Document notification received: keyword=${data['Keyword']}, entityTypeId=$entityTypeId, entityId=$entityId, documentId=$documentId"); "🔔 Document notification received: keyword=${data['Keyword']}, entityTypeId=$entityTypeId, entityId=$entityId, documentId=$documentId");
// Refresh Document List
if (Get.isRegistered<DocumentController>()) { if (Get.isRegistered<DocumentController>()) {
_safeControllerUpdate<DocumentController>( _safeControllerUpdate<DocumentController>(
onFound: (controller) async { onFound: (controller) async {
@ -275,9 +255,11 @@ class NotificationActionHandler {
_logger.w('⚠️ DocumentController not registered, skipping list refresh.'); _logger.w('⚠️ DocumentController not registered, skipping list refresh.');
} }
// Refresh Document Details (if open)
if (documentId != null && Get.isRegistered<DocumentDetailsController>()) { if (documentId != null && Get.isRegistered<DocumentDetailsController>()) {
_safeControllerUpdate<DocumentDetailsController>( _safeControllerUpdate<DocumentDetailsController>(
onFound: (controller) async { onFound: (controller) async {
// Refresh details regardless of current document
await controller.fetchDocumentDetails(documentId); await controller.fetchDocumentDetails(documentId);
_logger.i( _logger.i(
"✅ DocumentDetailsController refreshed for Document $documentId"); "✅ DocumentDetailsController refreshed for Document $documentId");
@ -294,10 +276,13 @@ class NotificationActionHandler {
/// ---------------------- DIRECTORY HANDLERS ---------------------- /// ---------------------- DIRECTORY HANDLERS ----------------------
static void _handleContactModified(Map<String, dynamic> data) { static void _handleContactModified(Map<String, dynamic> data) {
final contactId = data['ContactId'];
// Always refresh the contact list
_safeControllerUpdate<DirectoryController>( _safeControllerUpdate<DirectoryController>(
onFound: (controller) { onFound: (controller) {
controller.fetchContacts(); controller.fetchContacts();
final contactId = data['ContactId']; // If a specific contact is provided, refresh its notes as well
if (contactId != null) { if (contactId != null) {
controller.fetchCommentsForContact(contactId); controller.fetchCommentsForContact(contactId);
} }
@ -308,6 +293,7 @@ class NotificationActionHandler {
'✅ Directory contacts (and notes if applicable) refreshed from notification.', '✅ Directory contacts (and notes if applicable) refreshed from notification.',
); );
// Refresh notes globally as well
_safeControllerUpdate<NotesController>( _safeControllerUpdate<NotesController>(
onFound: (controller) => controller.fetchNotes(), onFound: (controller) => controller.fetchNotes(),
notFoundMessage: '⚠️ NotesController not found, cannot refresh notes.', notFoundMessage: '⚠️ NotesController not found, cannot refresh notes.',
@ -316,6 +302,7 @@ class NotificationActionHandler {
} }
static void _handleContactNoteModified(Map<String, dynamic> data) { static void _handleContactNoteModified(Map<String, dynamic> data) {
// Refresh both contacts and notes when a note is modified
_handleContactModified(data); _handleContactModified(data);
} }
@ -337,11 +324,6 @@ class NotificationActionHandler {
/// ---------------------- DASHBOARD HANDLER ---------------------- /// ---------------------- DASHBOARD HANDLER ----------------------
static void _handleDashboardUpdate(Map<String, dynamic> data) { static void _handleDashboardUpdate(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored dashboard update from another project.");
return;
}
_safeControllerUpdate<DashboardController>( _safeControllerUpdate<DashboardController>(
onFound: (controller) async { onFound: (controller) async {
final type = data['type'] ?? ''; final type = data['type'] ?? '';
@ -365,9 +347,11 @@ class NotificationActionHandler {
controller.projectController.selectedProjectId.value; controller.projectController.selectedProjectId.value;
final projectIdsString = data['ProjectIds'] ?? ''; final projectIdsString = data['ProjectIds'] ?? '';
// Convert comma-separated string to List<String>
final notificationProjectIds = final notificationProjectIds =
projectIdsString.split(',').map((e) => e.trim()).toList(); projectIdsString.split(',').map((e) => e.trim()).toList();
// Refresh only if current project ID is in the list
if (notificationProjectIds.contains(currentProjectId)) { if (notificationProjectIds.contains(currentProjectId)) {
await controller.fetchDashboardTeams(projectId: currentProjectId); await controller.fetchDashboardTeams(projectId: currentProjectId);
} }
@ -391,40 +375,17 @@ class NotificationActionHandler {
/// ---------------------- UTILITY ---------------------- /// ---------------------- UTILITY ----------------------
static bool _isCurrentProject(Map<String, dynamic> data) {
try {
final dashboard = Get.find<DashboardController>();
final currentProjectId =
dashboard.projectController.selectedProjectId.value;
final notificationProjectId = data['ProjectId']?.toString();
if (notificationProjectId == null || notificationProjectId.isEmpty) {
return true; // No project info allow global refresh
}
return notificationProjectId == currentProjectId;
} catch (e) {
_logger.w("⚠️ Could not verify project context: $e");
return true;
}
}
static void _safeControllerUpdate<T>({ static void _safeControllerUpdate<T>({
required void Function(T controller) onFound, required void Function(T controller) onFound,
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,13 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:on_field_work/controller/project_controller.dart'; import 'package:marco/controller/project_controller.dart';
import 'package:on_field_work/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/api_endpoints.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';
import 'package:on_field_work/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:on_field_work/model/tenant/tenant_list_model.dart'; import 'package:marco/model/tenant/tenant_list_model.dart';
/// Abstract interface for tenant service functionality /// Abstract interface for tenant service functionality
abstract class ITenantService { abstract class ITenantService {
@ -63,39 +63,29 @@ class TenantService implements ITenantService {
{bool hasRetried = false}) async { {bool hasRetried = false}) async {
try { try {
final headers = await _authorizedHeaders(); final headers = await _authorizedHeaders();
logSafe("➡️ GET $_baseUrl/auth/get/user/tenants\nHeaders: $headers",
level: LogLevel.info);
final response = await http.get( final response = await http
Uri.parse("$_baseUrl/auth/get/user/tenants"), .get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers);
headers: headers, final data = jsonDecode(response.body);
);
// Handle empty response BEFORE decoding logSafe(
if (response.body.isEmpty || response.body.trim().isEmpty) { "⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
logSafe("❌ Empty tenant response — auto logout"); level: LogLevel.info);
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) { if (response.statusCode == 200 && data['success'] == true) {
final list = data['data']; logSafe("✅ Tenants fetched successfully.");
if (list is! List) return null; return List<Map<String, dynamic>>.from(data['data']);
return List<Map<String, dynamic>>.from(list);
} }
// TOKEN EXPIRED
if (response.statusCode == 401 && !hasRetried) { if (response.statusCode == 401 && !hasRetried) {
logSafe("⚠️ Unauthorized while fetching tenants. Refreshing token...",
level: LogLevel.warning);
final refreshed = await AuthService.refreshToken(); final refreshed = await AuthService.refreshToken();
if (refreshed) return getTenants(hasRetried: true); if (refreshed) return getTenants(hasRetried: true);
logSafe("❌ Token refresh failed while fetching tenants.",
level: LogLevel.error);
return null; return null;
} }
@ -140,7 +130,7 @@ class TenantService implements ITenantService {
} }
// 🔹 Register FCM token after tenant selection // 🔹 Register FCM token after tenant selection
final fcmToken = LocalStorage.getFcmToken(); final fcmToken = LocalStorage.getFcmToken();
if (fcmToken?.isNotEmpty ?? false) { if (fcmToken?.isNotEmpty ?? false) {
final success = await AuthService.registerDeviceToken(fcmToken!); final success = await AuthService.registerDeviceToken(fcmToken!);
logSafe( logSafe(

View File

@ -1,10 +1,35 @@
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 }
enum RightBarThemeType { light, dark } enum RightBarThemeType { light, dark }
enum ContentThemeColor {
primary,
secondary,
success,
info,
warning,
danger,
light,
dark,
pink,
green,
red,
brandRed;
Color get color {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black;
}
Color get onColor {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white;
}
}
class LeftBarTheme { class LeftBarTheme {
final Color background, onBackground; final Color background, onBackground;
final Color labelColor; final Color labelColor;
@ -18,15 +43,16 @@ class LeftBarTheme {
this.activeItemBackground = const Color(0x15663399), this.activeItemBackground = const Color(0x15663399),
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------//
static final LeftBarTheme lightLeftBarTheme = LeftBarTheme(); static final LeftBarTheme lightLeftBarTheme = LeftBarTheme();
static final LeftBarTheme darkLeftBarTheme = LeftBarTheme( static final LeftBarTheme darkLeftBarTheme = LeftBarTheme(
background: const Color(0xff282c32), background: const Color(0xff282c32),
onBackground: const Color(0xffdcdcdc), onBackground: const Color(0xffdcdcdc),
labelColor: const Color(0xff32BFAE), labelColor: const Color(0xff32BFAE),
activeItemBackground: const Color(0x1532BFAE), activeItemBackground: const Color(0x1532BFAE),
activeItemColor: const Color(0xff32BFAE), activeItemColor: const Color(0xff32BFAE));
);
static LeftBarTheme getThemeFromType(LeftBarThemeType leftBarThemeType) { static LeftBarTheme getThemeFromType(LeftBarThemeType leftBarThemeType) {
switch (leftBarThemeType) { switch (leftBarThemeType) {
@ -47,12 +73,11 @@ class TopBarTheme {
this.onBackground = const Color(0xff313a46), this.onBackground = const Color(0xff313a46),
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------//
static final TopBarTheme lightTopBarTheme = TopBarTheme(); static final TopBarTheme lightTopBarTheme = TopBarTheme();
static final TopBarTheme darkTopBarTheme = TopBarTheme( static final TopBarTheme darkTopBarTheme = TopBarTheme(background: const Color(0xff2c3036), onBackground: const Color(0xffdcdcdc));
background: const Color(0xff2c3036),
onBackground: const Color(0xffdcdcdc),
);
} }
class RightBarTheme { class RightBarTheme {
@ -66,41 +91,19 @@ class RightBarTheme {
this.onDisabled = const Color(0xff313a46), this.onDisabled = const Color(0xff313a46),
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------//
static final RightBarTheme lightRightBarTheme = RightBarTheme( static final RightBarTheme lightRightBarTheme = RightBarTheme(
disabled: const Color(0xffffffff), disabled: const Color(0xffffffff),
onDisabled: const Color(0xffdee2e6), onDisabled: const Color(0xffdee2e6),
activeSwitchBorderColor: const Color(0xff727cf5), activeSwitchBorderColor: const Color(0xff727cf5),
inactiveSwitchBorderColor: const Color(0xffdee2e6), inactiveSwitchBorderColor: const Color(0xffdee2e6));
);
static final RightBarTheme darkRightBarTheme = RightBarTheme( static final RightBarTheme darkRightBarTheme = RightBarTheme(
disabled: const Color(0xff444d57), disabled: const Color(0xff444d57),
activeSwitchBorderColor: const Color(0xff727cf5), activeSwitchBorderColor: const Color(0xff727cf5),
inactiveSwitchBorderColor: const Color(0xffdee2e6), inactiveSwitchBorderColor: const Color(0xffdee2e6),
onDisabled: const Color(0xff515a65), onDisabled: const Color(0xff515a65));
);
}
enum ContentThemeColor {
primary,
secondary,
success,
info,
warning,
danger,
light,
dark,
pink,
green,
red;
Color get color {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black;
}
Color get onColor {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white;
}
} }
class ContentTheme { class ContentTheme {
@ -117,11 +120,29 @@ class ContentTheme {
final Color purple, onPurple; final Color purple, onPurple;
final Color pink, onPink; final Color pink, onPink;
final Color red, onRed; final Color red, onRed;
final Color brandRed, onBrandRed;
final Color cardBackground, cardShadow, cardBorder, cardText, cardTextMuted; final Color cardBackground, cardShadow, cardBorder, cardText, cardTextMuted;
final Color title; final Color title;
final Color disabled, onDisabled; final Color disabled, onDisabled;
Map<ContentThemeColor, Map<String, Color>> get getMappedIntoThemeColor {
var c = AdminTheme.theme.contentTheme;
return {
ContentThemeColor.primary: {'color': c.primary, 'onColor': c.onPrimary},
ContentThemeColor.secondary: {'color': c.secondary, 'onColor': c.onSecondary},
ContentThemeColor.success: {'color': c.success, 'onColor': c.onSuccess},
ContentThemeColor.info: {'color': c.info, 'onColor': c.onInfo},
ContentThemeColor.warning: {'color': c.warning, 'onColor': c.onWarning},
ContentThemeColor.danger: {'color': c.danger, 'onColor': c.onDanger},
ContentThemeColor.light: {'color': c.light, 'onColor': c.onLight},
ContentThemeColor.dark: {'color': c.dark, 'onColor': c.onDark},
ContentThemeColor.pink: {'color': c.pink, 'onColor': c.onPink},
ContentThemeColor.red: {'color': c.red, 'onColor': c.onRed},
ContentThemeColor.brandRed: {'color': c.brandRed, 'onColor': c.onBrandRed},
};
}
ContentTheme({ ContentTheme({
this.background = const Color(0xfffafbfe), this.background = const Color(0xfffafbfe),
this.onBackground = const Color(0xffF1F1F2), this.onBackground = const Color(0xffF1F1F2),
@ -142,11 +163,13 @@ class ContentTheme {
this.dark = const Color(0xff313a46), this.dark = const Color(0xff313a46),
this.onDark = const Color(0xffffffff), this.onDark = const Color(0xffffffff),
this.purple = const Color(0xff800080), this.purple = const Color(0xff800080),
this.onPurple = const Color(0xffffffff), this.onPurple = const Color(0xffFF0000),
this.pink = const Color(0xffff1087), this.pink = const Color(0xffFF1087),
this.onPink = const Color(0xffffffff), this.onPink = const Color(0xffffffff),
this.red = const Color(0xffff0000), this.red = const Color(0xffFF0000),
this.onRed = const Color(0xffffffff), this.onRed = const Color(0xffffffff),
this.brandRed = const Color.fromARGB(255, 255, 0, 0),
this.onBrandRed = const Color(0xffffffff),
this.cardBackground = const Color(0xffffffff), this.cardBackground = const Color(0xffffffff),
this.cardShadow = const Color(0xffffffff), this.cardShadow = const Color(0xffffffff),
this.cardBorder = const Color(0xffffffff), this.cardBorder = const Color(0xffffffff),
@ -157,103 +180,44 @@ class ContentTheme {
this.onDisabled = const Color(0xffffffff), this.onDisabled = const Color(0xffffffff),
}); });
Map<ContentThemeColor, Map<String, Color>> get getMappedIntoThemeColor { //-------------------------------------- Left Bar Theme ----------------------------------------//
return {
ContentThemeColor.primary: {'color': primary, 'onColor': onPrimary},
ContentThemeColor.secondary: {'color': secondary, 'onColor': onSecondary},
ContentThemeColor.success: {'color': success, 'onColor': onSuccess},
ContentThemeColor.info: {'color': info, 'onColor': onInfo},
ContentThemeColor.warning: {'color': warning, 'onColor': onWarning},
ContentThemeColor.danger: {'color': danger, 'onColor': onDanger},
ContentThemeColor.light: {'color': light, 'onColor': onLight},
ContentThemeColor.dark: {'color': dark, 'onColor': onDark},
ContentThemeColor.pink: {'color': pink, 'onColor': onPink},
ContentThemeColor.red: {'color': red, 'onColor': onRed},
};
}
ContentTheme copyWith({ static final ContentTheme lightContentTheme = ContentTheme(
Color? primary, primary: Color(0xff663399),
Color? onPrimary, background: const Color(0xfffafbfe),
Color? secondary, onBackground: const Color(0xff313a46),
Color? onSecondary, cardBorder: const Color(0xffe8ecf1),
Color? background, cardBackground: const Color(0xffffffff),
Color? onBackground, cardShadow: const Color(0xff9aa1ab),
}) { cardText: const Color(0xff6c757d),
return ContentTheme( title: const Color(0xff6c757d),
primary: primary ?? this.primary, cardTextMuted: const Color(0xff98a6ad),
onPrimary: onPrimary ?? this.onPrimary, brandRed: const Color.fromARGB(255, 255, 0, 0),
secondary: secondary ?? this.secondary, onBrandRed: const Color(0xffffffff),
onSecondary: onSecondary ?? this.onSecondary, );
background: background ?? this.background,
onBackground: onBackground ?? this.onBackground,
success: success,
onSuccess: onSuccess,
danger: danger,
onDanger: onDanger,
warning: warning,
onWarning: onWarning,
info: info,
onInfo: onInfo,
light: light,
onLight: onLight,
dark: dark,
onDark: onDark,
purple: purple,
onPurple: onPurple,
pink: pink,
onPink: onPink,
red: red,
onRed: onRed,
cardBackground: cardBackground,
cardShadow: cardShadow,
cardBorder: cardBorder,
cardText: cardText,
cardTextMuted: cardTextMuted,
title: title,
disabled: disabled,
onDisabled: onDisabled,
);
}
static ContentTheme withColorTheme( static final ContentTheme darkContentTheme = ContentTheme(
ColorThemeType colorTheme, { primary: Color(0xff32BFAE),
ThemeMode mode = ThemeMode.light, background: const Color(0xff343a40),
}) { onBackground: const Color(0xffF1F1F2),
final baseTheme = mode == ThemeMode.light disabled: const Color(0xff444d57),
? ContentTheme() onDisabled: const Color(0xff515a65),
: ContentTheme( cardBorder: const Color(0xff464f5b),
primary: const Color(0xff32BFAE), cardBackground: const Color(0xff37404a),
background: const Color(0xff343a40), cardShadow: const Color(0xff01030E),
onBackground: const Color(0xffF1F1F2), cardText: const Color(0xffaab8c5),
cardBorder: const Color(0xff464f5b), title: const Color(0xffaab8c5),
cardBackground: const Color(0xff37404a), cardTextMuted: const Color(0xff8391a2),
cardShadow: const Color(0xff01030E), brandRed: const Color.fromARGB(255, 255, 0, 0),
cardText: const Color(0xffaab8c5), onBrandRed: const Color(0xffffffff),
title: const Color(0xffaab8c5), );
cardTextMuted: const Color(0xff8391a2),
);
switch (colorTheme) {
case ColorThemeType.purple:
return baseTheme.copyWith(primary: const Color(0xff663399), onPrimary: Colors.white);
case ColorThemeType.red:
return baseTheme.copyWith(primary: const Color(0xffff0000), onPrimary: Colors.white);
case ColorThemeType.green:
return baseTheme.copyWith(primary: const Color(0xff49BF3C), onPrimary: Colors.white);
case ColorThemeType.blue:
return baseTheme.copyWith(primary: const Color(0xff007bff), onPrimary: Colors.white);
}
}
} }
enum ColorThemeType { purple, red, green, blue }
class AdminTheme { class AdminTheme {
final ContentTheme contentTheme;
final LeftBarTheme leftBarTheme; final LeftBarTheme leftBarTheme;
final RightBarTheme rightBarTheme; final RightBarTheme rightBarTheme;
final TopBarTheme topBarTheme; final TopBarTheme topBarTheme;
final ContentTheme contentTheme;
AdminTheme({ AdminTheme({
required this.leftBarTheme, required this.leftBarTheme,
@ -262,22 +226,19 @@ class AdminTheme {
required this.contentTheme, required this.contentTheme,
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------//
static AdminTheme theme = AdminTheme( static AdminTheme theme = 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.lightContentTheme);
);
static void setTheme() { static void setTheme() {
final themeMode = ThemeCustomizer.instance.theme;
final colorTheme = ThemeCustomizer.instance.colorTheme;
theme = AdminTheme( theme = AdminTheme(
leftBarTheme: themeMode == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme, leftBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme,
topBarTheme: themeMode == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme, topBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme,
rightBarTheme: themeMode == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme, rightBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme,
contentTheme: ContentTheme.withColorTheme(colorTheme, mode: themeMode), contentTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? ContentTheme.darkContentTheme : ContentTheme.lightContentTheme);
);
} }
} }

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/dashboard'),
)); ));
bool isMobile = true; bool isMobile = true;
try { try {
@ -257,7 +257,7 @@ class AppColors {
static ColorGroup pink = ColorGroup(Color(0xffFFC2D9), Color(0xffF5005E)); static ColorGroup pink = ColorGroup(Color(0xffFFC2D9), Color(0xffF5005E));
static ColorGroup violet = ColorGroup(Color(0xffD0BADE), Color(0xff4E2E60)); static ColorGroup violet = ColorGroup(Color(0xffD0BADE), Color(0xff4E2E60));
static ColorGroup blue = ColorGroup(Color(0xffADD8FF), Color(0xff004A8F)); static ColorGroup blue = ColorGroup(Color(0xffADD8FF), Color.fromRGBO(0, 74, 143, 1));
static ColorGroup green = ColorGroup(Color(0xffAFE9DA), Color(0xff165041)); static ColorGroup green = ColorGroup(Color(0xffAFE9DA), Color(0xff165041));
static ColorGroup orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A)); static ColorGroup orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A));
static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC)); static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC));

View File

@ -1,14 +1,14 @@
import 'dart:convert'; import 'dart:convert';
import 'package:marco/helpers/services/json_decoder.dart';
import 'package:marco/helpers/services/localizations/language.dart';
import 'package:marco/helpers/services/localizations/translator.dart';
import 'package:marco/helpers/services/navigation_services.dart';
import 'package:marco/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/theme/app_notifier.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:on_field_work/helpers/services/json_decoder.dart';
import 'package:on_field_work/helpers/services/localizations/language.dart';
import 'package:on_field_work/helpers/services/localizations/translator.dart';
import 'package:on_field_work/helpers/services/navigation_services.dart';
import 'package:on_field_work/helpers/theme/admin_theme.dart';
import 'package:on_field_work/helpers/theme/app_notifier.dart';
import 'package:on_field_work/helpers/theme/app_theme.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
typedef ThemeChangeCallback = void Function( typedef ThemeChangeCallback = void Function(
ThemeCustomizer oldVal, ThemeCustomizer newVal); ThemeCustomizer oldVal, ThemeCustomizer newVal);
@ -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;
bool rightBarOpen = false; bool rightBarOpen = false;
bool leftBarCondensed = false; bool leftBarCondensed = false;
@ -33,8 +33,6 @@ class ThemeCustomizer {
static Future<void> init() async { static Future<void> init() async {
await initLanguage(); await initLanguage();
await _loadColorTheme();
_notify();
} }
static initLanguage() async { static initLanguage() async {
@ -42,7 +40,7 @@ class ThemeCustomizer {
} }
String toJSON() { String toJSON() {
return jsonEncode({'theme': theme.name, 'colorTheme': colorTheme.name}); return jsonEncode({'theme': theme.name});
} }
static ThemeCustomizer fromJSON(String? json) { static ThemeCustomizer fromJSON(String? json) {
@ -51,8 +49,6 @@ class ThemeCustomizer {
JSONDecoder decoder = JSONDecoder(json); JSONDecoder decoder = JSONDecoder(json);
instance.theme = instance.theme =
decoder.getEnum('theme', ThemeMode.values, ThemeMode.light); decoder.getEnum('theme', ThemeMode.values, ThemeMode.light);
instance.colorTheme = decoder.getEnum(
'colorTheme', ColorThemeType.values, ColorThemeType.red);
} }
return instance; return instance;
} }
@ -77,11 +73,6 @@ class ThemeCustomizer {
} }
} }
/// Public method to trigger theme updates externally
static void applyThemeChange() {
_notify();
}
static void notify() { static void notify() {
for (var value in _notifier) { for (var value in _notifier) {
value(oldInstance, instance); value(oldInstance, instance);
@ -121,46 +112,12 @@ class ThemeCustomizer {
tc.topBarTheme = topBarTheme; tc.topBarTheme = topBarTheme;
tc.rightBarOpen = rightBarOpen; tc.rightBarOpen = rightBarOpen;
tc.leftBarCondensed = leftBarCondensed; tc.leftBarCondensed = leftBarCondensed;
tc.colorTheme = colorTheme;
tc.currentLanguage = currentLanguage.clone(); tc.currentLanguage = currentLanguage.clone();
return tc; return tc;
} }
@override @override
String toString() { String toString() {
return 'ThemeCustomizer{theme: $theme, colorTheme: $colorTheme}'; return 'ThemeCustomizer{theme: $theme}';
}
// ---------------------------------------------------------------------------
// 🟢 Color Theme Persistence
// ---------------------------------------------------------------------------
static const _colorThemeKey = 'color_theme_type';
/// Save selected color theme
static Future<void> saveColorTheme(ColorThemeType type) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_colorThemeKey, type.name);
instance.colorTheme = type;
_notify();
}
/// Load saved color theme (called at startup)
static Future<void> _loadColorTheme() async {
final prefs = await SharedPreferences.getInstance();
final savedType = prefs.getString(_colorThemeKey);
if (savedType != null) {
instance.colorTheme = ColorThemeType.values.firstWhere(
(e) => e.name == savedType,
orElse: () => ColorThemeType.red,
);
}
}
/// Change color theme & persist
static Future<void> changeColorTheme(ColorThemeType type) async {
oldInstance = instance.clone();
instance.colorTheme = type;
await saveColorTheme(type);
} }
} }

View File

@ -1,273 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/widgets/wave_background.dart';
import 'package:on_field_work/helpers/theme/admin_theme.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
class ThemeOption {
final String label;
final Color primary;
final Color button;
final Color brand;
final ColorThemeType colorThemeType;
ThemeOption(
this.label, this.primary, this.button, this.brand, this.colorThemeType);
}
final List<ThemeOption> themeOptions = [
ThemeOption(
"Theme 1", Colors.red, Colors.red, Colors.red, ColorThemeType.red),
ThemeOption(
"Theme 2",
const Color(0xFF49BF3C),
const Color(0xFF49BF3C),
const Color(0xFF49BF3C),
ColorThemeType.green,
),
ThemeOption(
"Theme 3",
const Color(0xFF3F51B5),
const Color(0xFF3F51B5),
const Color(0xFF3F51B5),
ColorThemeType.blue,
),
ThemeOption(
"Theme 4",
const Color(0xFF663399),
const Color(0xFF663399),
const Color(0xFF663399),
ColorThemeType.purple,
),
];
class ThemeController extends GetxController {
RxInt selectedIndex = 0.obs;
RxBool showApplied = false.obs;
void init() {
final currentPrimary = AdminTheme.theme.contentTheme.primary;
int index = themeOptions
.indexWhere((opt) => opt.primary.value == currentPrimary.value);
selectedIndex.value = index == -1 ? 0 : index;
}
void applyTheme(int index) async {
selectedIndex.value = index;
showApplied.value = true;
ThemeCustomizer.instance.colorTheme = themeOptions[index].colorThemeType;
ThemeCustomizer.applyThemeChange();
await Future.delayed(const Duration(milliseconds: 600));
showApplied.value = false;
// Navigate to dashboard after applying theme
Get.offAllNamed('/dashboard');
}
}
class ThemeEditorWidget extends StatefulWidget {
final VoidCallback onClose;
const ThemeEditorWidget({super.key, required this.onClose});
@override
_ThemeEditorWidgetState createState() => _ThemeEditorWidgetState();
}
class _ThemeEditorWidgetState extends State<ThemeEditorWidget> {
final ThemeController themeController = Get.put(ThemeController());
@override
void initState() {
super.initState();
themeController.init();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header row with title and close button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyLarge("Theme Customization", fontWeight: 600),
IconButton(
icon: const Icon(Icons.close),
onPressed: widget.onClose,
tooltip: "Back",
iconSize: 20,
),
],
),
const SizedBox(height: 12),
// Theme cards wrapped in reactive Obx widget
Center(
child: Obx(
() => Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: List.generate(themeOptions.length, (i) {
return ThemeCard(
themeOption: themeOptions[i],
isSelected: themeController.selectedIndex.value == i,
onTap: () => themeController.applyTheme(i),
);
}),
),
),
),
const SizedBox(height: 12),
// Applied indicator reactive widget
Obx(
() => themeController.showApplied.value
? Padding(
padding: const EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle,
color:
themeOptions[themeController.selectedIndex.value]
.brand,
size: 20,
),
const SizedBox(width: 6),
Text(
"Theme Applied!",
style: TextStyle(
color: themeOptions[
themeController.selectedIndex.value]
.brand,
fontWeight: FontWeight.w700,
),
),
],
),
)
: const SizedBox(),
),
const SizedBox(height: 16),
const Text(
"Preview and select a theme. You can change this anytime.",
style: TextStyle(fontSize: 13, color: Colors.black54),
),
],
),
);
}
}
class ThemeCard extends StatelessWidget {
final ThemeOption themeOption;
final bool isSelected;
final VoidCallback onTap;
const ThemeCard({
Key? key,
required this.themeOption,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 80,
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
elevation: isSelected ? 4 : 1,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? themeOption.brand : Colors.transparent,
width: 2,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 80,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Stack(
fit: StackFit.expand,
children: [
CustomPaint(
painter: RedWavePainter(themeOption.brand, 0.15)),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Hello, User!",
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w600,
color: themeOption.primary,
fontSize: 12,
),
),
const SizedBox(height: 4),
SizedBox(
height: 18,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: themeOption.button,
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 1,
textStyle: const TextStyle(fontSize: 10),
),
onPressed: () {},
child: const Text("Welcome"),
),
),
],
),
),
],
),
),
),
const SizedBox(height: 6),
Text(
themeOption.label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: Colors.grey[700],
),
),
],
),
),
),
),
);
}
}

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,17 +1,15 @@
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';
class BaseBottomSheet extends StatefulWidget { class BaseBottomSheet extends StatelessWidget {
final String title; final String title;
final String? subtitle;
final Widget child; final Widget child;
final VoidCallback onCancel; final VoidCallback onCancel;
final VoidCallback onSubmit; final VoidCallback onSubmit;
final bool isSubmitting; final bool isSubmitting;
final String submitText; final String submitText;
final Color? submitColor; final Color submitColor;
final IconData submitIcon; final IconData submitIcon;
final bool showButtons; final bool showButtons;
final Widget? bottomContent; final Widget? bottomContent;
@ -22,26 +20,18 @@ class BaseBottomSheet extends StatefulWidget {
required this.child, required this.child,
required this.onCancel, required this.onCancel,
required this.onSubmit, required this.onSubmit,
this.subtitle,
this.isSubmitting = false, this.isSubmitting = false,
this.submitText = 'Submit', this.submitText = 'Submit',
this.submitColor, this.submitColor = Colors.indigo,
this.submitIcon = Icons.check_circle_outline, this.submitIcon = Icons.check_circle_outline,
this.showButtons = true, this.showButtons = true,
this.bottomContent, this.bottomContent,
}); });
@override
State<BaseBottomSheet> createState() => _BaseBottomSheetState();
}
class _BaseBottomSheetState extends State<BaseBottomSheet> with UIMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final effectiveSubmitColor =
widget.submitColor ?? contentTheme.primary;
return SingleChildScrollView( return SingleChildScrollView(
padding: mediaQuery.viewInsets, padding: mediaQuery.viewInsets,
@ -60,50 +50,33 @@ class _BaseBottomSheetState extends State<BaseBottomSheet> with UIMixin {
], ],
), ),
child: SafeArea( child: SafeArea(
// 👈 prevents overlap with nav bar
top: false, top: false,
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MySpacing.height(5), MySpacing.height(5),
Center( Container(
child: Container( width: 40,
width: 40, height: 5,
height: 5, decoration: BoxDecoration(
decoration: BoxDecoration( color: Colors.grey.shade300,
color: Colors.grey.shade300, borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10),
),
), ),
), ),
MySpacing.height(12), MySpacing.height(12),
Center( MyText.titleLarge(title, fontWeight: 700),
child: MyText.titleLarge(
widget.title,
fontWeight: 700,
textAlign: TextAlign.center,
),
),
if (widget.subtitle != null &&
widget.subtitle!.isNotEmpty) ...[
MySpacing.height(4),
MyText.bodySmall(
widget.subtitle!,
fontWeight: 600,
color: Colors.grey[700],
),
],
MySpacing.height(12), MySpacing.height(12),
widget.child, child,
MySpacing.height(12), MySpacing.height(12),
if (widget.showButtons) ...[ if (showButtons) ...[
Row( Row(
children: [ children: [
Expanded( Expanded(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: widget.onCancel, onPressed: onCancel,
icon: const Icon(Icons.close, color: Colors.white), icon: const Icon(Icons.close, color: Colors.white),
label: MyText.bodyMedium( label: MyText.bodyMedium(
"Cancel", "Cancel",
@ -115,40 +88,34 @@ class _BaseBottomSheetState extends State<BaseBottomSheet> with UIMixin {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
padding: padding: const EdgeInsets.symmetric(vertical: 8),
const EdgeInsets.symmetric(vertical: 8),
), ),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: onPressed: isSubmitting ? null : onSubmit,
widget.isSubmitting ? null : widget.onSubmit, icon: Icon(submitIcon, color: Colors.white),
icon:
Icon(widget.submitIcon, color: Colors.white),
label: MyText.bodyMedium( label: MyText.bodyMedium(
widget.isSubmitting isSubmitting ? "Submitting..." : submitText,
? "Submitting..."
: widget.submitText,
color: Colors.white, color: Colors.white,
fontWeight: 600, fontWeight: 600,
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: effectiveSubmitColor, backgroundColor: submitColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
padding: padding: const EdgeInsets.symmetric(vertical: 8),
const EdgeInsets.symmetric(vertical: 8),
), ),
), ),
), ),
], ],
), ),
if (widget.bottomContent != null) ...[ if (bottomContent != null) ...[
MySpacing.height(12), MySpacing.height(12),
widget.bottomContent!, bottomContent!,
], ],
], ],
], ],

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package: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 ContactPickerHelper { class ContactPickerHelper {
static Future<String?> pickIndianPhoneNumber(BuildContext context) async { static Future<String?> pickIndianPhoneNumber(BuildContext context) async {

View File

@ -1,9 +1,6 @@
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class DateTimeUtils { class DateTimeUtils {
/// Default date format
static const String defaultFormat = 'dd MMM yyyy';
/// Converts a UTC datetime string to local time and formats it. /// Converts a UTC datetime string to local time and formats it.
static String convertUtcToLocal(String utcTimeString, static String convertUtcToLocal(String utcTimeString,
{String format = 'dd-MM-yyyy'}) { {String format = 'dd-MM-yyyy'}) {

View File

@ -1,7 +1,7 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.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 LauncherUtils { class LauncherUtils {
/// Launches the phone dialer with the provided phone number /// Launches the phone dialer with the provided phone number

View File

@ -1,7 +1,7 @@
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_theme.dart'; import 'package:marco/helpers/theme/app_theme.dart';
import 'package:on_field_work/helpers/widgets/my_dashed_divider.dart'; import 'package:marco/helpers/widgets/my_dashed_divider.dart';
import 'package:on_field_work/helpers/widgets/my_navigation_mixin.dart'; import 'package:marco/helpers/widgets/my_navigation_mixin.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
mixin UIMixin { mixin UIMixin {

View File

@ -25,16 +25,14 @@ class Permissions {
// ------------------- Project Infrastructure -------------------------- // ------------------- Project Infrastructure --------------------------
/// Permission to manage project infrastructure (e.g., site details) /// Permission to manage project infrastructure (e.g., site details)
static const String manageProjectInfra = static const String manageProjectInfra = "cf2825ad-453b-46aa-91d9-27c124d63373";
"cf2825ad-453b-46aa-91d9-27c124d63373";
/// Permission to view infrastructure-related details /// Permission to view infrastructure-related details
static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"; static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4";
// ------------------- Attendance Management --------------------------- // ------------------- Attendance Management ---------------------------
/// Permission to regularize (edit/update) attendance records /// Permission to regularize (edit/update) attendance records
static const String regularizeAttendance = static const String regularizeAttendance = "57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
"57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
// ------------------- Task Management --------------------------------- // ------------------- Task Management ---------------------------------
/// Permission to create and manage tasks /// Permission to create and manage tasks
@ -92,8 +90,7 @@ class Permissions {
// ------------------- Application Roles ------------------------------- // ------------------- Application Roles -------------------------------
/// Application role ID for users with full expense management rights /// Application role ID for users with full expense management rights
static const String expenseManagement = static const String expenseManagement = "a4e25142-449b-4334-a6e5-22f70e4732d7";
"a4e25142-449b-4334-a6e5-22f70e4732d7";
// ------------------- Document Entities ------------------------------- // ------------------- Document Entities -------------------------------
/// Entity ID for project documents /// Entity ID for project documents
@ -121,73 +118,3 @@ class Permissions {
/// Permission to verify documents /// Permission to verify documents
static const String verifyDocument = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0"; static const String verifyDocument = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0";
} }
/// Contains constants for menu item IDs fetched from the sidebar menu API.
class MenuItems {
/// Dashboard menu
static const String dashboard = "29e03eda-03e8-4714-92fa-67ae0dc53202";
/// Daily Task Planning menu
static const String dailyTaskPlanning =
"77ac5205-f823-442e-b9e4-2420d658aa02";
/// Daily Progress Report menu
static const String dailyProgressReport =
"299e3cf5-d034-4403-b4a1-ea46d2714832";
/// Employees menu
static const String employees = "78f0206d-c6cc-44d0-832a-2031ed203018";
/// Attendance menu
static const String attendance = "2f212030-f36b-456c-8e7c-11f00f9ba42b";
/// Directory menu
static const String directory = "31bc367b-7c58-4604-95eb-da059a384103";
/// Expense & Reimbursement menu
static const String expenseReimbursement =
"0f0dc1a7-1aca-4cdb-9d7a-8a769ce40728";
/// Payment Requests menu
static const String paymentRequests = "b350a59f-2372-4f68-8dcf-f7cfc44523ca";
/// Advance Payment Statements menu
static const String advancePaymentStatements =
"e0251cc1-e6d9-417a-9c76-489cc4b6c347";
/// Finance menu
static const String finance = "5ac409dd-bbe0-4d56-bcb9-229bd3a6353c";
/// Documents menu
static const String documents = "92d2cc39-9e6a-46b2-ae50-84fbf83c95d3";
/// Service Projects
static const String serviceProjects = "7faddfe7-994b-4712-91c2-32ba44129d9b";
/// Infrastructure Projects
static const String infraProjects = "5fab4b88-c9a0-417b-aca2-130980fdb0cf";
}
/// Contains all job status IDs used across the application.
class JobStatus {
/// Level 1 - New
static const String newStatus = "32d76a02-8f44-4aa0-9b66-c3716c45a918";
/// Level 2 - Assigned
static const String assigned = "cfa1886d-055f-4ded-84c6-42a2a8a14a66";
/// Level 3 - In Progress
static const String inProgress = "5a6873a5-fed7-4745-a52f-8f61bf3bd72d";
/// Level 4 - Work Done
static const String workDone = "aab71020-2fb8-44d9-9430-c9a7e9bf33b0";
/// Level 5 - Review Done
static const String reviewDone = "ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7";
/// Level 6 - Closed
static const String closed = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69";
/// Level 7 - On Hold
static const String onHold = "75a0c8b8-9c6a-41af-80bf-b35bab722eb2";
}

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