Compare commits
93 Commits
OH_Dev_Man
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ec7dee0f1 | |||
| 48a96a703b | |||
| 8686d696f0 | |||
| 5d73fd6f4f | |||
| 1717cd5e2b | |||
| 717f0c92af | |||
| 633a75fe92 | |||
| 8fb32c7c8e | |||
| 3dfa6e5877 | |||
| 03e3e7b5db | |||
| cf85c17d75 | |||
| 66445b1e54 | |||
| 7c86d0c5c2 | |||
| 012d40cd57 | |||
| d4d678d98a | |||
| 6ed069d924 | |||
| c9e73771b0 | |||
| ed2eb014d8 | |||
| 3ad48016f3 | |||
| 37ce612fca | |||
| 7bef2e9d89 | |||
| 341d779499 | |||
| 65fbef3441 | |||
| 259f2aa928 | |||
| 84156167ea | |||
| 260b762724 | |||
| 951dd22ecc | |||
| 33ae5c0333 | |||
| 60bc53afef | |||
| 90a3a85753 | |||
| 71a48750bd | |||
| e2bee52820 | |||
| bb723b91e5 | |||
| 55122b5b13 | |||
| 2700864adf | |||
| 18fbfaa42d | |||
| 3e8bd1c41d | |||
| 4d2b05cdc2 | |||
| ddb1440211 | |||
| e4f55d82f7 | |||
| 081849f964 | |||
| a3b95b4d07 | |||
| ed4a558894 | |||
| e2897e4dde | |||
| 08777176df | |||
| 81f74004b8 | |||
| 3fa578f1b4 | |||
| 28c1c36e07 | |||
| 24bfccfdf6 | |||
| 5bed5bd2f4 | |||
| 41112a3eea | |||
| 9eb72a60ac | |||
| b401e98658 | |||
| 56602328ca | |||
| aece165c38 | |||
| 38626ebef0 | |||
| 6b58085434 | |||
| 5e1379b74b | |||
| c69e4280ef | |||
| 761cd1f7e8 | |||
| 516d6b0489 | |||
| e9075dcdf5 | |||
| 5c53a3f4be | |||
| 603e7ee7e5 | |||
| 2c98ac359c | |||
| fc78806af2 | |||
| cf7021a982 | |||
| efefb1c34b | |||
| 846ca64402 | |||
| 765f537cf9 | |||
| 87a7d19672 | |||
| d0b40a9822 | |||
| c44d10d35a | |||
| 02ef996753 | |||
| ec6a45ed43 | |||
| 92c739045c | |||
| 5dd09869ad | |||
| 8edd189479 | |||
| bbadcc4139 | |||
| d05e26bc87 | |||
| 49326e9929 | |||
| 0e575c393d | |||
| 804f0eba7b | |||
| 253aa55a80 | |||
| 17fc04f3ee | |||
| 474ecac53c | |||
| dc4ea7979c | |||
| 618ac6f27a | |||
| 2260382990 | |||
| 919310644d | |||
| 605617695e | |||
| 4d47ee3002 | |||
| 9dd66bd297 |
@ -1,4 +1,4 @@
|
|||||||
# marco
|
# On Field Work
|
||||||
|
|
||||||
A new Flutter project.
|
A new Flutter project.
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
// Define the namespace for your Android application
|
// Define the namespace for your Android application
|
||||||
namespace = "com.marco.aiot"
|
namespace = "com.marcoonfieldwork.aiot"
|
||||||
// Set the compile SDK version based on Flutter's configuration
|
// Set the compile SDK version based on Flutter's configuration
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
// Set the NDK version based on Flutter's configuration
|
// Set the NDK version based on Flutter's configuration
|
||||||
@ -37,7 +37,7 @@ android {
|
|||||||
// Default configuration for your application
|
// Default configuration for your application
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// Specify your unique Application ID. This identifies your app on Google Play.
|
// Specify your unique Application ID. This identifies your app on Google Play.
|
||||||
applicationId = "com.marco.aiot"
|
applicationId = "com.marcoonfieldwork.aiot"
|
||||||
// Set minimum and target SDK versions based on Flutter's configuration
|
// Set minimum and target SDK versions based on Flutter's configuration
|
||||||
minSdk = 23
|
minSdk = 23
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"client_info": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
|
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
|
||||||
"android_client_info": {
|
"android_client_info": {
|
||||||
"package_name": "com.marco.aiot"
|
"package_name": "com.marcoonfieldwork.aiot"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [],
|
"oauth_client": [],
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Marco"
|
android:label="On Field Work"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.marco.aiot
|
package com.marcoonfieldwork.aiot
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.6.0" apply false
|
id "com.android.application" version "8.6.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
id "org.jetbrains.kotlin.android" version "2.2.21" apply false
|
||||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ YELLOW='\033[1;33m'
|
|||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
# App info
|
# App info
|
||||||
APP_NAME="Marco"
|
APP_NAME="On Field Work"
|
||||||
BUILD_DIR="build/app/outputs"
|
BUILD_DIR="build/app/outputs"
|
||||||
|
|
||||||
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"
|
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"
|
||||||
|
|||||||
@ -368,7 +368,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -384,7 +384,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@ -401,7 +401,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@ -416,7 +416,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@ -547,7 +547,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@ -569,7 +569,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Marco</string>
|
<string>On Field Work</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>marco</string>
|
<string>on field work</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
@ -8,5 +8,5 @@ class AppConstant {
|
|||||||
static int iOSAppVersion = 1;
|
static int iOSAppVersion = 1;
|
||||||
static String version = "1.0.0";
|
static String version = "1.0.0";
|
||||||
|
|
||||||
static String get appName => 'Marco';
|
static String get appName => 'On Field Work';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,37 +1,41 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/controller/project_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
|
||||||
import 'package:marco/model/attendance/attendance_model.dart';
|
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:on_field_work/model/attendance/attendance_log_model.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:on_field_work/model/attendance/attendance_log_view_model.dart';
|
||||||
import 'package:marco/model/attendance/attendance_log_model.dart';
|
import 'package:on_field_work/model/attendance/attendance_model.dart';
|
||||||
import 'package:marco/model/regularization_log_model.dart';
|
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
|
||||||
import 'package:marco/model/attendance/attendance_log_view_model.dart';
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
|
import 'package:on_field_work/model/project_model.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:on_field_work/model/regularization_log_model.dart';
|
||||||
|
|
||||||
class AttendanceController extends GetxController {
|
class AttendanceController extends GetxController {
|
||||||
// ------------------ Data Models ------------------
|
// ------------------ Data Models ------------------
|
||||||
List<AttendanceModel> attendances = [];
|
final List<AttendanceModel> attendances = <AttendanceModel>[];
|
||||||
List<ProjectModel> projects = [];
|
final List<ProjectModel> projects = <ProjectModel>[];
|
||||||
List<EmployeeModel> employees = [];
|
final List<EmployeeModel> employees = <EmployeeModel>[];
|
||||||
List<AttendanceLogModel> attendanceLogs = [];
|
final List<AttendanceLogModel> attendanceLogs = <AttendanceLogModel>[];
|
||||||
List<RegularizationLogModel> regularizationLogs = [];
|
final List<RegularizationLogModel> regularizationLogs =
|
||||||
List<AttendanceLogViewModel> attendenceLogsView = [];
|
<RegularizationLogModel>[];
|
||||||
|
final List<AttendanceLogViewModel> attendenceLogsView =
|
||||||
|
<AttendanceLogViewModel>[];
|
||||||
|
|
||||||
// ------------------ Organizations ------------------
|
// ------------------ Organizations ------------------
|
||||||
List<Organization> organizations = [];
|
final List<Organization> organizations = <Organization>[];
|
||||||
Organization? selectedOrganization;
|
Organization? selectedOrganization;
|
||||||
final isLoadingOrganizations = false.obs;
|
final RxBool isLoadingOrganizations = false.obs;
|
||||||
|
|
||||||
// ------------------ States ------------------
|
// ------------------ States ------------------
|
||||||
String selectedTab = 'todaysAttendance';
|
String selectedTab = 'todaysAttendance';
|
||||||
@ -42,16 +46,17 @@ class AttendanceController extends GetxController {
|
|||||||
final Rx<DateTime> endDateAttendance =
|
final Rx<DateTime> endDateAttendance =
|
||||||
DateTime.now().subtract(const Duration(days: 1)).obs;
|
DateTime.now().subtract(const Duration(days: 1)).obs;
|
||||||
|
|
||||||
final isLoading = true.obs;
|
final RxBool isLoading = true.obs;
|
||||||
final isLoadingProjects = true.obs;
|
final RxBool isLoadingProjects = true.obs;
|
||||||
final isLoadingEmployees = true.obs;
|
final RxBool isLoadingEmployees = true.obs;
|
||||||
final isLoadingAttendanceLogs = true.obs;
|
final RxBool isLoadingAttendanceLogs = true.obs;
|
||||||
final isLoadingRegularizationLogs = true.obs;
|
final RxBool isLoadingRegularizationLogs = true.obs;
|
||||||
final isLoadingLogView = true.obs;
|
final RxBool isLoadingLogView = true.obs;
|
||||||
final uploadingStates = <String, RxBool>{}.obs;
|
|
||||||
var showPendingOnly = false.obs;
|
|
||||||
|
|
||||||
final searchQuery = ''.obs;
|
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
|
final RxBool showPendingOnly = false.obs;
|
||||||
|
final RxString searchQuery = ''.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -64,35 +69,43 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _setDefaultDateRange() {
|
void _setDefaultDateRange() {
|
||||||
final today = DateTime.now();
|
final DateTime today = DateTime.now();
|
||||||
startDateAttendance.value = today.subtract(const Duration(days: 7));
|
startDateAttendance.value = today.subtract(const Duration(days: 7));
|
||||||
endDateAttendance.value = today.subtract(const Duration(days: 1));
|
endDateAttendance.value = today.subtract(const Duration(days: 1));
|
||||||
logSafe(
|
logSafe(
|
||||||
"Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}");
|
'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Computed Filters ------------------
|
// ------------------ Computed Filters ------------------
|
||||||
List<EmployeeModel> get filteredEmployees {
|
List<EmployeeModel> get filteredEmployees {
|
||||||
if (searchQuery.value.isEmpty) return employees;
|
final String query = searchQuery.value.trim().toLowerCase();
|
||||||
|
if (query.isEmpty) return employees;
|
||||||
return employees
|
return employees
|
||||||
.where((e) =>
|
.where(
|
||||||
e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
(EmployeeModel e) => e.name.toLowerCase().contains(query),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AttendanceLogModel> get filteredLogs {
|
List<AttendanceLogModel> get filteredLogs {
|
||||||
if (searchQuery.value.isEmpty) return attendanceLogs;
|
final String query = searchQuery.value.trim().toLowerCase();
|
||||||
|
if (query.isEmpty) return attendanceLogs;
|
||||||
return attendanceLogs
|
return attendanceLogs
|
||||||
.where((log) =>
|
.where(
|
||||||
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
(AttendanceLogModel log) => log.name.toLowerCase().contains(query),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RegularizationLogModel> get filteredRegularizationLogs {
|
List<RegularizationLogModel> get filteredRegularizationLogs {
|
||||||
if (searchQuery.value.isEmpty) return regularizationLogs;
|
final String query = searchQuery.value.trim().toLowerCase();
|
||||||
|
if (query.isEmpty) return regularizationLogs;
|
||||||
return regularizationLogs
|
return regularizationLogs
|
||||||
.where((log) =>
|
.where(
|
||||||
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
(RegularizationLogModel log) =>
|
||||||
|
log.name.toLowerCase().contains(query),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,13 +113,16 @@ class AttendanceController extends GetxController {
|
|||||||
Future<void> refreshDataFromNotification({String? projectId}) async {
|
Future<void> refreshDataFromNotification({String? projectId}) async {
|
||||||
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
||||||
if (projectId == null) {
|
if (projectId == null) {
|
||||||
logSafe("No project selected for attendance refresh from notification",
|
logSafe(
|
||||||
level: LogLevel.warning);
|
'No project selected for attendance refresh from notification',
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await fetchProjectData(projectId);
|
await fetchProjectData(projectId);
|
||||||
logSafe(
|
logSafe(
|
||||||
"Attendance data refreshed from notification for project $projectId");
|
'Attendance data refreshed from notification for project $projectId',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchTodaysAttendance(String? projectId) async {
|
Future<void> fetchTodaysAttendance(String? projectId) async {
|
||||||
@ -114,19 +130,35 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
isLoadingEmployees.value = true;
|
isLoadingEmployees.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getTodaysAttendance(
|
final List<dynamic>? response = await ApiService.getTodaysAttendance(
|
||||||
projectId,
|
projectId,
|
||||||
organizationId: selectedOrganization?.id,
|
organizationId: selectedOrganization?.id,
|
||||||
);
|
);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
|
employees
|
||||||
for (var emp in employees) {
|
..clear()
|
||||||
|
..addAll(
|
||||||
|
response
|
||||||
|
.map<EmployeeModel>(
|
||||||
|
(dynamic e) => EmployeeModel.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final EmployeeModel emp in employees) {
|
||||||
uploadingStates[emp.id] = false.obs;
|
uploadingStates[emp.id] = false.obs;
|
||||||
}
|
}
|
||||||
logSafe("Employees fetched: ${employees.length} for project $projectId");
|
|
||||||
|
logSafe(
|
||||||
|
'Employees fetched: ${employees.length} for project $projectId',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch employees for project $projectId",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch employees for project $projectId',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingEmployees.value = false;
|
isLoadingEmployees.value = false;
|
||||||
@ -135,14 +167,22 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
Future<void> fetchOrganizations(String projectId) async {
|
Future<void> fetchOrganizations(String projectId) async {
|
||||||
isLoadingOrganizations.value = true;
|
isLoadingOrganizations.value = true;
|
||||||
|
|
||||||
|
// Keep original return type inference from your ApiService
|
||||||
final response = await ApiService.getAssignedOrganizations(projectId);
|
final response = await ApiService.getAssignedOrganizations(projectId);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
organizations = response.data;
|
organizations
|
||||||
logSafe("Organizations fetched: ${organizations.length}");
|
..clear()
|
||||||
|
..addAll(response.data);
|
||||||
|
logSafe('Organizations fetched: ${organizations.length}');
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch organizations for project $projectId",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch organizations for project $projectId',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingOrganizations.value = false;
|
isLoadingOrganizations.value = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@ -152,61 +192,43 @@ class AttendanceController extends GetxController {
|
|||||||
String id,
|
String id,
|
||||||
String employeeId,
|
String employeeId,
|
||||||
String projectId, {
|
String projectId, {
|
||||||
String comment = "Marked via mobile app",
|
String comment = 'Marked via mobile app',
|
||||||
required int action,
|
required int action,
|
||||||
bool imageCapture = true,
|
bool imageCapture = true,
|
||||||
String? markTime,
|
String? markTime,
|
||||||
String? date,
|
String? date,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
uploadingStates[employeeId]?.value = true;
|
_setUploading(employeeId, true);
|
||||||
|
|
||||||
XFile? image;
|
final XFile? image = await _captureAndPrepareImage(
|
||||||
if (imageCapture) {
|
employeeId: employeeId,
|
||||||
image = await ImagePicker()
|
imageCapture: imageCapture,
|
||||||
.pickImage(source: ImageSource.camera, imageQuality: 80);
|
);
|
||||||
if (image == null) {
|
if (imageCapture && image == null) {
|
||||||
logSafe("Image capture cancelled.", level: LogLevel.warning);
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final timestampedFile = await TimestampImageHelper.addTimestamp(
|
|
||||||
imageFile: File(image.path));
|
|
||||||
|
|
||||||
final compressedBytes =
|
|
||||||
await compressImageToUnder100KB(timestampedFile);
|
|
||||||
if (compressedBytes == null) {
|
|
||||||
logSafe("Image compression failed.", level: LogLevel.error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final compressedFile = await saveCompressedImageToFile(compressedBytes);
|
|
||||||
image = XFile(compressedFile.path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _handleLocationPermission()) return false;
|
final Position? position = await _getCurrentPositionSafely();
|
||||||
final position = await Geolocator.getCurrentPosition(
|
if (position == null) return false;
|
||||||
desiredAccuracy: LocationAccuracy.high);
|
|
||||||
|
|
||||||
final imageName = imageCapture
|
final String imageName = imageCapture
|
||||||
? ApiService.generateImageName(employeeId, employees.length + 1)
|
? ApiService.generateImageName(
|
||||||
: "";
|
employeeId,
|
||||||
|
employees.length + 1,
|
||||||
|
)
|
||||||
|
: '';
|
||||||
|
|
||||||
final now = DateTime.now();
|
final DateTime effectiveDate =
|
||||||
DateTime effectiveDate = now;
|
_resolveEffectiveDateForAction(action, employeeId);
|
||||||
|
|
||||||
if (action == 1) {
|
final DateTime now = DateTime.now();
|
||||||
final log = attendanceLogs.firstWhereOrNull(
|
final String formattedMarkTime =
|
||||||
(log) => log.employeeId == employeeId && log.checkOut == null,
|
markTime ?? DateFormat('hh:mm a').format(now);
|
||||||
);
|
final String formattedDate =
|
||||||
if (log?.checkIn != null) effectiveDate = log!.checkIn!;
|
|
||||||
}
|
|
||||||
|
|
||||||
final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now);
|
|
||||||
final formattedDate =
|
|
||||||
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
|
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
|
||||||
|
|
||||||
final result = await ApiService.uploadAttendanceImage(
|
final bool result = await ApiService.uploadAttendanceImage(
|
||||||
id,
|
id,
|
||||||
employeeId,
|
employeeId,
|
||||||
image,
|
image,
|
||||||
@ -221,15 +243,99 @@ class AttendanceController extends GetxController {
|
|||||||
date: formattedDate,
|
date: formattedDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
logSafe(
|
if (result) {
|
||||||
"Attendance uploaded for $employeeId, action: $action, date: $formattedDate");
|
logSafe(
|
||||||
|
'Attendance uploaded for $employeeId, action: $action, date: $formattedDate',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Get.isRegistered<DashboardController>()) {
|
||||||
|
final DashboardController dashboardController =
|
||||||
|
Get.find<DashboardController>();
|
||||||
|
await dashboardController.fetchTodaysAttendance(projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe("Error uploading attendance",
|
logSafe(
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
'Error uploading attendance',
|
||||||
|
level: LogLevel.error,
|
||||||
|
error: e,
|
||||||
|
stackTrace: stacktrace,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
uploadingStates[employeeId]?.value = false;
|
_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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,14 +345,19 @@ class AttendanceController extends GetxController {
|
|||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
logSafe('Location permissions are denied', level: LogLevel.warning);
|
logSafe(
|
||||||
|
'Location permissions are denied',
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission == LocationPermission.deniedForever) {
|
if (permission == LocationPermission.deniedForever) {
|
||||||
logSafe('Location permissions are permanently denied',
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Location permissions are permanently denied',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,25 +365,40 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Attendance Logs ------------------
|
// ------------------ Attendance Logs ------------------
|
||||||
Future<void> fetchAttendanceLogs(String? projectId,
|
Future<void> fetchAttendanceLogs(
|
||||||
{DateTime? dateFrom, DateTime? dateTo}) async {
|
String? projectId, {
|
||||||
|
DateTime? dateFrom,
|
||||||
|
DateTime? dateTo,
|
||||||
|
}) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoadingAttendanceLogs.value = true;
|
isLoadingAttendanceLogs.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getAttendanceLogs(
|
final List<dynamic>? response = await ApiService.getAttendanceLogs(
|
||||||
projectId,
|
projectId,
|
||||||
dateFrom: dateFrom,
|
dateFrom: dateFrom,
|
||||||
dateTo: dateTo,
|
dateTo: dateTo,
|
||||||
organizationId: selectedOrganization?.id,
|
organizationId: selectedOrganization?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendanceLogs =
|
attendanceLogs
|
||||||
response.map((e) => AttendanceLogModel.fromJson(e)).toList();
|
..clear()
|
||||||
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
|
..addAll(
|
||||||
|
response
|
||||||
|
.map<AttendanceLogModel>(
|
||||||
|
(dynamic e) => AttendanceLogModel.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
logSafe('Attendance logs fetched: ${attendanceLogs.length}');
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch attendance logs for project $projectId",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch attendance logs for project $projectId',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingAttendanceLogs.value = false;
|
isLoadingAttendanceLogs.value = false;
|
||||||
@ -280,25 +406,37 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
||||||
final groupedLogs = <String, List<AttendanceLogModel>>{};
|
final Map<String, List<AttendanceLogModel>> groupedLogs =
|
||||||
|
<String, List<AttendanceLogModel>>{};
|
||||||
|
|
||||||
for (var logItem in attendanceLogs) {
|
for (final AttendanceLogModel logItem in attendanceLogs) {
|
||||||
final checkInDate = logItem.checkIn != null
|
final String checkInDate = logItem.checkIn != null
|
||||||
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
|
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
|
||||||
: 'Unknown';
|
: 'Unknown';
|
||||||
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
|
|
||||||
|
groupedLogs.putIfAbsent(
|
||||||
|
checkInDate,
|
||||||
|
() => <AttendanceLogModel>[],
|
||||||
|
)..add(logItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
final sortedEntries = groupedLogs.entries.toList()
|
final List<MapEntry<String, List<AttendanceLogModel>>> sortedEntries =
|
||||||
..sort((a, b) {
|
groupedLogs.entries.toList()
|
||||||
if (a.key == 'Unknown') return 1;
|
..sort(
|
||||||
if (b.key == 'Unknown') return -1;
|
(MapEntry<String, List<AttendanceLogModel>> a,
|
||||||
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
|
MapEntry<String, List<AttendanceLogModel>> b) {
|
||||||
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
|
if (a.key == 'Unknown') return 1;
|
||||||
return dateB.compareTo(dateA);
|
if (b.key == 'Unknown') return -1;
|
||||||
});
|
|
||||||
|
|
||||||
return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Regularization Logs ------------------
|
// ------------------ Regularization Logs ------------------
|
||||||
@ -307,17 +445,31 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
isLoadingRegularizationLogs.value = true;
|
isLoadingRegularizationLogs.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getRegularizationLogs(
|
final List<dynamic>? response = await ApiService.getRegularizationLogs(
|
||||||
projectId,
|
projectId,
|
||||||
organizationId: selectedOrganization?.id,
|
organizationId: selectedOrganization?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
regularizationLogs =
|
regularizationLogs
|
||||||
response.map((e) => RegularizationLogModel.fromJson(e)).toList();
|
..clear()
|
||||||
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
|
..addAll(
|
||||||
|
response
|
||||||
|
.map<RegularizationLogModel>(
|
||||||
|
(dynamic e) => RegularizationLogModel.fromJson(
|
||||||
|
e as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
logSafe(
|
||||||
|
'Regularization logs fetched: ${regularizationLogs.length}',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch regularization logs for project $projectId",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch regularization logs for project $projectId',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingRegularizationLogs.value = false;
|
isLoadingRegularizationLogs.value = false;
|
||||||
@ -330,16 +482,33 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
isLoadingLogView.value = true;
|
isLoadingLogView.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getAttendanceLogView(id);
|
final List<dynamic>? response = await ApiService.getAttendanceLogView(id);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendenceLogsView =
|
attendenceLogsView
|
||||||
response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
|
..clear()
|
||||||
attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000))
|
..addAll(
|
||||||
.compareTo(a.activityTime ?? DateTime(2000)));
|
response
|
||||||
logSafe("Attendance log view fetched for ID: $id");
|
.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');
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch attendance log view for ID $id",
|
logSafe(
|
||||||
level: LogLevel.error);
|
'Failed to fetch attendance log view for ID $id',
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingLogView.value = false;
|
isLoadingLogView.value = false;
|
||||||
@ -375,16 +544,19 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logSafe(
|
logSafe(
|
||||||
"Project data fetched for project ID: $projectId, tab: $selectedTab");
|
'Project data fetched for project ID: $projectId, tab: $selectedTab',
|
||||||
|
);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ UI Interaction ------------------
|
// ------------------ UI Interaction ------------------
|
||||||
Future<void> selectDateRangeForAttendance(
|
Future<void> selectDateRangeForAttendance(
|
||||||
BuildContext context, AttendanceController controller) async {
|
BuildContext context,
|
||||||
final today = DateTime.now();
|
AttendanceController controller,
|
||||||
|
) async {
|
||||||
|
final DateTime today = DateTime.now();
|
||||||
|
|
||||||
final picked = await showDateRangePicker(
|
final DateTimeRange? picked = await showDateRangePicker(
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: DateTime(2022),
|
firstDate: DateTime(2022),
|
||||||
lastDate: today.subtract(const Duration(days: 1)),
|
lastDate: today.subtract(const Duration(days: 1)),
|
||||||
@ -399,7 +571,8 @@ class AttendanceController extends GetxController {
|
|||||||
endDateAttendance.value = picked.end;
|
endDateAttendance.value = picked.end;
|
||||||
|
|
||||||
logSafe(
|
logSafe(
|
||||||
"Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}");
|
'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}',
|
||||||
|
);
|
||||||
|
|
||||||
await controller.fetchAttendanceLogs(
|
await controller.fetchAttendanceLogs(
|
||||||
Get.find<ProjectController>().selectedProject?.id,
|
Get.find<ProjectController>().selectedProject?.id,
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
|
|
||||||
class ForgotPasswordController extends MyController {
|
class ForgotPasswordController extends MyController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class LoginController extends MyController {
|
class LoginController extends MyController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
|
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:on_field_work/controller/permission_controller.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:on_field_work/controller/project_controller.dart';
|
||||||
|
|
||||||
class MPINController extends GetxController {
|
class MPINController extends GetxController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class OTPController extends GetxController {
|
class OTPController extends GetxController {
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class RegisterAccountController extends MyController {
|
class RegisterAccountController extends MyController {
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class ResetPasswordController extends MyController {
|
class ResetPasswordController extends MyController {
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
|
|||||||
@ -1,197 +1,207 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:on_field_work/controller/project_controller.dart';
|
||||||
import 'package:marco/model/dashboard/project_progress_model.dart';
|
import 'package:on_field_work/model/dashboard/project_progress_model.dart';
|
||||||
import 'package:marco/model/dashboard/pending_expenses_model.dart';
|
import 'package:on_field_work/model/dashboard/pending_expenses_model.dart';
|
||||||
import 'package:marco/model/dashboard/expense_type_report_model.dart';
|
import 'package:on_field_work/model/dashboard/expense_type_report_model.dart';
|
||||||
import 'package:marco/model/dashboard/monthly_expence_model.dart';
|
import 'package:on_field_work/model/dashboard/monthly_expence_model.dart';
|
||||||
import 'package:marco/model/expense/expense_type_model.dart';
|
import 'package:on_field_work/model/expense/expense_type_model.dart';
|
||||||
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
|
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
|
||||||
|
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
||||||
|
|
||||||
class DashboardController extends GetxController {
|
class DashboardController extends GetxController {
|
||||||
// =========================
|
// Dependencies
|
||||||
// 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;
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Project progress overview
|
|
||||||
// =========================
|
|
||||||
final RxList<ChartTaskData> projectChartData = <ChartTaskData>[].obs;
|
|
||||||
final RxString projectSelectedRange = '15D'.obs;
|
|
||||||
final RxBool projectIsChartView = true.obs;
|
|
||||||
final RxBool isProjectLoading = false.obs;
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Projects overview
|
|
||||||
// =========================
|
|
||||||
final RxInt totalProjects = 0.obs;
|
|
||||||
final RxInt ongoingProjects = 0.obs;
|
|
||||||
final RxBool isProjectsLoading = false.obs;
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Tasks overview
|
|
||||||
// =========================
|
|
||||||
final RxInt totalTasks = 0.obs;
|
|
||||||
final RxInt completedTasks = 0.obs;
|
|
||||||
final RxBool isTasksLoading = false.obs;
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Teams overview
|
|
||||||
// =========================
|
|
||||||
final RxInt totalEmployees = 0.obs;
|
|
||||||
final RxInt inToday = 0.obs;
|
|
||||||
final RxBool isTeamsLoading = false.obs;
|
|
||||||
|
|
||||||
// Common ranges
|
|
||||||
final List<String> ranges = ['7D', '15D', '30D'];
|
|
||||||
|
|
||||||
// Inject ProjectController
|
|
||||||
final ProjectController projectController = Get.put(ProjectController());
|
final ProjectController projectController = Get.put(ProjectController());
|
||||||
// Pending Expenses overview
|
|
||||||
// =========================
|
|
||||||
final RxBool isPendingExpensesLoading = false.obs;
|
|
||||||
final Rx<PendingExpensesData?> pendingExpensesData =
|
|
||||||
Rx<PendingExpensesData?>(null);
|
|
||||||
// =========================
|
// =========================
|
||||||
// Expense Type Report
|
// 1. STATE VARIABLES
|
||||||
// =========================
|
// =========================
|
||||||
final RxBool isExpenseTypeReportLoading = false.obs;
|
|
||||||
final Rx<ExpenseTypeReportData?> expenseTypeReportData =
|
// Attendance
|
||||||
Rx<ExpenseTypeReportData?>(null);
|
final roleWiseData = <Map<String, dynamic>>[].obs;
|
||||||
final Rx<DateTime> expenseReportStartDate =
|
final attendanceSelectedRange = '15D'.obs;
|
||||||
|
final attendanceIsChartView = true.obs;
|
||||||
|
final isAttendanceLoading = false.obs;
|
||||||
|
|
||||||
|
// Project Progress
|
||||||
|
final projectChartData = <ChartTaskData>[].obs;
|
||||||
|
final projectSelectedRange = '15D'.obs;
|
||||||
|
final projectIsChartView = true.obs;
|
||||||
|
final isProjectLoading = false.obs;
|
||||||
|
|
||||||
|
// Overview Counts
|
||||||
|
final totalProjects = 0.obs;
|
||||||
|
final ongoingProjects = 0.obs;
|
||||||
|
final isProjectsLoading = 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;
|
DateTime.now().subtract(const Duration(days: 15)).obs;
|
||||||
final Rx<DateTime> expenseReportEndDate = DateTime.now().obs;
|
final expenseReportEndDate = DateTime.now().obs;
|
||||||
// =========================
|
|
||||||
// Monthly Expense Report
|
final isMonthlyExpenseLoading = false.obs;
|
||||||
// =========================
|
final monthlyExpenseList = <MonthlyExpenseData>[].obs;
|
||||||
final RxBool isMonthlyExpenseLoading = false.obs;
|
final selectedMonthlyExpenseDuration =
|
||||||
final RxList<MonthlyExpenseData> monthlyExpenseList =
|
|
||||||
<MonthlyExpenseData>[].obs;
|
|
||||||
// =========================
|
|
||||||
// Monthly Expense Report Filters
|
|
||||||
// =========================
|
|
||||||
final Rx<MonthlyExpenseDuration> selectedMonthlyExpenseDuration =
|
|
||||||
MonthlyExpenseDuration.twelveMonths.obs;
|
MonthlyExpenseDuration.twelveMonths.obs;
|
||||||
|
final selectedMonthsCount = 12.obs;
|
||||||
|
|
||||||
final RxInt selectedMonthsCount = 12.obs;
|
final expenseTypes = <ExpenseTypeModel>[].obs;
|
||||||
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
|
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
||||||
final Rx<ExpenseTypeModel?> selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
|
||||||
|
|
||||||
void updateSelectedExpenseType(ExpenseTypeModel? type) {
|
// Teams/Employees
|
||||||
selectedExpenseType.value = type;
|
final isLoadingEmployees = true.obs;
|
||||||
|
final employees = <EmployeeModel>[].obs;
|
||||||
|
final uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|
||||||
// Debug print to verify
|
// Collection
|
||||||
print('Selected: ${type?.name ?? "All Types"}');
|
final isCollectionOverviewLoading = true.obs;
|
||||||
|
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
||||||
|
// =========================
|
||||||
|
// Purchase Invoice Overview
|
||||||
|
// =========================
|
||||||
|
final isPurchaseInvoiceLoading = true.obs;
|
||||||
|
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
||||||
|
// Constants
|
||||||
|
final List<String> ranges = ['7D', '15D', '30D'];
|
||||||
|
static const _rangeDaysMap = {
|
||||||
|
'7D': 7,
|
||||||
|
'15D': 15,
|
||||||
|
'30D': 30,
|
||||||
|
'3M': 90,
|
||||||
|
'6M': 180
|
||||||
|
};
|
||||||
|
|
||||||
if (type == null) {
|
// =========================
|
||||||
fetchMonthlyExpenses();
|
// 2. COMPUTED PROPERTIES
|
||||||
} else {
|
// =========================
|
||||||
fetchMonthlyExpenses(categoryId: type.id);
|
|
||||||
}
|
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
|
||||||
|
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
|
||||||
|
|
||||||
|
// DSO Calculation Constants
|
||||||
|
static const double _w0_30 = 15.0;
|
||||||
|
static const double _w30_60 = 45.0;
|
||||||
|
static const double _w60_90 = 75.0;
|
||||||
|
static const double _w90_plus = 105.0;
|
||||||
|
|
||||||
|
double get calculatedDSO {
|
||||||
|
final data = collectionOverviewData.value;
|
||||||
|
if (data == null || data.totalDueAmount == 0) return 0.0;
|
||||||
|
|
||||||
|
final double weightedDue = (data.bucket0To30Amount * _w0_30) +
|
||||||
|
(data.bucket30To60Amount * _w30_60) +
|
||||||
|
(data.bucket60To90Amount * _w60_90) +
|
||||||
|
(data.bucket90PlusAmount * _w90_plus);
|
||||||
|
|
||||||
|
return weightedDue / data.totalDueAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// 3. LIFECYCLE
|
||||||
|
// =========================
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
logSafe('DashboardController initialized', level: LogLevel.info);
|
||||||
|
|
||||||
logSafe(
|
// Project Selection Listener
|
||||||
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
|
|
||||||
level: LogLevel.info,
|
|
||||||
);
|
|
||||||
|
|
||||||
fetchAllDashboardData();
|
|
||||||
|
|
||||||
// React to project change
|
|
||||||
ever<String>(projectController.selectedProjectId, (id) {
|
ever<String>(projectController.selectedProjectId, (id) {
|
||||||
fetchAllDashboardData();
|
if (id.isNotEmpty) {
|
||||||
|
fetchAllDashboardData();
|
||||||
|
fetchTodaysAttendance(id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Expense Report Date Listener
|
||||||
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
||||||
fetchExpenseTypeReport(
|
if (projectController.selectedProjectId.value.isNotEmpty) {
|
||||||
startDate: expenseReportStartDate.value,
|
fetchExpenseTypeReport(
|
||||||
endDate: expenseReportEndDate.value,
|
startDate: expenseReportStartDate.value,
|
||||||
);
|
endDate: expenseReportEndDate.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// React to range changes
|
|
||||||
|
// Chart Range Listeners
|
||||||
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
||||||
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Helper Methods
|
// 4. USER ACTIONS
|
||||||
// =========================
|
// =========================
|
||||||
int _getDaysFromRange(String range) {
|
|
||||||
switch (range) {
|
void updateAttendanceRange(String range) =>
|
||||||
case '7D':
|
attendanceSelectedRange.value = range;
|
||||||
return 7;
|
void updateProjectRange(String range) => projectSelectedRange.value = range;
|
||||||
case '15D':
|
void toggleAttendanceChartView(bool isChart) =>
|
||||||
return 15;
|
attendanceIsChartView.value = isChart;
|
||||||
case '30D':
|
void toggleProjectChartView(bool isChart) =>
|
||||||
return 30;
|
projectIsChartView.value = isChart;
|
||||||
case '3M':
|
|
||||||
return 90;
|
void updateSelectedExpenseType(ExpenseTypeModel? type) {
|
||||||
case '6M':
|
selectedExpenseType.value = type;
|
||||||
return 180;
|
fetchMonthlyExpenses(categoryId: type?.id);
|
||||||
default:
|
}
|
||||||
return 7;
|
|
||||||
|
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
|
||||||
|
selectedMonthlyExpenseDuration.value = duration;
|
||||||
|
|
||||||
|
// Efficient Map lookup instead of Switch
|
||||||
|
const durationMap = {
|
||||||
|
MonthlyExpenseDuration.oneMonth: 1,
|
||||||
|
MonthlyExpenseDuration.threeMonths: 3,
|
||||||
|
MonthlyExpenseDuration.sixMonths: 6,
|
||||||
|
MonthlyExpenseDuration.twelveMonths: 12,
|
||||||
|
MonthlyExpenseDuration.all: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
selectedMonthsCount.value = durationMap[duration] ?? 12;
|
||||||
|
fetchMonthlyExpenses();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshDashboard() => fetchAllDashboardData();
|
||||||
|
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
|
||||||
|
Future<void> refreshProjects() => fetchProjectProgress();
|
||||||
|
Future<void> refreshTasks() async {
|
||||||
|
final id = projectController.selectedProjectId.value;
|
||||||
|
if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// 5. DATA FETCHING (API)
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
/// Wrapper to reduce try-finally boilerplate for loading states
|
||||||
|
Future<void> _executeApiCall(
|
||||||
|
RxBool loader, Future<void> Function() apiLogic) async {
|
||||||
|
loader.value = true;
|
||||||
|
try {
|
||||||
|
await apiLogic();
|
||||||
|
} finally {
|
||||||
|
loader.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value);
|
|
||||||
int getProjectDays() => _getDaysFromRange(projectSelectedRange.value);
|
|
||||||
|
|
||||||
void updateAttendanceRange(String range) {
|
|
||||||
attendanceSelectedRange.value = range;
|
|
||||||
logSafe('Attendance range updated to $range', level: LogLevel.debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateProjectRange(String range) {
|
|
||||||
projectSelectedRange.value = range;
|
|
||||||
logSafe('Project range updated to $range', level: LogLevel.debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleAttendanceChartView(bool isChart) {
|
|
||||||
attendanceIsChartView.value = isChart;
|
|
||||||
logSafe('Attendance chart view toggled to: $isChart',
|
|
||||||
level: LogLevel.debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleProjectChartView(bool isChart) {
|
|
||||||
projectIsChartView.value = isChart;
|
|
||||||
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Manual Refresh Methods
|
|
||||||
// =========================
|
|
||||||
Future<void> refreshDashboard() async {
|
|
||||||
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
|
|
||||||
await fetchAllDashboardData();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refreshAttendance() async => fetchRoleWiseAttendance();
|
|
||||||
Future<void> refreshTasks() async {
|
|
||||||
final projectId = projectController.selectedProjectId.value;
|
|
||||||
if (projectId.isNotEmpty) await fetchDashboardTasks(projectId: projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refreshProjects() async => fetchProjectProgress();
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Fetch All Dashboard Data
|
|
||||||
// =========================
|
|
||||||
Future<void> fetchAllDashboardData() async {
|
Future<void> fetchAllDashboardData() async {
|
||||||
final String projectId = projectController.selectedProjectId.value;
|
final String projectId = projectController.selectedProjectId.value;
|
||||||
|
if (projectId.isEmpty) return;
|
||||||
if (projectId.isEmpty) {
|
|
||||||
logSafe('No project selected. Skipping dashboard API calls.',
|
|
||||||
level: LogLevel.warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
fetchRoleWiseAttendance(),
|
fetchRoleWiseAttendance(),
|
||||||
@ -204,248 +214,150 @@ class DashboardController extends GetxController {
|
|||||||
endDate: expenseReportEndDate.value,
|
endDate: expenseReportEndDate.value,
|
||||||
),
|
),
|
||||||
fetchMonthlyExpenses(),
|
fetchMonthlyExpenses(),
|
||||||
fetchMasterData()
|
fetchMasterData(),
|
||||||
|
fetchCollectionOverview(),
|
||||||
|
fetchPurchaseInvoiceOverview(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
|
Future<void> fetchCollectionOverview() async {
|
||||||
selectedMonthlyExpenseDuration.value = duration;
|
final projectId = projectController.selectedProjectId.value;
|
||||||
|
if (projectId.isEmpty) return;
|
||||||
|
|
||||||
// Set months count based on selection
|
await _executeApiCall(isCollectionOverviewLoading, () async {
|
||||||
switch (duration) {
|
final response =
|
||||||
case MonthlyExpenseDuration.oneMonth:
|
await ApiService.getCollectionOverview(projectId: projectId);
|
||||||
selectedMonthsCount.value = 1;
|
collectionOverviewData.value =
|
||||||
break;
|
(response?.success == true) ? response!.data : null;
|
||||||
case MonthlyExpenseDuration.threeMonths:
|
});
|
||||||
selectedMonthsCount.value = 3;
|
}
|
||||||
break;
|
|
||||||
case MonthlyExpenseDuration.sixMonths:
|
|
||||||
selectedMonthsCount.value = 6;
|
|
||||||
break;
|
|
||||||
case MonthlyExpenseDuration.twelveMonths:
|
|
||||||
selectedMonthsCount.value = 12;
|
|
||||||
break;
|
|
||||||
case MonthlyExpenseDuration.all:
|
|
||||||
selectedMonthsCount.value = 0; // 0 = All months in your API
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-fetch updated data
|
Future<void> fetchTodaysAttendance(String projectId) async {
|
||||||
fetchMonthlyExpenses();
|
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 {
|
Future<void> fetchMasterData() async {
|
||||||
try {
|
try {
|
||||||
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
final data = await ApiService.getMasterExpenseTypes();
|
||||||
if (expenseTypesData is List) {
|
if (data is List) {
|
||||||
expenseTypes.value =
|
expenseTypes.value =
|
||||||
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
data.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_) {}
|
||||||
logSafe('Error fetching master data', level: LogLevel.error, error: e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchMonthlyExpenses({String? categoryId}) async {
|
Future<void> fetchMonthlyExpenses({String? categoryId}) async {
|
||||||
try {
|
await _executeApiCall(isMonthlyExpenseLoading, () async {
|
||||||
isMonthlyExpenseLoading.value = true;
|
|
||||||
|
|
||||||
int months = selectedMonthsCount.value;
|
|
||||||
logSafe(
|
|
||||||
'Fetching Monthly Expense Report for last $months months'
|
|
||||||
'${categoryId != null ? ' (categoryId: $categoryId)' : ''}',
|
|
||||||
level: LogLevel.info,
|
|
||||||
);
|
|
||||||
|
|
||||||
final response = await ApiService.getDashboardMonthlyExpensesApi(
|
final response = await ApiService.getDashboardMonthlyExpensesApi(
|
||||||
categoryId: categoryId,
|
categoryId: categoryId,
|
||||||
months: months,
|
months: selectedMonthsCount.value,
|
||||||
);
|
);
|
||||||
|
monthlyExpenseList.value =
|
||||||
|
(response?.success == true) ? response!.data : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (response != null && response.success) {
|
Future<void> fetchPurchaseInvoiceOverview() async {
|
||||||
monthlyExpenseList.value = response.data;
|
final projectId = projectController.selectedProjectId.value;
|
||||||
logSafe('Monthly Expense Report fetched successfully.',
|
if (projectId.isEmpty) return;
|
||||||
level: LogLevel.info);
|
|
||||||
} else {
|
await _executeApiCall(isPurchaseInvoiceLoading, () async {
|
||||||
monthlyExpenseList.clear();
|
final response = await ApiService.getPurchaseInvoiceOverview(
|
||||||
logSafe('Failed to fetch Monthly Expense Report.',
|
projectId: projectId,
|
||||||
level: LogLevel.error);
|
);
|
||||||
}
|
purchaseInvoiceOverviewData.value =
|
||||||
} catch (e, st) {
|
(response?.success == true) ? response!.data : null;
|
||||||
monthlyExpenseList.clear();
|
});
|
||||||
logSafe('Error fetching Monthly Expense Report',
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
} finally {
|
|
||||||
isMonthlyExpenseLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchPendingExpenses() async {
|
Future<void> fetchPendingExpenses() async {
|
||||||
final String projectId = projectController.selectedProjectId.value;
|
final id = projectController.selectedProjectId.value;
|
||||||
if (projectId.isEmpty) return;
|
if (id.isEmpty) return;
|
||||||
|
|
||||||
try {
|
await _executeApiCall(isPendingExpensesLoading, () async {
|
||||||
isPendingExpensesLoading.value = true;
|
final response = await ApiService.getPendingExpensesApi(projectId: id);
|
||||||
final response =
|
pendingExpensesData.value =
|
||||||
await ApiService.getPendingExpensesApi(projectId: projectId);
|
(response?.success == true) ? response!.data : null;
|
||||||
|
});
|
||||||
if (response != null && response.success) {
|
|
||||||
pendingExpensesData.value = response.data;
|
|
||||||
logSafe('Pending expenses fetched successfully.', level: LogLevel.info);
|
|
||||||
} else {
|
|
||||||
pendingExpensesData.value = null;
|
|
||||||
logSafe('Failed to fetch pending expenses.', level: LogLevel.error);
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
pendingExpensesData.value = null;
|
|
||||||
logSafe('Error fetching pending expenses',
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
} finally {
|
|
||||||
isPendingExpensesLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
|
||||||
// API Calls
|
|
||||||
// =========================
|
|
||||||
Future<void> fetchRoleWiseAttendance() async {
|
Future<void> fetchRoleWiseAttendance() async {
|
||||||
final String projectId = projectController.selectedProjectId.value;
|
final id = projectController.selectedProjectId.value;
|
||||||
if (projectId.isEmpty) return;
|
if (id.isEmpty) return;
|
||||||
|
|
||||||
try {
|
await _executeApiCall(isAttendanceLoading, () async {
|
||||||
isAttendanceLoading.value = true;
|
final response = await ApiService.getDashboardAttendanceOverview(
|
||||||
final List<dynamic>? response =
|
id, getAttendanceDays());
|
||||||
await ApiService.getDashboardAttendanceOverview(
|
roleWiseData.value =
|
||||||
projectId, getAttendanceDays());
|
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
|
||||||
|
});
|
||||||
if (response != null) {
|
|
||||||
roleWiseData.value =
|
|
||||||
response.map((e) => Map<String, dynamic>.from(e)).toList();
|
|
||||||
logSafe('Attendance overview fetched successfully.',
|
|
||||||
level: LogLevel.info);
|
|
||||||
} else {
|
|
||||||
roleWiseData.clear();
|
|
||||||
logSafe('Failed to fetch attendance overview: response is null.',
|
|
||||||
level: LogLevel.error);
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
roleWiseData.clear();
|
|
||||||
logSafe('Error fetching attendance overview',
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
} finally {
|
|
||||||
isAttendanceLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchExpenseTypeReport({
|
Future<void> fetchExpenseTypeReport(
|
||||||
required DateTime startDate,
|
{required DateTime startDate, required DateTime endDate}) async {
|
||||||
required DateTime endDate,
|
final id = projectController.selectedProjectId.value;
|
||||||
}) async {
|
if (id.isEmpty) return;
|
||||||
final String projectId = projectController.selectedProjectId.value;
|
|
||||||
if (projectId.isEmpty) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isExpenseTypeReportLoading.value = true;
|
|
||||||
|
|
||||||
|
await _executeApiCall(isExpenseTypeReportLoading, () async {
|
||||||
final response = await ApiService.getExpenseTypeReportApi(
|
final response = await ApiService.getExpenseTypeReportApi(
|
||||||
projectId: projectId,
|
projectId: id,
|
||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
endDate: endDate,
|
endDate: endDate,
|
||||||
);
|
);
|
||||||
|
expenseTypeReportData.value =
|
||||||
if (response != null && response.success) {
|
(response?.success == true) ? response!.data : null;
|
||||||
expenseTypeReportData.value = response.data;
|
});
|
||||||
logSafe('Expense Type Report fetched successfully.',
|
|
||||||
level: LogLevel.info);
|
|
||||||
} else {
|
|
||||||
expenseTypeReportData.value = null;
|
|
||||||
logSafe('Failed to fetch Expense Type Report.', level: LogLevel.error);
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
expenseTypeReportData.value = null;
|
|
||||||
logSafe('Error fetching Expense Type Report',
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
} finally {
|
|
||||||
isExpenseTypeReportLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchProjectProgress() async {
|
Future<void> fetchProjectProgress() async {
|
||||||
final String projectId = projectController.selectedProjectId.value;
|
final id = projectController.selectedProjectId.value;
|
||||||
if (projectId.isEmpty) return;
|
if (id.isEmpty) return;
|
||||||
|
|
||||||
try {
|
await _executeApiCall(isProjectLoading, () async {
|
||||||
isProjectLoading.value = true;
|
|
||||||
final response = await ApiService.getProjectProgress(
|
final response = await ApiService.getProjectProgress(
|
||||||
projectId: projectId, days: getProjectDays());
|
projectId: id, days: getProjectDays());
|
||||||
|
if (response?.success == true) {
|
||||||
if (response != null && response.success) {
|
projectChartData.value = response!.data
|
||||||
projectChartData.value =
|
.map((d) => ChartTaskData.fromProjectData(d))
|
||||||
response.data.map((d) => ChartTaskData.fromProjectData(d)).toList();
|
.toList();
|
||||||
logSafe('Project progress data mapped for chart', level: LogLevel.info);
|
|
||||||
} else {
|
} else {
|
||||||
projectChartData.clear();
|
projectChartData.clear();
|
||||||
logSafe('Failed to fetch project progress', level: LogLevel.error);
|
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
});
|
||||||
projectChartData.clear();
|
|
||||||
logSafe('Error fetching project progress',
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
} finally {
|
|
||||||
isProjectLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDashboardTasks({required String projectId}) async {
|
Future<void> fetchDashboardTasks({required String projectId}) async {
|
||||||
if (projectId.isEmpty) return;
|
await _executeApiCall(isTasksLoading, () async {
|
||||||
|
|
||||||
try {
|
|
||||||
isTasksLoading.value = true;
|
|
||||||
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
||||||
|
if (response?.success == true) {
|
||||||
if (response != null && response.success) {
|
totalTasks.value = response!.data?.totalTasks ?? 0;
|
||||||
totalTasks.value = response.data?.totalTasks ?? 0;
|
|
||||||
completedTasks.value = response.data?.completedTasks ?? 0;
|
completedTasks.value = response.data?.completedTasks ?? 0;
|
||||||
logSafe('Dashboard tasks fetched', level: LogLevel.info);
|
|
||||||
} else {
|
} else {
|
||||||
totalTasks.value = 0;
|
totalTasks.value = 0;
|
||||||
completedTasks.value = 0;
|
completedTasks.value = 0;
|
||||||
logSafe('Failed to fetch tasks', level: LogLevel.error);
|
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
});
|
||||||
totalTasks.value = 0;
|
|
||||||
completedTasks.value = 0;
|
|
||||||
logSafe('Error fetching tasks',
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
} finally {
|
|
||||||
isTasksLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDashboardTeams({required String projectId}) async {
|
Future<void> fetchDashboardTeams({required String projectId}) async {
|
||||||
if (projectId.isEmpty) return;
|
await _executeApiCall(isTeamsLoading, () async {
|
||||||
|
|
||||||
try {
|
|
||||||
isTeamsLoading.value = true;
|
|
||||||
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
||||||
|
if (response?.success == true) {
|
||||||
if (response != null && response.success) {
|
totalEmployees.value = response!.data?.totalEmployees ?? 0;
|
||||||
totalEmployees.value = response.data?.totalEmployees ?? 0;
|
|
||||||
inToday.value = response.data?.inToday ?? 0;
|
inToday.value = response.data?.inToday ?? 0;
|
||||||
logSafe('Dashboard teams fetched', level: LogLevel.info);
|
|
||||||
} else {
|
} else {
|
||||||
totalEmployees.value = 0;
|
totalEmployees.value = 0;
|
||||||
inToday.value = 0;
|
inToday.value = 0;
|
||||||
logSafe('Failed to fetch teams', level: LogLevel.error);
|
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
});
|
||||||
totalEmployees.value = 0;
|
|
||||||
inToday.value = 0;
|
|
||||||
logSafe('Error fetching teams',
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
} finally {
|
|
||||||
isTeamsLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/controller/directory/directory_controller.dart';
|
import 'package:on_field_work/controller/directory/directory_controller.dart';
|
||||||
import 'package:marco/controller/directory/notes_controller.dart';
|
import 'package:on_field_work/controller/directory/notes_controller.dart';
|
||||||
|
|
||||||
class AddCommentController extends GetxController {
|
class AddCommentController extends GetxController {
|
||||||
final String contactId;
|
final String contactId;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
class AddContactController extends GetxController {
|
class AddContactController extends GetxController {
|
||||||
final RxList<String> categories = <String>[].obs;
|
final RxList<String> categories = <String>[].obs;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
class BucketController extends GetxController {
|
class BucketController extends GetxController {
|
||||||
RxBool isCreating = false.obs;
|
RxBool isCreating = false.obs;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/directory/contact_model.dart';
|
import 'package:on_field_work/model/directory/contact_model.dart';
|
||||||
import 'package:marco/model/directory/contact_bucket_list_model.dart';
|
import 'package:on_field_work/model/directory/contact_bucket_list_model.dart';
|
||||||
import 'package:marco/model/directory/directory_comment_model.dart';
|
import 'package:on_field_work/model/directory/directory_comment_model.dart';
|
||||||
|
|
||||||
class DirectoryController extends GetxController {
|
class DirectoryController extends GetxController {
|
||||||
// -------------------- CONTACTS --------------------
|
// -------------------- CONTACTS --------------------
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/controller/directory/directory_controller.dart';
|
import 'package:on_field_work/controller/directory/directory_controller.dart';
|
||||||
|
|
||||||
class ManageBucketController extends GetxController {
|
class ManageBucketController extends GetxController {
|
||||||
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/directory/note_list_response_model.dart';
|
import 'package:on_field_work/model/directory/note_list_response_model.dart';
|
||||||
|
|
||||||
class NotesController extends GetxController {
|
class NotesController extends GetxController {
|
||||||
RxList<NoteModel> notesList = <NoteModel>[].obs;
|
RxList<NoteModel> notesList = <NoteModel>[].obs;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/document/document_details_model.dart';
|
import 'package:on_field_work/model/document/document_details_model.dart';
|
||||||
import 'package:marco/model/document/document_version_model.dart';
|
import 'package:on_field_work/model/document/document_version_model.dart';
|
||||||
|
|
||||||
class DocumentDetailsController extends GetxController {
|
class DocumentDetailsController extends GetxController {
|
||||||
/// Observables
|
/// Observables
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/document/master_document_type_model.dart';
|
import 'package:on_field_work/model/document/master_document_type_model.dart';
|
||||||
import 'package:marco/model/document/master_document_tags.dart';
|
import 'package:on_field_work/model/document/master_document_tags.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
class DocumentUploadController extends GetxController {
|
class DocumentUploadController extends GetxController {
|
||||||
// Observables
|
// Observables
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/document/document_filter_model.dart';
|
import 'package:on_field_work/model/document/document_filter_model.dart';
|
||||||
import 'package:marco/model/document/documents_list_model.dart';
|
import 'package:on_field_work/model/document/documents_list_model.dart';
|
||||||
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
class DocumentController extends GetxController {
|
class DocumentController extends GetxController {
|
||||||
// ==================== Observables ====================
|
// ==================== Observables ====================
|
||||||
@ -38,7 +39,6 @@ class DocumentController extends GetxController {
|
|||||||
final endDate = Rxn<DateTime>();
|
final endDate = Rxn<DateTime>();
|
||||||
|
|
||||||
// ==================== Lifecycle ====================
|
// ==================== Lifecycle ====================
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
// Don't dispose searchController here - it's managed by the page
|
// Don't dispose searchController here - it's managed by the page
|
||||||
@ -87,13 +87,23 @@ class DocumentController extends GetxController {
|
|||||||
entityId: entityId,
|
entityId: entityId,
|
||||||
reset: true,
|
reset: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Show success snackbar
|
||||||
|
showAppSnackbar(
|
||||||
|
title: 'Success',
|
||||||
|
message: isActive ? 'Document deactivated' : 'Document activated',
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = 'Failed to update document state';
|
errorMessage.value = 'Failed to update document state';
|
||||||
|
_showError('Failed to update document state');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage.value = 'Error updating document: $e';
|
errorMessage.value = 'Error updating document: $e';
|
||||||
|
_showError('Error updating document: $e');
|
||||||
debugPrint('❌ Error toggling document state: $e');
|
debugPrint('❌ Error toggling document state: $e');
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@ -110,17 +120,13 @@ class DocumentController extends GetxController {
|
|||||||
bool reset = false,
|
bool reset = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// Reset pagination if needed
|
|
||||||
if (reset) {
|
if (reset) {
|
||||||
pageNumber.value = 1;
|
pageNumber.value = 1;
|
||||||
documents.clear();
|
documents.clear();
|
||||||
hasMore.value = true;
|
hasMore.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't fetch if no more data
|
|
||||||
if (!hasMore.value && !reset) return;
|
if (!hasMore.value && !reset) return;
|
||||||
|
|
||||||
// Prevent duplicate requests
|
|
||||||
if (isLoading.value) return;
|
if (isLoading.value) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
@ -136,8 +142,8 @@ class DocumentController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response != null && response.success) {
|
if (response != null && response.success) {
|
||||||
if (response.data.data.isNotEmpty) {
|
if (response.data?.data.isNotEmpty ?? false) {
|
||||||
documents.addAll(response.data.data);
|
documents.addAll(response.data!.data);
|
||||||
pageNumber.value++;
|
pageNumber.value++;
|
||||||
} else {
|
} else {
|
||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
@ -147,12 +153,24 @@ class DocumentController extends GetxController {
|
|||||||
errorMessage.value = response?.message ?? 'Failed to fetch documents';
|
errorMessage.value = response?.message ?? 'Failed to fetch documents';
|
||||||
if (documents.isEmpty) {
|
if (documents.isEmpty) {
|
||||||
_showError('Failed to load documents');
|
_showError('Failed to load documents');
|
||||||
|
} else {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: 'Warning',
|
||||||
|
message: 'No more documents to load',
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage.value = 'Error fetching documents: $e';
|
errorMessage.value = 'Error fetching documents: $e';
|
||||||
if (documents.isEmpty) {
|
if (documents.isEmpty) {
|
||||||
_showError('Error loading documents');
|
_showError('Error loading documents');
|
||||||
|
} else {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Error fetching additional documents',
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
debugPrint('❌ Error fetching documents: $e');
|
debugPrint('❌ Error fetching documents: $e');
|
||||||
} finally {
|
} finally {
|
||||||
@ -185,17 +203,12 @@ class DocumentController extends GetxController {
|
|||||||
isVerified.value != null;
|
isVerified.value != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show error message
|
/// Show error message via snackbar
|
||||||
void _showError(String message) {
|
void _showError(String message) {
|
||||||
Get.snackbar(
|
showAppSnackbar(
|
||||||
'Error',
|
title: 'Error',
|
||||||
message,
|
message: message,
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
type: SnackbarType.error,
|
||||||
backgroundColor: Colors.red.shade100,
|
|
||||||
colorText: Colors.red.shade900,
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
borderRadius: 8,
|
|
||||||
duration: const Duration(seconds: 3),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
|
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class DynamicMenuController extends GetxController {
|
class DynamicMenuController extends GetxController {
|
||||||
// UI reactive states
|
// UI reactive states
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/model/global_project_model.dart';
|
import 'package:on_field_work/model/global_project_model.dart';
|
||||||
import 'package:marco/model/employees/assigned_projects_model.dart';
|
import 'package:on_field_work/model/employees/assigned_projects_model.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:on_field_work/controller/project_controller.dart';
|
||||||
|
|
||||||
class AssignProjectController extends GetxController {
|
class AssignProjectController extends GetxController {
|
||||||
final String employeeId;
|
final String employeeId;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
import 'package:marco/model/employees/employee_details_model.dart';
|
import 'package:on_field_work/model/employees/employee_details_model.dart';
|
||||||
|
|
||||||
class EmployeesScreenController extends GetxController {
|
class EmployeesScreenController extends GetxController {
|
||||||
/// ✅ Data lists
|
/// ✅ Data lists
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
|
|
||||||
class ComingSoonController extends MyController {
|
class ComingSoonController extends MyController {
|
||||||
Timer? countdownTimer;
|
Timer? countdownTimer;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
|
|
||||||
class Error404Controller extends MyController {
|
class Error404Controller extends MyController {
|
||||||
void goToDashboardScreen() {
|
void goToDashboardScreen() {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
|
|
||||||
class Error500Controller extends MyController {
|
class Error500Controller extends MyController {
|
||||||
void goToDashboardScreen() {
|
void goToDashboardScreen() {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -10,14 +11,14 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
import 'package:on_field_work/controller/expense/expense_screen_controller.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
import 'package:marco/model/expense/expense_type_model.dart';
|
import 'package:on_field_work/model/expense/expense_type_model.dart';
|
||||||
import 'package:marco/model/expense/payment_types_model.dart';
|
import 'package:on_field_work/model/expense/payment_types_model.dart';
|
||||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
|
|
||||||
class AddExpenseController extends GetxController {
|
class AddExpenseController extends GetxController {
|
||||||
// --- Text Controllers ---
|
// --- Text Controllers ---
|
||||||
@ -50,10 +51,22 @@ class AddExpenseController extends GetxController {
|
|||||||
final isEditMode = false.obs;
|
final isEditMode = false.obs;
|
||||||
final isSearchingEmployees = false.obs;
|
final isSearchingEmployees = false.obs;
|
||||||
|
|
||||||
|
// --- Paid By (Single + Multi Selection Support) ---
|
||||||
|
|
||||||
|
// single selection
|
||||||
|
final selectedPaidBy = Rxn<EmployeeModel>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// helper setters
|
||||||
|
void setSelectedPaidBy(EmployeeModel? emp) {
|
||||||
|
selectedPaidBy.value = emp;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Dropdown Selections & Data ---
|
// --- Dropdown Selections & Data ---
|
||||||
final selectedPaymentMode = Rxn<PaymentModeModel>();
|
final selectedPaymentMode = Rxn<PaymentModeModel>();
|
||||||
final selectedExpenseType = Rxn<ExpenseTypeModel>();
|
final selectedExpenseType = Rxn<ExpenseTypeModel>();
|
||||||
final selectedPaidBy = Rxn<EmployeeModel>();
|
// final selectedPaidBy = Rxn<EmployeeModel>();
|
||||||
final selectedProject = ''.obs;
|
final selectedProject = ''.obs;
|
||||||
final selectedTransactionDate = Rxn<DateTime>();
|
final selectedTransactionDate = Rxn<DateTime>();
|
||||||
|
|
||||||
@ -196,7 +209,7 @@ class AddExpenseController extends GetxController {
|
|||||||
'Location: ${locationController.text}',
|
'Location: ${locationController.text}',
|
||||||
'Transaction Date: ${transactionDateController.text}',
|
'Transaction Date: ${transactionDateController.text}',
|
||||||
'No. of Persons: ${noOfPersonsController.text}',
|
'No. of Persons: ${noOfPersonsController.text}',
|
||||||
'Expense Type: ${selectedExpenseType.value?.name}',
|
'Expense Category: ${selectedExpenseType.value?.name}',
|
||||||
'Payment Mode: ${selectedPaymentMode.value?.name}',
|
'Payment Mode: ${selectedPaymentMode.value?.name}',
|
||||||
'Paid By: ${selectedPaidBy.value?.name}',
|
'Paid By: ${selectedPaidBy.value?.name}',
|
||||||
'Attachments: ${attachments.length}',
|
'Attachments: ${attachments.length}',
|
||||||
@ -445,7 +458,7 @@ class AddExpenseController extends GetxController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (expenseType == null) {
|
if (expenseType == null) {
|
||||||
_errorSnackbar("Expense type not selected");
|
_errorSnackbar("Expense Category not selected");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (paymentMode == null) {
|
if (paymentMode == null) {
|
||||||
@ -517,7 +530,7 @@ class AddExpenseController extends GetxController {
|
|||||||
final missing = <String>[];
|
final missing = <String>[];
|
||||||
|
|
||||||
if (selectedProject.value.isEmpty) missing.add("Project");
|
if (selectedProject.value.isEmpty) missing.add("Project");
|
||||||
if (selectedExpenseType.value == null) missing.add("Expense Type");
|
if (selectedExpenseType.value == null) missing.add("Expense Category");
|
||||||
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
|
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
|
||||||
if (selectedPaidBy.value == null) missing.add("Paid By");
|
if (selectedPaidBy.value == null) missing.add("Paid By");
|
||||||
if (amountController.text.trim().isEmpty) missing.add("Amount");
|
if (amountController.text.trim().isEmpty) missing.add("Amount");
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/model/expense/expense_detail_model.dart';
|
import 'package:on_field_work/model/expense/expense_detail_model.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ExpenseDetailController extends GetxController {
|
class ExpenseDetailController extends GetxController {
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/model/expense/expense_list_model.dart';
|
import 'package:on_field_work/model/expense/expense_list_model.dart';
|
||||||
import 'package:marco/model/expense/payment_types_model.dart';
|
import 'package:on_field_work/model/expense/payment_types_model.dart';
|
||||||
import 'package:marco/model/expense/expense_type_model.dart';
|
import 'package:on_field_work/model/expense/expense_type_model.dart';
|
||||||
import 'package:marco/model/expense/expense_status_model.dart';
|
import 'package:on_field_work/model/expense/expense_status_model.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ExpenseController extends GetxController {
|
class ExpenseController extends GetxController {
|
||||||
@ -213,7 +213,7 @@ class ExpenseController extends GetxController {
|
|||||||
selectedCreatedByEmployees.clear();
|
selectedCreatedByEmployees.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch master data: expense types, payment modes, and expense status
|
/// Fetch master data: Expense Categorys, payment modes, and expense status
|
||||||
Future<void> fetchMasterData() async {
|
Future<void> fetchMasterData() async {
|
||||||
try {
|
try {
|
||||||
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||||
|
|
||||||
class FaqsController extends MyController {
|
class FaqsController extends MyController {
|
||||||
final List<bool> dataExpansionPanel = [true, false, false, false, false, false];
|
final List<bool> dataExpansionPanel = [true, false, false, false, false, false];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
|
|
||||||
class PricingController extends MyController {
|
class PricingController extends MyController {
|
||||||
bool isMonth = false;
|
bool isMonth = false;
|
||||||
|
|||||||
@ -8,12 +8,13 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
import 'package:marco/model/finance/expense_category_model.dart';
|
import 'package:on_field_work/model/finance/expense_category_model.dart';
|
||||||
import 'package:marco/model/finance/currency_list_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 {
|
class AddPaymentRequestController extends GetxController {
|
||||||
// Loading States
|
// Loading States
|
||||||
@ -32,7 +33,7 @@ class AddPaymentRequestController extends GetxController {
|
|||||||
// Selected Values
|
// Selected Values
|
||||||
final selectedProject = Rx<Map<String, dynamic>?>(null);
|
final selectedProject = Rx<Map<String, dynamic>?>(null);
|
||||||
final selectedCategory = Rx<ExpenseCategory?>(null);
|
final selectedCategory = Rx<ExpenseCategory?>(null);
|
||||||
final selectedPayee = ''.obs;
|
final selectedPayee = Rx<EmployeeModel?>(null);
|
||||||
final selectedCurrency = Rx<Currency?>(null);
|
final selectedCurrency = Rx<Currency?>(null);
|
||||||
final isAdvancePayment = false.obs;
|
final isAdvancePayment = false.obs;
|
||||||
final selectedDueDate = Rx<DateTime?>(null);
|
final selectedDueDate = Rx<DateTime?>(null);
|
||||||
@ -161,7 +162,7 @@ class AddPaymentRequestController extends GetxController {
|
|||||||
try {
|
try {
|
||||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
|
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
|
||||||
if (pickedFile != null) {
|
if (pickedFile != null) {
|
||||||
isProcessingAttachment.value = true;
|
isProcessingAttachment.value = true;
|
||||||
File imageFile = File(pickedFile.path);
|
File imageFile = File(pickedFile.path);
|
||||||
|
|
||||||
// Add timestamp to the captured image
|
// Add timestamp to the captured image
|
||||||
@ -184,7 +185,7 @@ class AddPaymentRequestController extends GetxController {
|
|||||||
selectedProject.value = project;
|
selectedProject.value = project;
|
||||||
void selectCategory(ExpenseCategory category) =>
|
void selectCategory(ExpenseCategory category) =>
|
||||||
selectedCategory.value = category;
|
selectedCategory.value = category;
|
||||||
void selectPayee(String payee) => selectedPayee.value = payee;
|
void selectPayee(EmployeeModel payee) => selectedPayee.value = payee;
|
||||||
void selectCurrency(Currency currency) => selectedCurrency.value = currency;
|
void selectCurrency(Currency currency) => selectedCurrency.value = currency;
|
||||||
|
|
||||||
void addAttachment(File file) => attachments.add(file);
|
void addAttachment(File file) => attachments.add(file);
|
||||||
@ -268,7 +269,7 @@ class AddPaymentRequestController extends GetxController {
|
|||||||
"amount": double.tryParse(amountController.text.trim()) ?? 0,
|
"amount": double.tryParse(amountController.text.trim()) ?? 0,
|
||||||
"currencyId": selectedCurrency.value?.id ?? '',
|
"currencyId": selectedCurrency.value?.id ?? '',
|
||||||
"description": descriptionController.text.trim(),
|
"description": descriptionController.text.trim(),
|
||||||
"payee": selectedPayee.value,
|
"payee": selectedPayee.value?.id ?? "",
|
||||||
"dueDate": selectedDueDate.value?.toIso8601String(),
|
"dueDate": selectedDueDate.value?.toIso8601String(),
|
||||||
"isAdvancePayment": isAdvancePayment.value,
|
"isAdvancePayment": isAdvancePayment.value,
|
||||||
"billAttachments": billAttachments.map((a) {
|
"billAttachments": billAttachments.map((a) {
|
||||||
@ -337,7 +338,7 @@ class AddPaymentRequestController extends GetxController {
|
|||||||
"amount": double.tryParse(amountController.text.trim()) ?? 0,
|
"amount": double.tryParse(amountController.text.trim()) ?? 0,
|
||||||
"currencyId": selectedCurrency.value?.id ?? '',
|
"currencyId": selectedCurrency.value?.id ?? '',
|
||||||
"description": descriptionController.text.trim(),
|
"description": descriptionController.text.trim(),
|
||||||
"payee": selectedPayee.value,
|
"payee": selectedPayee.value?.id ?? "",
|
||||||
"dueDate": selectedDueDate.value?.toIso8601String(),
|
"dueDate": selectedDueDate.value?.toIso8601String(),
|
||||||
"isAdvancePayment": isAdvancePayment.value,
|
"isAdvancePayment": isAdvancePayment.value,
|
||||||
"billAttachments": billAttachments.map((a) {
|
"billAttachments": billAttachments.map((a) {
|
||||||
@ -388,7 +389,7 @@ class AddPaymentRequestController extends GetxController {
|
|||||||
return _errorSnackbar("Please select a project");
|
return _errorSnackbar("Please select a project");
|
||||||
if (selectedCategory.value == null)
|
if (selectedCategory.value == null)
|
||||||
return _errorSnackbar("Please select a category");
|
return _errorSnackbar("Please select a category");
|
||||||
if (selectedPayee.value.isEmpty)
|
if (selectedPayee.value == null)
|
||||||
return _errorSnackbar("Please select a payee");
|
return _errorSnackbar("Please select a payee");
|
||||||
if (selectedCurrency.value == null)
|
if (selectedCurrency.value == null)
|
||||||
return _errorSnackbar("Please select currency");
|
return _errorSnackbar("Please select currency");
|
||||||
@ -408,7 +409,7 @@ class AddPaymentRequestController extends GetxController {
|
|||||||
descriptionController.clear();
|
descriptionController.clear();
|
||||||
selectedProject.value = null;
|
selectedProject.value = null;
|
||||||
selectedCategory.value = null;
|
selectedCategory.value = null;
|
||||||
selectedPayee.value = '';
|
selectedPayee.value = null;
|
||||||
selectedCurrency.value = null;
|
selectedCurrency.value = null;
|
||||||
isAdvancePayment.value = false;
|
isAdvancePayment.value = false;
|
||||||
attachments.clear();
|
attachments.clear();
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/model/finance/advance_payment_model.dart';
|
import 'package:on_field_work/model/finance/advance_payment_model.dart';
|
||||||
import 'package:marco/model/finance/get_employee_model.dart';
|
import 'package:on_field_work/model/finance/get_employee_model.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
|
|
||||||
class AdvancePaymentController extends GetxController {
|
class AdvancePaymentController extends GetxController {
|
||||||
/// Advance payments list
|
/// Advance payments list
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/finance/payment_request_list_model.dart';
|
import 'package:on_field_work/model/finance/payment_request_list_model.dart';
|
||||||
import 'package:marco/model/finance/payment_request_filter.dart';
|
import 'package:on_field_work/model/finance/payment_request_filter.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class PaymentRequestController extends GetxController {
|
class PaymentRequestController extends GetxController {
|
||||||
// ---------------- Observables ----------------
|
// ---------------- Observables ----------------
|
||||||
@ -32,13 +32,14 @@ class PaymentRequestController extends GetxController {
|
|||||||
Future<void> fetchPaymentRequestFilterOptions() async {
|
Future<void> fetchPaymentRequestFilterOptions() async {
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.getExpensePaymentRequestFilterApi();
|
final response = await ApiService.getExpensePaymentRequestFilterApi();
|
||||||
if (response != null) {
|
|
||||||
projects.assignAll(response.data.projects);
|
if (response != null && response.data != null) {
|
||||||
payees.assignAll(response.data.payees);
|
projects.assignAll(response.data!.projects ?? []);
|
||||||
categories.assignAll(response.data.expenseCategory);
|
payees.assignAll(response.data!.payees ?? []);
|
||||||
currencies.assignAll(response.data.currency);
|
categories.assignAll(response.data!.expenseCategory ?? []);
|
||||||
statuses.assignAll(response.data.status);
|
currencies.assignAll(response.data!.currency ?? []);
|
||||||
createdBy.assignAll(response.data.createdBy);
|
statuses.assignAll(response.data!.status ?? []);
|
||||||
|
createdBy.assignAll(response.data!.createdBy ?? []);
|
||||||
} else {
|
} else {
|
||||||
logSafe("Payment request filter API returned null",
|
logSafe("Payment request filter API returned null",
|
||||||
level: LogLevel.warning);
|
level: LogLevel.warning);
|
||||||
@ -63,7 +64,7 @@ class PaymentRequestController extends GetxController {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Load More ----------------
|
// ---------------- Load More ----------------
|
||||||
Future<void> loadMorePaymentRequests() async {
|
Future<void> loadMorePaymentRequests() async {
|
||||||
if (isLoading.value || !_hasMoreData) return;
|
if (isLoading.value || !_hasMoreData) return;
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ class PaymentRequestController extends GetxController {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Internal API Call ----------------
|
// ---------------- Internal API Call ----------------
|
||||||
Future<void> _fetchPaymentRequestsFromApi() async {
|
Future<void> _fetchPaymentRequestsFromApi() async {
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.getExpensePaymentRequestListApi(
|
final response = await ApiService.getExpensePaymentRequestListApi(
|
||||||
@ -84,17 +85,17 @@ class PaymentRequestController extends GetxController {
|
|||||||
searchString: searchString.value,
|
searchString: searchString.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null && response.data.data.isNotEmpty) {
|
final data = response?.data;
|
||||||
|
final reqList = data?.data ?? [];
|
||||||
|
|
||||||
|
if (response != null && data != null && reqList.isNotEmpty) {
|
||||||
if (_pageNumber == 1) {
|
if (_pageNumber == 1) {
|
||||||
// First page, replace the list
|
paymentRequests.assignAll(reqList);
|
||||||
paymentRequests.assignAll(response.data.data);
|
|
||||||
} else {
|
} else {
|
||||||
// Append next page items at the end
|
paymentRequests.addAll(reqList);
|
||||||
paymentRequests.addAll(response.data.data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If returned data is less than page size, no more data
|
if (reqList.length < _pageSize) {
|
||||||
if (response.data.data.length < _pageSize) {
|
|
||||||
_hasMoreData = false;
|
_hasMoreData = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
import 'package:marco/model/finance/payment_request_details_model.dart';
|
import 'package:on_field_work/model/finance/payment_request_details_model.dart';
|
||||||
import 'package:marco/model/expense/payment_types_model.dart';
|
import 'package:on_field_work/model/expense/payment_types_model.dart';
|
||||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
@ -13,6 +13,7 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:geocoding/geocoding.dart';
|
import 'package:geocoding/geocoding.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
|
import 'package:on_field_work/controller/finance/payment_request_controller.dart';
|
||||||
|
|
||||||
class PaymentRequestDetailController extends GetxController {
|
class PaymentRequestDetailController extends GetxController {
|
||||||
final Rx<PaymentRequestData?> paymentRequest = Rx<PaymentRequestData?>(null);
|
final Rx<PaymentRequestData?> paymentRequest = Rx<PaymentRequestData?>(null);
|
||||||
@ -26,6 +27,8 @@ class PaymentRequestDetailController extends GetxController {
|
|||||||
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
|
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
|
||||||
final TextEditingController employeeSearchController =
|
final TextEditingController employeeSearchController =
|
||||||
TextEditingController();
|
TextEditingController();
|
||||||
|
PaymentRequestController get paymentRequestController =>
|
||||||
|
Get.find<PaymentRequestController>();
|
||||||
final RxBool isSearchingEmployees = false.obs;
|
final RxBool isSearchingEmployees = false.obs;
|
||||||
|
|
||||||
// Attachments
|
// Attachments
|
||||||
@ -278,6 +281,7 @@ class PaymentRequestDetailController extends GetxController {
|
|||||||
String? tdsPercentage,
|
String? tdsPercentage,
|
||||||
}) async {
|
}) async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final success = await ApiService.updateExpensePaymentRequestStatusApi(
|
final success = await ApiService.updateExpensePaymentRequestStatusApi(
|
||||||
paymentRequestId: _requestId,
|
paymentRequestId: _requestId,
|
||||||
@ -292,24 +296,14 @@ class PaymentRequestDetailController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
showAppSnackbar(
|
// Controller refreshes the data but does not show snackbars.
|
||||||
title: 'Success',
|
|
||||||
message: 'Payment submitted successfully',
|
|
||||||
type: SnackbarType.success);
|
|
||||||
await fetchPaymentRequestDetail();
|
await fetchPaymentRequestDetail();
|
||||||
} else {
|
paymentRequestController.fetchPaymentRequests();
|
||||||
showAppSnackbar(
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to update status. Please try again.',
|
|
||||||
type: SnackbarType.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showAppSnackbar(
|
// Controller returns false on error; UI will show the snackbar.
|
||||||
title: 'Error',
|
|
||||||
message: 'Something went wrong: $e',
|
|
||||||
type: SnackbarType.error);
|
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,3 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
|
|
||||||
class AuthLayout2Controller extends MyController {}
|
class AuthLayout2Controller extends MyController {}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AuthLayoutController extends MyController {
|
class AuthLayoutController extends MyController {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:on_field_work/model/project_model.dart';
|
||||||
|
|
||||||
class LayoutController extends GetxController {
|
class LayoutController extends GetxController {
|
||||||
// Theme Customization
|
// Theme Customization
|
||||||
@ -55,7 +55,7 @@ class LayoutController extends GetxController {
|
|||||||
isLoadingProjects.value = true;
|
isLoadingProjects.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.getProjects();
|
final response = await ApiService.getGlobalProjects();
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:get/get_state_manager/get_state_manager.dart';
|
import 'package:get/get_state_manager/get_state_manager.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||||
|
|
||||||
abstract class MyController extends GetxController {
|
abstract class MyController extends GetxController {
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/permission_service.dart';
|
import 'package:on_field_work/helpers/services/permission_service.dart';
|
||||||
import 'package:marco/model/user_permission.dart';
|
import 'package:on_field_work/model/user_permission.dart';
|
||||||
import 'package:marco/model/employees/employee_info.dart';
|
import 'package:on_field_work/model/employees/employee_info.dart';
|
||||||
import 'package:marco/model/projects_model.dart';
|
import 'package:on_field_work/model/projects_model.dart';
|
||||||
|
|
||||||
class PermissionController extends GetxController {
|
class PermissionController extends GetxController {
|
||||||
var permissions = <UserPermission>[].obs;
|
var permissions = <UserPermission>[].obs;
|
||||||
@ -15,6 +15,9 @@ class PermissionController extends GetxController {
|
|||||||
Timer? _refreshTimer;
|
Timer? _refreshTimer;
|
||||||
var isLoading = true.obs;
|
var isLoading = true.obs;
|
||||||
|
|
||||||
|
/// ← NEW: reactive flag to signal permissions are loaded
|
||||||
|
var permissionsLoaded = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -52,6 +55,10 @@ class PermissionController extends GetxController {
|
|||||||
_updateState(userData);
|
_updateState(userData);
|
||||||
await _storeData();
|
await _storeData();
|
||||||
logSafe("Data loaded and state updated successfully.");
|
logSafe("Data loaded and state updated successfully.");
|
||||||
|
|
||||||
|
// ← NEW: mark permissions as loaded
|
||||||
|
permissionsLoaded.value = true;
|
||||||
|
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe("Error loading data from API",
|
logSafe("Error loading data from API",
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
@ -103,7 +110,7 @@ class PermissionController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _startAutoRefresh() {
|
void _startAutoRefresh() {
|
||||||
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
|
_refreshTimer = Timer.periodic(const Duration(minutes: 30), (timer) async {
|
||||||
logSafe("Auto-refresh triggered.");
|
logSafe("Auto-refresh triggered.");
|
||||||
final token = await _getAuthToken();
|
final token = await _getAuthToken();
|
||||||
if (token?.isNotEmpty ?? false) {
|
if (token?.isNotEmpty ?? false) {
|
||||||
@ -117,8 +124,6 @@ class PermissionController extends GetxController {
|
|||||||
|
|
||||||
bool hasPermission(String permissionId) {
|
bool hasPermission(String permissionId) {
|
||||||
final hasPerm = permissions.any((p) => p.id == permissionId);
|
final hasPerm = permissions.any((p) => p.id == permissionId);
|
||||||
// logSafe("Checking permission $permissionId: $hasPerm",
|
|
||||||
// level: LogLevel.debug);
|
|
||||||
return hasPerm;
|
return hasPerm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/global_project_model.dart';
|
import 'package:on_field_work/model/global_project_model.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
|
|
||||||
class ProjectController extends GetxController {
|
class ProjectController extends GetxController {
|
||||||
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
|
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
|
||||||
|
|||||||
@ -1,56 +1,58 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/controller/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 {
|
class AddServiceProjectJobController extends GetxController {
|
||||||
// Form Controllers
|
// FORM CONTROLLERS
|
||||||
final titleCtrl = TextEditingController();
|
final titleCtrl = TextEditingController();
|
||||||
final descCtrl = TextEditingController();
|
final descCtrl = TextEditingController();
|
||||||
final tagCtrl = TextEditingController();
|
final tagCtrl = TextEditingController();
|
||||||
final FocusNode searchFocusNode = FocusNode();
|
final searchFocusNode = FocusNode();
|
||||||
final RxBool showEmployeePicker = true.obs;
|
|
||||||
|
|
||||||
// Observables
|
// OBSERVABLES
|
||||||
final startDate = Rx<DateTime?>(DateTime.now());
|
final startDate = Rx<DateTime?>(DateTime.now());
|
||||||
final dueDate = Rx<DateTime?>(DateTime.now().add(const Duration(days: 1)));
|
final dueDate = Rx<DateTime?>(DateTime.now().add(const Duration(days: 1)));
|
||||||
|
|
||||||
final enteredTags = <String>[].obs;
|
final enteredTags = <String>[].obs;
|
||||||
|
|
||||||
final employees = <EmployeeModel>[].obs;
|
|
||||||
final selectedAssignees = <EmployeeModel>[].obs;
|
final selectedAssignees = <EmployeeModel>[].obs;
|
||||||
final isSearchingEmployees = false.obs;
|
|
||||||
|
|
||||||
// Loading states
|
// Branches
|
||||||
|
final branches = <Branch>[].obs;
|
||||||
|
final selectedBranch = Rxn<Branch>();
|
||||||
|
final isBranchLoading = false.obs;
|
||||||
|
|
||||||
|
// Loading
|
||||||
final isLoading = false.obs;
|
final isLoading = false.obs;
|
||||||
final isAllEmployeeLoading = false.obs;
|
|
||||||
final allEmployees = <EmployeeModel>[].obs;
|
|
||||||
final employeeSearchResults = <EmployeeModel>[].obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
titleCtrl.dispose();
|
titleCtrl.dispose();
|
||||||
descCtrl.dispose();
|
descCtrl.dispose();
|
||||||
tagCtrl.dispose();
|
tagCtrl.dispose();
|
||||||
|
searchFocusNode.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FETCH BRANCHES
|
||||||
|
Future<void> fetchBranches(String projectId) async {
|
||||||
|
isBranchLoading.value = true;
|
||||||
|
|
||||||
/// Toggle employee selection
|
final response = await ApiService.getServiceProjectBranchesFull(
|
||||||
void toggleAssignee(EmployeeModel employee) {
|
projectId: projectId,
|
||||||
if (selectedAssignees.contains(employee)) {
|
);
|
||||||
selectedAssignees.remove(employee);
|
|
||||||
} else {
|
if (response != null && response.success) {
|
||||||
selectedAssignees.add(employee);
|
branches.assignAll(response.data?.data ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBranchLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create Service Project Job API call
|
// CREATE JOB
|
||||||
Future<void> createJob(String projectId) async {
|
Future<void> createJob(String projectId) async {
|
||||||
if (titleCtrl.text.trim().isEmpty || descCtrl.text.trim().isEmpty) {
|
if (titleCtrl.text.trim().isEmpty || descCtrl.text.trim().isEmpty) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
@ -61,19 +63,36 @@ class AddServiceProjectJobController extends GetxController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final assigneeIds = selectedAssignees.map((e) => e.id).toList();
|
isLoading.value = true;
|
||||||
|
|
||||||
final success = await ApiService.createServiceProjectJobApi(
|
final jobId = await ApiService.createServiceProjectJobApi(
|
||||||
title: titleCtrl.text.trim(),
|
title: titleCtrl.text.trim(),
|
||||||
description: descCtrl.text.trim(),
|
description: descCtrl.text.trim(),
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
assignees: assigneeIds.map((id) => {"id": id}).toList(),
|
branchId: selectedBranch.value?.id,
|
||||||
|
assignees: selectedAssignees // payload mapping
|
||||||
|
.map((e) => {"employeeId": e.id, "isActive": true})
|
||||||
|
.toList(),
|
||||||
startDate: startDate.value!,
|
startDate: startDate.value!,
|
||||||
dueDate: dueDate.value!,
|
dueDate: dueDate.value!,
|
||||||
tags: enteredTags.map((tag) => {"name": tag}).toList(),
|
tags: enteredTags
|
||||||
|
.map((tag) => {"id": null, "name": tag, "isActive": true})
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
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();
|
Get.back();
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
|
|||||||
@ -0,0 +1,80 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,25 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/service_project/service_projects_details_model.dart';
|
import 'package:on_field_work/model/service_project/service_projects_details_model.dart';
|
||||||
import 'package:marco/model/service_project/job_list_model.dart';
|
import 'package:on_field_work/model/service_project/job_list_model.dart';
|
||||||
import 'package:marco/model/service_project/service_project_job_detail_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 {
|
class ServiceProjectDetailsController extends GetxController {
|
||||||
// -------------------- Observables --------------------
|
// -------------------- Observables --------------------
|
||||||
var projectId = ''.obs;
|
var projectId = ''.obs;
|
||||||
var projectDetail = Rxn<ProjectDetail>();
|
var projectDetail = Rxn<ProjectDetail>();
|
||||||
var jobList = <JobEntity>[].obs;
|
var jobList = <JobEntity>[].obs;
|
||||||
var jobDetail = Rxn<JobDetailsResponse>();
|
var jobDetail = Rxn<JobDetailsResponse>();
|
||||||
|
var showArchivedJobs = false.obs; // true = archived, false = active
|
||||||
|
|
||||||
// Loading states
|
// Loading states
|
||||||
var isLoading = false.obs;
|
var isLoading = false.obs;
|
||||||
@ -20,26 +30,132 @@ class ServiceProjectDetailsController extends GetxController {
|
|||||||
var errorMessage = ''.obs;
|
var errorMessage = ''.obs;
|
||||||
var jobErrorMessage = ''.obs;
|
var jobErrorMessage = ''.obs;
|
||||||
var jobDetailErrorMessage = ''.obs;
|
var jobDetailErrorMessage = ''.obs;
|
||||||
|
final ImagePicker picker = ImagePicker();
|
||||||
|
var isProcessingAttachment = false.obs;
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
var pageNumber = 1;
|
var pageNumber = 1;
|
||||||
final int pageSize = 20;
|
final int pageSize = 20;
|
||||||
var hasMoreJobs = true.obs;
|
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 --------------------
|
// -------------------- Lifecycle --------------------
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
fetchProjectJobs(initialLoad: true); // fetch job list initially
|
fetchProjectJobs();
|
||||||
|
filteredJobList.value = jobList;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Project --------------------
|
// -------------------- Project --------------------
|
||||||
void setProjectId(String id) {
|
void setProjectId(String id) {
|
||||||
|
if (projectId.value == id) return;
|
||||||
projectId.value = id;
|
projectId.value = id;
|
||||||
fetchProjectDetail();
|
|
||||||
|
// Reset pagination and list
|
||||||
pageNumber = 1;
|
pageNumber = 1;
|
||||||
hasMoreJobs.value = true;
|
hasMoreJobs.value = true;
|
||||||
fetchProjectJobs(initialLoad: 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 {
|
Future<void> fetchProjectDetail() async {
|
||||||
@ -68,14 +184,37 @@ class ServiceProjectDetailsController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Job List --------------------
|
Future<void> fetchJobAttendanceLog(String attendanceId) async {
|
||||||
Future<void> fetchProjectJobs({bool initialLoad = false}) async {
|
if (attendanceId.isEmpty) {
|
||||||
if (projectId.value.isEmpty && !initialLoad) {
|
attendanceMessage.value = "Invalid attendance ID";
|
||||||
jobErrorMessage.value = "Invalid project ID";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasMoreJobs.value && !initialLoad) 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;
|
isJobLoading.value = true;
|
||||||
jobErrorMessage.value = '';
|
jobErrorMessage.value = '';
|
||||||
@ -86,19 +225,24 @@ class ServiceProjectDetailsController extends GetxController {
|
|||||||
pageNumber: pageNumber,
|
pageNumber: pageNumber,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
isArchive: showArchivedJobs.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null && result.data != null) {
|
if (result != null && result.data != null) {
|
||||||
if (initialLoad) {
|
final newJobs = result.data?.data ?? [];
|
||||||
jobList.value = result.data?.data ?? [];
|
|
||||||
|
if (refresh || pageNumber == 1) {
|
||||||
|
jobList.value = newJobs;
|
||||||
} else {
|
} else {
|
||||||
jobList.addAll(result.data?.data ?? []);
|
jobList.addAll(newJobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasMoreJobs.value = (result.data?.data?.length ?? 0) == pageSize;
|
filteredJobList.value = jobList;
|
||||||
|
|
||||||
|
hasMoreJobs.value = newJobs.length == pageSize;
|
||||||
if (hasMoreJobs.value) pageNumber++;
|
if (hasMoreJobs.value) pageNumber++;
|
||||||
} else {
|
} else {
|
||||||
jobErrorMessage.value = result?.message ?? "Failed to fetch job list";
|
jobErrorMessage.value = result?.message ?? "Failed to fetch jobs";
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
jobErrorMessage.value = "Error fetching jobs: $e";
|
jobErrorMessage.value = "Error fetching jobs: $e";
|
||||||
@ -113,9 +257,10 @@ class ServiceProjectDetailsController extends GetxController {
|
|||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
pageNumber = 1;
|
pageNumber = 1;
|
||||||
hasMoreJobs.value = true;
|
hasMoreJobs.value = true;
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
fetchProjectDetail(),
|
fetchProjectDetail(),
|
||||||
fetchProjectJobs(initialLoad: true),
|
fetchProjectJobs(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,4 +287,193 @@ class ServiceProjectDetailsController extends GetxController {
|
|||||||
isJobDetailLoading.value = false;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/service_project/service_projects_list_model.dart';
|
import 'package:on_field_work/model/service_project/service_projects_list_model.dart';
|
||||||
|
|
||||||
class ServiceProjectController extends GetxController {
|
class ServiceProjectController extends GetxController {
|
||||||
final projects = <ProjectItem>[].obs;
|
final projects = <ProjectItem>[].obs;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart';
|
import 'package:on_field_work/model/dailyTaskPlanning/master_work_category_model.dart';
|
||||||
|
|
||||||
class AddTaskController extends GetxController {
|
class AddTaskController extends GetxController {
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:on_field_work/model/project_model.dart';
|
||||||
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
|
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_model.dart';
|
||||||
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
|
import 'package:on_field_work/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
|
||||||
|
|
||||||
class DailyTaskController extends GetxController {
|
class DailyTaskController extends GetxController {
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:on_field_work/model/project_model.dart';
|
||||||
import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart';
|
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_planning_model.dart';
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
|
|
||||||
class DailyTaskPlanningController extends GetxController {
|
class DailyTaskPlanningController extends GetxController {
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
|
|||||||
@ -4,16 +4,16 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
|
import 'package:on_field_work/model/dailyTaskPlanning/work_status_model.dart';
|
||||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
enum ApiStatus { idle, loading, success, failure }
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
|
||||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
enum ApiStatus { idle, loading, success, failure }
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/all_organization_model.dart';
|
import 'package:on_field_work/model/all_organization_model.dart';
|
||||||
|
|
||||||
class AllOrganizationController extends GetxController {
|
class AllOrganizationController extends GetxController {
|
||||||
RxList<AllOrganization> organizations = <AllOrganization>[].obs;
|
RxList<AllOrganization> organizations = <AllOrganization>[].obs;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
|
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
|
||||||
|
|
||||||
class OrganizationController extends GetxController {
|
class OrganizationController extends GetxController {
|
||||||
/// List of organizations assigned to the selected project
|
/// List of organizations assigned to the selected project
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/model/tenant/tenant_services_model.dart';
|
import 'package:on_field_work/model/tenant/tenant_services_model.dart';
|
||||||
|
|
||||||
class ServiceController extends GetxController {
|
class ServiceController extends GetxController {
|
||||||
List<Service> services = [];
|
List<Service> services = [];
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/tenant_service.dart';
|
import 'package:on_field_work/helpers/services/tenant_service.dart';
|
||||||
import 'package:marco/model/tenant/tenant_list_model.dart';
|
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:on_field_work/controller/permission_controller.dart';
|
||||||
|
|
||||||
class TenantSelectionController extends GetxController {
|
class TenantSelectionController extends GetxController {
|
||||||
final TenantService _tenantService = TenantService();
|
final TenantService _tenantService = TenantService();
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/tenant_service.dart';
|
import 'package:on_field_work/helpers/services/tenant_service.dart';
|
||||||
import 'package:marco/model/tenant/tenant_list_model.dart';
|
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:on_field_work/controller/permission_controller.dart';
|
||||||
|
|
||||||
class TenantSwitchController extends GetxController {
|
class TenantSwitchController extends GetxController {
|
||||||
final TenantService _tenantService = TenantService();
|
final TenantService _tenantService = TenantService();
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
|
|
||||||
class ButtonsController extends MyController {
|
class ButtonsController extends MyController {
|
||||||
List<bool> selected = List.filled(3, false);
|
List<bool> selected = List.filled(3, false);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:carousel_slider/carousel_controller.dart';
|
import 'package:carousel_slider/carousel_controller.dart';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class CarouselsController extends MyController {
|
class CarouselsController extends MyController {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||||
|
|
||||||
class DialogsController extends MyController {
|
class DialogsController extends MyController {
|
||||||
List<String> dummyTexts =
|
List<String> dummyTexts =
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
|
|
||||||
class LoadersController extends MyController {}
|
class LoadersController extends MyController {}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||||
import 'package:flutter/animation.dart';
|
import 'package:flutter/animation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/extensions/string.dart';
|
import 'package:on_field_work/helpers/extensions/string.dart';
|
||||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||||
import 'package:marco/helpers/widgets/my_button.dart';
|
import 'package:on_field_work/helpers/widgets/my_button.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class TabsController extends MyController {
|
class TabsController extends MyController {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import 'package:marco/controller/my_controller.dart';
|
import 'package:on_field_work/controller/my_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ToastMessageController extends MyController {
|
class ToastMessageController extends MyController {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:marco/helpers/services/localizations/language.dart';
|
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||||
import 'package:marco/helpers/theme/app_notifier.dart';
|
import 'package:on_field_work/helpers/theme/app_notifier.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:marco/helpers/services/localizations/translator.dart';
|
import 'package:on_field_work/helpers/services/localizations/translator.dart';
|
||||||
|
|
||||||
extension StringUtil on String {
|
extension StringUtil on String {
|
||||||
Color get toColor {
|
Color get toColor {
|
||||||
|
|||||||
@ -3,6 +3,8 @@ class ApiEndpoints {
|
|||||||
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
||||||
|
// static const String baseUrl = "https://api.onfieldwork.com/api";
|
||||||
|
|
||||||
|
|
||||||
static const String getMasterCurrencies = "/Master/currencies/list";
|
static const String getMasterCurrencies = "/Master/currencies/list";
|
||||||
static const String getMasterExpensesCategories =
|
static const String getMasterExpensesCategories =
|
||||||
@ -34,6 +36,10 @@ class ApiEndpoints {
|
|||||||
"/Dashboard/expense/monthly";
|
"/Dashboard/expense/monthly";
|
||||||
static const String getExpenseTypeReport = "/Dashboard/expense/type";
|
static const String getExpenseTypeReport = "/Dashboard/expense/type";
|
||||||
static const String getPendingExpenses = "/Dashboard/expense/pendings";
|
static const String getPendingExpenses = "/Dashboard/expense/pendings";
|
||||||
|
static const String getCollectionOverview = "/dashboard/collection-overview";
|
||||||
|
|
||||||
|
static const String getPurchaseInvoiceOverview =
|
||||||
|
"/dashboard/purchase-invoice-overview";
|
||||||
|
|
||||||
///// Projects Module API Endpoints
|
///// Projects Module API Endpoints
|
||||||
static const String createProject = "/project";
|
static const String createProject = "/project";
|
||||||
@ -42,6 +48,7 @@ class ApiEndpoints {
|
|||||||
static const String getProjects = "/project/list";
|
static const String getProjects = "/project/list";
|
||||||
static const String getGlobalProjects = "/project/list/basic";
|
static const String getGlobalProjects = "/project/list/basic";
|
||||||
static const String getTodaysAttendance = "/attendance/project/team";
|
static const String getTodaysAttendance = "/attendance/project/team";
|
||||||
|
static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId";
|
||||||
static const String getAttendanceLogs = "/attendance/project/log";
|
static const String getAttendanceLogs = "/attendance/project/log";
|
||||||
static const String getAttendanceLogView = "/attendance/log/attendance";
|
static const String getAttendanceLogView = "/attendance/log/attendance";
|
||||||
static const String getRegularizationLogs = "/attendance/regularize";
|
static const String getRegularizationLogs = "/attendance/regularize";
|
||||||
@ -144,4 +151,20 @@ class ApiEndpoints {
|
|||||||
"/serviceproject/job/details";
|
"/serviceproject/job/details";
|
||||||
static const String editServiceProjectJob = "/serviceproject/job/edit";
|
static const String editServiceProjectJob = "/serviceproject/job/edit";
|
||||||
static const String createServiceProjectJob = "/serviceproject/job/create";
|
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";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,39 +4,50 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||||
import 'package:marco/model/dashboard/project_progress_model.dart';
|
import 'package:on_field_work/model/dashboard/project_progress_model.dart';
|
||||||
import 'package:marco/model/dashboard/dashboard_tasks_model.dart';
|
import 'package:on_field_work/model/dashboard/dashboard_tasks_model.dart';
|
||||||
import 'package:marco/model/dashboard/dashboard_teams_model.dart';
|
import 'package:on_field_work/model/dashboard/dashboard_teams_model.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/model/document/document_filter_model.dart';
|
import 'package:on_field_work/model/document/document_filter_model.dart';
|
||||||
import 'package:marco/model/document/documents_list_model.dart';
|
import 'package:on_field_work/model/document/documents_list_model.dart';
|
||||||
import 'package:marco/model/document/master_document_tags.dart';
|
import 'package:on_field_work/model/document/master_document_tags.dart';
|
||||||
import 'package:marco/model/document/master_document_type_model.dart';
|
import 'package:on_field_work/model/document/master_document_type_model.dart';
|
||||||
import 'package:marco/model/document/document_details_model.dart';
|
import 'package:on_field_work/model/document/document_details_model.dart';
|
||||||
import 'package:marco/model/document/document_version_model.dart';
|
import 'package:on_field_work/model/document/document_version_model.dart';
|
||||||
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
|
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
|
||||||
import 'package:marco/model/tenant/tenant_services_model.dart';
|
import 'package:on_field_work/model/tenant/tenant_services_model.dart';
|
||||||
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
|
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_model.dart';
|
||||||
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
|
import 'package:on_field_work/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
|
||||||
import 'package:marco/model/all_organization_model.dart';
|
import 'package:on_field_work/model/all_organization_model.dart';
|
||||||
import 'package:marco/model/dashboard/pending_expenses_model.dart';
|
import 'package:on_field_work/model/dashboard/pending_expenses_model.dart';
|
||||||
import 'package:marco/model/dashboard/expense_type_report_model.dart';
|
import 'package:on_field_work/model/dashboard/expense_type_report_model.dart';
|
||||||
import 'package:marco/model/dashboard/monthly_expence_model.dart';
|
import 'package:on_field_work/model/dashboard/monthly_expence_model.dart';
|
||||||
import 'package:marco/model/finance/expense_category_model.dart';
|
import 'package:on_field_work/model/finance/expense_category_model.dart';
|
||||||
import 'package:marco/model/finance/currency_list_model.dart';
|
import 'package:on_field_work/model/finance/currency_list_model.dart';
|
||||||
import 'package:marco/model/finance/payment_payee_request_model.dart';
|
import 'package:on_field_work/model/finance/payment_payee_request_model.dart';
|
||||||
import 'package:marco/model/finance/payment_request_list_model.dart';
|
import 'package:on_field_work/model/finance/payment_request_list_model.dart';
|
||||||
import 'package:marco/model/finance/payment_request_filter.dart';
|
import 'package:on_field_work/model/finance/payment_request_filter.dart';
|
||||||
import 'package:marco/model/finance/payment_request_details_model.dart';
|
import 'package:on_field_work/model/finance/payment_request_details_model.dart';
|
||||||
import 'package:marco/model/finance/advance_payment_model.dart';
|
import 'package:on_field_work/model/finance/advance_payment_model.dart';
|
||||||
import 'package:marco/model/service_project/service_projects_list_model.dart';
|
import 'package:on_field_work/model/service_project/service_projects_list_model.dart';
|
||||||
import 'package:marco/model/service_project/service_projects_details_model.dart';
|
import 'package:on_field_work/model/service_project/service_projects_details_model.dart';
|
||||||
import 'package:marco/model/service_project/job_list_model.dart';
|
import 'package:on_field_work/model/service_project/job_list_model.dart';
|
||||||
import 'package:marco/model/service_project/service_project_job_detail_model.dart';
|
import 'package:on_field_work/model/service_project/service_project_job_detail_model.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/service_project_branches_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 'package:on_field_work/model/employees/employee_model.dart';
|
||||||
|
import 'package:on_field_work/model/infra_project/infra_project_list.dart';
|
||||||
|
import 'package:on_field_work/model/infra_project/infra_project_details.dart';
|
||||||
|
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
|
||||||
|
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
||||||
|
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
static const bool enableLogs = true;
|
static const bool enableLogs = true;
|
||||||
@ -94,8 +105,10 @@ class ApiService {
|
|||||||
'Authorization': 'Bearer $token',
|
'Authorization': 'Bearer $token',
|
||||||
};
|
};
|
||||||
|
|
||||||
static void _log(String message) {
|
static void _log(String message, {LogLevel level = LogLevel.info}) {
|
||||||
if (enableLogs) logSafe(message);
|
if (enableLogs) {
|
||||||
|
logSafe(message, level: level);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static dynamic _parseResponse(http.Response response, {String label = ''}) {
|
static dynamic _parseResponse(http.Response response, {String label = ''}) {
|
||||||
@ -306,7 +319,481 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// ============================================
|
||||||
|
/// GET PURCHASE INVOICE OVERVIEW (Dashboard)
|
||||||
|
/// ============================================
|
||||||
|
static Future<PurchaseInvoiceOverviewResponse?> getPurchaseInvoiceOverview({
|
||||||
|
String? projectId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final queryParams = <String, String>{};
|
||||||
|
if (projectId != null && projectId.isNotEmpty) {
|
||||||
|
queryParams['projectId'] = projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _getRequest(
|
||||||
|
ApiEndpoints.getPurchaseInvoiceOverview,
|
||||||
|
queryParams: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getPurchaseInvoiceOverview: No response from server",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson = _parseResponseForAllData(
|
||||||
|
response,
|
||||||
|
label: "PurchaseInvoiceOverview",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
return PurchaseInvoiceOverviewResponse.fromJson(parsedJson);
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getPurchaseInvoiceOverview: $e\n$stack",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// ============================================
|
||||||
|
/// GET COLLECTION OVERVIEW (Dashboard)
|
||||||
|
/// ============================================
|
||||||
|
static Future<CollectionOverviewResponse?> getCollectionOverview({
|
||||||
|
String? projectId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Build query params (only add projectId if not null)
|
||||||
|
final queryParams = <String, String>{};
|
||||||
|
if (projectId != null && projectId.isNotEmpty) {
|
||||||
|
queryParams['projectId'] = projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _getRequest(
|
||||||
|
ApiEndpoints.getCollectionOverview,
|
||||||
|
queryParams: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getCollectionOverview: No response from server",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse full JSON (success, message, data, etc.)
|
||||||
|
final parsedJson =
|
||||||
|
_parseResponseForAllData(response, label: "CollectionOverview");
|
||||||
|
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
return CollectionOverviewResponse.fromJson(parsedJson);
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getCollectionOverview: $e\n$stack",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infra Project Module APIs
|
||||||
|
|
||||||
|
/// ================================
|
||||||
|
/// GET INFRA PROJECT DETAILS
|
||||||
|
/// ================================
|
||||||
|
static Future<ProjectDetailsResponse?> getInfraProjectDetails({
|
||||||
|
required String projectId,
|
||||||
|
}) async {
|
||||||
|
final endpoint = "${ApiEndpoints.getInfraProjectDetail}/$projectId";
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(endpoint);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getInfraProjectDetails: No response from server",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson =
|
||||||
|
_parseResponseForAllData(response, label: "InfraProjectDetails");
|
||||||
|
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
return ProjectDetailsResponse.fromJson(parsedJson);
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getInfraProjectDetails: $e\n$stack",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ================================
|
||||||
|
/// GET INFRA PROJECTS LIST
|
||||||
|
/// ================================
|
||||||
|
static Future<ProjectsResponse?> getInfraProjectsList({
|
||||||
|
int pageSize = 20,
|
||||||
|
int pageNumber = 1,
|
||||||
|
String searchString = "",
|
||||||
|
}) async {
|
||||||
|
final queryParams = {
|
||||||
|
"pageSize": pageSize.toString(),
|
||||||
|
"pageNumber": pageNumber.toString(),
|
||||||
|
"searchString": searchString,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(
|
||||||
|
ApiEndpoints.getInfraProjectsList,
|
||||||
|
queryParams: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getInfraProjectsList: No response from server",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson =
|
||||||
|
_parseResponseForAllData(response, label: "InfraProjectsList");
|
||||||
|
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
return ProjectsResponse.fromJson(parsedJson);
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getInfraProjectsList: $e\n$stack",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<JobCommentResponse?> getJobCommentList({
|
||||||
|
required String jobTicketId,
|
||||||
|
int pageNumber = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
}) async {
|
||||||
|
final queryParams = {
|
||||||
|
'jobTicketId': jobTicketId,
|
||||||
|
'pageNumber': pageNumber.toString(),
|
||||||
|
'pageSize': pageSize.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(
|
||||||
|
ApiEndpoints.getJobCommentList,
|
||||||
|
queryParams: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getJobCommentList: No response from server",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson =
|
||||||
|
_parseResponseForAllData(response, label: "JobCommentList");
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
return JobCommentResponse.fromJson(parsedJson);
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getJobCommentList: $e\n$stack", level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> addJobComment({
|
||||||
|
required String jobTicketId,
|
||||||
|
required String comment,
|
||||||
|
List<Map<String, dynamic>> attachments = const [],
|
||||||
|
}) async {
|
||||||
|
final body = {
|
||||||
|
"jobTicketId": jobTicketId,
|
||||||
|
"comment": comment,
|
||||||
|
"attachments": attachments,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _postRequest(
|
||||||
|
ApiEndpoints.addJobComment,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("addJobComment: No response from server", level: LogLevel.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 201 Created as success manually
|
||||||
|
if (response.statusCode == 201) {
|
||||||
|
_log("AddJobComment: Comment added successfully (201).",
|
||||||
|
level: LogLevel.info);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise fallback to existing _parseResponse
|
||||||
|
final parsed = _parseResponse(response, label: "AddJobComment");
|
||||||
|
|
||||||
|
if (parsed != null && parsed['success'] == true) {
|
||||||
|
_log("AddJobComment: Comment added successfully.",
|
||||||
|
level: LogLevel.info);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
_log(
|
||||||
|
"AddJobComment failed: ${parsed?['message'] ?? 'Unknown error'}",
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in addJobComment: $e\n$stack", level: LogLevel.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<JobStatus>?> getMasterJobStatus({
|
||||||
|
required String statusId,
|
||||||
|
required String projectId,
|
||||||
|
}) async {
|
||||||
|
final queryParams = {
|
||||||
|
'statusId': statusId,
|
||||||
|
'projectId': projectId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(
|
||||||
|
ApiEndpoints.getMasterJobStatus,
|
||||||
|
queryParams: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getMasterJobStatus: No response received.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson =
|
||||||
|
_parseResponseForAllData(response, label: "MasterJobStatus");
|
||||||
|
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
// Directly parse JobStatus list
|
||||||
|
final dataList = (parsedJson['data'] as List<dynamic>?)
|
||||||
|
?.map((e) => JobStatus.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return dataList;
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getMasterJobStatus: $e\n$stack",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch Service Project Branches with full response
|
||||||
|
static Future<ServiceProjectBranchesResponse?> getServiceProjectBranchesFull({
|
||||||
|
required String projectId,
|
||||||
|
int pageNumber = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
String searchString = '',
|
||||||
|
bool isActive = true,
|
||||||
|
}) async {
|
||||||
|
final queryParams = {
|
||||||
|
'pageNumber': pageNumber.toString(),
|
||||||
|
'pageSize': pageSize.toString(),
|
||||||
|
'searchString': searchString,
|
||||||
|
'isActive': isActive.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
final endpoint = "${ApiEndpoints.getServiceProjectBranches}/$projectId";
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(
|
||||||
|
endpoint,
|
||||||
|
queryParams: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getServiceProjectBranchesFull: No response received.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson = _parseResponseForAllData(
|
||||||
|
response,
|
||||||
|
label: "ServiceProjectBranchesFull",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
return ServiceProjectBranchesResponse.fromJson(parsedJson);
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log(
|
||||||
|
"Exception in getServiceProjectBranchesFull: $e\n$stack",
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Service Project Module APIs
|
// Service Project Module APIs
|
||||||
|
static Future<List<TeamRole>?> getTeamRoles() async {
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(ApiEndpoints.getTeamRoles);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getTeamRoles: No response received.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson = _parseResponseForAllData(response, label: "TeamRoles");
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
// Map the 'data' array to List<TeamRole>
|
||||||
|
final List<dynamic> dataList = parsedJson['data'] as List<dynamic>;
|
||||||
|
return dataList
|
||||||
|
.map((e) => TeamRole.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getTeamRoles: $e\n$stack", level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch Service Project Allocation List
|
||||||
|
|
||||||
|
static Future<List<ServiceProjectAllocation>?>
|
||||||
|
getServiceProjectAllocationList({
|
||||||
|
required String projectId,
|
||||||
|
bool isActive = true,
|
||||||
|
}) async {
|
||||||
|
final queryParams = {
|
||||||
|
'projectId': projectId,
|
||||||
|
'isActive': isActive.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(
|
||||||
|
ApiEndpoints.getServiceProjectUpateJobAllocationList,
|
||||||
|
queryParams: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getServiceProjectAllocationList: No response received.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson = _parseResponseForAllData(response,
|
||||||
|
label: "ServiceProjectAllocationList");
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
final dataList = (parsedJson['data'] as List<dynamic>)
|
||||||
|
.map((e) => ServiceProjectAllocation.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return dataList;
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getServiceProjectAllocationList: $e\n$stack");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manage Service Project Allocation
|
||||||
|
static Future<bool> manageServiceProjectAllocation({
|
||||||
|
required List<Map<String, dynamic>> payload,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await _postRequest(
|
||||||
|
ApiEndpoints.manageServiceProjectUpateJobAllocation,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("manageServiceProjectAllocation: No response received.",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
if (json['success'] == true) {
|
||||||
|
_log(
|
||||||
|
"Service Project Allocation updated successfully: ${json['data']}");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
_log(
|
||||||
|
"Failed to update Service Project Allocation: ${json['message'] ?? 'Unknown error'}",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception during manageServiceProjectAllocation: $e",
|
||||||
|
level: LogLevel.error);
|
||||||
|
_log("StackTrace: $stack", level: LogLevel.debug);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<JobAttendanceResponse?> getJobAttendanceLog({
|
||||||
|
required String attendanceId,
|
||||||
|
}) async {
|
||||||
|
final endpoint =
|
||||||
|
"${ApiEndpoints.serviceProjectUpateJobAttendanceLog}/$attendanceId";
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(endpoint);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
_log("getJobAttendanceLog: No response received.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedJson =
|
||||||
|
_parseResponseForAllData(response, label: "JobAttendanceLog");
|
||||||
|
if (parsedJson == null) return null;
|
||||||
|
|
||||||
|
return JobAttendanceResponse.fromJson(parsedJson);
|
||||||
|
} catch (e, stack) {
|
||||||
|
_log("Exception in getJobAttendanceLog: $e\n$stack");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update Service Project Job Attendance
|
||||||
|
static Future<bool> updateServiceProjectJobAttendance({
|
||||||
|
required Map<String, dynamic> payload,
|
||||||
|
}) async {
|
||||||
|
const endpoint = ApiEndpoints.serviceProjectUpateJobAttendance;
|
||||||
|
|
||||||
|
logSafe("Updating Service Project Job Attendance with payload: $payload");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _postRequest(endpoint, payload);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
logSafe("Update Job Attendance failed: null response",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logSafe("Update Job Attendance response status: ${response.statusCode}");
|
||||||
|
logSafe("Update Job Attendance response body: ${response.body}");
|
||||||
|
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
if (json['success'] == true) {
|
||||||
|
logSafe("Job Attendance updated successfully: ${json['data']}");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logSafe(
|
||||||
|
"Failed to update Job Attendance: ${json['message'] ?? 'Unknown error'}",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
logSafe("Exception during updateServiceProjectJobAttendance: $e",
|
||||||
|
level: LogLevel.error);
|
||||||
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Edit a Service Project Job
|
/// Edit a Service Project Job
|
||||||
static Future<bool> editServiceProjectJobApi({
|
static Future<bool> editServiceProjectJobApi({
|
||||||
required String jobId,
|
required String jobId,
|
||||||
@ -319,7 +806,7 @@ class ApiService {
|
|||||||
try {
|
try {
|
||||||
// PATCH request is usually similar to PUT, but with http.patch
|
// PATCH request is usually similar to PUT, but with http.patch
|
||||||
String? token = await _getToken();
|
String? token = await _getToken();
|
||||||
if (token == null) return false;
|
if (token == null) return false;
|
||||||
|
|
||||||
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
||||||
|
|
||||||
@ -384,8 +871,7 @@ class ApiService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new Service Project Job
|
static Future<String?> createServiceProjectJobApi({
|
||||||
static Future<bool> createServiceProjectJobApi({
|
|
||||||
required String title,
|
required String title,
|
||||||
required String description,
|
required String description,
|
||||||
required String projectId,
|
required String projectId,
|
||||||
@ -393,6 +879,7 @@ class ApiService {
|
|||||||
required DateTime startDate,
|
required DateTime startDate,
|
||||||
required DateTime dueDate,
|
required DateTime dueDate,
|
||||||
required List<Map<String, dynamic>> tags,
|
required List<Map<String, dynamic>> tags,
|
||||||
|
required String? branchId,
|
||||||
}) async {
|
}) async {
|
||||||
const endpoint = ApiEndpoints.createServiceProjectJob;
|
const endpoint = ApiEndpoints.createServiceProjectJob;
|
||||||
logSafe("Creating Service Project Job for projectId: $projectId");
|
logSafe("Creating Service Project Job for projectId: $projectId");
|
||||||
@ -405,49 +892,43 @@ class ApiService {
|
|||||||
"startDate": startDate.toIso8601String(),
|
"startDate": startDate.toIso8601String(),
|
||||||
"dueDate": dueDate.toIso8601String(),
|
"dueDate": dueDate.toIso8601String(),
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
|
"projectBranchId": branchId,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await _postRequest(endpoint, body);
|
final response = await _postRequest(endpoint, body);
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) return null;
|
||||||
logSafe("Create Service Project Job failed: null response",
|
|
||||||
level: LogLevel.error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logSafe(
|
|
||||||
"Create Service Project Job response status: ${response.statusCode}");
|
|
||||||
logSafe("Create Service Project Job response body: ${response.body}");
|
|
||||||
|
|
||||||
final json = jsonDecode(response.body);
|
final json = jsonDecode(response.body);
|
||||||
|
|
||||||
if (json['success'] == true) {
|
if (json['success'] == true) {
|
||||||
logSafe("Service Project Job created successfully: ${json['data']}");
|
final jobId = json['data']?['id'];
|
||||||
return true;
|
logSafe("Service Project Job created successfully: $jobId");
|
||||||
} else {
|
return jobId;
|
||||||
logSafe(
|
|
||||||
"Failed to create Service Project Job: ${json['message'] ?? 'Unknown error'}",
|
|
||||||
level: LogLevel.warning,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logSafe("Exception during createServiceProjectJobApi: $e",
|
logSafe("Exception during createServiceProjectJobApi: $e",
|
||||||
level: LogLevel.error);
|
level: LogLevel.error);
|
||||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Service Project Job List
|
/// Get Service Project Job List (Active or Archived)
|
||||||
static Future<JobResponse?> getServiceProjectJobListApi({
|
static Future<JobResponse?> getServiceProjectJobListApi({
|
||||||
required String projectId,
|
required String projectId,
|
||||||
int pageNumber = 1,
|
int pageNumber = 1,
|
||||||
int pageSize = 20,
|
int pageSize = 20,
|
||||||
bool isActive = true,
|
bool isActive = true,
|
||||||
|
bool isArchive = false, // new parameter to fetch archived jobs
|
||||||
}) async {
|
}) async {
|
||||||
const endpoint = ApiEndpoints.getServiceProjectJobList;
|
const endpoint = ApiEndpoints.getServiceProjectJobList;
|
||||||
logSafe("Fetching Job List for Service Project: $projectId");
|
logSafe(
|
||||||
|
"Fetching Job List for Service Project: $projectId | isActive: $isActive | isArchive: $isArchive",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final queryParams = {
|
final queryParams = {
|
||||||
@ -455,27 +936,34 @@ class ApiService {
|
|||||||
'pageNumber': pageNumber.toString(),
|
'pageNumber': pageNumber.toString(),
|
||||||
'pageSize': pageSize.toString(),
|
'pageSize': pageSize.toString(),
|
||||||
'isActive': isActive.toString(),
|
'isActive': isActive.toString(),
|
||||||
|
if (isArchive) 'isArchive': 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await _getRequest(endpoint, queryParams: queryParams);
|
final response = await _getRequest(endpoint, queryParams: queryParams);
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
logSafe("Service Project Job List request failed: null response",
|
logSafe(
|
||||||
level: LogLevel.error);
|
"Service Project Job List request failed: null response",
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final jsonResponse = _parseResponseForAllData(
|
final jsonResponse = _parseResponseForAllData(
|
||||||
response,
|
response,
|
||||||
label: "Service Project Job List",
|
label: isArchive
|
||||||
|
? "Archived Service Project Job List"
|
||||||
|
: "Active Service Project Job List",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (jsonResponse != null) {
|
if (jsonResponse != null) {
|
||||||
return JobResponse.fromJson(jsonResponse);
|
return JobResponse.fromJson(jsonResponse);
|
||||||
}
|
}
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logSafe("Exception during getServiceProjectJobListApi: $e",
|
logSafe(
|
||||||
level: LogLevel.error);
|
"Exception during getServiceProjectJobListApi: $e",
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -504,11 +992,6 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get details of a single service project
|
/// Get details of a single service project
|
||||||
/// Get details of a single service project
|
|
||||||
static Future<ServiceProjectDetailModel?> getServiceProjectDetailApi(
|
|
||||||
String projectId) async {
|
|
||||||
final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId";
|
|
||||||
logSafe("Fetching details for Service Project ID: $projectId");
|
|
||||||
static Future<ServiceProjectDetailModel?> getServiceProjectDetailApi(
|
static Future<ServiceProjectDetailModel?> getServiceProjectDetailApi(
|
||||||
String projectId) async {
|
String projectId) async {
|
||||||
final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId";
|
final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId";
|
||||||
@ -524,11 +1007,6 @@ class ApiService {
|
|||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (response == null) {
|
|
||||||
logSafe("Service Project Detail request failed: null response",
|
|
||||||
level: LogLevel.error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final jsonResponse = _parseResponseForAllData(
|
final jsonResponse = _parseResponseForAllData(
|
||||||
response,
|
response,
|
||||||
@ -548,14 +1026,6 @@ class ApiService {
|
|||||||
level: LogLevel.debug,
|
level: LogLevel.debug,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (jsonResponse != null) {
|
|
||||||
return ServiceProjectDetailModel.fromJson(jsonResponse);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Exception during getServiceProjectDetailApi: $e",
|
|
||||||
level: LogLevel.error);
|
|
||||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -1134,14 +1604,14 @@ class ApiService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Expense Type Report
|
/// Get Expense Category Report
|
||||||
static Future<ExpenseTypeReportResponse?> getExpenseTypeReportApi({
|
static Future<ExpenseTypeReportResponse?> getExpenseTypeReportApi({
|
||||||
required String projectId,
|
required String projectId,
|
||||||
required DateTime startDate,
|
required DateTime startDate,
|
||||||
required DateTime endDate,
|
required DateTime endDate,
|
||||||
}) async {
|
}) async {
|
||||||
const endpoint = ApiEndpoints.getExpenseTypeReport;
|
const endpoint = ApiEndpoints.getExpenseTypeReport;
|
||||||
logSafe("Fetching Expense Type Report for projectId: $projectId");
|
logSafe("Fetching Expense Category Report for projectId: $projectId");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await _getRequest(
|
final response = await _getRequest(
|
||||||
@ -1154,13 +1624,13 @@ class ApiService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
logSafe("Expense Type Report request failed: null response",
|
logSafe("Expense Category Report request failed: null response",
|
||||||
level: LogLevel.error);
|
level: LogLevel.error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final jsonResponse =
|
final jsonResponse =
|
||||||
_parseResponseForAllData(response, label: "Expense Type Report");
|
_parseResponseForAllData(response, label: "Expense Category Report");
|
||||||
|
|
||||||
if (jsonResponse != null) {
|
if (jsonResponse != null) {
|
||||||
return ExpenseTypeReportResponse.fromJson(jsonResponse);
|
return ExpenseTypeReportResponse.fromJson(jsonResponse);
|
||||||
@ -2233,11 +2703,11 @@ class ApiService {
|
|||||||
: null);
|
: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch Master Expense Types
|
/// Fetch Master Expense Categorys
|
||||||
static Future<List<dynamic>?> getMasterExpenseTypes() async {
|
static Future<List<dynamic>?> getMasterExpenseTypes() async {
|
||||||
const endpoint = ApiEndpoints.getMasterExpenseCategory;
|
const endpoint = ApiEndpoints.getMasterExpenseCategory;
|
||||||
return _getRequest(endpoint).then((res) => res != null
|
return _getRequest(endpoint).then((res) => res != null
|
||||||
? _parseResponse(res, label: 'Master Expense Types')
|
? _parseResponse(res, label: 'Master Expense Categorys')
|
||||||
: null);
|
: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2821,7 +3291,7 @@ class ApiService {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<dynamic>?> getDirectoryComments(
|
static Future<List<dynamic>?> getDirectoryComments(
|
||||||
String contactId, {
|
String contactId, {
|
||||||
bool active = true,
|
bool active = true,
|
||||||
@ -3003,6 +3473,30 @@ class ApiService {
|
|||||||
res != null ? _parseResponse(res, label: 'Employees') : null);
|
res != null ? _parseResponse(res, label: 'Employees') : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<EmployeeModel>?> getAttendanceForDashboard(
|
||||||
|
String projectId) async {
|
||||||
|
String endpoint = ApiEndpoints.getAttendanceForDashboard.replaceFirst(
|
||||||
|
':projectId',
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
final res = await _getRequest(endpoint);
|
||||||
|
|
||||||
|
if (res == null) return null;
|
||||||
|
|
||||||
|
final data = _parseResponse(res, label: 'Dashboard Attendance');
|
||||||
|
if (data == null) return null;
|
||||||
|
|
||||||
|
// Wrap single object in a list if needed
|
||||||
|
if (data is Map<String, dynamic>) {
|
||||||
|
return [EmployeeModel.fromJson(data)];
|
||||||
|
} else if (data is List) {
|
||||||
|
return data.map((e) => EmployeeModel.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<List<dynamic>?> getRegularizationLogs(
|
static Future<List<dynamic>?> getRegularizationLogs(
|
||||||
String projectId, {
|
String projectId, {
|
||||||
String? organizationId,
|
String? organizationId,
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:url_strategy/url_strategy.dart';
|
import 'package:url_strategy/url_strategy.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
|
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
|
||||||
import 'package:marco/helpers/services/device_info_service.dart';
|
import 'package:on_field_work/helpers/services/device_info_service.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
||||||
|
|
||||||
Future<void> initializeApp() async {
|
Future<void> initializeApp() async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'dart:io';
|
|||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||||
|
|
||||||
/// Global logger instance
|
/// Global logger instance
|
||||||
Logger? _appLogger;
|
Logger? _appLogger;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/local_notification_service.dart';
|
import 'package:on_field_work/helpers/services/local_notification_service.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/notification_action_handler.dart';
|
import 'package:on_field_work/helpers/services/notification_action_handler.dart';
|
||||||
|
|
||||||
/// Firebase Notification Service
|
/// Firebase Notification Service
|
||||||
class FirebaseNotificationService {
|
class FirebaseNotificationService {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Language {
|
class Language {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/localizations/language.dart';
|
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get_utils/src/extensions/string_extensions.dart';
|
import 'package:get/get_utils/src/extensions/string_extensions.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/attendance/attendance_screen_controller.dart';
|
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart';
|
||||||
import 'package:marco/controller/task_planning/daily_task_controller.dart';
|
import 'package:on_field_work/controller/task_planning/daily_task_controller.dart';
|
||||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
import 'package:on_field_work/controller/expense/expense_screen_controller.dart';
|
||||||
import 'package:marco/controller/expense/expense_detail_controller.dart';
|
import 'package:on_field_work/controller/expense/expense_detail_controller.dart';
|
||||||
import 'package:marco/controller/directory/directory_controller.dart';
|
import 'package:on_field_work/controller/directory/directory_controller.dart';
|
||||||
import 'package:marco/controller/directory/notes_controller.dart';
|
import 'package:on_field_work/controller/directory/notes_controller.dart';
|
||||||
import 'package:marco/controller/document/user_document_controller.dart';
|
import 'package:on_field_work/controller/document/user_document_controller.dart';
|
||||||
import 'package:marco/controller/document/document_details_controller.dart';
|
import 'package:on_field_work/controller/document/document_details_controller.dart';
|
||||||
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
|
||||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||||
|
|
||||||
/// Handles incoming FCM notification actions and updates UI/controllers.
|
/// Handles incoming FCM notification actions and updates UI/controllers.
|
||||||
class NotificationActionHandler {
|
class NotificationActionHandler {
|
||||||
@ -414,12 +414,17 @@ class NotificationActionHandler {
|
|||||||
required String notFoundMessage,
|
required String notFoundMessage,
|
||||||
required String successMessage,
|
required String successMessage,
|
||||||
}) {
|
}) {
|
||||||
|
if (!Get.isRegistered<T>()) {
|
||||||
|
_logger.w(notFoundMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final controller = Get.find<T>();
|
final controller = Get.find<T>();
|
||||||
onFound(controller);
|
onFound(controller);
|
||||||
_logger.i(successMessage);
|
_logger.i(successMessage);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.w(notFoundMessage);
|
_logger.w('⚠️ Error updating controller: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import 'dart:convert';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/model/user_permission.dart';
|
import 'package:on_field_work/model/user_permission.dart';
|
||||||
import 'package:marco/model/employees/employee_info.dart';
|
import 'package:on_field_work/model/employees/employee_info.dart';
|
||||||
import 'package:marco/model/projects_model.dart';
|
import 'package:on_field_work/model/projects_model.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
||||||
|
|
||||||
class PermissionService {
|
class PermissionService {
|
||||||
// In-memory cache keyed by user token
|
// In-memory cache keyed by user token
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import 'dart:convert';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:on_field_work/controller/project_controller.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/localizations/language.dart';
|
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:marco/model/employees/employee_info.dart';
|
import 'package:on_field_work/model/employees/employee_info.dart';
|
||||||
import 'package:marco/model/user_permission.dart';
|
import 'package:on_field_work/model/user_permission.dart';
|
||||||
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
|
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
|
||||||
|
|
||||||
class LocalStorage {
|
class LocalStorage {
|
||||||
static const String _loggedInUserKey = "user";
|
static const String _loggedInUserKey = "user";
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:on_field_work/controller/project_controller.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/model/tenant/tenant_list_model.dart';
|
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
||||||
|
|
||||||
/// Abstract interface for tenant service functionality
|
/// Abstract interface for tenant service functionality
|
||||||
abstract class ITenantService {
|
abstract class ITenantService {
|
||||||
@ -63,29 +63,39 @@ class TenantService implements ITenantService {
|
|||||||
{bool hasRetried = false}) async {
|
{bool hasRetried = false}) async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authorizedHeaders();
|
final headers = await _authorizedHeaders();
|
||||||
logSafe("➡️ GET $_baseUrl/auth/get/user/tenants\nHeaders: $headers",
|
|
||||||
level: LogLevel.info);
|
|
||||||
|
|
||||||
final response = await http
|
final response = await http.get(
|
||||||
.get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers);
|
Uri.parse("$_baseUrl/auth/get/user/tenants"),
|
||||||
final data = jsonDecode(response.body);
|
headers: headers,
|
||||||
|
);
|
||||||
|
|
||||||
logSafe(
|
// ✅ Handle empty response BEFORE decoding
|
||||||
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
|
if (response.body.isEmpty || response.body.trim().isEmpty) {
|
||||||
level: LogLevel.info);
|
logSafe("❌ Empty tenant response — auto logout");
|
||||||
|
await LocalStorage.logout();
|
||||||
if (response.statusCode == 200 && data['success'] == true) {
|
return null;
|
||||||
logSafe("✅ Tenants fetched successfully.");
|
|
||||||
return List<Map<String, dynamic>>.from(data['data']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> data;
|
||||||
|
try {
|
||||||
|
data = jsonDecode(response.body);
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("❌ Invalid JSON in tenant response — auto logout");
|
||||||
|
await LocalStorage.logout();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SUCCESS CASE
|
||||||
|
if (response.statusCode == 200 && data['success'] == true) {
|
||||||
|
final list = data['data'];
|
||||||
|
if (list is! List) return null;
|
||||||
|
return List<Map<String, dynamic>>.from(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOKEN EXPIRED
|
||||||
if (response.statusCode == 401 && !hasRetried) {
|
if (response.statusCode == 401 && !hasRetried) {
|
||||||
logSafe("⚠️ Unauthorized while fetching tenants. Refreshing token...",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
final refreshed = await AuthService.refreshToken();
|
final refreshed = await AuthService.refreshToken();
|
||||||
if (refreshed) return getTenants(hasRetried: true);
|
if (refreshed) return getTenants(hasRetried: true);
|
||||||
logSafe("❌ Token refresh failed while fetching tenants.",
|
|
||||||
level: LogLevel.error);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +140,7 @@ class TenantService implements ITenantService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Register FCM token after tenant selection
|
// 🔹 Register FCM token after tenant selection
|
||||||
final fcmToken = LocalStorage.getFcmToken();
|
final fcmToken = LocalStorage.getFcmToken();
|
||||||
if (fcmToken?.isNotEmpty ?? false) {
|
if (fcmToken?.isNotEmpty ?? false) {
|
||||||
final success = await AuthService.registerDeviceToken(fcmToken!);
|
final success = await AuthService.registerDeviceToken(fcmToken!);
|
||||||
logSafe(
|
logSafe(
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||||
|
|
||||||
enum LeftBarThemeType { light, dark }
|
enum LeftBarThemeType { light, dark }
|
||||||
enum ContentThemeType { light, dark }
|
enum ContentThemeType { light, dark }
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:marco/helpers/services/localizations/language.dart';
|
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:marco/helpers/widgets/my.dart';
|
import 'package:on_field_work/helpers/widgets/my.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,13 @@
|
|||||||
* */
|
* */
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:marco/helpers/widgets/my.dart';
|
import 'package:on_field_work/helpers/widgets/my.dart';
|
||||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
import 'package:on_field_work/helpers/widgets/my_breadcrumb_item.dart';
|
||||||
import 'package:marco/helpers/widgets/my_constant.dart';
|
import 'package:on_field_work/helpers/widgets/my_constant.dart';
|
||||||
import 'package:marco/helpers/widgets/my_screen_media.dart';
|
import 'package:on_field_work/helpers/widgets/my_screen_media.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
import 'package:on_field_work/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
@ -230,7 +230,7 @@ class AppStyle {
|
|||||||
containerRadius: AppStyle.containerRadius.medium,
|
containerRadius: AppStyle.containerRadius.medium,
|
||||||
cardRadius: AppStyle.cardRadius.medium,
|
cardRadius: AppStyle.cardRadius.medium,
|
||||||
buttonRadius: AppStyle.buttonRadius.medium,
|
buttonRadius: AppStyle.buttonRadius.medium,
|
||||||
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'Marco', route: '/client/dashboard'),
|
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'On Field Work', route: '/client/dashboard'),
|
||||||
));
|
));
|
||||||
bool isMobile = true;
|
bool isMobile = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:marco/helpers/services/json_decoder.dart';
|
import 'package:on_field_work/helpers/services/json_decoder.dart';
|
||||||
import 'package:marco/helpers/services/localizations/language.dart';
|
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||||
import 'package:marco/helpers/services/localizations/translator.dart';
|
import 'package:on_field_work/helpers/services/localizations/translator.dart';
|
||||||
import 'package:marco/helpers/services/navigation_services.dart';
|
import 'package:on_field_work/helpers/services/navigation_services.dart';
|
||||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||||
import 'package:marco/helpers/theme/app_notifier.dart';
|
import 'package:on_field_work/helpers/theme/app_notifier.dart';
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/widgets/wave_background.dart';
|
import 'package:on_field_work/helpers/widgets/wave_background.dart';
|
||||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||||
|
|
||||||
class ThemeOption {
|
class ThemeOption {
|
||||||
final String label;
|
final String label;
|
||||||
@ -63,6 +63,9 @@ class ThemeController extends GetxController {
|
|||||||
|
|
||||||
await Future.delayed(const Duration(milliseconds: 600));
|
await Future.delayed(const Duration(milliseconds: 600));
|
||||||
showApplied.value = false;
|
showApplied.value = false;
|
||||||
|
|
||||||
|
// Navigate to dashboard after applying theme
|
||||||
|
Get.offAllNamed('/dashboard');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||||
|
|
||||||
class BaseBottomSheet extends StatefulWidget {
|
class BaseBottomSheet extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class ContactPickerHelper {
|
class ContactPickerHelper {
|
||||||
static Future<String?> pickIndianPhoneNumber(BuildContext context) async {
|
static Future<String?> pickIndianPhoneNumber(BuildContext context) async {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class LauncherUtils {
|
class LauncherUtils {
|
||||||
/// Launches the phone dialer with the provided phone number
|
/// Launches the phone dialer with the provided phone number
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
||||||
import 'package:marco/helpers/widgets/my_dashed_divider.dart';
|
import 'package:on_field_work/helpers/widgets/my_dashed_divider.dart';
|
||||||
import 'package:marco/helpers/widgets/my_navigation_mixin.dart';
|
import 'package:on_field_work/helpers/widgets/my_navigation_mixin.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
mixin UIMixin {
|
mixin UIMixin {
|
||||||
|
|||||||
@ -161,6 +161,33 @@ class MenuItems {
|
|||||||
/// Documents menu
|
/// Documents menu
|
||||||
static const String documents = "92d2cc39-9e6a-46b2-ae50-84fbf83c95d3";
|
static const String documents = "92d2cc39-9e6a-46b2-ae50-84fbf83c95d3";
|
||||||
|
|
||||||
/// Service Projects
|
/// Service Projects
|
||||||
static const String serviceProjects = "7faddfe7-994b-4712-91c2-32ba44129d9b";
|
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";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:marco/helpers/extensions/date_time_extension.dart';
|
import 'package:on_field_work/helpers/extensions/date_time_extension.dart';
|
||||||
|
|
||||||
class Utils {
|
class Utils {
|
||||||
static getDateStringFromDateTime(DateTime dateTime,
|
static getDateStringFromDateTime(DateTime dateTime,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:marco/helpers/widgets/my_container.dart';
|
import 'package:on_field_work/helpers/widgets/my_container.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||||
|
|
||||||
class Avatar extends StatelessWidget {
|
class Avatar extends StatelessWidget {
|
||||||
final String firstName;
|
final String firstName;
|
||||||
|
|||||||
@ -1,90 +1,230 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:on_field_work/controller/project_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||||
|
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||||
|
|
||||||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
class CustomAppBar extends StatefulWidget
|
||||||
|
with UIMixin
|
||||||
|
implements PreferredSizeWidget {
|
||||||
final String title;
|
final String title;
|
||||||
|
final String? projectName; // If passed, show static text
|
||||||
final VoidCallback? onBackPressed;
|
final VoidCallback? onBackPressed;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
const CustomAppBar({
|
CustomAppBar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
|
this.projectName,
|
||||||
this.onBackPressed,
|
this.onBackPressed,
|
||||||
|
this.backgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Size get preferredSize => const Size.fromHeight(72);
|
||||||
return PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(72),
|
|
||||||
child: AppBar(
|
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
|
||||||
elevation: 0.5,
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
titleSpacing: 0,
|
|
||||||
title: Padding(
|
|
||||||
padding: MySpacing.xy(16, 0),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back_ios_new,
|
|
||||||
color: Colors.black,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
onPressed: onBackPressed ?? () => Get.back(),
|
|
||||||
),
|
|
||||||
MySpacing.width(5),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// TITLE
|
|
||||||
MyText.titleLarge(
|
|
||||||
title,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
|
|
||||||
MySpacing.height(2),
|
@override
|
||||||
|
State<CustomAppBar> createState() => _CustomAppBarState();
|
||||||
|
}
|
||||||
|
|
||||||
// PROJECT NAME ROW (copied exactly)
|
class _CustomAppBarState extends State<CustomAppBar> with UIMixin {
|
||||||
GetBuilder<ProjectController>(
|
final ProjectController projectController = Get.find();
|
||||||
builder: (projectController) {
|
OverlayEntry? _overlayEntry;
|
||||||
final projectName =
|
final LayerLink _layerLink = LayerLink();
|
||||||
projectController.selectedProject?.name ??
|
|
||||||
'Select Project';
|
|
||||||
|
|
||||||
return Row(
|
void _toggleDropdown() {
|
||||||
children: [
|
if (_overlayEntry == null) {
|
||||||
const Icon(Icons.work_outline,
|
_overlayEntry = _createOverlayEntry();
|
||||||
size: 14, color: Colors.grey),
|
Overlay.of(context).insert(_overlayEntry!);
|
||||||
MySpacing.width(4),
|
} else {
|
||||||
Expanded(
|
_overlayEntry?.remove();
|
||||||
child: MyText.bodySmall(
|
_overlayEntry = null;
|
||||||
projectName,
|
}
|
||||||
fontWeight: 600,
|
}
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
color: Colors.grey[700],
|
OverlayEntry _createOverlayEntry() {
|
||||||
),
|
final renderBox = context.findRenderObject() as RenderBox;
|
||||||
),
|
final size = renderBox.size;
|
||||||
],
|
final offset = renderBox.localToGlobal(Offset.zero);
|
||||||
);
|
|
||||||
},
|
return OverlayEntry(
|
||||||
),
|
builder: (context) => GestureDetector(
|
||||||
],
|
onTap: () {
|
||||||
|
_toggleDropdown();
|
||||||
|
},
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: offset.dx + 16,
|
||||||
|
top: offset.dy + size.height,
|
||||||
|
width: size.width - 32,
|
||||||
|
child: Material(
|
||||||
|
elevation: 4,
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
child: Container(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.33,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search project...",
|
||||||
|
isDense: true,
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: projectController.projects.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final project = projectController.projects[index];
|
||||||
|
return RadioListTile<String>(
|
||||||
|
dense: true,
|
||||||
|
value: project.id,
|
||||||
|
groupValue:
|
||||||
|
projectController.selectedProjectId.value,
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) {
|
||||||
|
projectController.updateSelectedProject(v);
|
||||||
|
_toggleDropdown();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: Text(project.name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(72);
|
Widget build(BuildContext context) {
|
||||||
|
final Color effectiveBackgroundColor =
|
||||||
|
widget.backgroundColor ?? contentTheme.primary;
|
||||||
|
const Color onPrimaryColor = Colors.white;
|
||||||
|
|
||||||
|
final bool showDropdown = widget.projectName == null;
|
||||||
|
|
||||||
|
return AppBar(
|
||||||
|
backgroundColor: effectiveBackgroundColor,
|
||||||
|
elevation: 0,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
titleSpacing: 0,
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
leading: Padding(
|
||||||
|
padding: MySpacing.only(left: 16),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_ios_new,
|
||||||
|
color: onPrimaryColor,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
onPressed: widget.onBackPressed ?? () => Get.back(),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Padding(
|
||||||
|
padding: MySpacing.only(right: 16, left: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MyText.titleLarge(
|
||||||
|
widget.title,
|
||||||
|
fontWeight: 800,
|
||||||
|
color: onPrimaryColor,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
MySpacing.height(3),
|
||||||
|
showDropdown
|
||||||
|
? CompositedTransformTarget(
|
||||||
|
link: _layerLink,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _toggleDropdown,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.folder_open,
|
||||||
|
size: 14, color: onPrimaryColor),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Flexible(
|
||||||
|
child: Obx(() {
|
||||||
|
final projectName = projectController
|
||||||
|
.selectedProject?.name ??
|
||||||
|
'Select Project';
|
||||||
|
return MyText.bodySmall(
|
||||||
|
projectName,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: onPrimaryColor.withOpacity(0.8),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
MySpacing.width(2),
|
||||||
|
const Icon(Icons.keyboard_arrow_down,
|
||||||
|
size: 18, color: onPrimaryColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.folder_open,
|
||||||
|
size: 14, color: onPrimaryColor),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Flexible(
|
||||||
|
child: MyText.bodySmall(
|
||||||
|
widget.projectName!,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: onPrimaryColor.withOpacity(0.8),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: MySpacing.only(right: 16),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.home, color: onPrimaryColor, size: 24),
|
||||||
|
onPressed: () => Get.offAllNamed('/dashboard'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_overlayEntry?.remove();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user