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.

View File

@ -15,7 +15,7 @@ if (keystorePropertiesFile.exists()) {
android {
// 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
compileSdk = flutter.compileSdkVersion
// Set the NDK version based on Flutter's configuration
@ -37,7 +37,7 @@ android {
// Default configuration for your application
defaultConfig {
// 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
minSdk = 23
targetSdk = flutter.targetSdkVersion

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
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
}

View File

@ -14,7 +14,7 @@ YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# App info
APP_NAME="On Field Work"
APP_NAME="Marco"
BUILD_DIR="build/app/outputs"
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"

View File

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

View File

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

View File

@ -8,5 +8,5 @@ class AppConstant {
static int iOSAppVersion = 1;
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:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:geolocator/geolocator.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/controller/project_controller.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_image_compressor.dart';
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
import 'package:on_field_work/model/attendance/attendance_log_model.dart';
import 'package:on_field_work/model/attendance/attendance_log_view_model.dart';
import 'package:on_field_work/model/attendance/attendance_model.dart';
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/project_model.dart';
import 'package:on_field_work/model/regularization_log_model.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:marco/model/attendance/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/attendance/attendance_log_model.dart';
import 'package:marco/model/regularization_log_model.dart';
import 'package:marco/model/attendance/attendance_log_view_model.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
import 'package:marco/controller/project_controller.dart';
class AttendanceController extends GetxController {
// ------------------ Data Models ------------------
final List<AttendanceModel> attendances = <AttendanceModel>[];
final List<ProjectModel> projects = <ProjectModel>[];
final List<EmployeeModel> employees = <EmployeeModel>[];
final List<AttendanceLogModel> attendanceLogs = <AttendanceLogModel>[];
final List<RegularizationLogModel> regularizationLogs =
<RegularizationLogModel>[];
final List<AttendanceLogViewModel> attendenceLogsView =
<AttendanceLogViewModel>[];
// Data models
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
List<EmployeeModel> employees = [];
List<AttendanceLogModel> attendanceLogs = [];
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
// ------------------ Organizations ------------------
final List<Organization> organizations = <Organization>[];
List<Organization> organizations = [];
Organization? selectedOrganization;
final RxBool isLoadingOrganizations = false.obs;
final isLoadingOrganizations = false.obs;
// ------------------ States ------------------
String selectedTab = 'todaysAttendance';
// States
String selectedTab = 'todaysAttendance';
DateTime? startDateAttendance;
DateTime? endDateAttendance;
// Reactive date range
final Rx<DateTime> startDateAttendance =
DateTime.now().subtract(const Duration(days: 7)).obs;
final Rx<DateTime> endDateAttendance =
DateTime.now().subtract(const Duration(days: 1)).obs;
final RxBool isLoading = true.obs;
final RxBool isLoadingProjects = true.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;
final isLoading = true.obs;
final isLoadingProjects = true.obs;
final isLoadingEmployees = true.obs;
final isLoadingAttendanceLogs = true.obs;
final isLoadingRegularizationLogs = true.obs;
final isLoadingLogView = true.obs;
final uploadingStates = <String, RxBool>{}.obs;
var showPendingOnly = false.obs;
@override
void onInit() {
super.onInit();
_initializeDefaults();
// 🔹 Fetch organizations for the selected project
final projectId = Get.find<ProjectController>().selectedProject?.id;
if (projectId != null) {
fetchOrganizations(projectId);
}
}
void _initializeDefaults() {
@ -69,60 +62,55 @@ class AttendanceController extends GetxController {
}
void _setDefaultDateRange() {
final DateTime today = DateTime.now();
startDateAttendance.value = today.subtract(const Duration(days: 7));
endDateAttendance.value = today.subtract(const Duration(days: 1));
final today = DateTime.now();
startDateAttendance = today.subtract(const Duration(days: 7));
endDateAttendance = today.subtract(const Duration(days: 1));
logSafe(
'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}',
);
"Default date range set: $startDateAttendance to $endDateAttendance");
}
// ------------------ Computed Filters ------------------
List<EmployeeModel> get filteredEmployees {
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 ------------------
// ------------------ Project & Employee ------------------
/// Called when a notification says attendance has been updated
Future<void> refreshDataFromNotification({String? projectId}) async {
projectId ??= Get.find<ProjectController>().selectedProject?.id;
if (projectId == null) {
logSafe(
'No project selected for attendance refresh from notification',
level: LogLevel.warning,
);
logSafe("No project selected for attendance refresh from notification",
level: LogLevel.warning);
return;
}
await fetchProjectData(projectId);
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 {
@ -130,105 +118,105 @@ class AttendanceController extends GetxController {
isLoadingEmployees.value = true;
final List<dynamic>? response = await ApiService.getTodaysAttendance(
final response = await ApiService.getTodaysAttendance(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
employees
..clear()
..addAll(
response
.map<EmployeeModel>(
(dynamic e) => EmployeeModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
for (final EmployeeModel emp in employees) {
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
logSafe(
'Employees fetched: ${employees.length} for project $projectId',
);
logSafe("Employees fetched: ${employees.length} for project $projectId");
} else {
logSafe(
'Failed to fetch employees for project $projectId',
level: LogLevel.error,
);
logSafe("Failed to fetch employees for project $projectId",
level: LogLevel.error);
}
isLoadingEmployees.value = false;
update();
}
Future<void> fetchOrganizations(String projectId) async {
isLoadingOrganizations.value = true;
// Keep original return type inference from your ApiService
final response = await ApiService.getAssignedOrganizations(projectId);
if (response != null) {
organizations
..clear()
..addAll(response.data);
logSafe('Organizations fetched: ${organizations.length}');
organizations = response.data;
logSafe("Organizations fetched: ${organizations.length}");
} else {
logSafe(
'Failed to fetch organizations for project $projectId',
level: LogLevel.error,
);
logSafe("Failed to fetch organizations for project $projectId",
level: LogLevel.error);
}
isLoadingOrganizations.value = false;
update();
}
// ------------------ Attendance Capture ------------------
Future<bool> captureAndUploadAttendance(
String id,
String employeeId,
String projectId, {
String comment = 'Marked via mobile app',
String comment = "Marked via mobile app",
required int action,
bool imageCapture = true,
String? markTime,
String? date,
String? markTime, // still optional in controller
String? date, // new optional param
}) async {
try {
_setUploading(employeeId, true);
uploadingStates[employeeId]?.value = true;
final XFile? image = await _captureAndPrepareImage(
employeeId: employeeId,
imageCapture: imageCapture,
);
if (imageCapture && image == null) {
return false;
XFile? image;
if (imageCapture) {
image = await ImagePicker()
.pickImage(source: ImageSource.camera, imageQuality: 80);
if (image == null) {
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 (position == null) return false;
if (!await _handleLocationPermission()) return false;
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
final String imageName = imageCapture
? ApiService.generateImageName(
employeeId,
employees.length + 1,
)
: '';
final imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1)
: "";
final DateTime effectiveDate =
_resolveEffectiveDateForAction(action, employeeId);
// ---------------- DATE / TIME LOGIC ----------------
final now = DateTime.now();
final DateTime now = DateTime.now();
final String formattedMarkTime =
markTime ?? DateFormat('hh:mm a').format(now);
final String formattedDate =
// Default effectiveDate = now
DateTime effectiveDate = now;
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);
final bool result = await ApiService.uploadAttendanceImage(
// ---------------- API CALL ----------------
final result = await ApiService.uploadAttendanceImage(
id,
employeeId,
image,
@ -243,99 +231,15 @@ class AttendanceController extends GetxController {
date: formattedDate,
);
if (result) {
logSafe(
'Attendance uploaded for $employeeId, action: $action, date: $formattedDate',
);
if (Get.isRegistered<DashboardController>()) {
final DashboardController dashboardController =
Get.find<DashboardController>();
await dashboardController.fetchTodaysAttendance(projectId);
}
}
logSafe(
"Attendance uploaded for $employeeId, action: $action, date: $formattedDate");
return result;
} catch (e, stacktrace) {
logSafe(
'Error uploading attendance',
level: LogLevel.error,
error: e,
stackTrace: stacktrace,
);
logSafe("Error uploading attendance",
level: LogLevel.error, error: e, stackTrace: stacktrace);
return false;
} finally {
_setUploading(employeeId, 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;
uploadingStates[employeeId]?.value = false;
}
}
@ -345,19 +249,14 @@ class AttendanceController extends GetxController {
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
logSafe(
'Location permissions are denied',
level: LogLevel.warning,
);
logSafe('Location permissions are denied', level: LogLevel.warning);
return false;
}
}
if (permission == LocationPermission.deniedForever) {
logSafe(
'Location permissions are permanently denied',
level: LogLevel.error,
);
logSafe('Location permissions are permanently denied',
level: LogLevel.error);
return false;
}
@ -365,40 +264,26 @@ class AttendanceController extends GetxController {
}
// ------------------ Attendance Logs ------------------
Future<void> fetchAttendanceLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
Future<void> fetchAttendanceLogs(String? projectId,
{DateTime? dateFrom, DateTime? dateTo}) async {
if (projectId == null) return;
isLoadingAttendanceLogs.value = true;
final List<dynamic>? response = await ApiService.getAttendanceLogs(
final response = await ApiService.getAttendanceLogs(
projectId,
dateFrom: dateFrom,
dateTo: dateTo,
organizationId: selectedOrganization?.id,
);
if (response != null) {
attendanceLogs
..clear()
..addAll(
response
.map<AttendanceLogModel>(
(dynamic e) => AttendanceLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
logSafe('Attendance logs fetched: ${attendanceLogs.length}');
attendanceLogs =
response.map((e) => AttendanceLogModel.fromJson(e)).toList();
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
} else {
logSafe(
'Failed to fetch attendance logs for project $projectId',
level: LogLevel.error,
);
logSafe("Failed to fetch attendance logs for project $projectId",
level: LogLevel.error);
}
isLoadingAttendanceLogs.value = false;
@ -406,70 +291,45 @@ class AttendanceController extends GetxController {
}
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final Map<String, List<AttendanceLogModel>> groupedLogs =
<String, List<AttendanceLogModel>>{};
final groupedLogs = <String, List<AttendanceLogModel>>{};
for (final AttendanceLogModel logItem in attendanceLogs) {
final String checkInDate = logItem.checkIn != null
for (var logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown';
groupedLogs.putIfAbsent(
checkInDate,
() => <AttendanceLogModel>[],
)..add(logItem);
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
}
final List<MapEntry<String, List<AttendanceLogModel>>> sortedEntries =
groupedLogs.entries.toList()
..sort(
(MapEntry<String, List<AttendanceLogModel>> a,
MapEntry<String, List<AttendanceLogModel>> b) {
if (a.key == 'Unknown') return 1;
if (b.key == 'Unknown') return -1;
final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) {
if (a.key == 'Unknown') return 1;
if (b.key == 'Unknown') return -1;
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
});
final DateTime dateA = DateFormat('dd MMM yyyy').parse(a.key);
final DateTime dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
},
);
return Map<String, List<AttendanceLogModel>>.fromEntries(
sortedEntries,
);
return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
}
// ------------------ Regularization Logs ------------------
Future<void> fetchRegularizationLogs(String? projectId) async {
if (projectId == null) return;
isLoadingRegularizationLogs.value = true;
final List<dynamic>? response = await ApiService.getRegularizationLogs(
final response = await ApiService.getRegularizationLogs(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
regularizationLogs
..clear()
..addAll(
response
.map<RegularizationLogModel>(
(dynamic e) => RegularizationLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.toList(),
);
logSafe(
'Regularization logs fetched: ${regularizationLogs.length}',
);
regularizationLogs =
response.map((e) => RegularizationLogModel.fromJson(e)).toList();
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
} else {
logSafe(
'Failed to fetch regularization logs for project $projectId',
level: LogLevel.error,
);
logSafe("Failed to fetch regularization logs for project $projectId",
level: LogLevel.error);
}
isLoadingRegularizationLogs.value = false;
@ -477,38 +337,22 @@ class AttendanceController extends GetxController {
}
// ------------------ Attendance Log View ------------------
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
isLoadingLogView.value = true;
final List<dynamic>? response = await ApiService.getAttendanceLogView(id);
final response = await ApiService.getAttendanceLogView(id);
if (response != null) {
attendenceLogsView
..clear()
..addAll(
response
.map<AttendanceLogViewModel>(
(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');
attendenceLogsView =
response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000))
.compareTo(a.activityTime ?? DateTime(2000)));
logSafe("Attendance log view fetched for ID: $id");
} else {
logSafe(
'Failed to fetch attendance log view for ID $id',
level: LogLevel.error,
);
logSafe("Failed to fetch attendance log view for ID $id",
level: LogLevel.error);
}
isLoadingLogView.value = false;
@ -516,6 +360,7 @@ class AttendanceController extends GetxController {
}
// ------------------ Combined Load ------------------
Future<void> loadAttendanceData(String projectId) async {
isLoading.value = true;
await fetchProjectData(projectId);
@ -527,6 +372,7 @@ class AttendanceController extends GetxController {
await fetchOrganizations(projectId);
// Call APIs depending on the selected tab only
switch (selectedTab) {
case 'todaysAttendance':
await fetchTodaysAttendance(projectId);
@ -534,8 +380,8 @@ class AttendanceController extends GetxController {
case 'attendanceLogs':
await fetchAttendanceLogs(
projectId,
dateFrom: startDateAttendance.value,
dateTo: endDateAttendance.value,
dateFrom: startDateAttendance,
dateTo: endDateAttendance,
);
break;
case 'regularizationRequests':
@ -544,35 +390,31 @@ class AttendanceController extends GetxController {
}
logSafe(
'Project data fetched for project ID: $projectId, tab: $selectedTab',
);
"Project data fetched for project ID: $projectId, tab: $selectedTab");
update();
}
// ------------------ 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,
firstDate: DateTime(2022),
lastDate: today.subtract(const Duration(days: 1)),
initialDateRange: DateTimeRange(
start: startDateAttendance.value,
end: endDateAttendance.value,
start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
end: endDateAttendance ?? today.subtract(const Duration(days: 1)),
),
);
if (picked != null) {
startDateAttendance.value = picked.start;
endDateAttendance.value = picked.end;
startDateAttendance = picked.start;
endDateAttendance = picked.end;
logSafe(
'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}',
);
"Date range selected: $startDateAttendance to $endDateAttendance");
await controller.fetchAttendanceLogs(
Get.find<ProjectController>().selectedProject?.id,

View File

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

View File

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

View File

@ -1,13 +1,13 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:on_field_work/controller/permission_controller.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/project_controller.dart';
class MPINController extends GetxController {
final MyFormValidator basicValidator = MyFormValidator();

View File

@ -1,10 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart';
class OTPController extends GetxController {
final formKey = GlobalKey<FormState>();

View File

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

View File

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

View File

@ -1,370 +1,263 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:on_field_work/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';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/model/dashboard/project_progress_model.dart';
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;
final attendanceSelectedRange = '15D'.obs;
final attendanceIsChartView = true.obs;
final isAttendanceLoading = false.obs;
// =========================
// Projects overview
// =========================
final RxInt totalProjects = 0.obs;
final RxInt ongoingProjects = 0.obs;
final RxBool isProjectsLoading = false.obs;
// Project Progress
final projectChartData = <ChartTaskData>[].obs;
final projectSelectedRange = '15D'.obs;
final projectIsChartView = true.obs;
final isProjectLoading = false.obs;
// =========================
// Tasks overview
// =========================
final RxInt totalTasks = 0.obs;
final RxInt completedTasks = 0.obs;
final RxBool isTasksLoading = false.obs;
// Overview Counts
final totalProjects = 0.obs;
final ongoingProjects = 0.obs;
final isProjectsLoading = false.obs;
// =========================
// Teams overview
// =========================
final RxInt totalEmployees = 0.obs;
final RxInt inToday = 0.obs;
final RxBool isTeamsLoading = false.obs;
final totalTasks = 0.obs;
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
// Common ranges
final List<String> ranges = ['7D', '15D', '30D'];
static const _rangeDaysMap = {
'7D': 7,
'15D': 15,
'30D': 30,
'3M': 90,
'6M': 180
};
// =========================
// 2. COMPUTED PROPERTIES
// =========================
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
// =========================
// Inside your DashboardController
final ProjectController projectController =
Get.put(ProjectController(), permanent: true);
@override
void 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) {
if (id.isNotEmpty) {
fetchAllDashboardData();
fetchTodaysAttendance(id);
}
fetchAllDashboardData();
});
// Expense Report Date Listener
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
if (projectController.selectedProjectId.value.isNotEmpty) {
fetchExpenseTypeReport(
startDate: expenseReportStartDate.value,
endDate: expenseReportEndDate.value,
);
}
});
// Chart Range Listeners
// React to range changes
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
ever(projectSelectedRange, (_) => fetchProjectProgress());
}
// =========================
// 4. USER ACTIONS
// Helper Methods
// =========================
void updateAttendanceRange(String range) =>
attendanceSelectedRange.value = range;
void updateProjectRange(String range) => projectSelectedRange.value = range;
void toggleAttendanceChartView(bool isChart) =>
attendanceIsChartView.value = isChart;
void toggleProjectChartView(bool isChart) =>
projectIsChartView.value = isChart;
void updateSelectedExpenseType(ExpenseTypeModel? type) {
selectedExpenseType.value = type;
fetchMonthlyExpenses(categoryId: type?.id);
}
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 _getDaysFromRange(String range) {
switch (range) {
case '7D':
return 7;
case '15D':
return 15;
case '30D':
return 30;
case '3M':
return 90;
case '6M':
return 180;
default:
return 7;
}
}
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 {
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([
fetchRoleWiseAttendance(),
fetchProjectProgress(),
fetchDashboardTasks(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;
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;
});
}
// =========================
// API Calls
// =========================
Future<void> fetchRoleWiseAttendance() async {
final id = projectController.selectedProjectId.value;
if (id.isEmpty) return;
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
await _executeApiCall(isAttendanceLoading, () async {
final response = await ApiService.getDashboardAttendanceOverview(
id, getAttendanceDays());
roleWiseData.value =
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
});
}
try {
isAttendanceLoading.value = true;
final List<dynamic>? response =
await ApiService.getDashboardAttendanceOverview(
projectId, getAttendanceDays());
Future<void> fetchExpenseTypeReport(
{required DateTime startDate, required DateTime endDate}) async {
final id = projectController.selectedProjectId.value;
if (id.isEmpty) return;
await _executeApiCall(isExpenseTypeReportLoading, () async {
final response = await ApiService.getExpenseTypeReportApi(
projectId: id,
startDate: startDate,
endDate: endDate,
);
expenseTypeReportData.value =
(response?.success == true) ? response!.data : null;
});
if (response != null) {
roleWiseData.value =
response.map((e) => Map<String, dynamic>.from(e)).toList();
logSafe('Attendance overview fetched successfully.',
level: LogLevel.info);
} else {
roleWiseData.clear();
logSafe('Failed to fetch attendance overview: response is null.',
level: LogLevel.error);
}
} catch (e, st) {
roleWiseData.clear();
logSafe('Error fetching attendance overview',
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isAttendanceLoading.value = false;
}
}
Future<void> fetchProjectProgress() async {
final id = projectController.selectedProjectId.value;
if (id.isEmpty) return;
final String projectId = projectController.selectedProjectId.value;
if (projectId.isEmpty) return;
await _executeApiCall(isProjectLoading, () async {
try {
isProjectLoading.value = true;
final response = await ApiService.getProjectProgress(
projectId: id, days: getProjectDays());
if (response?.success == true) {
projectChartData.value = response!.data
.map((d) => ChartTaskData.fromProjectData(d))
.toList();
projectId: projectId, days: getProjectDays());
if (response != null && response.success) {
projectChartData.value =
response.data.map((d) => ChartTaskData.fromProjectData(d)).toList();
logSafe('Project progress data mapped for chart', level: LogLevel.info);
} else {
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 {
await _executeApiCall(isTasksLoading, () async {
if (projectId.isEmpty) return;
try {
isTasksLoading.value = true;
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;
logSafe('Dashboard tasks fetched', level: LogLevel.info);
} else {
totalTasks.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 {
await _executeApiCall(isTeamsLoading, () async {
if (projectId.isEmpty) return;
try {
isTeamsLoading.value = true;
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;
logSafe('Dashboard teams fetched', level: LogLevel.info);
} else {
totalEmployees.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: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/controller/directory/directory_controller.dart';
import 'package:on_field_work/controller/directory/notes_controller.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/directory/directory_controller.dart';
import 'package:marco/controller/directory/notes_controller.dart';
class AddCommentController extends GetxController {
final String contactId;

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/directory/contact_model.dart';
import 'package:on_field_work/model/directory/contact_bucket_list_model.dart';
import 'package:on_field_work/model/directory/directory_comment_model.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/directory/contact_model.dart';
import 'package:marco/model/directory/contact_bucket_list_model.dart';
import 'package:marco/model/directory/directory_comment_model.dart';
class DirectoryController extends GetxController {
// -------------------- CONTACTS --------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
class DynamicMenuController extends GetxController {
// UI reactive states
@ -30,6 +30,15 @@ class DynamicMenuController extends GetxController {
final menuResponse = MenuResponse.fromJson(responseData);
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");
} else {
_handleApiFailure("Menu API returned null response");

View File

@ -1,11 +1,11 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/controller/my_controller.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_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart';

View File

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

View File

@ -1,60 +1,91 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/model/employees/employee_details_model.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance/attendance_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 {
/// Data lists
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeDetailsModel> employeeDetails = [];
RxBool isAllEmployeeSelected = false.obs;
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
Rxn<EmployeeDetailsModel>();
/// Loading states
RxBool isLoading = 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
void 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 {
isLoading.value = true;
update(['employee_screen_controller']);
await _handleApiCall(
() => ApiService.getAllEmployees(organizationId: organizationId),
() => ApiService.getAllEmployees(
organizationId: organizationId), // pass orgId to API
onSuccess: (data) {
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
logSafe(
"All Employees fetched: ${employees.length} employees loaded.",
level: LogLevel.info,
);
// Reset selection states when new data arrives
selectedEmployeeIds.clear();
isAllEmployeeSelected.value = false;
},
onEmpty: () {
employees.clear();
selectedEmployeeIds.clear();
isAllEmployeeSelected.value = false;
logSafe("No Employee data found or API call failed",
level: LogLevel.warning);
logSafe(
"No Employee data found or API call failed",
level: LogLevel.warning,
);
},
);
@ -62,7 +93,28 @@ class EmployeesScreenController extends GetxController {
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 {
if (employeeId == null || employeeId.isEmpty) return;
@ -72,80 +124,31 @@ class EmployeesScreenController extends GetxController {
() => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
logSafe("Employee details loaded for $employeeId",
level: LogLevel.info);
logSafe(
"Employee details loaded for $employeeId",
level: LogLevel.info,
);
},
onEmpty: () {
selectedEmployeeDetails.value = null;
logSafe("No employee details found for $employeeId",
level: LogLevel.warning);
logSafe(
"No employee details found for $employeeId",
level: LogLevel.warning,
);
},
onError: (e) {
selectedEmployeeDetails.value = null;
logSafe("Error fetching employee details for $employeeId",
level: LogLevel.error, error: e);
logSafe(
"Error fetching employee details for $employeeId",
level: LogLevel.error,
error: e,
);
},
);
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<List<dynamic>?> Function() apiCall, {
required Function(List<dynamic>) onSuccess,
@ -168,7 +171,6 @@ class EmployeesScreenController extends GetxController {
}
}
/// 🔹 Generic handler for single-object API responses
Future<void> _handleSingleApiCall(
Future<Map<String, dynamic>?> Function() apiCall, {
required Function(Map<String, dynamic>) onSuccess,

View File

@ -1,7 +1,7 @@
import 'dart:async';
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 {
Timer? countdownTimer;

View File

@ -1,5 +1,5 @@
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 {
void goToDashboardScreen() {

View File

@ -1,5 +1,5 @@
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 {
void goToDashboardScreen() {

View File

@ -1,6 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
@ -11,14 +10,13 @@ import 'package:intl/intl.dart';
import 'package:mime/mime.dart';
import 'package:image_picker/image_picker.dart';
import 'package:on_field_work/controller/expense/expense_screen_controller.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/model/employees/employee_model.dart';
import 'package:on_field_work/model/expense/expense_type_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:marco/controller/expense/expense_screen_controller.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/expense/expense_type_model.dart';
import 'package:marco/model/expense/payment_types_model.dart';
class AddExpenseController extends GetxController {
// --- Text Controllers ---
@ -51,22 +49,10 @@ class AddExpenseController extends GetxController {
final isEditMode = 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 ---
final selectedPaymentMode = Rxn<PaymentModeModel>();
final selectedExpenseType = Rxn<ExpenseTypeModel>();
// final selectedPaidBy = Rxn<EmployeeModel>();
final selectedPaidBy = Rxn<EmployeeModel>();
final selectedProject = ''.obs;
final selectedTransactionDate = Rxn<DateTime>();
@ -79,7 +65,6 @@ class AddExpenseController extends GetxController {
final paymentModes = <PaymentModeModel>[].obs;
final allEmployees = <EmployeeModel>[].obs;
final employeeSearchResults = <EmployeeModel>[].obs;
final isProcessingAttachment = false.obs;
String? editingExpenseId;
@ -209,7 +194,7 @@ class AddExpenseController extends GetxController {
'Location: ${locationController.text}',
'Transaction Date: ${transactionDateController.text}',
'No. of Persons: ${noOfPersonsController.text}',
'Expense Category: ${selectedExpenseType.value?.name}',
'Expense Type: ${selectedExpenseType.value?.name}',
'Payment Mode: ${selectedPaymentMode.value?.name}',
'Paid By: ${selectedPaidBy.value?.name}',
'Attachments: ${attachments.length}',
@ -267,22 +252,9 @@ class AddExpenseController extends GetxController {
Future<void> pickFromCamera() async {
try {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
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
}
if (pickedFile != null) attachments.add(File(pickedFile.path));
} catch (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 {
if (payload == null) {
_errorSnackbar("Payload is empty. Cannot submit.");
return false;
}
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;
Future<bool> _submitToApi(Map<String, dynamic> payload) async {
if (isEditMode.value && editingExpenseId != null) {
return ApiService.editExpenseApi(
expenseId: editingExpenseId!,
payload: payload,
);
}
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();
// --- 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
? existingAttachments
.map((e) => {
"documentId": e['documentId'],
"fileName": e['fileName'] ?? "",
"contentType": e['contentType'] ?? "",
"fileName": e['fileName'],
"contentType": e['contentType'],
"fileSize": 0,
"description": "",
"url": e['url'] ?? "",
"url": e['url'],
"isActive": e['isActive'] ?? true,
"base64Data": "",
})
.toList()
: <Map<String, dynamic>>[];
// --- Process new attachments ---
final newPayload = await Future.wait(
attachments.map((file) async {
final bytes = await file.readAsBytes();
@ -501,36 +434,38 @@ class AddExpenseController extends GetxController {
}),
);
// --- Build final payload ---
final payload = {
final type = selectedExpenseType.value!;
return {
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
"projectId": projectId,
"expenseCategoryId": expenseType.id,
"paymentModeId": paymentMode.id,
"paidById": paidBy.id,
"projectId": projectsMap[selectedProject.value]!,
"expensesTypeId": type.id,
"paymentModeId": selectedPaymentMode.value!.id,
"paidById": selectedPaidBy.value!.id,
"transactionDate":
(selectedTransactionDate.value ?? now).toUtc().toIso8601String(),
"transactionId": transactionIdController.text.trim(),
"description": descriptionController.text.trim(),
"location": locationController.text.trim(),
"supplerName": supplierController.text.trim(),
"amount": double.tryParse(amountController.text.trim()) ?? 0,
"noOfPersons": expenseType.noOfPersonsRequired == true
"transactionId": transactionIdController.text,
"description": descriptionController.text,
"location": locationController.text,
"supplerName": supplierController.text,
"amount": double.parse(amountController.text.trim()),
"noOfPersons": type.noOfPersonsRequired == true
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
: 0,
"billAttachments": [...existingPayload, ...newPayload].isEmpty
"billAttachments": [
...existingPayload,
...newPayload,
].isEmpty
? null
: [...existingPayload, ...newPayload],
};
return payload;
}
String validateForm() {
final missing = <String>[];
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 (selectedPaidBy.value == null) missing.add("Paid By");
if (amountController.text.trim().isEmpty) missing.add("Amount");

View File

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

View File

@ -1,13 +1,13 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/model/expense/expense_list_model.dart';
import 'package:on_field_work/model/expense/payment_types_model.dart';
import 'package:on_field_work/model/expense/expense_type_model.dart';
import 'package:on_field_work/model/expense/expense_status_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/expense/expense_list_model.dart';
import 'package:marco/model/expense/payment_types_model.dart';
import 'package:marco/model/expense/expense_type_model.dart';
import 'package:marco/model/expense/expense_status_model.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter/material.dart';
class ExpenseController extends GetxController {
@ -213,7 +213,7 @@ class ExpenseController extends GetxController {
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 {
try {
final expenseTypesData = await ApiService.getMasterExpenseTypes();

View File

@ -1,5 +1,5 @@
import 'package:on_field_work/controller/my_controller.dart';
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart';
class FaqsController extends MyController {
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 {
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 {}

View File

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

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:on_field_work/model/project_model.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/model/project_model.dart';
class LayoutController extends GetxController {
// Theme Customization
@ -55,7 +55,7 @@ class LayoutController extends GetxController {
isLoadingProjects.value = true;
try {
final response = await ApiService.getGlobalProjects();
final response = await ApiService.getProjects();
if (response != null && response.isNotEmpty) {
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:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
abstract class MyController extends GetxController {
@override

View File

@ -2,11 +2,11 @@ import 'dart:async';
import 'dart:convert';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/permission_service.dart';
import 'package:on_field_work/model/user_permission.dart';
import 'package:on_field_work/model/employees/employee_info.dart';
import 'package:on_field_work/model/projects_model.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/permission_service.dart';
import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/model/projects_model.dart';
class PermissionController extends GetxController {
var permissions = <UserPermission>[].obs;
@ -15,9 +15,6 @@ class PermissionController extends GetxController {
Timer? _refreshTimer;
var isLoading = true.obs;
/// NEW: reactive flag to signal permissions are loaded
var permissionsLoaded = false.obs;
@override
void onInit() {
super.onInit();
@ -55,10 +52,6 @@ class PermissionController extends GetxController {
_updateState(userData);
await _storeData();
logSafe("Data loaded and state updated successfully.");
// NEW: mark permissions as loaded
permissionsLoaded.value = true;
} catch (e, stacktrace) {
logSafe("Error loading data from API",
level: LogLevel.error, error: e, stackTrace: stacktrace);
@ -110,7 +103,7 @@ class PermissionController extends GetxController {
}
void _startAutoRefresh() {
_refreshTimer = Timer.periodic(const Duration(minutes: 30), (timer) async {
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
logSafe("Auto-refresh triggered.");
final token = await _getAuthToken();
if (token?.isNotEmpty ?? false) {
@ -124,6 +117,8 @@ class PermissionController extends GetxController {
bool hasPermission(String permissionId) {
final hasPerm = permissions.any((p) => p.id == permissionId);
logSafe("Checking permission $permissionId: $hasPerm",
level: LogLevel.debug);
return hasPerm;
}

View File

@ -1,8 +1,8 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/global_project_model.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/global_project_model.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
class ProjectController extends GetxController {
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:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/dailyTaskPlanning/master_work_category_model.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart';
class AddTaskController extends GetxController {
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;

View File

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

View File

@ -1,11 +1,11 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/model/project_model.dart';
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_planning_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart';
import 'package:marco/model/employees/employee_model.dart';
class DailyTaskPlanningController extends GetxController {
List<ProjectModel> projects = [];
@ -23,10 +23,6 @@ class DailyTaskPlanningController extends GetxController {
RxBool isFetchingProjects = true.obs;
RxBool isFetchingEmployees = true.obs;
/// New: track per-building loading and loaded state for lazy infra loading
RxMap<String, RxBool> buildingLoadingStates = <String, RxBool>{}.obs;
final Set<String> buildingsWithDetails = <String>{};
@override
void onInit() {
super.onInit();
@ -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 {
if (projectId == null) return;
@ -127,24 +123,25 @@ class DailyTaskPlanningController extends GetxController {
return;
}
// Filter buildings with 0 planned & completed work
final filteredBuildings = infraData.where((b) {
final planned = (b['plannedWork'] as num?)?.toDouble() ?? 0;
final completed = (b['completedWork'] as num?)?.toDouble() ?? 0;
return planned > 0 || completed > 0;
}).toList();
dailyTasks = filteredBuildings.map((buildingJson) {
dailyTasks = infraData.map((buildingJson) {
final building = Building(
id: buildingJson['id'],
name: buildingJson['buildingName'],
description: buildingJson['description'],
floors: [],
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
completedWork:
(buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
floors: (buildingJson['floors'] as List<dynamic>)
.map((floorJson) => Floor(
id: floorJson['id'],
floorName: floorJson['floorName'],
workAreas: (floorJson['workAreas'] as List<dynamic>)
.map((areaJson) => WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [],
))
.toList(),
))
.toList(),
);
return TaskPlanningDetailsModel(
id: building.id,
name: building.name,
@ -157,75 +154,14 @@ class DailyTaskPlanningController extends GetxController {
);
}).toList();
buildingLoadingStates.clear();
buildingsWithDetails.clear();
} catch (e, stack) {
logSafe("Error fetching daily task data",
level: LogLevel.error, error: e, stackTrace: stack);
} finally {
isFetchingTasks.value = false;
update();
}
}
/// Fetch full infra for a single building (floors, workAreas, workItems).
/// Called lazily when user expands a building in the UI.
Future<void> fetchBuildingInfra(
String buildingId, String projectId, String? serviceId) async {
if (buildingId.isEmpty) return;
// mark loading
buildingLoadingStates.putIfAbsent(buildingId, () => true.obs);
buildingLoadingStates[buildingId]!.value = true;
update();
try {
// Re-use getInfraDetails and find the building entry for the requested buildingId
final infraResponse =
await ApiService.getInfraDetails(projectId, serviceId: serviceId);
final infraData = infraResponse?['data'] as List<dynamic>? ?? [];
final buildingJson = infraData.firstWhere(
(b) => b['id'].toString() == buildingId.toString(),
orElse: () => null,
);
if (buildingJson == null) {
logSafe("Building $buildingId not found in infra response",
level: LogLevel.warning);
return;
}
// Build floors & workAreas for this building
final building = Building(
id: buildingJson['id'],
name: buildingJson['buildingName'],
description: buildingJson['description'],
floors:
(buildingJson['floors'] as List<dynamic>? ?? []).map((floorJson) {
return Floor(
id: floorJson['id'],
floorName: floorJson['floorName'],
workAreas: (floorJson['workAreas'] as List<dynamic>? ?? [])
.map((areaJson) {
return WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [], // will populate later
);
}).toList(),
);
}).toList(),
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
completedWork: (buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
);
// For each workArea, fetch its work items and populate
await Future.wait(
building.floors.expand((f) => f.workAreas).map((area) async {
await Future.wait(dailyTasks
.expand((task) => task.buildings)
.expand((b) => b.floors)
.expand((f) => f.workAreas)
.map((area) async {
try {
final taskResponse = await ApiService.getWorkItemsByWorkArea(area.id,
serviceId: serviceId);
final taskResponse =
await ApiService.getWorkItemsByWorkArea(area.id, serviceId: serviceId);
final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper(
workItemId: taskJson['id'],
@ -235,14 +171,11 @@ class DailyTaskPlanningController extends GetxController {
? ActivityMaster.fromJson(taskJson['activityMaster'])
: null,
workCategoryMaster: taskJson['workCategoryMaster'] != null
? WorkCategoryMaster.fromJson(
taskJson['workCategoryMaster'])
? WorkCategoryMaster.fromJson(taskJson['workCategoryMaster'])
: null,
plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(),
completedWork:
(taskJson['completedWork'] as num?)?.toDouble(),
todaysAssigned:
(taskJson['todaysAssigned'] as num?)?.toDouble(),
completedWork: (taskJson['completedWork'] as num?)?.toDouble(),
todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(),
description: taskJson['description'] as String?,
taskDate: taskJson['taskDate'] != null
? DateTime.tryParse(taskJson['taskDate'])
@ -254,40 +187,11 @@ class DailyTaskPlanningController extends GetxController {
level: LogLevel.error, error: e, stackTrace: stack);
}
}));
// Merge/replace the building into dailyTasks
bool merged = false;
for (var t in dailyTasks) {
final idx = t.buildings
.indexWhere((b) => b.id.toString() == building.id.toString());
if (idx != -1) {
t.buildings[idx] = building;
merged = true;
break;
}
}
if (!merged) {
// If not present, add a new TaskPlanningDetailsModel wrapper (fallback)
dailyTasks.add(TaskPlanningDetailsModel(
id: building.id,
name: building.name,
projectAddress: "",
contactPerson: "",
startDate: DateTime.now(),
endDate: DateTime.now(),
projectStatusId: "",
buildings: [building],
));
}
// Mark as loaded
buildingsWithDetails.add(buildingId.toString());
} catch (e, stack) {
logSafe("Error fetching infra for building $buildingId",
logSafe("Error fetching daily task data",
level: LogLevel.error, error: e, stackTrace: stack);
} finally {
buildingLoadingStates.putIfAbsent(buildingId, () => false.obs);
buildingLoadingStates[buildingId]!.value = false;
isFetchingTasks.value = false;
update();
}
}
@ -307,8 +211,7 @@ class DailyTaskPlanningController extends GetxController {
);
if (response != null && response.isNotEmpty) {
employees
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
employees.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
if (serviceId == null && organizationId == null) {
allEmployeesCache = List.from(employees);
@ -316,14 +219,12 @@ class DailyTaskPlanningController extends GetxController {
final currentEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates
.removeWhere((key, _) => !currentEmployeeIds.contains(key));
uploadingStates.removeWhere((key, _) => !currentEmployeeIds.contains(key));
employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs);
});
selectedEmployees
.removeWhere((e) => !currentEmployeeIds.contains(e.id));
selectedEmployees.removeWhere((e) => !currentEmployeeIds.contains(e.id));
logSafe("Employees fetched: ${employees.length}", level: LogLevel.info);
} else {
@ -338,17 +239,13 @@ class DailyTaskPlanningController extends GetxController {
);
}
} catch (e, stack) {
logSafe("Error fetching employees",
level: LogLevel.error, error: e, stackTrace: stack);
logSafe("Error fetching employees", level: LogLevel.error, error: e, stackTrace: stack);
if (serviceId == null &&
organizationId == null &&
allEmployeesCache.isNotEmpty) {
if (serviceId == null && organizationId == null && allEmployeesCache.isNotEmpty) {
employees.assignAll(allEmployeesCache);
final cachedEmployeeIds = employees.map((e) => e.id).toSet();
uploadingStates
.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
uploadingStates.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
employees.forEach((emp) {
uploadingStates.putIfAbsent(emp.id, () => false.obs);
});

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
class OrganizationController extends GetxController {
/// List of organizations assigned to the selected project

View File

@ -1,7 +1,7 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/api_service.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/model/tenant/tenant_services_model.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/tenant/tenant_services_model.dart';
class ServiceController extends GetxController {
List<Service> services = [];

View File

@ -1,10 +1,10 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/tenant_service.dart';
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/controller/permission_controller.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/tenant_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/controller/permission_controller.dart';
class TenantSelectionController extends GetxController {
final TenantService _tenantService = TenantService();

View File

@ -1,10 +1,10 @@
import 'package:get/get.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/tenant_service.dart';
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/controller/permission_controller.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/tenant_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/controller/permission_controller.dart';
class TenantSwitchController extends GetxController {
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 {
List<bool> selected = List.filled(3, false);

View File

@ -1,7 +1,7 @@
import 'dart:async';
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';
class CarouselsController extends MyController {

View File

@ -1,5 +1,5 @@
import 'package:on_field_work/controller/my_controller.dart';
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart';
class DialogsController extends MyController {
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 {}

View File

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

View File

@ -1,10 +1,10 @@
import 'dart:async';
import 'package:on_field_work/controller/my_controller.dart';
import 'package:on_field_work/helpers/extensions/string.dart';
import 'package:on_field_work/helpers/theme/admin_theme.dart';
import 'package:on_field_work/helpers/widgets/my_button.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/extensions/string.dart';
import 'package:marco/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:flutter/material.dart';
import 'package:get/get.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:on_field_work/helpers/widgets/my_text_utils.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_text_utils.dart';
import 'package:flutter/material.dart';
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';
class ToastMessageController extends MyController {

View File

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

View File

@ -1,5 +1,5 @@
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 {
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://api.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
static const String getDashboardAttendanceOverview =
"/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 getDashboardTasks = "/dashboard/tasks";
static const String getDashboardTeams = "/dashboard/teams";
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
static const String getProjects = "/project/list";
static const String getGlobalProjects = "/project/list/basic";
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 getAttendanceLogView = "/attendance/log/attendance";
static const String getRegularizationLogs = "/attendance/regularize";
@ -104,7 +70,7 @@ class ApiEndpoints {
static const String editExpense = "/Expense/edit";
static const String getMasterPaymentModes = "/master/payment-modes";
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 deleteExpense = "/expense/delete";
@ -134,37 +100,4 @@ class ApiEndpoints {
static const getAllOrganizations = "/organization/list";
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:url_strategy/url_strategy.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:on_field_work/helpers/services/device_info_service.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:on_field_work/helpers/theme/app_theme.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
import 'package:marco/helpers/services/device_info_service.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/theme/app_theme.dart';
Future<void> initializeApp() async {
try {

View File

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:logger/logger.dart';
import 'package:intl/intl.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
Logger? _appLogger;

View File

@ -1,8 +1,8 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:on_field_work/helpers/services/api_endpoints.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart';
class AuthService {
static const String _baseUrl = ApiEndpoints.baseUrl;

View File

@ -1,10 +1,10 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:logger/logger.dart';
import 'package:on_field_work/helpers/services/local_notification_service.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/notification_action_handler.dart';
import 'package:marco/helpers/services/local_notification_service.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/notification_action_handler.dart';
/// Firebase Notification Service
class FirebaseNotificationService {

View File

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

View File

@ -1,7 +1,7 @@
import 'dart:convert';
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:get/get_utils/src/extensions/string_extensions.dart';
import 'package:shared_preferences/shared_preferences.dart';

View File

@ -1,17 +1,17 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart';
import 'package:on_field_work/controller/task_planning/daily_task_controller.dart';
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:on_field_work/controller/expense/expense_screen_controller.dart';
import 'package:on_field_work/controller/expense/expense_detail_controller.dart';
import 'package:on_field_work/controller/directory/directory_controller.dart';
import 'package:on_field_work/controller/directory/notes_controller.dart';
import 'package:on_field_work/controller/document/user_document_controller.dart';
import 'package:on_field_work/controller/document/document_details_controller.dart';
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
import 'package:on_field_work/helpers/utils/permission_constants.dart';
import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/controller/task_planning/daily_task_controller.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/controller/expense/expense_screen_controller.dart';
import 'package:marco/controller/expense/expense_detail_controller.dart';
import 'package:marco/controller/directory/directory_controller.dart';
import 'package:marco/controller/directory/notes_controller.dart';
import 'package:marco/controller/document/user_document_controller.dart';
import 'package:marco/controller/document/document_details_controller.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/utils/permission_constants.dart';
/// Handles incoming FCM notification actions and updates UI/controllers.
class NotificationActionHandler {
@ -69,6 +69,7 @@ class NotificationActionHandler {
}
break;
case 'Team_Modified':
// Call method to handle team modifications and dashboard update
_handleDashboardUpdate(data);
break;
@ -128,11 +129,6 @@ class NotificationActionHandler {
/// ---------------------- HANDLERS ----------------------
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored task planning update from another project.");
return;
}
final projectId = data['ProjectId'];
if (projectId == null) {
_logger.w("⚠️ TaskPlanning update received without ProjectId: $data");
@ -163,17 +159,13 @@ class NotificationActionHandler {
}
static void _handleExpenseUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored expense update from another project.");
return;
}
final expenseId = data['ExpenseId'];
if (expenseId == null) {
_logger.w("⚠️ Expense update received without ExpenseId: $data");
return;
}
// Update Expense List
_safeControllerUpdate<ExpenseController>(
onFound: (controller) async {
await controller.fetchExpenses();
@ -183,6 +175,7 @@ class NotificationActionHandler {
'✅ ExpenseController refreshed from expense notification.',
);
// Update Expense Detail (if open and matches this expenseId)
_safeControllerUpdate<ExpenseDetailController>(
onFound: (controller) async {
if (controller.expense.value?.id == expenseId) {
@ -197,11 +190,6 @@ class NotificationActionHandler {
}
static void _handleAttendanceUpdated(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored attendance update from another project.");
return;
}
_safeControllerUpdate<AttendanceController>(
onFound: (controller) => controller.refreshDataFromNotification(
projectId: data['ProjectId'],
@ -213,11 +201,6 @@ class NotificationActionHandler {
static void _handleTaskUpdated(Map<String, dynamic> data,
{required bool isComment}) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored task update from another project.");
return;
}
_safeControllerUpdate<DailyTaskController>(
onFound: (controller) => controller.refreshTasksFromNotification(
projectId: data['ProjectId'],
@ -230,15 +213,11 @@ class NotificationActionHandler {
/// ---------------------- DOCUMENT HANDLER ----------------------
static void _handleDocumentModified(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored document update from another project.");
return;
}
String entityTypeId;
String entityId;
String? documentId = data['DocumentId'];
// Determine entity type and ID
if (data['Keyword'] == 'Employee_Document_Modified') {
entityTypeId = Permissions.employeeEntity;
entityId = data['EmployeeId'] ?? '';
@ -258,6 +237,7 @@ class NotificationActionHandler {
_logger.i(
"🔔 Document notification received: keyword=${data['Keyword']}, entityTypeId=$entityTypeId, entityId=$entityId, documentId=$documentId");
// Refresh Document List
if (Get.isRegistered<DocumentController>()) {
_safeControllerUpdate<DocumentController>(
onFound: (controller) async {
@ -275,9 +255,11 @@ class NotificationActionHandler {
_logger.w('⚠️ DocumentController not registered, skipping list refresh.');
}
// Refresh Document Details (if open)
if (documentId != null && Get.isRegistered<DocumentDetailsController>()) {
_safeControllerUpdate<DocumentDetailsController>(
onFound: (controller) async {
// Refresh details regardless of current document
await controller.fetchDocumentDetails(documentId);
_logger.i(
"✅ DocumentDetailsController refreshed for Document $documentId");
@ -294,10 +276,13 @@ class NotificationActionHandler {
/// ---------------------- DIRECTORY HANDLERS ----------------------
static void _handleContactModified(Map<String, dynamic> data) {
final contactId = data['ContactId'];
// Always refresh the contact list
_safeControllerUpdate<DirectoryController>(
onFound: (controller) {
controller.fetchContacts();
final contactId = data['ContactId'];
// If a specific contact is provided, refresh its notes as well
if (contactId != null) {
controller.fetchCommentsForContact(contactId);
}
@ -308,6 +293,7 @@ class NotificationActionHandler {
'✅ Directory contacts (and notes if applicable) refreshed from notification.',
);
// Refresh notes globally as well
_safeControllerUpdate<NotesController>(
onFound: (controller) => controller.fetchNotes(),
notFoundMessage: '⚠️ NotesController not found, cannot refresh notes.',
@ -316,6 +302,7 @@ class NotificationActionHandler {
}
static void _handleContactNoteModified(Map<String, dynamic> data) {
// Refresh both contacts and notes when a note is modified
_handleContactModified(data);
}
@ -337,11 +324,6 @@ class NotificationActionHandler {
/// ---------------------- DASHBOARD HANDLER ----------------------
static void _handleDashboardUpdate(Map<String, dynamic> data) {
if (!_isCurrentProject(data)) {
_logger.i(" Ignored dashboard update from another project.");
return;
}
_safeControllerUpdate<DashboardController>(
onFound: (controller) async {
final type = data['type'] ?? '';
@ -365,9 +347,11 @@ class NotificationActionHandler {
controller.projectController.selectedProjectId.value;
final projectIdsString = data['ProjectIds'] ?? '';
// Convert comma-separated string to List<String>
final notificationProjectIds =
projectIdsString.split(',').map((e) => e.trim()).toList();
// Refresh only if current project ID is in the list
if (notificationProjectIds.contains(currentProjectId)) {
await controller.fetchDashboardTeams(projectId: currentProjectId);
}
@ -391,40 +375,17 @@ class NotificationActionHandler {
/// ---------------------- 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>({
required void Function(T controller) onFound,
required String notFoundMessage,
required String successMessage,
}) {
if (!Get.isRegistered<T>()) {
_logger.w(notFoundMessage);
return;
}
try {
final controller = Get.find<T>();
onFound(controller);
_logger.i(successMessage);
} 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:http/http.dart' as http;
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/model/user_permission.dart';
import 'package:on_field_work/model/employees/employee_info.dart';
import 'package:on_field_work/model/projects_model.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:on_field_work/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employees/employee_info.dart';
import 'package:marco/model/projects_model.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/api_endpoints.dart';
class PermissionService {
// In-memory cache keyed by user token

View File

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

View File

@ -1,13 +1,13 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:get/get.dart';
import 'package:on_field_work/controller/project_controller.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:on_field_work/helpers/services/api_endpoints.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/helpers/services/auth_service.dart';
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart';
/// Abstract interface for tenant service functionality
abstract class ITenantService {
@ -63,39 +63,29 @@ class TenantService implements ITenantService {
{bool hasRetried = false}) async {
try {
final headers = await _authorizedHeaders();
logSafe("➡️ GET $_baseUrl/auth/get/user/tenants\nHeaders: $headers",
level: LogLevel.info);
final response = await http.get(
Uri.parse("$_baseUrl/auth/get/user/tenants"),
headers: headers,
);
final response = await http
.get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers);
final data = jsonDecode(response.body);
// Handle empty response BEFORE decoding
if (response.body.isEmpty || response.body.trim().isEmpty) {
logSafe("❌ Empty tenant response — auto logout");
await LocalStorage.logout();
return null;
}
logSafe(
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
level: LogLevel.info);
Map<String, dynamic> data;
try {
data = jsonDecode(response.body);
} catch (e) {
logSafe("❌ Invalid JSON in tenant response — auto logout");
await LocalStorage.logout();
return null;
}
// SUCCESS CASE
if (response.statusCode == 200 && data['success'] == true) {
final list = data['data'];
if (list is! List) return null;
return List<Map<String, dynamic>>.from(list);
logSafe("✅ Tenants fetched successfully.");
return List<Map<String, dynamic>>.from(data['data']);
}
// TOKEN EXPIRED
if (response.statusCode == 401 && !hasRetried) {
logSafe("⚠️ Unauthorized while fetching tenants. Refreshing token...",
level: LogLevel.warning);
final refreshed = await AuthService.refreshToken();
if (refreshed) return getTenants(hasRetried: true);
logSafe("❌ Token refresh failed while fetching tenants.",
level: LogLevel.error);
return null;
}
@ -140,7 +130,7 @@ class TenantService implements ITenantService {
}
// 🔹 Register FCM token after tenant selection
final fcmToken = LocalStorage.getFcmToken();
final fcmToken = LocalStorage.getFcmToken();
if (fcmToken?.isNotEmpty ?? false) {
final success = await AuthService.registerDeviceToken(fcmToken!);
logSafe(

View File

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

View File

@ -6,13 +6,13 @@
* */
import 'dart:io';
import 'dart:math';
import 'package:on_field_work/helpers/theme/admin_theme.dart';
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
import 'package:on_field_work/helpers/widgets/my.dart';
import 'package:on_field_work/helpers/widgets/my_breadcrumb_item.dart';
import 'package:on_field_work/helpers/widgets/my_constant.dart';
import 'package:on_field_work/helpers/widgets/my_screen_media.dart';
import 'package:on_field_work/helpers/widgets/my_text_style.dart';
import 'package:marco/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/widgets/my.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_constant.dart';
import 'package:marco/helpers/widgets/my_screen_media.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
@ -230,7 +230,7 @@ class AppStyle {
containerRadius: AppStyle.containerRadius.medium,
cardRadius: AppStyle.cardRadius.medium,
buttonRadius: AppStyle.buttonRadius.medium,
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'On Field Work', route: '/client/dashboard'),
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'Marco', route: '/client/dashboard'),
));
bool isMobile = true;
try {
@ -257,7 +257,7 @@ class AppColors {
static ColorGroup pink = ColorGroup(Color(0xffFFC2D9), Color(0xffF5005E));
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 orange = ColorGroup(Color(0xffFFCEC2), Color(0xffFF3B0A));
static ColorGroup skyBlue = ColorGroup(Color(0xffC2F0FF), Color(0xff0099CC));

View File

@ -1,14 +1,14 @@
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: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:shared_preferences/shared_preferences.dart';
typedef ThemeChangeCallback = void Function(
ThemeCustomizer oldVal, ThemeCustomizer newVal);
@ -24,7 +24,7 @@ class ThemeCustomizer {
ThemeMode leftBarTheme = ThemeMode.light;
ThemeMode rightBarTheme = ThemeMode.light;
ThemeMode topBarTheme = ThemeMode.light;
ColorThemeType colorTheme = ColorThemeType.red;
bool rightBarOpen = false;
bool leftBarCondensed = false;
@ -33,8 +33,6 @@ class ThemeCustomizer {
static Future<void> init() async {
await initLanguage();
await _loadColorTheme();
_notify();
}
static initLanguage() async {
@ -42,7 +40,7 @@ class ThemeCustomizer {
}
String toJSON() {
return jsonEncode({'theme': theme.name, 'colorTheme': colorTheme.name});
return jsonEncode({'theme': theme.name});
}
static ThemeCustomizer fromJSON(String? json) {
@ -51,8 +49,6 @@ class ThemeCustomizer {
JSONDecoder decoder = JSONDecoder(json);
instance.theme =
decoder.getEnum('theme', ThemeMode.values, ThemeMode.light);
instance.colorTheme = decoder.getEnum(
'colorTheme', ColorThemeType.values, ColorThemeType.red);
}
return instance;
}
@ -77,11 +73,6 @@ class ThemeCustomizer {
}
}
/// Public method to trigger theme updates externally
static void applyThemeChange() {
_notify();
}
static void notify() {
for (var value in _notifier) {
value(oldInstance, instance);
@ -121,46 +112,12 @@ class ThemeCustomizer {
tc.topBarTheme = topBarTheme;
tc.rightBarOpen = rightBarOpen;
tc.leftBarCondensed = leftBarCondensed;
tc.colorTheme = colorTheme;
tc.currentLanguage = currentLanguage.clone();
return tc;
}
@override
String toString() {
return 'ThemeCustomizer{theme: $theme, colorTheme: $colorTheme}';
}
// ---------------------------------------------------------------------------
// 🟢 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);
return 'ThemeCustomizer{theme: $theme}';
}
}

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 isTodayApproved,
required int activity,

View File

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

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/app_logger.dart';
class ContactPickerHelper {
static Future<String?> pickIndianPhoneNumber(BuildContext context) async {

View File

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

View File

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

View File

@ -25,16 +25,14 @@ class Permissions {
// ------------------- Project Infrastructure --------------------------
/// Permission to manage project infrastructure (e.g., site details)
static const String manageProjectInfra =
"cf2825ad-453b-46aa-91d9-27c124d63373";
static const String manageProjectInfra = "cf2825ad-453b-46aa-91d9-27c124d63373";
/// Permission to view infrastructure-related details
static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4";
// ------------------- Attendance Management ---------------------------
/// Permission to regularize (edit/update) attendance records
static const String regularizeAttendance =
"57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
static const String regularizeAttendance = "57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
// ------------------- Task Management ---------------------------------
/// Permission to create and manage tasks
@ -92,8 +90,7 @@ class Permissions {
// ------------------- Application Roles -------------------------------
/// Application role ID for users with full expense management rights
static const String expenseManagement =
"a4e25142-449b-4334-a6e5-22f70e4732d7";
static const String expenseManagement = "a4e25142-449b-4334-a6e5-22f70e4732d7";
// ------------------- Document Entities -------------------------------
/// Entity ID for project documents
@ -121,73 +118,3 @@ class Permissions {
/// Permission to verify documents
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