Compare commits
28 Commits
main
...
feature/pa
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cf0016ccc | |||
| 3008a6ab79 | |||
| e674d18542 | |||
| 3ad3515d8d | |||
| 32d74d23dd | |||
| 79c34a62d7 | |||
| b0b68d688e | |||
| 4aaf8a6bf1 | |||
| 21adc5e556 | |||
| 7e427237c3 | |||
| 12fbef19c5 | |||
| c556df1f80 | |||
| 70fcc2e662 | |||
| 2d95e92edb | |||
| 8415b6549d | |||
| 6c0f325a44 | |||
| ff9c712a7e | |||
| 7e64e8c091 | |||
| c5ebf17dbc | |||
| 9dfdac0323 | |||
| ea82d65a81 | |||
| 8f15a04c88 | |||
| 4a1bd85435 | |||
| f70138238b | |||
| d071fa6c39 | |||
| 27de1a5306 | |||
| dad3d98896 | |||
| 1d8ec14a87 |
@ -15,7 +15,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
|
||||
android {
|
||||
// Define the namespace for your Android application
|
||||
namespace = "com.marcoonfieldwork.aiot"
|
||||
namespace = "com.marco.aiot"
|
||||
// Set the compile SDK version based on Flutter's configuration
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
// Set the NDK version based on Flutter's configuration
|
||||
@ -37,7 +37,7 @@ android {
|
||||
// Default configuration for your application
|
||||
defaultConfig {
|
||||
// Specify your unique Application ID. This identifies your app on Google Play.
|
||||
applicationId = "com.marcoonfieldwork.aiot"
|
||||
applicationId = "com.marco.aiot"
|
||||
// Set minimum and target SDK versions based on Flutter's configuration
|
||||
minSdk = 23
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
|
||||
"android_client_info": {
|
||||
"package_name": "com.marcoonfieldwork.aiot"
|
||||
"package_name": "com.marco.aiot"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<application
|
||||
android:label="On Field Work"
|
||||
android:label="Marco"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.marcoonfieldwork.aiot
|
||||
package com.marco.aiot
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ pluginManagement {
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.6.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.2.21" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# App info
|
||||
APP_NAME="On Field Work"
|
||||
APP_NAME="Marco"
|
||||
BUILD_DIR="build/app/outputs"
|
||||
|
||||
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"
|
||||
|
||||
@ -368,7 +368,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@ -384,7 +384,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@ -401,7 +401,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@ -416,7 +416,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@ -547,7 +547,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@ -569,7 +569,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>On Field Work</string>
|
||||
<string>Marco</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>on field work</string>
|
||||
<string>marco</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@ -8,5 +8,5 @@ class AppConstant {
|
||||
static int iOSAppVersion = 1;
|
||||
static String version = "1.0.0";
|
||||
|
||||
static String get appName => 'On Field Work';
|
||||
static String get appName => 'Marco';
|
||||
}
|
||||
|
||||
@ -1,67 +1,61 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
|
||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||
import 'package:on_field_work/model/attendance/attendance_log_model.dart';
|
||||
import 'package:on_field_work/model/attendance/attendance_log_view_model.dart';
|
||||
import 'package:on_field_work/model/attendance/attendance_model.dart';
|
||||
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/model/project_model.dart';
|
||||
import 'package:on_field_work/model/regularization_log_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
||||
|
||||
import 'package:marco/model/attendance/attendance_model.dart';
|
||||
import 'package:marco/model/project_model.dart';
|
||||
import 'package:marco/model/employees/employee_model.dart';
|
||||
import 'package:marco/model/attendance/attendance_log_model.dart';
|
||||
import 'package:marco/model/regularization_log_model.dart';
|
||||
import 'package:marco/model/attendance/attendance_log_view_model.dart';
|
||||
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class AttendanceController extends GetxController {
|
||||
// ------------------ Data Models ------------------
|
||||
final List<AttendanceModel> attendances = <AttendanceModel>[];
|
||||
final List<ProjectModel> projects = <ProjectModel>[];
|
||||
final List<EmployeeModel> employees = <EmployeeModel>[];
|
||||
final List<AttendanceLogModel> attendanceLogs = <AttendanceLogModel>[];
|
||||
final List<RegularizationLogModel> regularizationLogs =
|
||||
<RegularizationLogModel>[];
|
||||
final List<AttendanceLogViewModel> attendenceLogsView =
|
||||
<AttendanceLogViewModel>[];
|
||||
|
||||
// Data models
|
||||
List<AttendanceModel> attendances = [];
|
||||
List<ProjectModel> projects = [];
|
||||
List<EmployeeModel> employees = [];
|
||||
List<AttendanceLogModel> attendanceLogs = [];
|
||||
List<RegularizationLogModel> regularizationLogs = [];
|
||||
List<AttendanceLogViewModel> attendenceLogsView = [];
|
||||
// ------------------ Organizations ------------------
|
||||
final List<Organization> organizations = <Organization>[];
|
||||
List<Organization> organizations = [];
|
||||
Organization? selectedOrganization;
|
||||
final RxBool isLoadingOrganizations = false.obs;
|
||||
final isLoadingOrganizations = false.obs;
|
||||
|
||||
// ------------------ States ------------------
|
||||
// States
|
||||
String selectedTab = 'todaysAttendance';
|
||||
DateTime? startDateAttendance;
|
||||
DateTime? endDateAttendance;
|
||||
|
||||
// ✅ Reactive date range
|
||||
final Rx<DateTime> startDateAttendance =
|
||||
DateTime.now().subtract(const Duration(days: 7)).obs;
|
||||
final Rx<DateTime> endDateAttendance =
|
||||
DateTime.now().subtract(const Duration(days: 1)).obs;
|
||||
|
||||
final RxBool isLoading = true.obs;
|
||||
final RxBool isLoadingProjects = true.obs;
|
||||
final RxBool isLoadingEmployees = true.obs;
|
||||
final RxBool isLoadingAttendanceLogs = true.obs;
|
||||
final RxBool isLoadingRegularizationLogs = true.obs;
|
||||
final RxBool isLoadingLogView = true.obs;
|
||||
|
||||
final RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
final RxBool showPendingOnly = false.obs;
|
||||
final RxString searchQuery = ''.obs;
|
||||
final isLoading = true.obs;
|
||||
final isLoadingProjects = true.obs;
|
||||
final isLoadingEmployees = true.obs;
|
||||
final isLoadingAttendanceLogs = true.obs;
|
||||
final isLoadingRegularizationLogs = true.obs;
|
||||
final isLoadingLogView = true.obs;
|
||||
final uploadingStates = <String, RxBool>{}.obs;
|
||||
var showPendingOnly = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initializeDefaults();
|
||||
|
||||
// 🔹 Fetch organizations for the selected project
|
||||
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
||||
if (projectId != null) {
|
||||
fetchOrganizations(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeDefaults() {
|
||||
@ -69,60 +63,55 @@ class AttendanceController extends GetxController {
|
||||
}
|
||||
|
||||
void _setDefaultDateRange() {
|
||||
final DateTime today = DateTime.now();
|
||||
startDateAttendance.value = today.subtract(const Duration(days: 7));
|
||||
endDateAttendance.value = today.subtract(const Duration(days: 1));
|
||||
final today = DateTime.now();
|
||||
startDateAttendance = today.subtract(const Duration(days: 7));
|
||||
endDateAttendance = today.subtract(const Duration(days: 1));
|
||||
logSafe(
|
||||
'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}',
|
||||
);
|
||||
"Default date range set: $startDateAttendance to $endDateAttendance");
|
||||
}
|
||||
|
||||
// ------------------ Computed Filters ------------------
|
||||
List<EmployeeModel> get filteredEmployees {
|
||||
final String query = searchQuery.value.trim().toLowerCase();
|
||||
if (query.isEmpty) return employees;
|
||||
return employees
|
||||
.where(
|
||||
(EmployeeModel e) => e.name.toLowerCase().contains(query),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<AttendanceLogModel> get filteredLogs {
|
||||
final String query = searchQuery.value.trim().toLowerCase();
|
||||
if (query.isEmpty) return attendanceLogs;
|
||||
return attendanceLogs
|
||||
.where(
|
||||
(AttendanceLogModel log) => log.name.toLowerCase().contains(query),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<RegularizationLogModel> get filteredRegularizationLogs {
|
||||
final String query = searchQuery.value.trim().toLowerCase();
|
||||
if (query.isEmpty) return regularizationLogs;
|
||||
return regularizationLogs
|
||||
.where(
|
||||
(RegularizationLogModel log) =>
|
||||
log.name.toLowerCase().contains(query),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ------------------ Project & Employee APIs ------------------
|
||||
// ------------------ Project & Employee ------------------
|
||||
/// Called when a notification says attendance has been updated
|
||||
Future<void> refreshDataFromNotification({String? projectId}) async {
|
||||
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
||||
if (projectId == null) {
|
||||
logSafe(
|
||||
'No project selected for attendance refresh from notification',
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
logSafe("No project selected for attendance refresh from notification",
|
||||
level: LogLevel.warning);
|
||||
return;
|
||||
}
|
||||
await fetchProjectData(projectId);
|
||||
logSafe(
|
||||
'Attendance data refreshed from notification for project $projectId',
|
||||
);
|
||||
"Attendance data refreshed from notification for project $projectId");
|
||||
}
|
||||
|
||||
// 🔍 Search query
|
||||
final searchQuery = ''.obs;
|
||||
|
||||
// Computed filtered employees
|
||||
List<EmployeeModel> get filteredEmployees {
|
||||
if (searchQuery.value.isEmpty) return employees;
|
||||
return employees
|
||||
.where((e) =>
|
||||
e.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Computed filtered logs
|
||||
List<AttendanceLogModel> get filteredLogs {
|
||||
if (searchQuery.value.isEmpty) return attendanceLogs;
|
||||
return attendanceLogs
|
||||
.where((log) =>
|
||||
(log.name).toLowerCase().contains(searchQuery.value.toLowerCase()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Computed filtered regularization logs
|
||||
List<RegularizationLogModel> get filteredRegularizationLogs {
|
||||
if (searchQuery.value.isEmpty) return regularizationLogs;
|
||||
return regularizationLogs
|
||||
.where((log) =>
|
||||
log.name.toLowerCase().contains(searchQuery.value.toLowerCase()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> fetchTodaysAttendance(String? projectId) async {
|
||||
@ -130,105 +119,109 @@ class AttendanceController extends GetxController {
|
||||
|
||||
isLoadingEmployees.value = true;
|
||||
|
||||
final List<dynamic>? response = await ApiService.getTodaysAttendance(
|
||||
final response = await ApiService.getTodaysAttendance(
|
||||
projectId,
|
||||
organizationId: selectedOrganization?.id,
|
||||
);
|
||||
if (response != null) {
|
||||
employees
|
||||
..clear()
|
||||
..addAll(
|
||||
response
|
||||
.map<EmployeeModel>(
|
||||
(dynamic e) => EmployeeModel.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
for (final EmployeeModel emp in employees) {
|
||||
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
|
||||
for (var emp in employees) {
|
||||
uploadingStates[emp.id] = false.obs;
|
||||
}
|
||||
|
||||
logSafe(
|
||||
'Employees fetched: ${employees.length} for project $projectId',
|
||||
);
|
||||
logSafe("Employees fetched: ${employees.length} for project $projectId");
|
||||
} else {
|
||||
logSafe(
|
||||
'Failed to fetch employees for project $projectId',
|
||||
level: LogLevel.error,
|
||||
);
|
||||
logSafe("Failed to fetch employees for project $projectId",
|
||||
level: LogLevel.error);
|
||||
}
|
||||
|
||||
isLoadingEmployees.value = false;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> fetchOrganizations(String projectId) async {
|
||||
isLoadingOrganizations.value = true;
|
||||
|
||||
// Keep original return type inference from your ApiService
|
||||
final response = await ApiService.getAssignedOrganizations(projectId);
|
||||
|
||||
if (response != null) {
|
||||
organizations
|
||||
..clear()
|
||||
..addAll(response.data);
|
||||
logSafe('Organizations fetched: ${organizations.length}');
|
||||
organizations = response.data;
|
||||
logSafe("Organizations fetched: ${organizations.length}");
|
||||
} else {
|
||||
logSafe(
|
||||
'Failed to fetch organizations for project $projectId',
|
||||
level: LogLevel.error,
|
||||
);
|
||||
logSafe("Failed to fetch organizations for project $projectId",
|
||||
level: LogLevel.error);
|
||||
}
|
||||
|
||||
isLoadingOrganizations.value = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// ------------------ Attendance Capture ------------------
|
||||
|
||||
Future<bool> captureAndUploadAttendance(
|
||||
String id,
|
||||
String employeeId,
|
||||
String projectId, {
|
||||
String comment = 'Marked via mobile app',
|
||||
String comment = "Marked via mobile app",
|
||||
required int action,
|
||||
bool imageCapture = true,
|
||||
String? markTime,
|
||||
String? date,
|
||||
String? markTime, // still optional in controller
|
||||
String? date, // new optional param
|
||||
}) async {
|
||||
try {
|
||||
_setUploading(employeeId, true);
|
||||
uploadingStates[employeeId]?.value = true;
|
||||
|
||||
final XFile? image = await _captureAndPrepareImage(
|
||||
employeeId: employeeId,
|
||||
imageCapture: imageCapture,
|
||||
);
|
||||
if (imageCapture && image == null) {
|
||||
XFile? image;
|
||||
if (imageCapture) {
|
||||
image = await ImagePicker()
|
||||
.pickImage(source: ImageSource.camera, imageQuality: 80);
|
||||
if (image == null) {
|
||||
logSafe("Image capture cancelled.", level: LogLevel.warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
final Position? position = await _getCurrentPositionSafely();
|
||||
if (position == null) return false;
|
||||
// 🔹 Add timestamp to the image
|
||||
final timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||
imageFile: File(image.path));
|
||||
|
||||
final String imageName = imageCapture
|
||||
? ApiService.generateImageName(
|
||||
employeeId,
|
||||
employees.length + 1,
|
||||
)
|
||||
: '';
|
||||
final compressedBytes =
|
||||
await compressImageToUnder100KB(timestampedFile);
|
||||
if (compressedBytes == null) {
|
||||
logSafe("Image compression failed.", level: LogLevel.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
final DateTime effectiveDate =
|
||||
_resolveEffectiveDateForAction(action, employeeId);
|
||||
final compressedFile = await saveCompressedImageToFile(compressedBytes);
|
||||
image = XFile(compressedFile.path);
|
||||
}
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final String formattedMarkTime =
|
||||
markTime ?? DateFormat('hh:mm a').format(now);
|
||||
final String formattedDate =
|
||||
if (!await _handleLocationPermission()) return false;
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
|
||||
final imageName = imageCapture
|
||||
? ApiService.generateImageName(employeeId, employees.length + 1)
|
||||
: "";
|
||||
|
||||
// ---------------- DATE / TIME LOGIC ----------------
|
||||
final now = DateTime.now();
|
||||
|
||||
// Default effectiveDate = now
|
||||
DateTime effectiveDate = now;
|
||||
|
||||
if (action == 1) {
|
||||
// Checkout
|
||||
// Try to find today's open log for this employee
|
||||
final log = attendanceLogs.firstWhereOrNull(
|
||||
(log) => log.employeeId == employeeId && log.checkOut == null,
|
||||
);
|
||||
if (log?.checkIn != null) {
|
||||
effectiveDate = log!.checkIn!; // use check-in date
|
||||
}
|
||||
}
|
||||
|
||||
final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now);
|
||||
|
||||
final formattedDate =
|
||||
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
|
||||
|
||||
final bool result = await ApiService.uploadAttendanceImage(
|
||||
// ---------------- API CALL ----------------
|
||||
final result = await ApiService.uploadAttendanceImage(
|
||||
id,
|
||||
employeeId,
|
||||
image,
|
||||
@ -243,99 +236,15 @@ class AttendanceController extends GetxController {
|
||||
date: formattedDate,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
logSafe(
|
||||
'Attendance uploaded for $employeeId, action: $action, date: $formattedDate',
|
||||
);
|
||||
|
||||
if (Get.isRegistered<DashboardController>()) {
|
||||
final DashboardController dashboardController =
|
||||
Get.find<DashboardController>();
|
||||
await dashboardController.fetchTodaysAttendance(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
"Attendance uploaded for $employeeId, action: $action, date: $formattedDate");
|
||||
return result;
|
||||
} catch (e, stacktrace) {
|
||||
logSafe(
|
||||
'Error uploading attendance',
|
||||
level: LogLevel.error,
|
||||
error: e,
|
||||
stackTrace: stacktrace,
|
||||
);
|
||||
logSafe("Error uploading attendance",
|
||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||
return false;
|
||||
} finally {
|
||||
_setUploading(employeeId, false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<XFile?> _captureAndPrepareImage({
|
||||
required String employeeId,
|
||||
required bool imageCapture,
|
||||
}) async {
|
||||
if (!imageCapture) return null;
|
||||
|
||||
final XFile? rawImage = await ImagePicker().pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 80,
|
||||
);
|
||||
if (rawImage == null) {
|
||||
logSafe(
|
||||
'Image capture cancelled.',
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final File timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||
imageFile: File(rawImage.path),
|
||||
);
|
||||
|
||||
final List<int>? compressedBytes =
|
||||
await compressImageToUnder100KB(timestampedFile);
|
||||
if (compressedBytes == null) {
|
||||
logSafe(
|
||||
'Image compression failed.',
|
||||
level: LogLevel.error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// FIX: convert List<int> -> Uint8List
|
||||
final Uint8List compressedUint8List = Uint8List.fromList(compressedBytes);
|
||||
|
||||
final File compressedFile =
|
||||
await saveCompressedImageToFile(compressedUint8List);
|
||||
return XFile(compressedFile.path);
|
||||
}
|
||||
|
||||
Future<Position?> _getCurrentPositionSafely() async {
|
||||
final bool permissionGranted = await _handleLocationPermission();
|
||||
if (!permissionGranted) return null;
|
||||
|
||||
return Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _resolveEffectiveDateForAction(int action, String employeeId) {
|
||||
final DateTime now = DateTime.now();
|
||||
if (action != 1) return now;
|
||||
|
||||
final AttendanceLogModel? log = attendanceLogs.firstWhereOrNull(
|
||||
(AttendanceLogModel log) =>
|
||||
log.employeeId == employeeId && log.checkOut == null,
|
||||
);
|
||||
|
||||
return log?.checkIn ?? now;
|
||||
}
|
||||
|
||||
void _setUploading(String employeeId, bool value) {
|
||||
final RxBool? state = uploadingStates[employeeId];
|
||||
if (state != null) {
|
||||
state.value = value;
|
||||
} else {
|
||||
uploadingStates[employeeId] = value.obs;
|
||||
uploadingStates[employeeId]?.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,19 +254,14 @@ class AttendanceController extends GetxController {
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
logSafe(
|
||||
'Location permissions are denied',
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
logSafe('Location permissions are denied', level: LogLevel.warning);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
logSafe(
|
||||
'Location permissions are permanently denied',
|
||||
level: LogLevel.error,
|
||||
);
|
||||
logSafe('Location permissions are permanently denied',
|
||||
level: LogLevel.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -365,40 +269,26 @@ class AttendanceController extends GetxController {
|
||||
}
|
||||
|
||||
// ------------------ Attendance Logs ------------------
|
||||
Future<void> fetchAttendanceLogs(
|
||||
String? projectId, {
|
||||
DateTime? dateFrom,
|
||||
DateTime? dateTo,
|
||||
}) async {
|
||||
|
||||
Future<void> fetchAttendanceLogs(String? projectId,
|
||||
{DateTime? dateFrom, DateTime? dateTo}) async {
|
||||
if (projectId == null) return;
|
||||
|
||||
isLoadingAttendanceLogs.value = true;
|
||||
|
||||
final List<dynamic>? response = await ApiService.getAttendanceLogs(
|
||||
final response = await ApiService.getAttendanceLogs(
|
||||
projectId,
|
||||
dateFrom: dateFrom,
|
||||
dateTo: dateTo,
|
||||
organizationId: selectedOrganization?.id,
|
||||
);
|
||||
|
||||
if (response != null) {
|
||||
attendanceLogs
|
||||
..clear()
|
||||
..addAll(
|
||||
response
|
||||
.map<AttendanceLogModel>(
|
||||
(dynamic e) => AttendanceLogModel.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
logSafe('Attendance logs fetched: ${attendanceLogs.length}');
|
||||
attendanceLogs =
|
||||
response.map((e) => AttendanceLogModel.fromJson(e)).toList();
|
||||
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
|
||||
} else {
|
||||
logSafe(
|
||||
'Failed to fetch attendance logs for project $projectId',
|
||||
level: LogLevel.error,
|
||||
);
|
||||
logSafe("Failed to fetch attendance logs for project $projectId",
|
||||
level: LogLevel.error);
|
||||
}
|
||||
|
||||
isLoadingAttendanceLogs.value = false;
|
||||
@ -406,70 +296,45 @@ class AttendanceController extends GetxController {
|
||||
}
|
||||
|
||||
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
||||
final Map<String, List<AttendanceLogModel>> groupedLogs =
|
||||
<String, List<AttendanceLogModel>>{};
|
||||
final groupedLogs = <String, List<AttendanceLogModel>>{};
|
||||
|
||||
for (final AttendanceLogModel logItem in attendanceLogs) {
|
||||
final String checkInDate = logItem.checkIn != null
|
||||
for (var logItem in attendanceLogs) {
|
||||
final checkInDate = logItem.checkIn != null
|
||||
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
|
||||
: 'Unknown';
|
||||
|
||||
groupedLogs.putIfAbsent(
|
||||
checkInDate,
|
||||
() => <AttendanceLogModel>[],
|
||||
)..add(logItem);
|
||||
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
|
||||
}
|
||||
|
||||
final List<MapEntry<String, List<AttendanceLogModel>>> sortedEntries =
|
||||
groupedLogs.entries.toList()
|
||||
..sort(
|
||||
(MapEntry<String, List<AttendanceLogModel>> a,
|
||||
MapEntry<String, List<AttendanceLogModel>> b) {
|
||||
final sortedEntries = groupedLogs.entries.toList()
|
||||
..sort((a, b) {
|
||||
if (a.key == 'Unknown') return 1;
|
||||
if (b.key == 'Unknown') return -1;
|
||||
|
||||
final DateTime dateA = DateFormat('dd MMM yyyy').parse(a.key);
|
||||
final DateTime dateB = DateFormat('dd MMM yyyy').parse(b.key);
|
||||
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
|
||||
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
|
||||
return dateB.compareTo(dateA);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return Map<String, List<AttendanceLogModel>>.fromEntries(
|
||||
sortedEntries,
|
||||
);
|
||||
return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
|
||||
}
|
||||
|
||||
// ------------------ Regularization Logs ------------------
|
||||
|
||||
Future<void> fetchRegularizationLogs(String? projectId) async {
|
||||
if (projectId == null) return;
|
||||
|
||||
isLoadingRegularizationLogs.value = true;
|
||||
|
||||
final List<dynamic>? response = await ApiService.getRegularizationLogs(
|
||||
final response = await ApiService.getRegularizationLogs(
|
||||
projectId,
|
||||
organizationId: selectedOrganization?.id,
|
||||
);
|
||||
|
||||
if (response != null) {
|
||||
regularizationLogs
|
||||
..clear()
|
||||
..addAll(
|
||||
response
|
||||
.map<RegularizationLogModel>(
|
||||
(dynamic e) => RegularizationLogModel.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
logSafe(
|
||||
'Regularization logs fetched: ${regularizationLogs.length}',
|
||||
);
|
||||
regularizationLogs =
|
||||
response.map((e) => RegularizationLogModel.fromJson(e)).toList();
|
||||
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
|
||||
} else {
|
||||
logSafe(
|
||||
'Failed to fetch regularization logs for project $projectId',
|
||||
level: LogLevel.error,
|
||||
);
|
||||
logSafe("Failed to fetch regularization logs for project $projectId",
|
||||
level: LogLevel.error);
|
||||
}
|
||||
|
||||
isLoadingRegularizationLogs.value = false;
|
||||
@ -477,38 +342,22 @@ class AttendanceController extends GetxController {
|
||||
}
|
||||
|
||||
// ------------------ Attendance Log View ------------------
|
||||
|
||||
Future<void> fetchLogsView(String? id) async {
|
||||
if (id == null) return;
|
||||
|
||||
isLoadingLogView.value = true;
|
||||
|
||||
final List<dynamic>? response = await ApiService.getAttendanceLogView(id);
|
||||
|
||||
final response = await ApiService.getAttendanceLogView(id);
|
||||
if (response != null) {
|
||||
attendenceLogsView
|
||||
..clear()
|
||||
..addAll(
|
||||
response
|
||||
.map<AttendanceLogViewModel>(
|
||||
(dynamic e) => AttendanceLogViewModel.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
attendenceLogsView.sort(
|
||||
(AttendanceLogViewModel a, AttendanceLogViewModel b) =>
|
||||
(b.activityTime ?? DateTime(2000))
|
||||
.compareTo(a.activityTime ?? DateTime(2000)),
|
||||
);
|
||||
|
||||
logSafe('Attendance log view fetched for ID: $id');
|
||||
attendenceLogsView =
|
||||
response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
|
||||
attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000))
|
||||
.compareTo(a.activityTime ?? DateTime(2000)));
|
||||
logSafe("Attendance log view fetched for ID: $id");
|
||||
} else {
|
||||
logSafe(
|
||||
'Failed to fetch attendance log view for ID $id',
|
||||
level: LogLevel.error,
|
||||
);
|
||||
logSafe("Failed to fetch attendance log view for ID $id",
|
||||
level: LogLevel.error);
|
||||
}
|
||||
|
||||
isLoadingLogView.value = false;
|
||||
@ -516,6 +365,7 @@ class AttendanceController extends GetxController {
|
||||
}
|
||||
|
||||
// ------------------ Combined Load ------------------
|
||||
|
||||
Future<void> loadAttendanceData(String projectId) async {
|
||||
isLoading.value = true;
|
||||
await fetchProjectData(projectId);
|
||||
@ -527,6 +377,7 @@ class AttendanceController extends GetxController {
|
||||
|
||||
await fetchOrganizations(projectId);
|
||||
|
||||
// Call APIs depending on the selected tab only
|
||||
switch (selectedTab) {
|
||||
case 'todaysAttendance':
|
||||
await fetchTodaysAttendance(projectId);
|
||||
@ -534,8 +385,8 @@ class AttendanceController extends GetxController {
|
||||
case 'attendanceLogs':
|
||||
await fetchAttendanceLogs(
|
||||
projectId,
|
||||
dateFrom: startDateAttendance.value,
|
||||
dateTo: endDateAttendance.value,
|
||||
dateFrom: startDateAttendance,
|
||||
dateTo: endDateAttendance,
|
||||
);
|
||||
break;
|
||||
case 'regularizationRequests':
|
||||
@ -544,35 +395,31 @@ class AttendanceController extends GetxController {
|
||||
}
|
||||
|
||||
logSafe(
|
||||
'Project data fetched for project ID: $projectId, tab: $selectedTab',
|
||||
);
|
||||
"Project data fetched for project ID: $projectId, tab: $selectedTab");
|
||||
update();
|
||||
}
|
||||
|
||||
// ------------------ UI Interaction ------------------
|
||||
Future<void> selectDateRangeForAttendance(
|
||||
BuildContext context,
|
||||
AttendanceController controller,
|
||||
) async {
|
||||
final DateTime today = DateTime.now();
|
||||
|
||||
final DateTimeRange? picked = await showDateRangePicker(
|
||||
Future<void> selectDateRangeForAttendance(
|
||||
BuildContext context, AttendanceController controller) async {
|
||||
final today = DateTime.now();
|
||||
|
||||
final picked = await showDateRangePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(2022),
|
||||
lastDate: today.subtract(const Duration(days: 1)),
|
||||
initialDateRange: DateTimeRange(
|
||||
start: startDateAttendance.value,
|
||||
end: endDateAttendance.value,
|
||||
start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
|
||||
end: endDateAttendance ?? today.subtract(const Duration(days: 1)),
|
||||
),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
startDateAttendance.value = picked.start;
|
||||
endDateAttendance.value = picked.end;
|
||||
|
||||
startDateAttendance = picked.start;
|
||||
endDateAttendance = picked.end;
|
||||
logSafe(
|
||||
'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}',
|
||||
);
|
||||
"Date range selected: $startDateAttendance to $endDateAttendance");
|
||||
|
||||
await controller.fetchAttendanceLogs(
|
||||
Get.find<ProjectController>().selectedProject?.id,
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
|
||||
class ForgotPasswordController extends MyController {
|
||||
final MyFormValidator basicValidator = MyFormValidator();
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class LoginController extends MyController {
|
||||
final MyFormValidator basicValidator = MyFormValidator();
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
|
||||
import 'package:on_field_work/controller/permission_controller.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class MPINController extends GetxController {
|
||||
final MyFormValidator basicValidator = MyFormValidator();
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class OTPController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class RegisterAccountController extends MyController {
|
||||
MyFormValidator basicValidator = MyFormValidator();
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class ResetPasswordController extends MyController {
|
||||
MyFormValidator basicValidator = MyFormValidator();
|
||||
|
||||
@ -1,370 +1,263 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:on_field_work/model/dashboard/project_progress_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/pending_expenses_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/expense_type_report_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/monthly_expence_model.dart';
|
||||
import 'package:on_field_work/model/expense/expense_type_model.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/model/dashboard/project_progress_model.dart';
|
||||
|
||||
class DashboardController extends GetxController {
|
||||
// Dependencies
|
||||
final ProjectController projectController = Get.put(ProjectController());
|
||||
// =========================
|
||||
// Attendance overview
|
||||
// =========================
|
||||
final RxList<Map<String, dynamic>> roleWiseData =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
final RxString attendanceSelectedRange = '15D'.obs;
|
||||
final RxBool attendanceIsChartView = true.obs;
|
||||
final RxBool isAttendanceLoading = false.obs;
|
||||
|
||||
// =========================
|
||||
// 1. STATE VARIABLES
|
||||
// Project progress overview
|
||||
// =========================
|
||||
final RxList<ChartTaskData> projectChartData = <ChartTaskData>[].obs;
|
||||
final RxString projectSelectedRange = '15D'.obs;
|
||||
final RxBool projectIsChartView = true.obs;
|
||||
final RxBool isProjectLoading = false.obs;
|
||||
|
||||
// Attendance
|
||||
final roleWiseData = <Map<String, dynamic>>[].obs;
|
||||
final attendanceSelectedRange = '15D'.obs;
|
||||
final attendanceIsChartView = true.obs;
|
||||
final isAttendanceLoading = false.obs;
|
||||
// =========================
|
||||
// Projects overview
|
||||
// =========================
|
||||
final RxInt totalProjects = 0.obs;
|
||||
final RxInt ongoingProjects = 0.obs;
|
||||
final RxBool isProjectsLoading = false.obs;
|
||||
|
||||
// Project Progress
|
||||
final projectChartData = <ChartTaskData>[].obs;
|
||||
final projectSelectedRange = '15D'.obs;
|
||||
final projectIsChartView = true.obs;
|
||||
final isProjectLoading = false.obs;
|
||||
// =========================
|
||||
// Tasks overview
|
||||
// =========================
|
||||
final RxInt totalTasks = 0.obs;
|
||||
final RxInt completedTasks = 0.obs;
|
||||
final RxBool isTasksLoading = false.obs;
|
||||
|
||||
// Overview Counts
|
||||
final totalProjects = 0.obs;
|
||||
final ongoingProjects = 0.obs;
|
||||
final isProjectsLoading = false.obs;
|
||||
// =========================
|
||||
// Teams overview
|
||||
// =========================
|
||||
final RxInt totalEmployees = 0.obs;
|
||||
final RxInt inToday = 0.obs;
|
||||
final RxBool isTeamsLoading = false.obs;
|
||||
|
||||
final totalTasks = 0.obs;
|
||||
final completedTasks = 0.obs;
|
||||
final isTasksLoading = false.obs;
|
||||
|
||||
final totalEmployees = 0.obs;
|
||||
final inToday = 0.obs;
|
||||
final isTeamsLoading = false.obs;
|
||||
|
||||
// Expenses & Reports
|
||||
final isPendingExpensesLoading = false.obs;
|
||||
final pendingExpensesData = Rx<PendingExpensesData?>(null);
|
||||
|
||||
final isExpenseTypeReportLoading = false.obs;
|
||||
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
|
||||
final expenseReportStartDate =
|
||||
DateTime.now().subtract(const Duration(days: 15)).obs;
|
||||
final expenseReportEndDate = DateTime.now().obs;
|
||||
|
||||
final isMonthlyExpenseLoading = false.obs;
|
||||
final monthlyExpenseList = <MonthlyExpenseData>[].obs;
|
||||
final selectedMonthlyExpenseDuration =
|
||||
MonthlyExpenseDuration.twelveMonths.obs;
|
||||
final selectedMonthsCount = 12.obs;
|
||||
|
||||
final expenseTypes = <ExpenseTypeModel>[].obs;
|
||||
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
||||
|
||||
// Teams/Employees
|
||||
final isLoadingEmployees = true.obs;
|
||||
final employees = <EmployeeModel>[].obs;
|
||||
final uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
// Collection
|
||||
final isCollectionOverviewLoading = true.obs;
|
||||
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
||||
// =========================
|
||||
// Purchase Invoice Overview
|
||||
// =========================
|
||||
final isPurchaseInvoiceLoading = true.obs;
|
||||
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
||||
// Constants
|
||||
// Common ranges
|
||||
final List<String> ranges = ['7D', '15D', '30D'];
|
||||
static const _rangeDaysMap = {
|
||||
'7D': 7,
|
||||
'15D': 15,
|
||||
'30D': 30,
|
||||
'3M': 90,
|
||||
'6M': 180
|
||||
};
|
||||
|
||||
// =========================
|
||||
// 2. COMPUTED PROPERTIES
|
||||
// =========================
|
||||
|
||||
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
|
||||
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
|
||||
|
||||
// DSO Calculation Constants
|
||||
static const double _w0_30 = 15.0;
|
||||
static const double _w30_60 = 45.0;
|
||||
static const double _w60_90 = 75.0;
|
||||
static const double _w90_plus = 105.0;
|
||||
|
||||
double get calculatedDSO {
|
||||
final data = collectionOverviewData.value;
|
||||
if (data == null || data.totalDueAmount == 0) return 0.0;
|
||||
|
||||
final double weightedDue = (data.bucket0To30Amount * _w0_30) +
|
||||
(data.bucket30To60Amount * _w30_60) +
|
||||
(data.bucket60To90Amount * _w60_90) +
|
||||
(data.bucket90PlusAmount * _w90_plus);
|
||||
|
||||
return weightedDue / data.totalDueAmount;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 3. LIFECYCLE
|
||||
// =========================
|
||||
// Inside your DashboardController
|
||||
final ProjectController projectController =
|
||||
Get.put(ProjectController(), permanent: true);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
logSafe('DashboardController initialized', level: LogLevel.info);
|
||||
|
||||
// Project Selection Listener
|
||||
ever<String>(projectController.selectedProjectId, (id) {
|
||||
if (id.isNotEmpty) {
|
||||
fetchAllDashboardData();
|
||||
fetchTodaysAttendance(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Expense Report Date Listener
|
||||
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
||||
if (projectController.selectedProjectId.value.isNotEmpty) {
|
||||
fetchExpenseTypeReport(
|
||||
startDate: expenseReportStartDate.value,
|
||||
endDate: expenseReportEndDate.value,
|
||||
logSafe(
|
||||
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
|
||||
level: LogLevel.info,
|
||||
);
|
||||
}
|
||||
|
||||
fetchAllDashboardData();
|
||||
|
||||
// React to project change
|
||||
ever<String>(projectController.selectedProjectId, (id) {
|
||||
fetchAllDashboardData();
|
||||
});
|
||||
|
||||
// Chart Range Listeners
|
||||
// React to range changes
|
||||
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
||||
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 4. USER ACTIONS
|
||||
// Helper Methods
|
||||
// =========================
|
||||
int _getDaysFromRange(String range) {
|
||||
switch (range) {
|
||||
case '7D':
|
||||
return 7;
|
||||
case '15D':
|
||||
return 15;
|
||||
case '30D':
|
||||
return 30;
|
||||
case '3M':
|
||||
return 90;
|
||||
case '6M':
|
||||
return 180;
|
||||
default:
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
void updateAttendanceRange(String range) =>
|
||||
int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value);
|
||||
int getProjectDays() => _getDaysFromRange(projectSelectedRange.value);
|
||||
|
||||
void updateAttendanceRange(String range) {
|
||||
attendanceSelectedRange.value = range;
|
||||
void updateProjectRange(String range) => projectSelectedRange.value = range;
|
||||
void toggleAttendanceChartView(bool isChart) =>
|
||||
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;
|
||||
void toggleProjectChartView(bool isChart) =>
|
||||
logSafe('Attendance chart view toggled to: $isChart',
|
||||
level: LogLevel.debug);
|
||||
}
|
||||
|
||||
void toggleProjectChartView(bool isChart) {
|
||||
projectIsChartView.value = isChart;
|
||||
|
||||
void updateSelectedExpenseType(ExpenseTypeModel? type) {
|
||||
selectedExpenseType.value = type;
|
||||
fetchMonthlyExpenses(categoryId: type?.id);
|
||||
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
|
||||
}
|
||||
|
||||
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();
|
||||
// =========================
|
||||
// Manual Refresh Methods
|
||||
// =========================
|
||||
Future<void> refreshDashboard() async {
|
||||
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
|
||||
await fetchAllDashboardData();
|
||||
}
|
||||
|
||||
Future<void> refreshDashboard() => fetchAllDashboardData();
|
||||
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
|
||||
Future<void> refreshProjects() => fetchProjectProgress();
|
||||
Future<void> refreshAttendance() async => fetchRoleWiseAttendance();
|
||||
Future<void> refreshTasks() async {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isNotEmpty) await fetchDashboardTasks(projectId: projectId);
|
||||
}
|
||||
|
||||
Future<void> refreshProjects() async => fetchProjectProgress();
|
||||
|
||||
// =========================
|
||||
// 5. DATA FETCHING (API)
|
||||
// Fetch All Dashboard Data
|
||||
// =========================
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchAllDashboardData() async {
|
||||
final String projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
if (projectId.isEmpty) {
|
||||
logSafe('No project selected. Skipping dashboard API calls.',
|
||||
level: LogLevel.warning);
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
fetchRoleWiseAttendance(),
|
||||
fetchProjectProgress(),
|
||||
fetchDashboardTasks(projectId: projectId),
|
||||
fetchDashboardTeams(projectId: projectId),
|
||||
fetchPendingExpenses(),
|
||||
fetchExpenseTypeReport(
|
||||
startDate: expenseReportStartDate.value,
|
||||
endDate: expenseReportEndDate.value,
|
||||
),
|
||||
fetchMonthlyExpenses(),
|
||||
fetchMasterData(),
|
||||
fetchCollectionOverview(),
|
||||
fetchPurchaseInvoiceOverview(),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> fetchCollectionOverview() async {
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
await _executeApiCall(isCollectionOverviewLoading, () async {
|
||||
final response =
|
||||
await ApiService.getCollectionOverview(projectId: projectId);
|
||||
collectionOverviewData.value =
|
||||
(response?.success == true) ? response!.data : null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchTodaysAttendance(String projectId) async {
|
||||
await _executeApiCall(isLoadingEmployees, () async {
|
||||
final response = await ApiService.getAttendanceForDashboard(projectId);
|
||||
if (response != null) {
|
||||
employees.value = response;
|
||||
for (var emp in employees) {
|
||||
uploadingStates.putIfAbsent(emp.id, () => false.obs);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchMasterData() async {
|
||||
try {
|
||||
final data = await ApiService.getMasterExpenseTypes();
|
||||
if (data is List) {
|
||||
expenseTypes.value =
|
||||
data.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> fetchMonthlyExpenses({String? categoryId}) async {
|
||||
await _executeApiCall(isMonthlyExpenseLoading, () async {
|
||||
final response = await ApiService.getDashboardMonthlyExpensesApi(
|
||||
categoryId: categoryId,
|
||||
months: selectedMonthsCount.value,
|
||||
);
|
||||
monthlyExpenseList.value =
|
||||
(response?.success == true) ? response!.data : [];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchPurchaseInvoiceOverview() async {
|
||||
final projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
await _executeApiCall(isPurchaseInvoiceLoading, () async {
|
||||
final response = await ApiService.getPurchaseInvoiceOverview(
|
||||
projectId: projectId,
|
||||
);
|
||||
purchaseInvoiceOverviewData.value =
|
||||
(response?.success == true) ? response!.data : null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchPendingExpenses() async {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
if (id.isEmpty) return;
|
||||
|
||||
await _executeApiCall(isPendingExpensesLoading, () async {
|
||||
final response = await ApiService.getPendingExpensesApi(projectId: id);
|
||||
pendingExpensesData.value =
|
||||
(response?.success == true) ? response!.data : null;
|
||||
});
|
||||
}
|
||||
|
||||
// =========================
|
||||
// API Calls
|
||||
// =========================
|
||||
Future<void> fetchRoleWiseAttendance() async {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
if (id.isEmpty) return;
|
||||
final String projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
await _executeApiCall(isAttendanceLoading, () async {
|
||||
final response = await ApiService.getDashboardAttendanceOverview(
|
||||
id, getAttendanceDays());
|
||||
try {
|
||||
isAttendanceLoading.value = true;
|
||||
final List<dynamic>? response =
|
||||
await ApiService.getDashboardAttendanceOverview(
|
||||
projectId, getAttendanceDays());
|
||||
|
||||
if (response != null) {
|
||||
roleWiseData.value =
|
||||
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
|
||||
});
|
||||
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(
|
||||
{required DateTime startDate, required DateTime endDate}) async {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
if (id.isEmpty) return;
|
||||
|
||||
await _executeApiCall(isExpenseTypeReportLoading, () async {
|
||||
final response = await ApiService.getExpenseTypeReportApi(
|
||||
projectId: id,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
);
|
||||
expenseTypeReportData.value =
|
||||
(response?.success == true) ? response!.data : null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchProjectProgress() async {
|
||||
final id = projectController.selectedProjectId.value;
|
||||
if (id.isEmpty) return;
|
||||
final String projectId = projectController.selectedProjectId.value;
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
await _executeApiCall(isProjectLoading, () async {
|
||||
try {
|
||||
isProjectLoading.value = true;
|
||||
final response = await ApiService.getProjectProgress(
|
||||
projectId: id, days: getProjectDays());
|
||||
if (response?.success == true) {
|
||||
projectChartData.value = response!.data
|
||||
.map((d) => ChartTaskData.fromProjectData(d))
|
||||
.toList();
|
||||
projectId: projectId, days: getProjectDays());
|
||||
|
||||
if (response != null && response.success) {
|
||||
projectChartData.value =
|
||||
response.data.map((d) => ChartTaskData.fromProjectData(d)).toList();
|
||||
logSafe('Project progress data mapped for chart', level: LogLevel.info);
|
||||
} else {
|
||||
projectChartData.clear();
|
||||
logSafe('Failed to fetch project progress', level: LogLevel.error);
|
||||
}
|
||||
} catch (e, st) {
|
||||
projectChartData.clear();
|
||||
logSafe('Error fetching project progress',
|
||||
level: LogLevel.error, error: e, stackTrace: st);
|
||||
} finally {
|
||||
isProjectLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchDashboardTasks({required String projectId}) async {
|
||||
await _executeApiCall(isTasksLoading, () async {
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
try {
|
||||
isTasksLoading.value = true;
|
||||
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
||||
if (response?.success == true) {
|
||||
totalTasks.value = response!.data?.totalTasks ?? 0;
|
||||
|
||||
if (response != null && response.success) {
|
||||
totalTasks.value = response.data?.totalTasks ?? 0;
|
||||
completedTasks.value = response.data?.completedTasks ?? 0;
|
||||
logSafe('Dashboard tasks fetched', level: LogLevel.info);
|
||||
} else {
|
||||
totalTasks.value = 0;
|
||||
completedTasks.value = 0;
|
||||
logSafe('Failed to fetch tasks', level: LogLevel.error);
|
||||
}
|
||||
} catch (e, st) {
|
||||
totalTasks.value = 0;
|
||||
completedTasks.value = 0;
|
||||
logSafe('Error fetching tasks',
|
||||
level: LogLevel.error, error: e, stackTrace: st);
|
||||
} finally {
|
||||
isTasksLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchDashboardTeams({required String projectId}) async {
|
||||
await _executeApiCall(isTeamsLoading, () async {
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
try {
|
||||
isTeamsLoading.value = true;
|
||||
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
||||
if (response?.success == true) {
|
||||
totalEmployees.value = response!.data?.totalEmployees ?? 0;
|
||||
|
||||
if (response != null && response.success) {
|
||||
totalEmployees.value = response.data?.totalEmployees ?? 0;
|
||||
inToday.value = response.data?.inToday ?? 0;
|
||||
logSafe('Dashboard teams fetched', level: LogLevel.info);
|
||||
} else {
|
||||
totalEmployees.value = 0;
|
||||
inToday.value = 0;
|
||||
logSafe('Failed to fetch teams', level: LogLevel.error);
|
||||
}
|
||||
} catch (e, st) {
|
||||
totalEmployees.value = 0;
|
||||
inToday.value = 0;
|
||||
logSafe('Error fetching teams',
|
||||
level: LogLevel.error, error: e, stackTrace: st);
|
||||
} finally {
|
||||
isTeamsLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum MonthlyExpenseDuration {
|
||||
oneMonth,
|
||||
threeMonths,
|
||||
sixMonths,
|
||||
twelveMonths,
|
||||
all,
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/controller/directory/directory_controller.dart';
|
||||
import 'package:on_field_work/controller/directory/notes_controller.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/controller/directory/directory_controller.dart';
|
||||
import 'package:marco/controller/directory/notes_controller.dart';
|
||||
|
||||
class AddCommentController extends GetxController {
|
||||
final String contactId;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
|
||||
class AddContactController extends GetxController {
|
||||
final RxList<String> categories = <String>[].obs;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
|
||||
class BucketController extends GetxController {
|
||||
RxBool isCreating = false.obs;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/model/directory/contact_model.dart';
|
||||
import 'package:on_field_work/model/directory/contact_bucket_list_model.dart';
|
||||
import 'package:on_field_work/model/directory/directory_comment_model.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/model/directory/contact_model.dart';
|
||||
import 'package:marco/model/directory/contact_bucket_list_model.dart';
|
||||
import 'package:marco/model/directory/directory_comment_model.dart';
|
||||
|
||||
class DirectoryController extends GetxController {
|
||||
// -------------------- CONTACTS --------------------
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/controller/directory/directory_controller.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/employees/employee_model.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/controller/directory/directory_controller.dart';
|
||||
|
||||
class ManageBucketController extends GetxController {
|
||||
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/model/directory/note_list_response_model.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/model/directory/note_list_response_model.dart';
|
||||
|
||||
class NotesController extends GetxController {
|
||||
RxList<NoteModel> notesList = <NoteModel>[].obs;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/document/document_details_model.dart';
|
||||
import 'package:on_field_work/model/document/document_version_model.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/document/document_details_model.dart';
|
||||
import 'package:marco/model/document/document_version_model.dart';
|
||||
|
||||
class DocumentDetailsController extends GetxController {
|
||||
/// Observables
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/document/master_document_type_model.dart';
|
||||
import 'package:on_field_work/model/document/master_document_tags.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/document/master_document_type_model.dart';
|
||||
import 'package:marco/model/document/master_document_tags.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
|
||||
class DocumentUploadController extends GetxController {
|
||||
// Observables
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/document/document_filter_model.dart';
|
||||
import 'package:on_field_work/model/document/documents_list_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/document/document_filter_model.dart';
|
||||
import 'package:marco/model/document/documents_list_model.dart';
|
||||
|
||||
class DocumentController extends GetxController {
|
||||
// ==================== Observables ====================
|
||||
@ -35,10 +34,11 @@ class DocumentController extends GetxController {
|
||||
// Additional filters
|
||||
final isUploadedAt = true.obs;
|
||||
final isVerified = RxnBool();
|
||||
final startDate = Rxn<DateTime>();
|
||||
final endDate = Rxn<DateTime>();
|
||||
final startDate = Rxn<String>();
|
||||
final endDate = Rxn<String>();
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// Don't dispose searchController here - it's managed by the page
|
||||
@ -87,23 +87,13 @@ class DocumentController extends GetxController {
|
||||
entityId: entityId,
|
||||
reset: true,
|
||||
);
|
||||
|
||||
// Show success snackbar
|
||||
showAppSnackbar(
|
||||
title: 'Success',
|
||||
message: isActive ? 'Document deactivated' : 'Document activated',
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
errorMessage.value = 'Failed to update document state';
|
||||
_showError('Failed to update document state');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Error updating document: $e';
|
||||
_showError('Error updating document: $e');
|
||||
debugPrint('❌ Error toggling document state: $e');
|
||||
return false;
|
||||
} finally {
|
||||
@ -120,13 +110,17 @@ class DocumentController extends GetxController {
|
||||
bool reset = false,
|
||||
}) async {
|
||||
try {
|
||||
// Reset pagination if needed
|
||||
if (reset) {
|
||||
pageNumber.value = 1;
|
||||
documents.clear();
|
||||
hasMore.value = true;
|
||||
}
|
||||
|
||||
// Don't fetch if no more data
|
||||
if (!hasMore.value && !reset) return;
|
||||
|
||||
// Prevent duplicate requests
|
||||
if (isLoading.value) return;
|
||||
|
||||
isLoading.value = true;
|
||||
@ -142,8 +136,8 @@ class DocumentController extends GetxController {
|
||||
);
|
||||
|
||||
if (response != null && response.success) {
|
||||
if (response.data?.data.isNotEmpty ?? false) {
|
||||
documents.addAll(response.data!.data);
|
||||
if (response.data.data.isNotEmpty) {
|
||||
documents.addAll(response.data.data);
|
||||
pageNumber.value++;
|
||||
} else {
|
||||
hasMore.value = false;
|
||||
@ -153,24 +147,12 @@ class DocumentController extends GetxController {
|
||||
errorMessage.value = response?.message ?? 'Failed to fetch documents';
|
||||
if (documents.isEmpty) {
|
||||
_showError('Failed to load documents');
|
||||
} else {
|
||||
showAppSnackbar(
|
||||
title: 'Warning',
|
||||
message: 'No more documents to load',
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Error fetching documents: $e';
|
||||
if (documents.isEmpty) {
|
||||
_showError('Error loading documents');
|
||||
} else {
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: 'Error fetching additional documents',
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
debugPrint('❌ Error fetching documents: $e');
|
||||
} finally {
|
||||
@ -203,12 +185,17 @@ class DocumentController extends GetxController {
|
||||
isVerified.value != null;
|
||||
}
|
||||
|
||||
/// Show error message via snackbar
|
||||
/// Show error message
|
||||
void _showError(String message) {
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: message,
|
||||
type: SnackbarType.error,
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
message,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
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 'package:get/get.dart';
|
||||
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class DynamicMenuController extends GetxController {
|
||||
// UI reactive states
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/model/global_project_model.dart';
|
||||
import 'package:on_field_work/model/employees/assigned_projects_model.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/model/global_project_model.dart';
|
||||
import 'package:marco/model/employees/assigned_projects_model.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class AssignProjectController extends GetxController {
|
||||
final String employeeId;
|
||||
|
||||
@ -1,60 +1,91 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/model/employees/employee_details_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/attendance/attendance_model.dart';
|
||||
import 'package:marco/model/project_model.dart';
|
||||
import 'package:marco/model/employees/employee_model.dart';
|
||||
import 'package:marco/model/employees/employee_details_model.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class EmployeesScreenController extends GetxController {
|
||||
/// ✅ Data lists
|
||||
List<AttendanceModel> attendances = [];
|
||||
List<ProjectModel> projects = [];
|
||||
String? selectedProjectId;
|
||||
List<EmployeeDetailsModel> employeeDetails = [];
|
||||
RxBool isAllEmployeeSelected = false.obs;
|
||||
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
|
||||
|
||||
RxBool isLoading = false.obs;
|
||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
|
||||
Rxn<EmployeeDetailsModel>();
|
||||
|
||||
/// ✅ Loading states
|
||||
RxBool isLoading = false.obs;
|
||||
RxBool isLoadingEmployeeDetails = false.obs;
|
||||
|
||||
/// ✅ Selection state
|
||||
RxBool isAllEmployeeSelected = false.obs;
|
||||
RxSet<String> selectedEmployeeIds = <String>{}.obs;
|
||||
|
||||
/// ✅ Upload state tracking (if needed later)
|
||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
RxList<EmployeeModel> selectedEmployeePrimaryManagers = <EmployeeModel>[].obs;
|
||||
RxList<EmployeeModel> selectedEmployeeSecondaryManagers =
|
||||
<EmployeeModel>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
isLoading.value = true;
|
||||
fetchAllProjects().then((_) {
|
||||
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
||||
if (projectId != null) {
|
||||
selectedProjectId = projectId;
|
||||
fetchEmployeesByProject(projectId);
|
||||
} else if (isAllEmployeeSelected.value) {
|
||||
fetchAllEmployees();
|
||||
} else {
|
||||
clearEmployees();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchAllProjects() async {
|
||||
isLoading.value = true;
|
||||
|
||||
await _handleApiCall(
|
||||
ApiService.getProjects,
|
||||
onSuccess: (data) {
|
||||
projects = data.map((json) => ProjectModel.fromJson(json)).toList();
|
||||
logSafe(
|
||||
"Projects fetched: ${projects.length} projects loaded.",
|
||||
level: LogLevel.info,
|
||||
);
|
||||
},
|
||||
onEmpty: () {
|
||||
logSafe("No project data found or API call failed.",
|
||||
level: LogLevel.warning);
|
||||
},
|
||||
);
|
||||
|
||||
isLoading.value = false;
|
||||
update();
|
||||
}
|
||||
|
||||
void clearEmployees() {
|
||||
employees.clear();
|
||||
logSafe("Employees cleared", level: LogLevel.info);
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
/// 🔹 Fetch all employees (no project filter)
|
||||
Future<void> fetchAllEmployees({String? organizationId}) async {
|
||||
isLoading.value = true;
|
||||
update(['employee_screen_controller']);
|
||||
|
||||
await _handleApiCall(
|
||||
() => ApiService.getAllEmployees(organizationId: organizationId),
|
||||
() => ApiService.getAllEmployees(
|
||||
organizationId: organizationId), // pass orgId to API
|
||||
onSuccess: (data) {
|
||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||
logSafe(
|
||||
"All Employees fetched: ${employees.length} employees loaded.",
|
||||
level: LogLevel.info,
|
||||
);
|
||||
|
||||
// Reset selection states when new data arrives
|
||||
selectedEmployeeIds.clear();
|
||||
isAllEmployeeSelected.value = false;
|
||||
},
|
||||
onEmpty: () {
|
||||
employees.clear();
|
||||
selectedEmployeeIds.clear();
|
||||
isAllEmployeeSelected.value = false;
|
||||
logSafe("No Employee data found or API call failed",
|
||||
level: LogLevel.warning);
|
||||
logSafe(
|
||||
"No Employee data found or API call failed",
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -62,7 +93,28 @@ class EmployeesScreenController extends GetxController {
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
/// 🔹 Fetch details for a specific employee
|
||||
Future<void> fetchEmployeesByProject(String projectId,
|
||||
{String? organizationId}) async {
|
||||
if (projectId.isEmpty) return;
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
await _handleApiCall(
|
||||
() => ApiService.getAllEmployeesByProject(projectId,
|
||||
organizationId: organizationId),
|
||||
onSuccess: (data) {
|
||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||
for (var emp in employees) {
|
||||
uploadingStates[emp.id] = false.obs;
|
||||
}
|
||||
},
|
||||
onEmpty: () => employees.clear(),
|
||||
);
|
||||
|
||||
isLoading.value = false;
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
Future<void> fetchEmployeeDetails(String? employeeId) async {
|
||||
if (employeeId == null || employeeId.isEmpty) return;
|
||||
|
||||
@ -72,80 +124,31 @@ class EmployeesScreenController extends GetxController {
|
||||
() => ApiService.getEmployeeDetails(employeeId),
|
||||
onSuccess: (data) {
|
||||
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
|
||||
logSafe("Employee details loaded for $employeeId",
|
||||
level: LogLevel.info);
|
||||
logSafe(
|
||||
"Employee details loaded for $employeeId",
|
||||
level: LogLevel.info,
|
||||
);
|
||||
},
|
||||
onEmpty: () {
|
||||
selectedEmployeeDetails.value = null;
|
||||
logSafe("No employee details found for $employeeId",
|
||||
level: LogLevel.warning);
|
||||
logSafe(
|
||||
"No employee details found for $employeeId",
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
},
|
||||
onError: (e) {
|
||||
selectedEmployeeDetails.value = null;
|
||||
logSafe("Error fetching employee details for $employeeId",
|
||||
level: LogLevel.error, error: e);
|
||||
logSafe(
|
||||
"Error fetching employee details for $employeeId",
|
||||
level: LogLevel.error,
|
||||
error: e,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
isLoadingEmployeeDetails.value = false;
|
||||
}
|
||||
|
||||
/// Fetch reporting managers for a specific employee from /organization/hierarchy/list/:employeeId
|
||||
Future<void> fetchReportingManagers(String? employeeId) async {
|
||||
if (employeeId == null || employeeId.isEmpty) return;
|
||||
|
||||
try {
|
||||
// ✅ Always clear before new fetch (to avoid mixing old data)
|
||||
selectedEmployeePrimaryManagers.clear();
|
||||
selectedEmployeeSecondaryManagers.clear();
|
||||
|
||||
// Fetch from existing API helper
|
||||
final data = await ApiService.getOrganizationHierarchyList(employeeId);
|
||||
|
||||
if (data == null || data.isEmpty) {
|
||||
update(['employee_screen_controller']);
|
||||
return;
|
||||
}
|
||||
|
||||
for (final item in data) {
|
||||
try {
|
||||
final reportTo = item['reportTo'];
|
||||
if (reportTo == null) continue;
|
||||
|
||||
final emp = EmployeeModel.fromJson(reportTo);
|
||||
final isPrimary = item['isPrimary'] == true;
|
||||
|
||||
if (isPrimary) {
|
||||
if (!selectedEmployeePrimaryManagers.any((e) => e.id == emp.id)) {
|
||||
selectedEmployeePrimaryManagers.add(emp);
|
||||
}
|
||||
} else {
|
||||
if (!selectedEmployeeSecondaryManagers.any((e) => e.id == emp.id)) {
|
||||
selectedEmployeeSecondaryManagers.add(emp);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore malformed items
|
||||
}
|
||||
}
|
||||
|
||||
update(['employee_screen_controller']);
|
||||
} catch (e) {
|
||||
logSafe("Error fetching reporting managers for $employeeId",
|
||||
level: LogLevel.error, error: e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔹 Clear all employee data
|
||||
void clearEmployees() {
|
||||
employees.clear();
|
||||
selectedEmployeeIds.clear();
|
||||
isAllEmployeeSelected.value = false;
|
||||
logSafe("Employees cleared", level: LogLevel.info);
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
/// 🔹 Generic handler for list API responses
|
||||
Future<void> _handleApiCall(
|
||||
Future<List<dynamic>?> Function() apiCall, {
|
||||
required Function(List<dynamic>) onSuccess,
|
||||
@ -168,7 +171,6 @@ class EmployeesScreenController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔹 Generic handler for single-object API responses
|
||||
Future<void> _handleSingleApiCall(
|
||||
Future<Map<String, dynamic>?> Function() apiCall, {
|
||||
required Function(Map<String, dynamic>) onSuccess,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
|
||||
class ComingSoonController extends MyController {
|
||||
Timer? countdownTimer;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
|
||||
class Error404Controller extends MyController {
|
||||
void goToDashboardScreen() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
|
||||
class Error500Controller extends MyController {
|
||||
void goToDashboardScreen() {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -11,14 +10,14 @@ import 'package:intl/intl.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'package:on_field_work/controller/expense/expense_screen_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/model/expense/expense_type_model.dart';
|
||||
import 'package:on_field_work/model/expense/payment_types_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/model/employees/employee_model.dart';
|
||||
import 'package:marco/model/expense/expense_type_model.dart';
|
||||
import 'package:marco/model/expense/payment_types_model.dart';
|
||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
||||
|
||||
class AddExpenseController extends GetxController {
|
||||
// --- Text Controllers ---
|
||||
@ -51,22 +50,10 @@ class AddExpenseController extends GetxController {
|
||||
final isEditMode = false.obs;
|
||||
final isSearchingEmployees = false.obs;
|
||||
|
||||
// --- Paid By (Single + Multi Selection Support) ---
|
||||
|
||||
// single selection
|
||||
final selectedPaidBy = Rxn<EmployeeModel>();
|
||||
|
||||
|
||||
|
||||
// helper setters
|
||||
void setSelectedPaidBy(EmployeeModel? emp) {
|
||||
selectedPaidBy.value = emp;
|
||||
}
|
||||
|
||||
// --- Dropdown Selections & Data ---
|
||||
final selectedPaymentMode = Rxn<PaymentModeModel>();
|
||||
final selectedExpenseType = Rxn<ExpenseTypeModel>();
|
||||
// final selectedPaidBy = Rxn<EmployeeModel>();
|
||||
final selectedPaidBy = Rxn<EmployeeModel>();
|
||||
final selectedProject = ''.obs;
|
||||
final selectedTransactionDate = Rxn<DateTime>();
|
||||
|
||||
@ -209,7 +196,7 @@ class AddExpenseController extends GetxController {
|
||||
'Location: ${locationController.text}',
|
||||
'Transaction Date: ${transactionDateController.text}',
|
||||
'No. of Persons: ${noOfPersonsController.text}',
|
||||
'Expense Category: ${selectedExpenseType.value?.name}',
|
||||
'Expense Type: ${selectedExpenseType.value?.name}',
|
||||
'Payment Mode: ${selectedPaymentMode.value?.name}',
|
||||
'Paid By: ${selectedPaidBy.value?.name}',
|
||||
'Attachments: ${attachments.length}',
|
||||
@ -407,24 +394,16 @@ class AddExpenseController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _submitToApi(Map<String, dynamic>? payload) async {
|
||||
if (payload == null) {
|
||||
_errorSnackbar("Payload is empty. Cannot submit.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Future<bool> _submitToApi(Map<String, dynamic> payload) async {
|
||||
if (isEditMode.value && editingExpenseId != null) {
|
||||
// Edit existing expense
|
||||
return await ApiService.editExpenseApi(
|
||||
return ApiService.editExpenseApi(
|
||||
expenseId: editingExpenseId!,
|
||||
payload: payload,
|
||||
);
|
||||
} else {
|
||||
// Create new expense
|
||||
return await ApiService.createExpenseApi(
|
||||
}
|
||||
return ApiService.createExpenseApi(
|
||||
projectId: payload['projectId'],
|
||||
expensesTypeId: payload['expenseCategoryId'],
|
||||
expensesTypeId: payload['expensesTypeId'],
|
||||
paymentModeId: payload['paymentModeId'],
|
||||
paidById: payload['paidById'],
|
||||
transactionDate: DateTime.parse(payload['transactionDate']),
|
||||
@ -437,56 +416,25 @@ class AddExpenseController extends GetxController {
|
||||
billAttachments: payload['billAttachments'],
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_errorSnackbar("Failed to submit expense: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _buildExpensePayload() async {
|
||||
Future<Map<String, dynamic>> _buildExpensePayload() async {
|
||||
final now = DateTime.now();
|
||||
|
||||
// --- Get IDs safely ---
|
||||
final projectId = projectsMap[selectedProject.value];
|
||||
final expenseType = selectedExpenseType.value;
|
||||
final paymentMode = selectedPaymentMode.value;
|
||||
final paidBy = selectedPaidBy.value;
|
||||
|
||||
// --- Validate essential fields ---
|
||||
if (projectId == null) {
|
||||
_errorSnackbar("Project not selected or invalid");
|
||||
return null;
|
||||
}
|
||||
if (expenseType == null) {
|
||||
_errorSnackbar("Expense Category not selected");
|
||||
return null;
|
||||
}
|
||||
if (paymentMode == null) {
|
||||
_errorSnackbar("Payment mode not selected");
|
||||
return null;
|
||||
}
|
||||
if (paidBy == null) {
|
||||
_errorSnackbar("Paid By not selected");
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Process existing attachments (for edit mode) ---
|
||||
final existingPayload = isEditMode.value
|
||||
? existingAttachments
|
||||
.map((e) => {
|
||||
"documentId": e['documentId'],
|
||||
"fileName": e['fileName'] ?? "",
|
||||
"contentType": e['contentType'] ?? "",
|
||||
"fileName": e['fileName'],
|
||||
"contentType": e['contentType'],
|
||||
"fileSize": 0,
|
||||
"description": "",
|
||||
"url": e['url'] ?? "",
|
||||
"url": e['url'],
|
||||
"isActive": e['isActive'] ?? true,
|
||||
"base64Data": "",
|
||||
})
|
||||
.toList()
|
||||
: <Map<String, dynamic>>[];
|
||||
|
||||
// --- Process new attachments ---
|
||||
final newPayload = await Future.wait(
|
||||
attachments.map((file) async {
|
||||
final bytes = await file.readAsBytes();
|
||||
@ -501,36 +449,38 @@ class AddExpenseController extends GetxController {
|
||||
}),
|
||||
);
|
||||
|
||||
// --- Build final payload ---
|
||||
final payload = {
|
||||
final type = selectedExpenseType.value!;
|
||||
|
||||
return {
|
||||
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
|
||||
"projectId": projectId,
|
||||
"expenseCategoryId": expenseType.id,
|
||||
"paymentModeId": paymentMode.id,
|
||||
"paidById": paidBy.id,
|
||||
"projectId": projectsMap[selectedProject.value]!,
|
||||
"expensesTypeId": type.id,
|
||||
"paymentModeId": selectedPaymentMode.value!.id,
|
||||
"paidById": selectedPaidBy.value!.id,
|
||||
"transactionDate":
|
||||
(selectedTransactionDate.value ?? now).toUtc().toIso8601String(),
|
||||
"transactionId": transactionIdController.text.trim(),
|
||||
"description": descriptionController.text.trim(),
|
||||
"location": locationController.text.trim(),
|
||||
"supplerName": supplierController.text.trim(),
|
||||
"amount": double.tryParse(amountController.text.trim()) ?? 0,
|
||||
"noOfPersons": expenseType.noOfPersonsRequired == true
|
||||
"transactionId": transactionIdController.text,
|
||||
"description": descriptionController.text,
|
||||
"location": locationController.text,
|
||||
"supplerName": supplierController.text,
|
||||
"amount": double.parse(amountController.text.trim()),
|
||||
"noOfPersons": type.noOfPersonsRequired == true
|
||||
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
|
||||
: 0,
|
||||
"billAttachments": [...existingPayload, ...newPayload].isEmpty
|
||||
"billAttachments": [
|
||||
...existingPayload,
|
||||
...newPayload,
|
||||
].isEmpty
|
||||
? null
|
||||
: [...existingPayload, ...newPayload],
|
||||
};
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
String validateForm() {
|
||||
final missing = <String>[];
|
||||
|
||||
if (selectedProject.value.isEmpty) missing.add("Project");
|
||||
if (selectedExpenseType.value == null) missing.add("Expense Category");
|
||||
if (selectedExpenseType.value == null) missing.add("Expense Type");
|
||||
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
|
||||
if (selectedPaidBy.value == null) missing.add("Paid By");
|
||||
if (amountController.text.trim().isEmpty) missing.add("Amount");
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/model/expense/expense_detail_model.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/model/expense/expense_detail_model.dart';
|
||||
import 'package:marco/model/employees/employee_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ExpenseDetailController extends GetxController {
|
||||
@ -142,10 +142,6 @@ class ExpenseDetailController extends GetxController {
|
||||
required String reimburseDate,
|
||||
required String reimburseById,
|
||||
required String statusId,
|
||||
double? baseAmount,
|
||||
double? taxAmount,
|
||||
double? tdsPercent,
|
||||
double? netPayable,
|
||||
}) async {
|
||||
final success = await _apiCallWrapper(
|
||||
() => ApiService.updateExpenseStatusApi(
|
||||
@ -155,16 +151,13 @@ class ExpenseDetailController extends GetxController {
|
||||
reimburseTransactionId: reimburseTransactionId,
|
||||
reimburseDate: reimburseDate,
|
||||
reimbursedById: reimburseById,
|
||||
baseAmount: baseAmount,
|
||||
taxAmount: taxAmount,
|
||||
tdsPercent: tdsPercent,
|
||||
netPayable: netPayable,
|
||||
),
|
||||
"submit reimbursement",
|
||||
);
|
||||
|
||||
if (success == true) {
|
||||
await fetchExpenseDetails();
|
||||
// Explicitly check for true as _apiCallWrapper returns T?
|
||||
await fetchExpenseDetails(); // Refresh details after successful update
|
||||
return true;
|
||||
} else {
|
||||
errorMessage.value = "Failed to submit reimbursement.";
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/model/expense/expense_list_model.dart';
|
||||
import 'package:on_field_work/model/expense/payment_types_model.dart';
|
||||
import 'package:on_field_work/model/expense/expense_type_model.dart';
|
||||
import 'package:on_field_work/model/expense/expense_status_model.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/model/expense/expense_list_model.dart';
|
||||
import 'package:marco/model/expense/payment_types_model.dart';
|
||||
import 'package:marco/model/expense/expense_type_model.dart';
|
||||
import 'package:marco/model/expense/expense_status_model.dart';
|
||||
import 'package:marco/model/employees/employee_model.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ExpenseController extends GetxController {
|
||||
@ -213,7 +213,7 @@ class ExpenseController extends GetxController {
|
||||
selectedCreatedByEmployees.clear();
|
||||
}
|
||||
|
||||
/// Fetch master data: Expense Categorys, payment modes, and expense status
|
||||
/// Fetch master data: expense types, payment modes, and expense status
|
||||
Future<void> fetchMasterData() async {
|
||||
try {
|
||||
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||
|
||||
class FaqsController extends MyController {
|
||||
final List<bool> dataExpansionPanel = [true, false, false, false, false, false];
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
|
||||
class PricingController extends MyController {
|
||||
bool isMonth = false;
|
||||
|
||||
@ -1,419 +0,0 @@
|
||||
// payment_request_controller.dart
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||
import 'package:on_field_work/model/finance/expense_category_model.dart';
|
||||
import 'package:on_field_work/model/finance/currency_list_model.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
|
||||
class AddPaymentRequestController extends GetxController {
|
||||
// Loading States
|
||||
final isLoadingPayees = false.obs;
|
||||
final isLoadingCategories = false.obs;
|
||||
final isLoadingCurrencies = false.obs;
|
||||
final isProcessingAttachment = false.obs;
|
||||
final isSubmitting = false.obs;
|
||||
|
||||
// Data Lists
|
||||
final payees = <String>[].obs;
|
||||
final categories = <ExpenseCategory>[].obs;
|
||||
final currencies = <Currency>[].obs;
|
||||
final globalProjects = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Selected Values
|
||||
final selectedProject = Rx<Map<String, dynamic>?>(null);
|
||||
final selectedCategory = Rx<ExpenseCategory?>(null);
|
||||
final selectedPayee = Rx<EmployeeModel?>(null);
|
||||
final selectedCurrency = Rx<Currency?>(null);
|
||||
final isAdvancePayment = false.obs;
|
||||
final selectedDueDate = Rx<DateTime?>(null);
|
||||
|
||||
// Text Controllers
|
||||
final titleController = TextEditingController();
|
||||
final dueDateController = TextEditingController();
|
||||
final amountController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
final removedAttachments = <Map<String, dynamic>>[].obs;
|
||||
|
||||
// Attachments
|
||||
final attachments = <File>[].obs;
|
||||
final existingAttachments = <Map<String, dynamic>>[].obs;
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchAllMasterData();
|
||||
fetchGlobalProjects();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
titleController.dispose();
|
||||
dueDateController.dispose();
|
||||
amountController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// Fetch all master data concurrently
|
||||
Future<void> fetchAllMasterData() async {
|
||||
await Future.wait([
|
||||
_fetchData(
|
||||
payees, ApiService.getExpensePaymentRequestPayeeApi, isLoadingPayees),
|
||||
_fetchData(categories, ApiService.getMasterExpenseCategoriesApi,
|
||||
isLoadingCategories),
|
||||
_fetchData(
|
||||
currencies, ApiService.getMasterCurrenciesApi, isLoadingCurrencies),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Generic fetch handler
|
||||
Future<void> _fetchData<T>(
|
||||
RxList<T> list, Future<dynamic> Function() apiCall, RxBool loader) async {
|
||||
try {
|
||||
loader.value = true;
|
||||
final response = await apiCall();
|
||||
if (response != null && response.data.isNotEmpty) {
|
||||
list.value = response.data;
|
||||
} else {
|
||||
list.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
logSafe("Error fetching data: $e", level: LogLevel.error);
|
||||
list.clear();
|
||||
} finally {
|
||||
loader.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch projects
|
||||
Future<void> fetchGlobalProjects() async {
|
||||
try {
|
||||
final response = await ApiService.getGlobalProjects();
|
||||
globalProjects.value = (response ?? [])
|
||||
.map<Map<String, dynamic>>((e) => {
|
||||
'id': e['id']?.toString() ?? '',
|
||||
'name': e['name']?.toString().trim() ?? '',
|
||||
})
|
||||
.where((p) => p['id']!.isNotEmpty && p['name']!.isNotEmpty)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
logSafe("Error fetching projects: $e", level: LogLevel.error);
|
||||
globalProjects.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick due date
|
||||
Future<void> pickDueDate(BuildContext context) async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDueDate.value ?? DateTime.now(),
|
||||
firstDate: DateTime(DateTime.now().year - 5),
|
||||
lastDate: DateTime(DateTime.now().year + 5),
|
||||
);
|
||||
|
||||
if (pickedDate != null) {
|
||||
selectedDueDate.value = pickedDate;
|
||||
dueDateController.text = DateFormat('dd MMM yyyy').format(pickedDate);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic file picker for multiple sources
|
||||
Future<void> pickAttachments(
|
||||
{bool fromGallery = false, bool fromCamera = false}) async {
|
||||
try {
|
||||
if (fromCamera) {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
|
||||
if (pickedFile != null) {
|
||||
isProcessingAttachment.value = true;
|
||||
final timestamped = await TimestampImageHelper.addTimestamp(
|
||||
imageFile: File(pickedFile.path));
|
||||
attachments.add(timestamped);
|
||||
}
|
||||
} else if (fromGallery) {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
|
||||
if (pickedFile != null) attachments.add(File(pickedFile.path));
|
||||
} else {
|
||||
final result = await FilePicker.platform
|
||||
.pickFiles(type: FileType.any, allowMultiple: true);
|
||||
if (result != null && result.paths.isNotEmpty)
|
||||
attachments.addAll(result.paths.whereType<String>().map(File.new));
|
||||
}
|
||||
attachments.refresh();
|
||||
} catch (e) {
|
||||
_errorSnackbar("Attachment error: $e");
|
||||
} finally {
|
||||
isProcessingAttachment.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pickFromCamera() async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
|
||||
if (pickedFile != null) {
|
||||
isProcessingAttachment.value = true;
|
||||
File imageFile = File(pickedFile.path);
|
||||
|
||||
// Add timestamp to the captured image
|
||||
File timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||
imageFile: imageFile,
|
||||
);
|
||||
|
||||
attachments.add(timestampedFile);
|
||||
attachments.refresh(); // refresh UI
|
||||
}
|
||||
} catch (e) {
|
||||
_errorSnackbar("Camera error: $e");
|
||||
} finally {
|
||||
isProcessingAttachment.value = false; // stop loading
|
||||
}
|
||||
}
|
||||
|
||||
/// Selection handlers
|
||||
void selectProject(Map<String, dynamic> project) =>
|
||||
selectedProject.value = project;
|
||||
void selectCategory(ExpenseCategory category) =>
|
||||
selectedCategory.value = category;
|
||||
void selectPayee(EmployeeModel payee) => selectedPayee.value = payee;
|
||||
void selectCurrency(Currency currency) => selectedCurrency.value = currency;
|
||||
|
||||
void addAttachment(File file) => attachments.add(file);
|
||||
void removeAttachment(File file) {
|
||||
if (attachments.contains(file)) {
|
||||
attachments.remove(file);
|
||||
}
|
||||
}
|
||||
|
||||
void removeExistingAttachment(Map<String, dynamic> existingAttachment) {
|
||||
final index = existingAttachments.indexWhere(
|
||||
(e) => e['id'] == existingAttachment['id']); // match by normalized id
|
||||
|
||||
if (index != -1) {
|
||||
// Mark as inactive
|
||||
existingAttachments[index]['isActive'] = false;
|
||||
existingAttachments.refresh();
|
||||
|
||||
// Add to removedAttachments to inform API
|
||||
removedAttachments.add({
|
||||
"documentId": existingAttachment['id'], // ensure API receives id
|
||||
"isActive": false,
|
||||
});
|
||||
|
||||
// Show snackbar feedback
|
||||
showAppSnackbar(
|
||||
title: 'Removed',
|
||||
message: 'Attachment has been removed.',
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Build attachment payload
|
||||
Future<List<Map<String, dynamic>>> buildAttachmentPayload() async {
|
||||
final existingPayload = existingAttachments
|
||||
.map((e) => {
|
||||
"documentId": e['id'], // use the normalized id
|
||||
"fileName": e['fileName'],
|
||||
"contentType": e['contentType'] ?? 'application/octet-stream',
|
||||
"fileSize": e['fileSize'] ?? 0,
|
||||
"description": "",
|
||||
"url": e['url'],
|
||||
"isActive": e['isActive'] ?? true,
|
||||
})
|
||||
.toList();
|
||||
|
||||
final newPayload = await Future.wait(attachments.map((file) async {
|
||||
final bytes = await file.readAsBytes();
|
||||
return {
|
||||
"fileName": file.path.split('/').last,
|
||||
"base64Data": base64Encode(bytes),
|
||||
"contentType": lookupMimeType(file.path) ?? 'application/octet-stream',
|
||||
"fileSize": await file.length(),
|
||||
"description": "",
|
||||
};
|
||||
}));
|
||||
|
||||
// Combine active + removed attachments
|
||||
return [...existingPayload, ...newPayload, ...removedAttachments];
|
||||
}
|
||||
|
||||
/// Submit edited payment request
|
||||
Future<bool> submitEditedPaymentRequest({required String requestId}) async {
|
||||
if (isSubmitting.value) return false;
|
||||
|
||||
try {
|
||||
isSubmitting.value = true;
|
||||
|
||||
// Validate form
|
||||
if (!_validateForm()) return false;
|
||||
|
||||
// Build attachment payload
|
||||
final billAttachments = await buildAttachmentPayload();
|
||||
|
||||
final payload = {
|
||||
"id": requestId,
|
||||
"title": titleController.text.trim(),
|
||||
"projectId": selectedProject.value?['id'] ?? '',
|
||||
"expenseCategoryId": selectedCategory.value?.id ?? '',
|
||||
"amount": double.tryParse(amountController.text.trim()) ?? 0,
|
||||
"currencyId": selectedCurrency.value?.id ?? '',
|
||||
"description": descriptionController.text.trim(),
|
||||
"payee": selectedPayee.value?.id ?? "",
|
||||
"dueDate": selectedDueDate.value?.toIso8601String(),
|
||||
"isAdvancePayment": isAdvancePayment.value,
|
||||
"billAttachments": billAttachments.map((a) {
|
||||
return {
|
||||
"documentId": a['documentId'],
|
||||
"fileName": a['fileName'],
|
||||
"base64Data": a['base64Data'] ?? "",
|
||||
"contentType": a['contentType'],
|
||||
"fileSize": a['fileSize'],
|
||||
"description": a['description'] ?? "",
|
||||
"isActive": a['isActive'] ?? true,
|
||||
};
|
||||
}).toList(),
|
||||
};
|
||||
|
||||
logSafe("💡 Submitting Edited Payment Request: ${jsonEncode(payload)}");
|
||||
|
||||
final success = await ApiService.editExpensePaymentRequestApi(
|
||||
id: payload['id'],
|
||||
title: payload['title'],
|
||||
projectId: payload['projectId'],
|
||||
expenseCategoryId: payload['expenseCategoryId'],
|
||||
amount: payload['amount'],
|
||||
currencyId: payload['currencyId'],
|
||||
description: payload['description'],
|
||||
payee: payload['payee'],
|
||||
dueDate: payload['dueDate'] ?? '',
|
||||
isAdvancePayment: payload['isAdvancePayment'],
|
||||
billAttachments: payload['billAttachments'],
|
||||
);
|
||||
|
||||
logSafe("💡 Edit Payment Request API Response: $success");
|
||||
|
||||
if (success == true) {
|
||||
logSafe("✅ Payment request edited successfully.");
|
||||
return true;
|
||||
} else {
|
||||
return _errorSnackbar("Failed to edit payment request.");
|
||||
}
|
||||
} catch (e, st) {
|
||||
logSafe("💥 Submit Edited Payment Request Error: $e\n$st",
|
||||
level: LogLevel.error);
|
||||
return _errorSnackbar("Something went wrong. Please try again later.");
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit payment request (Project API style)
|
||||
Future<bool> submitPaymentRequest() async {
|
||||
if (isSubmitting.value) return false;
|
||||
|
||||
try {
|
||||
isSubmitting.value = true;
|
||||
|
||||
// Validate form
|
||||
if (!_validateForm()) return false;
|
||||
|
||||
// Build attachment payload
|
||||
final billAttachments = await buildAttachmentPayload();
|
||||
|
||||
final payload = {
|
||||
"title": titleController.text.trim(),
|
||||
"projectId": selectedProject.value?['id'] ?? '',
|
||||
"expenseCategoryId": selectedCategory.value?.id ?? '',
|
||||
"amount": double.tryParse(amountController.text.trim()) ?? 0,
|
||||
"currencyId": selectedCurrency.value?.id ?? '',
|
||||
"description": descriptionController.text.trim(),
|
||||
"payee": selectedPayee.value?.id ?? "",
|
||||
"dueDate": selectedDueDate.value?.toIso8601String(),
|
||||
"isAdvancePayment": isAdvancePayment.value,
|
||||
"billAttachments": billAttachments.map((a) {
|
||||
return {
|
||||
"fileName": a['fileName'],
|
||||
"fileSize": a['fileSize'],
|
||||
"contentType": a['contentType'],
|
||||
};
|
||||
}).toList(),
|
||||
};
|
||||
|
||||
logSafe("💡 Submitting Payment Request: ${jsonEncode(payload)}");
|
||||
|
||||
final success = await ApiService.createExpensePaymentRequestApi(
|
||||
title: payload['title'],
|
||||
projectId: payload['projectId'],
|
||||
expenseCategoryId: payload['expenseCategoryId'],
|
||||
amount: payload['amount'],
|
||||
currencyId: payload['currencyId'],
|
||||
description: payload['description'],
|
||||
payee: payload['payee'],
|
||||
dueDate: selectedDueDate.value,
|
||||
isAdvancePayment: payload['isAdvancePayment'],
|
||||
billAttachments: billAttachments,
|
||||
);
|
||||
|
||||
logSafe("💡 Payment Request API Response: $success");
|
||||
|
||||
if (success == true) {
|
||||
logSafe("✅ Payment request created successfully.");
|
||||
return true;
|
||||
} else {
|
||||
return _errorSnackbar("Failed to create payment request.");
|
||||
}
|
||||
} catch (e, st) {
|
||||
logSafe("💥 Submit Payment Request Error: $e\n$st",
|
||||
level: LogLevel.error);
|
||||
return _errorSnackbar("Something went wrong. Please try again later.");
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Form validation
|
||||
bool _validateForm() {
|
||||
if (selectedProject.value == null ||
|
||||
selectedProject.value!['id'].toString().isEmpty)
|
||||
return _errorSnackbar("Please select a project");
|
||||
if (selectedCategory.value == null)
|
||||
return _errorSnackbar("Please select a category");
|
||||
if (selectedPayee.value == null)
|
||||
return _errorSnackbar("Please select a payee");
|
||||
if (selectedCurrency.value == null)
|
||||
return _errorSnackbar("Please select currency");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _errorSnackbar(String msg, [String title = "Error"]) {
|
||||
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Clear form
|
||||
void clearForm() {
|
||||
titleController.clear();
|
||||
dueDateController.clear();
|
||||
amountController.clear();
|
||||
descriptionController.clear();
|
||||
selectedProject.value = null;
|
||||
selectedCategory.value = null;
|
||||
selectedPayee.value = null;
|
||||
selectedCurrency.value = null;
|
||||
isAdvancePayment.value = false;
|
||||
attachments.clear();
|
||||
existingAttachments.clear();
|
||||
removedAttachments.clear();
|
||||
}
|
||||
}
|
||||
@ -1,149 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/model/finance/advance_payment_model.dart';
|
||||
import 'package:on_field_work/model/finance/get_employee_model.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
|
||||
class AdvancePaymentController extends GetxController {
|
||||
/// Advance payments list
|
||||
var payments = <AdvancePayment>[].obs;
|
||||
var isLoading = false.obs;
|
||||
|
||||
/// Employees for dropdown search
|
||||
var employees = <Employee>[].obs;
|
||||
var allEmployees = <Employee>[]; // cache of last API response
|
||||
var employeesLoading = false.obs;
|
||||
var searchQuery = ''.obs;
|
||||
var selectedEmployee = Rxn<Employee>();
|
||||
|
||||
/// Prevents unwanted API calls while programmatically updating search
|
||||
var _suppressSearch = false.obs;
|
||||
|
||||
Timer? _debounceTimer;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
ever<String>(searchQuery, (q) {
|
||||
if (_suppressSearch.value) return; // Skip while selecting employee
|
||||
|
||||
// 🔹 When user types new text, clear previous employee + payments instantly
|
||||
if (selectedEmployee.value != null) {
|
||||
selectedEmployee.value = null;
|
||||
payments.clear();
|
||||
}
|
||||
|
||||
// 🔹 Show fresh dropdown results for new query
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 400), () {
|
||||
if (q.isNotEmpty) {
|
||||
fetchEmployees(q); // repopulate dropdown
|
||||
} else {
|
||||
employees.clear(); // hide dropdown when search cleared
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_debounceTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// Fetch employees by query
|
||||
Future<void> fetchEmployees(String q) async {
|
||||
if (q.isEmpty) {
|
||||
employees.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (employeesLoading.value) return;
|
||||
|
||||
try {
|
||||
employeesLoading.value = true;
|
||||
|
||||
// Build query params
|
||||
final queryParams = {
|
||||
'allEmployee': 'true',
|
||||
if (q.isNotEmpty) 'q': q, // only include search query if not empty
|
||||
};
|
||||
|
||||
final list = await ApiService.getEmployees(queryParams: queryParams);
|
||||
final parsed = Employee.listFromJson(list);
|
||||
logSafe(
|
||||
"✅ Employees fetched from API: ${parsed.map((e) => e.name).toList()}");
|
||||
|
||||
allEmployees = parsed;
|
||||
_filterEmployees(q);
|
||||
} catch (e, s) {
|
||||
logSafe("❌ fetchEmployees error: $e\n$s", level: LogLevel.error);
|
||||
employees.clear();
|
||||
} finally {
|
||||
employeesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Local filter to update list based on search text
|
||||
void _filterEmployees(String query) {
|
||||
final q = query.toLowerCase();
|
||||
employees
|
||||
..clear()
|
||||
..addAll(allEmployees.where((e) {
|
||||
return e.name.toLowerCase().contains(q) ||
|
||||
e.email.toLowerCase().contains(q);
|
||||
}));
|
||||
}
|
||||
|
||||
/// When user selects employee
|
||||
void selectEmployee(Employee emp) {
|
||||
_suppressSearch.value = true;
|
||||
|
||||
selectedEmployee.value = emp;
|
||||
employees.clear(); // hide dropdown
|
||||
searchQuery.value = emp.name;
|
||||
|
||||
fetchAdvancePayments(emp.id);
|
||||
|
||||
// Re-enable search after a short delay
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
_suppressSearch.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// Fetch advance payments for the selected employee
|
||||
Future<void> fetchAdvancePayments(String employeeId) async {
|
||||
if (employeeId.isEmpty) {
|
||||
payments.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
final list = await ApiService.getAdvancePayments(employeeId);
|
||||
payments.assignAll(list);
|
||||
} catch (e, s) {
|
||||
logSafe("❌ fetchAdvancePayments error: $e\n$s", level: LogLevel.error);
|
||||
payments.clear();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear employee selection
|
||||
void clearSelection() {
|
||||
selectedEmployee.value = null;
|
||||
payments.clear();
|
||||
employees.clear();
|
||||
searchQuery.value = '';
|
||||
}
|
||||
|
||||
void resetSelectionOnNewSearch() {
|
||||
if (selectedEmployee.value != null) {
|
||||
selectedEmployee.value = null;
|
||||
payments.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/finance/payment_request_list_model.dart';
|
||||
import 'package:on_field_work/model/finance/payment_request_filter.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
|
||||
class PaymentRequestController extends GetxController {
|
||||
// ---------------- Observables ----------------
|
||||
final RxList<PaymentRequest> paymentRequests = <PaymentRequest>[].obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxString errorMessage = ''.obs;
|
||||
final RxBool isFilterApplied = false.obs;
|
||||
|
||||
// ---------------- Pagination ----------------
|
||||
int _pageSize = 20;
|
||||
int _pageNumber = 1;
|
||||
bool _hasMoreData = true;
|
||||
|
||||
// ---------------- Filters ----------------
|
||||
RxMap<String, dynamic> appliedFilter = <String, dynamic>{}.obs;
|
||||
RxString searchString = ''.obs;
|
||||
|
||||
// ---------------- Filter Options ----------------
|
||||
RxList<IdNameModel> projects = <IdNameModel>[].obs;
|
||||
RxList<IdNameModel> payees = <IdNameModel>[].obs;
|
||||
RxList<IdNameModel> categories = <IdNameModel>[].obs;
|
||||
RxList<IdNameModel> currencies = <IdNameModel>[].obs;
|
||||
RxList<IdNameModel> statuses = <IdNameModel>[].obs;
|
||||
RxList<IdNameModel> createdBy = <IdNameModel>[].obs;
|
||||
|
||||
// ---------------- Fetch Filter Options ----------------
|
||||
Future<void> fetchPaymentRequestFilterOptions() async {
|
||||
try {
|
||||
final response = await ApiService.getExpensePaymentRequestFilterApi();
|
||||
|
||||
if (response != null && response.data != null) {
|
||||
projects.assignAll(response.data!.projects ?? []);
|
||||
payees.assignAll(response.data!.payees ?? []);
|
||||
categories.assignAll(response.data!.expenseCategory ?? []);
|
||||
currencies.assignAll(response.data!.currency ?? []);
|
||||
statuses.assignAll(response.data!.status ?? []);
|
||||
createdBy.assignAll(response.data!.createdBy ?? []);
|
||||
} else {
|
||||
logSafe("Payment request filter API returned null",
|
||||
level: LogLevel.warning);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception in fetchPaymentRequestFilterOptions: $e",
|
||||
level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Fetch Payment Requests ----------------
|
||||
Future<void> fetchPaymentRequests({int pageSize = 20}) async {
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
_pageNumber = 1;
|
||||
_pageSize = pageSize;
|
||||
_hasMoreData = true;
|
||||
paymentRequests.clear();
|
||||
|
||||
await _fetchPaymentRequestsFromApi();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// ---------------- Load More ----------------
|
||||
Future<void> loadMorePaymentRequests() async {
|
||||
if (isLoading.value || !_hasMoreData) return;
|
||||
|
||||
_pageNumber += 1;
|
||||
isLoading.value = true;
|
||||
|
||||
await _fetchPaymentRequestsFromApi();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// ---------------- Internal API Call ----------------
|
||||
Future<void> _fetchPaymentRequestsFromApi() async {
|
||||
try {
|
||||
final response = await ApiService.getExpensePaymentRequestListApi(
|
||||
pageSize: _pageSize,
|
||||
pageNumber: _pageNumber,
|
||||
filter: appliedFilter,
|
||||
searchString: searchString.value,
|
||||
);
|
||||
|
||||
final data = response?.data;
|
||||
final reqList = data?.data ?? [];
|
||||
|
||||
if (response != null && data != null && reqList.isNotEmpty) {
|
||||
if (_pageNumber == 1) {
|
||||
paymentRequests.assignAll(reqList);
|
||||
} else {
|
||||
paymentRequests.addAll(reqList);
|
||||
}
|
||||
|
||||
if (reqList.length < _pageSize) {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} else {
|
||||
if (_pageNumber == 1) {
|
||||
errorMessage.value = 'No payment requests found.';
|
||||
}
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
errorMessage.value = 'Failed to fetch payment requests.';
|
||||
logSafe("Exception in _fetchPaymentRequestsFromApi: $e",
|
||||
level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
_hasMoreData = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Filter Management ----------------
|
||||
void setFilterApplied(bool applied) {
|
||||
isFilterApplied.value = applied;
|
||||
}
|
||||
|
||||
void applyFilter(Map<String, dynamic> filter, {String search = ''}) {
|
||||
appliedFilter.assignAll(filter);
|
||||
searchString.value = search;
|
||||
isFilterApplied.value = filter.isNotEmpty || search.isNotEmpty;
|
||||
fetchPaymentRequests();
|
||||
}
|
||||
|
||||
void clearFilter() {
|
||||
appliedFilter.clear();
|
||||
searchString.value = '';
|
||||
isFilterApplied.value = false;
|
||||
fetchPaymentRequests();
|
||||
}
|
||||
}
|
||||
@ -1,363 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/model/finance/payment_request_details_model.dart';
|
||||
import 'package:on_field_work/model/expense/payment_types_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:on_field_work/controller/finance/payment_request_controller.dart';
|
||||
|
||||
class PaymentRequestDetailController extends GetxController {
|
||||
final Rx<PaymentRequestData?> paymentRequest = Rx<PaymentRequestData?>(null);
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxString errorMessage = ''.obs;
|
||||
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
|
||||
|
||||
// Employee selection
|
||||
final Rx<EmployeeModel?> selectedReimbursedBy = Rx<EmployeeModel?>(null);
|
||||
final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
||||
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
|
||||
final TextEditingController employeeSearchController =
|
||||
TextEditingController();
|
||||
PaymentRequestController get paymentRequestController =>
|
||||
Get.find<PaymentRequestController>();
|
||||
final RxBool isSearchingEmployees = false.obs;
|
||||
|
||||
// Attachments
|
||||
final RxList<File> attachments = <File>[].obs;
|
||||
final RxList<Map<String, dynamic>> existingAttachments =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
final isProcessingAttachment = false.obs;
|
||||
|
||||
// Payment mode
|
||||
final selectedPaymentMode = Rxn<PaymentModeModel>();
|
||||
|
||||
// Text controllers for form
|
||||
final TextEditingController locationController = TextEditingController();
|
||||
final TextEditingController gstNumberController = TextEditingController();
|
||||
|
||||
// Form submission state
|
||||
final RxBool isSubmitting = false.obs;
|
||||
|
||||
late String _requestId;
|
||||
bool _isInitialized = false;
|
||||
RxBool paymentSheetOpened = false.obs;
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
/// Initialize controller
|
||||
void init(String requestId) {
|
||||
if (_isInitialized) return;
|
||||
_isInitialized = true;
|
||||
|
||||
_requestId = requestId;
|
||||
|
||||
// Fetch payment request details + employees concurrently
|
||||
Future.wait([
|
||||
fetchPaymentRequestDetail(),
|
||||
fetchAllEmployees(),
|
||||
fetchPaymentModes(),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Generic API wrapper for error handling
|
||||
Future<T?> _apiCallWrapper<T>(
|
||||
Future<T?> Function() apiCall, String operationName) async {
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
try {
|
||||
final result = await apiCall();
|
||||
return result;
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Error during $operationName: $e';
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: errorMessage.value,
|
||||
type: SnackbarType.error);
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch payment request details
|
||||
Future<void> fetchPaymentRequestDetail() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final response =
|
||||
await ApiService.getExpensePaymentRequestDetailApi(_requestId);
|
||||
if (response != null) {
|
||||
paymentRequest.value = response.data;
|
||||
} else {
|
||||
errorMessage.value = "Failed to fetch payment request details";
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: errorMessage.value,
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = "Error fetching payment request details: $e";
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: errorMessage.value,
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick files from gallery or file picker
|
||||
Future<void> pickAttachments() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
|
||||
allowMultiple: true,
|
||||
);
|
||||
if (result != null) {
|
||||
attachments.addAll(
|
||||
result.paths.whereType<String>().map(File.new),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_errorSnackbar("Attachment error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void removeAttachment(File file) => attachments.remove(file);
|
||||
|
||||
Future<void> pickFromCamera() async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
|
||||
if (pickedFile != null) {
|
||||
isProcessingAttachment.value = true;
|
||||
File imageFile = File(pickedFile.path);
|
||||
|
||||
File timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||
imageFile: imageFile,
|
||||
);
|
||||
|
||||
attachments.add(timestampedFile);
|
||||
attachments.refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
_errorSnackbar("Camera error: $e");
|
||||
} finally {
|
||||
isProcessingAttachment.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Location ---
|
||||
final RxBool isFetchingLocation = false.obs;
|
||||
|
||||
Future<void> fetchCurrentLocation() async {
|
||||
isFetchingLocation.value = true;
|
||||
try {
|
||||
if (!await _ensureLocationPermission()) return;
|
||||
|
||||
final position = await Geolocator.getCurrentPosition();
|
||||
final placemarks =
|
||||
await placemarkFromCoordinates(position.latitude, position.longitude);
|
||||
|
||||
locationController.text = placemarks.isNotEmpty
|
||||
? [
|
||||
placemarks.first.name,
|
||||
placemarks.first.street,
|
||||
placemarks.first.locality,
|
||||
placemarks.first.administrativeArea,
|
||||
placemarks.first.country,
|
||||
].where((e) => e?.isNotEmpty == true).join(", ")
|
||||
: "${position.latitude}, ${position.longitude}";
|
||||
} catch (e) {
|
||||
_errorSnackbar("Location error: $e");
|
||||
} finally {
|
||||
isFetchingLocation.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _ensureLocationPermission() async {
|
||||
var permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
_errorSnackbar("Location permission denied.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!await Geolocator.isLocationServiceEnabled()) {
|
||||
_errorSnackbar("Location service disabled.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Fetch all employees
|
||||
Future<void> fetchAllEmployees() async {
|
||||
final response = await _apiCallWrapper(
|
||||
() => ApiService.getAllEmployees(), "fetch all employees");
|
||||
|
||||
if (response != null && response.isNotEmpty) {
|
||||
try {
|
||||
allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e)));
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Failed to parse employee data: $e';
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: errorMessage.value,
|
||||
type: SnackbarType.error);
|
||||
}
|
||||
} else {
|
||||
allEmployees.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch payment modes
|
||||
Future<void> fetchPaymentModes() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final paymentModesData = await ApiService.getMasterPaymentModes();
|
||||
if (paymentModesData is List) {
|
||||
paymentModes.value =
|
||||
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
|
||||
} else {
|
||||
paymentModes.clear();
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: 'Failed to fetch payment modes',
|
||||
type: SnackbarType.error);
|
||||
}
|
||||
} catch (e) {
|
||||
paymentModes.clear();
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: 'Error fetching payment modes: $e',
|
||||
type: SnackbarType.error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Search employees
|
||||
Future<void> searchEmployees(String query) async {
|
||||
if (query.trim().isEmpty) {
|
||||
employeeSearchResults.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
isSearchingEmployees.value = true;
|
||||
try {
|
||||
final data =
|
||||
await ApiService.searchEmployeesBasic(searchString: query.trim());
|
||||
employeeSearchResults.assignAll(
|
||||
(data ?? []).map((e) => EmployeeModel.fromJson(e)),
|
||||
);
|
||||
} catch (e) {
|
||||
employeeSearchResults.clear();
|
||||
} finally {
|
||||
isSearchingEmployees.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update payment request status
|
||||
Future<bool> updatePaymentRequestStatus({
|
||||
required String statusId,
|
||||
required String comment,
|
||||
String? paidTransactionId,
|
||||
String? paidById,
|
||||
DateTime? paidAt,
|
||||
double? baseAmount,
|
||||
double? taxAmount,
|
||||
String? tdsPercentage,
|
||||
}) async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
final success = await ApiService.updateExpensePaymentRequestStatusApi(
|
||||
paymentRequestId: _requestId,
|
||||
statusId: statusId,
|
||||
comment: comment,
|
||||
paidTransactionId: paidTransactionId,
|
||||
paidById: paidById,
|
||||
paidAt: paidAt,
|
||||
baseAmount: baseAmount,
|
||||
taxAmount: taxAmount,
|
||||
tdsPercentage: tdsPercentage,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
// Controller refreshes the data but does not show snackbars.
|
||||
await fetchPaymentRequestDetail();
|
||||
paymentRequestController.fetchPaymentRequests();
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (e) {
|
||||
// Controller returns false on error; UI will show the snackbar.
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Snackbar Helper ---
|
||||
void _errorSnackbar(String msg, [String title = "Error"]) {
|
||||
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
|
||||
}
|
||||
|
||||
// --- Payment Mode Selection ---
|
||||
void selectPaymentMode(PaymentModeModel mode) {
|
||||
selectedPaymentMode.value = mode;
|
||||
}
|
||||
|
||||
// --- Submit Expense ---
|
||||
Future<bool> submitExpense(
|
||||
{required String statusId, String? comment}) async {
|
||||
if (selectedPaymentMode.value == null) return false;
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
// prepare attachments
|
||||
final success = await ApiService.createExpenseForPRApi(
|
||||
paymentModeId: selectedPaymentMode.value!.id,
|
||||
location: locationController.text,
|
||||
gstNumber: gstNumberController.text,
|
||||
paymentRequestId: _requestId,
|
||||
billAttachments: attachments.map((file) {
|
||||
final bytes = file.readAsBytesSync();
|
||||
final mimeType =
|
||||
lookupMimeType(file.path) ?? 'application/octet-stream';
|
||||
return {
|
||||
"fileName": file.path.split('/').last,
|
||||
"base64Data": base64Encode(bytes),
|
||||
"contentType": mimeType,
|
||||
"description": "",
|
||||
"fileSize": bytes.length,
|
||||
"isActive": true,
|
||||
};
|
||||
}).toList(),
|
||||
statusId: statusId,
|
||||
comment: comment ?? '',
|
||||
);
|
||||
|
||||
if (success) {
|
||||
// Refresh the payment request details so the UI updates
|
||||
await fetchPaymentRequestDetail();
|
||||
}
|
||||
|
||||
return success;
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_project_list.dart';
|
||||
|
||||
class InfraProjectController extends GetxController {
|
||||
final projects = <ProjectData>[].obs;
|
||||
final isLoading = false.obs;
|
||||
final searchQuery = ''.obs;
|
||||
|
||||
// Filtered list
|
||||
List<ProjectData> get filteredProjects {
|
||||
final q = searchQuery.value.trim().toLowerCase();
|
||||
if (q.isEmpty) return projects;
|
||||
|
||||
return projects.where((p) {
|
||||
return (p.name?.toLowerCase().contains(q) ?? false) ||
|
||||
(p.shortName?.toLowerCase().contains(q) ?? false) ||
|
||||
(p.projectAddress?.toLowerCase().contains(q) ?? false) ||
|
||||
(p.contactPerson?.toLowerCase().contains(q) ?? false);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Fetch Projects
|
||||
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
final response = await ApiService.getInfraProjectsList(
|
||||
pageNumber: pageNumber,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
|
||||
if (response != null && response.data != null) {
|
||||
projects.assignAll(response.data!.data ?? []);
|
||||
} else {
|
||||
projects.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void updateSearch(String query) {
|
||||
searchQuery.value = query;
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_project_details.dart';
|
||||
|
||||
class InfraProjectDetailsController extends GetxController {
|
||||
final String projectId;
|
||||
|
||||
InfraProjectDetailsController({required this.projectId});
|
||||
|
||||
var isLoading = true.obs;
|
||||
var projectDetails = Rxn<ProjectData>();
|
||||
var errorMessage = ''.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProjectDetails();
|
||||
}
|
||||
|
||||
Future<void> fetchProjectDetails() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
final response = await ApiService.getInfraProjectDetails(projectId: projectId);
|
||||
|
||||
if (response != null && response.success == true && response.data != null) {
|
||||
projectDetails.value = response.data;
|
||||
isLoading.value = false;
|
||||
|
||||
} else {
|
||||
errorMessage.value = response?.message ?? "Failed to load project details";
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = "Error fetching project details: $e";
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,3 @@
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
|
||||
class AuthLayout2Controller extends MyController {}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AuthLayoutController extends MyController {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||
import 'package:on_field_work/model/project_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/model/project_model.dart';
|
||||
|
||||
class LayoutController extends GetxController {
|
||||
// Theme Customization
|
||||
@ -55,7 +55,7 @@ class LayoutController extends GetxController {
|
||||
isLoadingProjects.value = true;
|
||||
|
||||
try {
|
||||
final response = await ApiService.getGlobalProjects();
|
||||
final response = await ApiService.getProjects();
|
||||
|
||||
if (response != null && response.isNotEmpty) {
|
||||
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:get/get_state_manager/get_state_manager.dart';
|
||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
|
||||
abstract class MyController extends GetxController {
|
||||
@override
|
||||
|
||||
284
lib/controller/payment/payment_controller.dart
Normal file
284
lib/controller/payment/payment_controller.dart
Normal file
@ -0,0 +1,284 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:razorpay_flutter/razorpay_flutter.dart';
|
||||
import 'package:marco/helpers/services/payment_service.dart';
|
||||
import 'package:marco/helpers/services/tenant_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PaymentController with ChangeNotifier {
|
||||
Razorpay? _razorpay;
|
||||
final PaymentService _paymentService = PaymentService();
|
||||
final TenantService _tenantService = TenantService();
|
||||
|
||||
bool isProcessing = false;
|
||||
|
||||
// Pending values to use after payment verification
|
||||
String? _pendingTenantEnquireId;
|
||||
String? _pendingPlanId;
|
||||
|
||||
/// ==============================
|
||||
/// START PAYMENT (Safe init)
|
||||
/// ==============================
|
||||
Future<void> startPayment({
|
||||
required double amount,
|
||||
required String description,
|
||||
required BuildContext context,
|
||||
String? tenantEnquireId,
|
||||
String? planId,
|
||||
}) async {
|
||||
try {
|
||||
isProcessing = true;
|
||||
notifyListeners();
|
||||
|
||||
// Save pending ids for post-payment subscription call
|
||||
_pendingTenantEnquireId = tenantEnquireId;
|
||||
_pendingPlanId = planId;
|
||||
|
||||
// Step 1: Create payment order
|
||||
final result = await _paymentService.createOrder(amount);
|
||||
logSafe("🧩 Raw response in PaymentController: $result");
|
||||
|
||||
// Step 2: Validate result before accessing keys
|
||||
if (result == null) {
|
||||
_showError("Failed to create order. Server returned null response.");
|
||||
isProcessing = false;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Handle both wrapped and unwrapped formats
|
||||
final data = result['data'] ?? result;
|
||||
final orderId = data?['orderId'];
|
||||
final key = data?['key'];
|
||||
|
||||
if (orderId == null || key == null) {
|
||||
_showError("Invalid response from server. Missing orderId or key.");
|
||||
logSafe("💥 Invalid response structure: $result");
|
||||
isProcessing = false;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 4: Initialize Razorpay if needed
|
||||
_razorpay ??= Razorpay();
|
||||
_razorpay!.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
|
||||
_razorpay!.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
|
||||
_razorpay!.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
|
||||
|
||||
// Step 5: Open Razorpay checkout
|
||||
final options = {
|
||||
'key': key,
|
||||
'amount': (amount * 100).toInt(), // Razorpay expects amount in paise
|
||||
'name': 'Marco',
|
||||
'description': description,
|
||||
'order_id': orderId,
|
||||
'prefill': {'contact': '9999999999', 'email': 'test@marco.com'},
|
||||
};
|
||||
|
||||
logSafe("🟠 Opening Razorpay checkout with options: $options");
|
||||
_razorpay!.open(options);
|
||||
} catch (e, s) {
|
||||
_showError("Payment initialization failed. Please try again.");
|
||||
logSafe("💥 Exception in startPayment: $e\n$s");
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// ==============================
|
||||
/// EVENT HANDLERS
|
||||
/// ==============================
|
||||
void _handlePaymentSuccess(PaymentSuccessResponse response) async {
|
||||
logSafe("✅ Payment Success: ${response.paymentId}");
|
||||
isProcessing = true;
|
||||
notifyListeners();
|
||||
|
||||
Map<String, dynamic>? verificationResult;
|
||||
|
||||
try {
|
||||
logSafe("🟢 Verifying payment via backend...");
|
||||
verificationResult = await _paymentService
|
||||
.verifyPayment(
|
||||
paymentId: response.paymentId!,
|
||||
orderId: response.orderId!,
|
||||
signature: response.signature!,
|
||||
)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
logSafe("🧩 Verification result: $verificationResult");
|
||||
} catch (e) {
|
||||
logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error);
|
||||
}
|
||||
|
||||
isProcessing = false;
|
||||
notifyListeners();
|
||||
|
||||
// Handle backend verification response properly
|
||||
if (verificationResult != null) {
|
||||
final isVerified = verificationResult['verified'] == true;
|
||||
final msg = verificationResult['message'] ?? '';
|
||||
|
||||
if (isVerified) {
|
||||
// If we have pending tenant and plan IDs, call subscription API
|
||||
await _maybeSubscribeTenantAfterPayment(
|
||||
verificationResult: verificationResult,
|
||||
razorpayResponse: response,
|
||||
);
|
||||
|
||||
_showDialog(
|
||||
title: "Payment Successful 🎉",
|
||||
message: msg.isNotEmpty ? msg : "Your payment was verified successfully.",
|
||||
success: true,
|
||||
);
|
||||
} else {
|
||||
_showDialog(
|
||||
title: "Verification Failed ❌",
|
||||
message: msg.isNotEmpty ? msg : "Payment completed but verification failed.",
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_showDialog(
|
||||
title: "Verification Failed ❌",
|
||||
message: "Payment completed but backend verification returned null.",
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
|
||||
_cleanup();
|
||||
}
|
||||
|
||||
Future<void> _maybeSubscribeTenantAfterPayment({
|
||||
required Map<String, dynamic> verificationResult,
|
||||
required PaymentSuccessResponse razorpayResponse,
|
||||
}) async {
|
||||
if (_pendingTenantEnquireId == null || _pendingPlanId == null) {
|
||||
logSafe("ℹ️ No pending tenant/plan id to subscribe.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine paymentDetailId — prefer backend value if provided
|
||||
final paymentDetailId = verificationResult['paymentDetailId'] ?? verificationResult['paymentId'] ?? razorpayResponse.paymentId;
|
||||
|
||||
final subscribePayload = {
|
||||
"tenantEnquireId": _pendingTenantEnquireId,
|
||||
"paymentDetailId": paymentDetailId,
|
||||
"planId": _pendingPlanId,
|
||||
};
|
||||
|
||||
logSafe("🟢 Subscribing tenant automatically: $subscribePayload");
|
||||
|
||||
final subResp = await _tenantService.subscribeTenant(subscribePayload);
|
||||
|
||||
// Clear pending values immediately to avoid double-invoke
|
||||
_pendingTenantEnquireId = null;
|
||||
_pendingPlanId = null;
|
||||
|
||||
if (subResp == null) {
|
||||
logSafe("❌ subscribeTenant returned null");
|
||||
_showDialog(
|
||||
title: "Subscription Failed",
|
||||
message: "Failed to call subscription API. Please contact support.",
|
||||
success: false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final data = subResp['data'] ?? subResp;
|
||||
// backend success flag might be in different places; check robustly
|
||||
final bool success = (data is Map && (data['success'] == true || subResp['success'] == true)) || (subResp['statusCode'] == 200);
|
||||
|
||||
if (success) {
|
||||
_showDialog(
|
||||
title: "Subscription Active ✅",
|
||||
message: data['message'] ?? "Tenant subscribed successfully.",
|
||||
success: true,
|
||||
);
|
||||
} else {
|
||||
_showDialog(
|
||||
title: "Subscription Failed",
|
||||
message: data['message'] ?? "Subscription API did not confirm success.",
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePaymentError(PaymentFailureResponse response) {
|
||||
logSafe("❌ Payment Failed: ${response.message}");
|
||||
isProcessing = false;
|
||||
notifyListeners();
|
||||
|
||||
_showDialog(
|
||||
title: "Payment Failed ❌",
|
||||
message: "Reason: ${response.message ?? 'Unknown error'}",
|
||||
success: false,
|
||||
);
|
||||
|
||||
_cleanup();
|
||||
}
|
||||
|
||||
void _handleExternalWallet(ExternalWalletResponse response) {
|
||||
logSafe("ℹ️ External Wallet Used: ${response.walletName}");
|
||||
}
|
||||
|
||||
/// ==============================
|
||||
/// CLEANUP / DISPOSE
|
||||
/// ==============================
|
||||
void _cleanup() {
|
||||
try {
|
||||
_razorpay?.clear();
|
||||
} catch (_) {}
|
||||
_razorpay = null;
|
||||
logSafe("🧹 Razorpay instance cleaned up.");
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cleanup();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void disposeController() => _cleanup();
|
||||
|
||||
/// ==============================
|
||||
/// HELPER UI FUNCTIONS
|
||||
/// ==============================
|
||||
void _showDialog({
|
||||
required String title,
|
||||
required String message,
|
||||
required bool success,
|
||||
}) {
|
||||
if (Get.isDialogOpen == true) Get.back(); // Close any existing dialogs
|
||||
|
||||
Get.defaultDialog(
|
||||
title: title,
|
||||
middleText: message,
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back(); // close dialog
|
||||
Get.snackbar(
|
||||
success ? "Payment Successful 🎉" : "Payment Failed ❌",
|
||||
success
|
||||
? "Payment verified successfully!"
|
||||
: "Payment failed or could not be verified.",
|
||||
backgroundColor: success ? Colors.green : Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
child: const Text("OK"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
if (Get.isDialogOpen == true) Get.back(); // Close any open dialog
|
||||
Get.snackbar(
|
||||
"Payment Error",
|
||||
message,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,11 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/permission_service.dart';
|
||||
import 'package:on_field_work/model/user_permission.dart';
|
||||
import 'package:on_field_work/model/employees/employee_info.dart';
|
||||
import 'package:on_field_work/model/projects_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/permission_service.dart';
|
||||
import 'package:marco/model/user_permission.dart';
|
||||
import 'package:marco/model/employees/employee_info.dart';
|
||||
import 'package:marco/model/projects_model.dart';
|
||||
|
||||
class PermissionController extends GetxController {
|
||||
var permissions = <UserPermission>[].obs;
|
||||
@ -15,9 +15,6 @@ class PermissionController extends GetxController {
|
||||
Timer? _refreshTimer;
|
||||
var isLoading = true.obs;
|
||||
|
||||
/// ← NEW: reactive flag to signal permissions are loaded
|
||||
var permissionsLoaded = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -55,10 +52,6 @@ class PermissionController extends GetxController {
|
||||
_updateState(userData);
|
||||
await _storeData();
|
||||
logSafe("Data loaded and state updated successfully.");
|
||||
|
||||
// ← NEW: mark permissions as loaded
|
||||
permissionsLoaded.value = true;
|
||||
|
||||
} catch (e, stacktrace) {
|
||||
logSafe("Error loading data from API",
|
||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||
@ -110,7 +103,7 @@ class PermissionController extends GetxController {
|
||||
}
|
||||
|
||||
void _startAutoRefresh() {
|
||||
_refreshTimer = Timer.periodic(const Duration(minutes: 30), (timer) async {
|
||||
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
|
||||
logSafe("Auto-refresh triggered.");
|
||||
final token = await _getAuthToken();
|
||||
if (token?.isNotEmpty ?? false) {
|
||||
@ -124,6 +117,8 @@ class PermissionController extends GetxController {
|
||||
|
||||
bool hasPermission(String permissionId) {
|
||||
final hasPerm = permissions.any((p) => p.id == permissionId);
|
||||
logSafe("Checking permission $permissionId: $hasPerm",
|
||||
level: LogLevel.debug);
|
||||
return hasPerm;
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/global_project_model.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/global_project_model.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
|
||||
class ProjectController extends GetxController {
|
||||
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/controller/service_project/service_project_details_screen_controller.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/model/service_project/service_project_branches_model.dart';
|
||||
|
||||
class AddServiceProjectJobController extends GetxController {
|
||||
// FORM CONTROLLERS
|
||||
final titleCtrl = TextEditingController();
|
||||
final descCtrl = TextEditingController();
|
||||
final tagCtrl = TextEditingController();
|
||||
final searchFocusNode = FocusNode();
|
||||
|
||||
// OBSERVABLES
|
||||
final startDate = Rx<DateTime?>(DateTime.now());
|
||||
final dueDate = Rx<DateTime?>(DateTime.now().add(const Duration(days: 1)));
|
||||
|
||||
final enteredTags = <String>[].obs;
|
||||
final selectedAssignees = <EmployeeModel>[].obs;
|
||||
|
||||
// Branches
|
||||
final branches = <Branch>[].obs;
|
||||
final selectedBranch = Rxn<Branch>();
|
||||
final isBranchLoading = false.obs;
|
||||
|
||||
// Loading
|
||||
final isLoading = false.obs;
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
titleCtrl.dispose();
|
||||
descCtrl.dispose();
|
||||
tagCtrl.dispose();
|
||||
searchFocusNode.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// FETCH BRANCHES
|
||||
Future<void> fetchBranches(String projectId) async {
|
||||
isBranchLoading.value = true;
|
||||
|
||||
final response = await ApiService.getServiceProjectBranchesFull(
|
||||
projectId: projectId,
|
||||
);
|
||||
|
||||
if (response != null && response.success) {
|
||||
branches.assignAll(response.data?.data ?? []);
|
||||
}
|
||||
|
||||
isBranchLoading.value = false;
|
||||
}
|
||||
|
||||
// CREATE JOB
|
||||
Future<void> createJob(String projectId) async {
|
||||
if (titleCtrl.text.trim().isEmpty || descCtrl.text.trim().isEmpty) {
|
||||
showAppSnackbar(
|
||||
title: "Validation",
|
||||
message: "Title and Description are required",
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
final jobId = await ApiService.createServiceProjectJobApi(
|
||||
title: titleCtrl.text.trim(),
|
||||
description: descCtrl.text.trim(),
|
||||
projectId: projectId,
|
||||
branchId: selectedBranch.value?.id,
|
||||
assignees: selectedAssignees // payload mapping
|
||||
.map((e) => {"employeeId": e.id, "isActive": true})
|
||||
.toList(),
|
||||
startDate: startDate.value!,
|
||||
dueDate: dueDate.value!,
|
||||
tags: enteredTags
|
||||
.map((tag) => {"id": null, "name": tag, "isActive": true})
|
||||
.toList(),
|
||||
);
|
||||
|
||||
isLoading.value = false;
|
||||
|
||||
if (jobId != null) {
|
||||
if (Get.isRegistered<ServiceProjectDetailsController>()) {
|
||||
final detailsCtrl = Get.find<ServiceProjectDetailsController>();
|
||||
|
||||
// 🔥 1. Refresh job LIST
|
||||
detailsCtrl.refreshJobsAfterAdd();
|
||||
|
||||
// 🔥 2. Refresh job DETAILS (FULL DATA - including tags and assignees)
|
||||
await detailsCtrl.fetchJobDetail(jobId);
|
||||
}
|
||||
|
||||
Get.back();
|
||||
showAppSnackbar(
|
||||
title: "Success",
|
||||
message: "Job created successfully",
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
} else {
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to create job",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
|
||||
class ServiceProjectAllocationController extends GetxController {
|
||||
final projectId = ''.obs;
|
||||
|
||||
// Roles
|
||||
var roles = <TeamRole>[].obs;
|
||||
var selectedRole = Rxn<TeamRole>();
|
||||
|
||||
// Employees
|
||||
var roleEmployees = <Employee>[].obs;
|
||||
var selectedEmployees = <Employee>[].obs;
|
||||
final displayController = TextEditingController();
|
||||
|
||||
// Loading
|
||||
var isLoading = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
ever(selectedEmployees, (_) {
|
||||
displayController.text = selectedEmployees.isEmpty
|
||||
? ''
|
||||
: selectedEmployees
|
||||
.map((e) => '${e.firstName} ${e.lastName}')
|
||||
.join(', ');
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch all roles
|
||||
Future<void> fetchRoles() async {
|
||||
isLoading.value = true;
|
||||
final result = await ApiService.getTeamRoles();
|
||||
if (result != null) {
|
||||
roles.assignAll(result);
|
||||
}
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// Fetch employees by role
|
||||
Future<void> fetchEmployeesByRole(String roleId) async {
|
||||
isLoading.value = true;
|
||||
final allocations = await ApiService.getServiceProjectAllocationList(
|
||||
projectId: projectId.value);
|
||||
|
||||
if (allocations != null) {
|
||||
roleEmployees.assignAll(
|
||||
allocations
|
||||
.where((a) => a.teamRole.id == roleId)
|
||||
.map((a) => a.employee)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
void toggleEmployee(Employee emp) {
|
||||
if (selectedEmployees.contains(emp)) {
|
||||
selectedEmployees.remove(emp);
|
||||
} else {
|
||||
selectedEmployees.add(emp);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> submitAllocation() async {
|
||||
final payload = selectedEmployees
|
||||
.map((e) => {
|
||||
"projectId": projectId.value,
|
||||
"employeeId": e.id,
|
||||
"teamRoleId": selectedRole.value?.id,
|
||||
"isActive": true,
|
||||
})
|
||||
.toList();
|
||||
|
||||
return await ApiService.manageServiceProjectAllocation(payload: payload);
|
||||
}
|
||||
}
|
||||
@ -1,479 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/service_project/service_projects_details_model.dart';
|
||||
import 'package:on_field_work/model/service_project/job_list_model.dart';
|
||||
import 'package:on_field_work/model/service_project/service_project_job_detail_model.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart';
|
||||
import 'package:on_field_work/model/service_project/job_allocation_model.dart';
|
||||
import 'package:on_field_work/model/service_project/job_status_response.dart';
|
||||
import 'package:on_field_work/model/service_project/job_comments.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class ServiceProjectDetailsController extends GetxController {
|
||||
// -------------------- Observables --------------------
|
||||
var projectId = ''.obs;
|
||||
var projectDetail = Rxn<ProjectDetail>();
|
||||
var jobList = <JobEntity>[].obs;
|
||||
var jobDetail = Rxn<JobDetailsResponse>();
|
||||
var showArchivedJobs = false.obs; // true = archived, false = active
|
||||
|
||||
// Loading states
|
||||
var isLoading = false.obs;
|
||||
var isJobLoading = false.obs;
|
||||
var isJobDetailLoading = false.obs;
|
||||
|
||||
// Error messages
|
||||
var errorMessage = ''.obs;
|
||||
var jobErrorMessage = ''.obs;
|
||||
var jobDetailErrorMessage = ''.obs;
|
||||
final ImagePicker picker = ImagePicker();
|
||||
var isProcessingAttachment = false.obs;
|
||||
|
||||
// Pagination
|
||||
var pageNumber = 1;
|
||||
final int pageSize = 20;
|
||||
var hasMoreJobs = true.obs;
|
||||
|
||||
var isTagging = false.obs;
|
||||
var attendanceMessage = ''.obs;
|
||||
var attendanceLog = Rxn<JobAttendanceResponse>();
|
||||
var teamList = <ServiceProjectAllocation>[].obs;
|
||||
var isTeamLoading = false.obs;
|
||||
var teamErrorMessage = ''.obs;
|
||||
var filteredJobList = <JobEntity>[].obs;
|
||||
// -------------------- Job Status --------------------
|
||||
// With this:
|
||||
var jobStatusList = <JobStatus>[].obs;
|
||||
var selectedJobStatus = Rx<JobStatus?>(null);
|
||||
var isJobStatusLoading = false.obs;
|
||||
var jobStatusErrorMessage = ''.obs;
|
||||
// -------------------- Job Comments --------------------
|
||||
var jobComments = <CommentItem>[].obs;
|
||||
var isCommentsLoading = false.obs;
|
||||
var commentsErrorMessage = ''.obs;
|
||||
// -------------------- Lifecycle --------------------
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProjectJobs();
|
||||
filteredJobList.value = jobList;
|
||||
}
|
||||
|
||||
// -------------------- Project --------------------
|
||||
void setProjectId(String id) {
|
||||
if (projectId.value == id) return;
|
||||
projectId.value = id;
|
||||
|
||||
// Reset pagination and list
|
||||
pageNumber = 1;
|
||||
hasMoreJobs.value = true;
|
||||
jobList.clear();
|
||||
filteredJobList.clear();
|
||||
|
||||
// Fetch project detail
|
||||
fetchProjectDetail();
|
||||
|
||||
// Always fetch jobs for this project
|
||||
fetchProjectJobs(refresh: true);
|
||||
}
|
||||
|
||||
void updateJobSearch(String searchText) {
|
||||
if (searchText.isEmpty) {
|
||||
filteredJobList.value = jobList;
|
||||
} else {
|
||||
filteredJobList.value = jobList.where((job) {
|
||||
final lowerSearch = searchText.toLowerCase();
|
||||
return job.title.toLowerCase().contains(lowerSearch) ||
|
||||
(job.description.toLowerCase().contains(lowerSearch)) ||
|
||||
(job.tags?.any(
|
||||
(tag) => tag.name.toLowerCase().contains(lowerSearch)) ??
|
||||
false);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchProjectTeams() async {
|
||||
if (projectId.value.isEmpty) {
|
||||
teamErrorMessage.value = "Invalid project ID";
|
||||
return;
|
||||
}
|
||||
|
||||
isTeamLoading.value = true;
|
||||
teamErrorMessage.value = '';
|
||||
|
||||
try {
|
||||
final result = await ApiService.getServiceProjectAllocationList(
|
||||
projectId: projectId.value,
|
||||
isActive: true,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
teamList.value = result;
|
||||
} else {
|
||||
teamErrorMessage.value = "No teams found";
|
||||
}
|
||||
} catch (e) {
|
||||
teamErrorMessage.value = "Error fetching teams: $e";
|
||||
} finally {
|
||||
isTeamLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchJobStatus({required String statusId}) async {
|
||||
if (projectId.value.isEmpty) {
|
||||
jobStatusErrorMessage.value = "Invalid project ID";
|
||||
return;
|
||||
}
|
||||
|
||||
isJobStatusLoading.value = true;
|
||||
jobStatusErrorMessage.value = '';
|
||||
|
||||
try {
|
||||
final statuses = await ApiService.getMasterJobStatus(
|
||||
projectId: projectId.value,
|
||||
statusId: statusId,
|
||||
);
|
||||
|
||||
if (statuses != null && statuses.isNotEmpty) {
|
||||
jobStatusList.value = statuses;
|
||||
|
||||
// Keep previously selected if exists, else pick first
|
||||
selectedJobStatus.value = statuses.firstWhere(
|
||||
(status) => status.id == selectedJobStatus.value?.id,
|
||||
orElse: () => statuses.first,
|
||||
);
|
||||
|
||||
print("Job Status List: ${jobStatusList.map((e) => e.name).toList()}");
|
||||
} else {
|
||||
jobStatusErrorMessage.value = "No job statuses found";
|
||||
}
|
||||
} catch (e) {
|
||||
jobStatusErrorMessage.value = "Error fetching job status: $e";
|
||||
} finally {
|
||||
isJobStatusLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchProjectDetail() async {
|
||||
if (projectId.value.isEmpty) {
|
||||
errorMessage.value = "Invalid project ID";
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
try {
|
||||
final result =
|
||||
await ApiService.getServiceProjectDetailApi(projectId.value);
|
||||
|
||||
if (result != null && result.data != null) {
|
||||
projectDetail.value = result.data!;
|
||||
} else {
|
||||
errorMessage.value =
|
||||
result?.message ?? "Failed to fetch project details";
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = "Error: $e";
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchJobAttendanceLog(String attendanceId) async {
|
||||
if (attendanceId.isEmpty) {
|
||||
attendanceMessage.value = "Invalid attendance ID";
|
||||
return;
|
||||
}
|
||||
|
||||
isJobDetailLoading.value = true;
|
||||
attendanceMessage.value = '';
|
||||
|
||||
try {
|
||||
final result =
|
||||
await ApiService.getJobAttendanceLog(attendanceId: attendanceId);
|
||||
|
||||
if (result != null) {
|
||||
attendanceLog.value = result;
|
||||
} else {
|
||||
attendanceMessage.value = "Attendance log not found or empty";
|
||||
}
|
||||
} catch (e) {
|
||||
attendanceMessage.value = "Error fetching attendance log: $e";
|
||||
} finally {
|
||||
isJobDetailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Job List (modified to always load) --------------------
|
||||
Future<void> fetchProjectJobs({bool refresh = false}) async {
|
||||
if (projectId.value.isEmpty) return;
|
||||
|
||||
if (refresh) pageNumber = 1;
|
||||
if (!hasMoreJobs.value && !refresh) return;
|
||||
|
||||
isJobLoading.value = true;
|
||||
jobErrorMessage.value = '';
|
||||
|
||||
try {
|
||||
final result = await ApiService.getServiceProjectJobListApi(
|
||||
projectId: projectId.value,
|
||||
pageNumber: pageNumber,
|
||||
pageSize: pageSize,
|
||||
isActive: true,
|
||||
isArchive: showArchivedJobs.value,
|
||||
);
|
||||
|
||||
if (result != null && result.data != null) {
|
||||
final newJobs = result.data?.data ?? [];
|
||||
|
||||
if (refresh || pageNumber == 1) {
|
||||
jobList.value = newJobs;
|
||||
} else {
|
||||
jobList.addAll(newJobs);
|
||||
}
|
||||
|
||||
filteredJobList.value = jobList;
|
||||
|
||||
hasMoreJobs.value = newJobs.length == pageSize;
|
||||
if (hasMoreJobs.value) pageNumber++;
|
||||
} else {
|
||||
jobErrorMessage.value = result?.message ?? "Failed to fetch jobs";
|
||||
}
|
||||
} catch (e) {
|
||||
jobErrorMessage.value = "Error fetching jobs: $e";
|
||||
} finally {
|
||||
isJobLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchMoreJobs() async => fetchProjectJobs();
|
||||
|
||||
// -------------------- Manual Refresh --------------------
|
||||
Future<void> refresh() async {
|
||||
pageNumber = 1;
|
||||
hasMoreJobs.value = true;
|
||||
|
||||
await Future.wait([
|
||||
fetchProjectDetail(),
|
||||
fetchProjectJobs(),
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------- Job Detail --------------------
|
||||
Future<void> fetchJobDetail(String jobId) async {
|
||||
if (jobId.isEmpty) {
|
||||
jobDetailErrorMessage.value = "Invalid job ID";
|
||||
return;
|
||||
}
|
||||
|
||||
isJobDetailLoading.value = true;
|
||||
jobDetailErrorMessage.value = '';
|
||||
|
||||
try {
|
||||
final result = await ApiService.getServiceProjectJobDetailApi(jobId);
|
||||
if (result != null) {
|
||||
jobDetail.value = result;
|
||||
} else {
|
||||
jobDetailErrorMessage.value = "Failed to fetch job details";
|
||||
}
|
||||
} catch (e) {
|
||||
jobDetailErrorMessage.value = "Error fetching job details: $e";
|
||||
} finally {
|
||||
isJobDetailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Position?> _getCurrentLocation() async {
|
||||
try {
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
attendanceMessage.value = "Location services are disabled.";
|
||||
return null;
|
||||
}
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
attendanceMessage.value = "Location permission denied";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
attendanceMessage.value =
|
||||
"Location permission permanently denied. Enable it from settings.";
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
} catch (e) {
|
||||
attendanceMessage.value = "Failed to get location: $e";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchJobComments({bool refresh = false}) async {
|
||||
if (jobDetail.value?.data?.id == null) {
|
||||
commentsErrorMessage.value = "Invalid job ID";
|
||||
return;
|
||||
}
|
||||
|
||||
if (refresh) pageNumber = 1;
|
||||
|
||||
isCommentsLoading.value = true;
|
||||
commentsErrorMessage.value = '';
|
||||
|
||||
try {
|
||||
final response = await ApiService.getJobCommentList(
|
||||
jobTicketId: jobDetail.value!.data!.id!,
|
||||
pageNumber: pageNumber,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
|
||||
if (response != null && response.data != null) {
|
||||
final newComments = response.data?.data ?? [];
|
||||
|
||||
if (refresh || pageNumber == 1) {
|
||||
jobComments.value = newComments;
|
||||
} else {
|
||||
jobComments.addAll(newComments);
|
||||
}
|
||||
|
||||
hasMoreJobs.value =
|
||||
(response.data?.totalEntities ?? 0) > (pageNumber * pageSize);
|
||||
if (hasMoreJobs.value) pageNumber++;
|
||||
} else {
|
||||
commentsErrorMessage.value =
|
||||
response?.message ?? "Failed to fetch comments";
|
||||
}
|
||||
} catch (e) {
|
||||
commentsErrorMessage.value = "Error fetching comments: $e";
|
||||
} finally {
|
||||
isCommentsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addJobComment({
|
||||
required String jobId,
|
||||
required String comment,
|
||||
List<File>? files,
|
||||
}) async {
|
||||
try {
|
||||
List<Map<String, dynamic>> attachments = [];
|
||||
|
||||
if (files != null && files.isNotEmpty) {
|
||||
for (final file in files) {
|
||||
final bytes = await file.readAsBytes();
|
||||
final base64Data = base64Encode(bytes);
|
||||
final mimeType =
|
||||
lookupMimeType(file.path) ?? "application/octet-stream";
|
||||
|
||||
attachments.add({
|
||||
"fileName": file.path.split('/').last,
|
||||
"base64Data": base64Data,
|
||||
"contentType": mimeType,
|
||||
"fileSize": bytes.length,
|
||||
"description": "",
|
||||
"isActive": true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final success = await ApiService.addJobComment(
|
||||
jobTicketId: jobId,
|
||||
comment: comment,
|
||||
attachments: attachments,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await fetchJobDetail(jobId);
|
||||
refresh();
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (e) {
|
||||
print("Error adding comment: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Tag In / Tag Out for a job with proper payload
|
||||
Future<void> updateJobAttendance({
|
||||
required String jobId,
|
||||
required int action,
|
||||
String comment = "Updated via app",
|
||||
File? attachment,
|
||||
}) async {
|
||||
if (jobId.isEmpty) return;
|
||||
|
||||
isTagging.value = true;
|
||||
attendanceMessage.value = '';
|
||||
|
||||
try {
|
||||
final position = await _getCurrentLocation();
|
||||
if (position == null) {
|
||||
isTagging.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? attachmentPayload;
|
||||
|
||||
if (attachment != null) {
|
||||
final bytes = await attachment.readAsBytes();
|
||||
final base64Data = base64Encode(bytes);
|
||||
final mimeType =
|
||||
lookupMimeType(attachment.path) ?? 'application/octet-stream';
|
||||
attachmentPayload = {
|
||||
"documentId": jobId,
|
||||
"fileName": attachment.path.split('/').last,
|
||||
"base64Data": base64Data,
|
||||
"contentType": mimeType,
|
||||
"fileSize": bytes.length,
|
||||
"description": "Attached via app",
|
||||
"isActive": true,
|
||||
};
|
||||
}
|
||||
|
||||
final payload = {
|
||||
"jobTcketId": jobId,
|
||||
"action": action,
|
||||
"latitude": position.latitude.toString(),
|
||||
"longitude": position.longitude.toString(),
|
||||
"comment": comment,
|
||||
"attachment": attachmentPayload,
|
||||
};
|
||||
|
||||
final success = await ApiService.updateServiceProjectJobAttendance(
|
||||
payload: payload,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
attendanceMessage.value =
|
||||
action == 0 ? "Tagged In successfully" : "Tagged Out successfully";
|
||||
await fetchJobDetail(jobId);
|
||||
} else {
|
||||
attendanceMessage.value = "Failed to update attendance";
|
||||
}
|
||||
} catch (e) {
|
||||
attendanceMessage.value = "Error updating attendance: $e";
|
||||
} finally {
|
||||
isTagging.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 🔥 AUTO REFRESH JOB LIST AFTER ADDING A JOB
|
||||
// ------------------------------------------------------------
|
||||
Future<void> refreshJobsAfterAdd() async {
|
||||
pageNumber = 1;
|
||||
hasMoreJobs.value = true;
|
||||
await fetchProjectJobs();
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/service_project/service_projects_list_model.dart';
|
||||
|
||||
class ServiceProjectController extends GetxController {
|
||||
final projects = <ProjectItem>[].obs;
|
||||
final isLoading = false.obs;
|
||||
final searchQuery = ''.obs;
|
||||
|
||||
/// Computed filtered project list
|
||||
List<ProjectItem> get filteredProjects {
|
||||
final query = searchQuery.value.trim().toLowerCase();
|
||||
if (query.isEmpty) return projects;
|
||||
|
||||
return projects.where((p) {
|
||||
final nameMatch = p.name.toLowerCase().contains(query);
|
||||
final shortNameMatch = p.shortName.toLowerCase().contains(query);
|
||||
final addressMatch = p.address.toLowerCase().contains(query);
|
||||
final contactMatch = p.contactName.toLowerCase().contains(query);
|
||||
final clientMatch = p.client != null &&
|
||||
(p.client!.name.toLowerCase().contains(query) ||
|
||||
p.client!.contactPerson.toLowerCase().contains(query));
|
||||
|
||||
return nameMatch ||
|
||||
shortNameMatch ||
|
||||
addressMatch ||
|
||||
contactMatch ||
|
||||
clientMatch;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Fetch projects from API
|
||||
Future<void> fetchProjects({int pageNumber = 1, int pageSize = 20}) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
final result = await ApiService.getServiceProjectsListApi(
|
||||
pageNumber: pageNumber,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
|
||||
if (result != null && result.data != null) {
|
||||
projects.assignAll(result.data!.data);
|
||||
} else {
|
||||
projects.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
// Optional: log or show error
|
||||
rethrow;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update search
|
||||
void updateSearch(String query) {
|
||||
searchQuery.value = query;
|
||||
}
|
||||
}
|
||||
38
lib/controller/subscriptions/subscription_controller.dart
Normal file
38
lib/controller/subscriptions/subscription_controller.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
|
||||
class SubscriptionController extends GetxController {
|
||||
var plans = <Map<String, dynamic>>[].obs;
|
||||
var isLoading = true.obs;
|
||||
|
||||
// Frequency tabs
|
||||
final frequencies = ['monthly', 'quarterly', 'half-yearly', 'yearly'];
|
||||
var selectedFrequency = 'monthly'.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchPlans(selectedFrequency.value);
|
||||
}
|
||||
|
||||
Future<void> fetchPlans(String frequency) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
selectedFrequency.value = frequency;
|
||||
|
||||
final response = await ApiService.getSubscriptionPlans(frequency);
|
||||
if (response != null &&
|
||||
response['success'] == true &&
|
||||
response['data'] != null) {
|
||||
plans.value = List<Map<String, dynamic>>.from(response['data']);
|
||||
} else {
|
||||
plans.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error fetching plans: $e");
|
||||
plans.clear();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/model/dailyTaskPlanning/master_work_category_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart';
|
||||
|
||||
class AddTaskController extends GetxController {
|
||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/project_model.dart';
|
||||
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_model.dart';
|
||||
import 'package:on_field_work/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/project_model.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
|
||||
|
||||
class DailyTaskController extends GetxController {
|
||||
List<ProjectModel> projects = [];
|
||||
@ -13,10 +13,6 @@ class DailyTaskController extends GetxController {
|
||||
DateTime? startDateTask;
|
||||
DateTime? endDateTask;
|
||||
|
||||
// Rx fields for DateRangePickerWidget
|
||||
Rx<DateTime> startDateTaskRx = DateTime.now().obs;
|
||||
Rx<DateTime> endDateTaskRx = DateTime.now().obs;
|
||||
|
||||
List<TaskModel> dailyTasks = [];
|
||||
final RxSet<String> expandedDates = <String>{}.obs;
|
||||
|
||||
@ -37,19 +33,14 @@ class DailyTaskController extends GetxController {
|
||||
RxBool isLoading = true.obs;
|
||||
RxBool isLoadingMore = false.obs;
|
||||
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
||||
|
||||
// Pagination
|
||||
int currentPage = 1;
|
||||
int pageSize = 20;
|
||||
bool hasMore = true;
|
||||
|
||||
FilterData? taskFilterData;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initializeDefaults();
|
||||
_initializeRxDates();
|
||||
}
|
||||
|
||||
void _initializeDefaults() {
|
||||
@ -67,12 +58,6 @@ class DailyTaskController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
void _initializeRxDates() {
|
||||
startDateTaskRx.value =
|
||||
startDateTask ?? DateTime.now().subtract(const Duration(days: 7));
|
||||
endDateTaskRx.value = endDateTask ?? DateTime.now();
|
||||
}
|
||||
|
||||
void clearTaskFilters() {
|
||||
selectedBuildings.clear();
|
||||
selectedFloors.clear();
|
||||
@ -80,26 +65,9 @@ class DailyTaskController extends GetxController {
|
||||
selectedServices.clear();
|
||||
startDateTask = null;
|
||||
endDateTask = null;
|
||||
|
||||
// reset Rx dates as well
|
||||
startDateTaskRx.value = DateTime.now().subtract(const Duration(days: 7));
|
||||
endDateTaskRx.value = DateTime.now();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void updateDateRange(DateTime? start, DateTime? end) {
|
||||
if (start != null && end != null) {
|
||||
startDateTask = start;
|
||||
endDateTask = end;
|
||||
|
||||
startDateTaskRx.value = start;
|
||||
endDateTaskRx.value = end;
|
||||
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchTaskData(
|
||||
String projectId, {
|
||||
int pageNumber = 1,
|
||||
@ -134,24 +102,11 @@ class DailyTaskController extends GetxController {
|
||||
);
|
||||
|
||||
if (response != null && response.isNotEmpty) {
|
||||
if (!isLoadMore) {
|
||||
groupedDailyTasks.clear();
|
||||
}
|
||||
|
||||
for (var task in response) {
|
||||
final assignmentDateKey =
|
||||
task.assignmentDate.toIso8601String().split('T')[0];
|
||||
|
||||
// Initialize list if not present
|
||||
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []);
|
||||
|
||||
// Only add task if it doesn't already exist (avoid duplicates)
|
||||
if (!groupedDailyTasks[assignmentDateKey]!
|
||||
.any((t) => t.id == task.id)) {
|
||||
groupedDailyTasks[assignmentDateKey]!.add(task);
|
||||
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
|
||||
}
|
||||
}
|
||||
|
||||
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
|
||||
currentPage = pageNumber;
|
||||
} else {
|
||||
@ -164,13 +119,16 @@ class DailyTaskController extends GetxController {
|
||||
update();
|
||||
}
|
||||
|
||||
FilterData? taskFilterData;
|
||||
|
||||
Future<void> fetchTaskFilter(String projectId) async {
|
||||
isFilterLoading.value = true;
|
||||
try {
|
||||
final filterResponse = await ApiService.getDailyTaskFilter(projectId);
|
||||
|
||||
if (filterResponse != null && filterResponse.success) {
|
||||
taskFilterData = filterResponse.data;
|
||||
taskFilterData =
|
||||
filterResponse.data; // now taskFilterData is FilterData?
|
||||
logSafe(
|
||||
"Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}",
|
||||
level: LogLevel.info,
|
||||
@ -213,15 +171,12 @@ class DailyTaskController extends GetxController {
|
||||
startDateTask = picked.start;
|
||||
endDateTask = picked.end;
|
||||
|
||||
// update Rx fields as well
|
||||
startDateTaskRx.value = picked.start;
|
||||
endDateTaskRx.value = picked.end;
|
||||
|
||||
logSafe(
|
||||
"Date range selected: $startDateTask to $endDateTask",
|
||||
level: LogLevel.info,
|
||||
);
|
||||
|
||||
// ✅ Add null check before calling fetchTaskData
|
||||
final projectId = controller.selectedProjectId;
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
await controller.fetchTaskData(projectId);
|
||||
@ -235,7 +190,9 @@ class DailyTaskController extends GetxController {
|
||||
required String projectId,
|
||||
required String taskAllocationId,
|
||||
}) async {
|
||||
// re-fetch tasks
|
||||
await fetchTaskData(projectId);
|
||||
update();
|
||||
|
||||
update(); // rebuilds UI
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/model/project_model.dart';
|
||||
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_planning_model.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/model/project_model.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart';
|
||||
import 'package:marco/model/employees/employee_model.dart';
|
||||
|
||||
class DailyTaskPlanningController extends GetxController {
|
||||
List<ProjectModel> projects = [];
|
||||
@ -23,10 +23,6 @@ class DailyTaskPlanningController extends GetxController {
|
||||
RxBool isFetchingProjects = true.obs;
|
||||
RxBool isFetchingEmployees = true.obs;
|
||||
|
||||
/// New: track per-building loading and loaded state for lazy infra loading
|
||||
RxMap<String, RxBool> buildingLoadingStates = <String, RxBool>{}.obs;
|
||||
final Set<String> buildingsWithDetails = <String>{};
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@ -110,7 +106,7 @@ class DailyTaskPlanningController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch buildings list only (no deep area/workItem calls) for initial load.
|
||||
/// Fetch Infra details and then tasks per work area
|
||||
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
|
||||
if (projectId == null) return;
|
||||
|
||||
@ -127,24 +123,25 @@ class DailyTaskPlanningController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter buildings with 0 planned & completed work
|
||||
final filteredBuildings = infraData.where((b) {
|
||||
final planned = (b['plannedWork'] as num?)?.toDouble() ?? 0;
|
||||
final completed = (b['completedWork'] as num?)?.toDouble() ?? 0;
|
||||
return planned > 0 || completed > 0;
|
||||
}).toList();
|
||||
|
||||
dailyTasks = filteredBuildings.map((buildingJson) {
|
||||
dailyTasks = infraData.map((buildingJson) {
|
||||
final building = Building(
|
||||
id: buildingJson['id'],
|
||||
name: buildingJson['buildingName'],
|
||||
description: buildingJson['description'],
|
||||
floors: [],
|
||||
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
|
||||
completedWork:
|
||||
(buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
|
||||
floors: (buildingJson['floors'] as List<dynamic>)
|
||||
.map((floorJson) => Floor(
|
||||
id: floorJson['id'],
|
||||
floorName: floorJson['floorName'],
|
||||
workAreas: (floorJson['workAreas'] as List<dynamic>)
|
||||
.map((areaJson) => WorkArea(
|
||||
id: areaJson['id'],
|
||||
areaName: areaJson['areaName'],
|
||||
workItems: [],
|
||||
))
|
||||
.toList(),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
return TaskPlanningDetailsModel(
|
||||
id: building.id,
|
||||
name: building.name,
|
||||
@ -157,75 +154,14 @@ class DailyTaskPlanningController extends GetxController {
|
||||
);
|
||||
}).toList();
|
||||
|
||||
buildingLoadingStates.clear();
|
||||
buildingsWithDetails.clear();
|
||||
} catch (e, stack) {
|
||||
logSafe("Error fetching daily task data",
|
||||
level: LogLevel.error, error: e, stackTrace: stack);
|
||||
} finally {
|
||||
isFetchingTasks.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch full infra for a single building (floors, workAreas, workItems).
|
||||
/// Called lazily when user expands a building in the UI.
|
||||
Future<void> fetchBuildingInfra(
|
||||
String buildingId, String projectId, String? serviceId) async {
|
||||
if (buildingId.isEmpty) return;
|
||||
|
||||
// mark loading
|
||||
buildingLoadingStates.putIfAbsent(buildingId, () => true.obs);
|
||||
buildingLoadingStates[buildingId]!.value = true;
|
||||
update();
|
||||
|
||||
await Future.wait(dailyTasks
|
||||
.expand((task) => task.buildings)
|
||||
.expand((b) => b.floors)
|
||||
.expand((f) => f.workAreas)
|
||||
.map((area) async {
|
||||
try {
|
||||
// Re-use getInfraDetails and find the building entry for the requested buildingId
|
||||
final infraResponse =
|
||||
await ApiService.getInfraDetails(projectId, serviceId: serviceId);
|
||||
final infraData = infraResponse?['data'] as List<dynamic>? ?? [];
|
||||
|
||||
final buildingJson = infraData.firstWhere(
|
||||
(b) => b['id'].toString() == buildingId.toString(),
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (buildingJson == null) {
|
||||
logSafe("Building $buildingId not found in infra response",
|
||||
level: LogLevel.warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build floors & workAreas for this building
|
||||
final building = Building(
|
||||
id: buildingJson['id'],
|
||||
name: buildingJson['buildingName'],
|
||||
description: buildingJson['description'],
|
||||
floors:
|
||||
(buildingJson['floors'] as List<dynamic>? ?? []).map((floorJson) {
|
||||
return Floor(
|
||||
id: floorJson['id'],
|
||||
floorName: floorJson['floorName'],
|
||||
workAreas: (floorJson['workAreas'] as List<dynamic>? ?? [])
|
||||
.map((areaJson) {
|
||||
return WorkArea(
|
||||
id: areaJson['id'],
|
||||
areaName: areaJson['areaName'],
|
||||
workItems: [], // will populate later
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
|
||||
completedWork: (buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
|
||||
// For each workArea, fetch its work items and populate
|
||||
await Future.wait(
|
||||
building.floors.expand((f) => f.workAreas).map((area) async {
|
||||
try {
|
||||
final taskResponse = await ApiService.getWorkItemsByWorkArea(area.id,
|
||||
serviceId: serviceId);
|
||||
final taskResponse =
|
||||
await ApiService.getWorkItemsByWorkArea(area.id, serviceId: serviceId);
|
||||
final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
|
||||
area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper(
|
||||
workItemId: taskJson['id'],
|
||||
@ -235,14 +171,11 @@ class DailyTaskPlanningController extends GetxController {
|
||||
? ActivityMaster.fromJson(taskJson['activityMaster'])
|
||||
: null,
|
||||
workCategoryMaster: taskJson['workCategoryMaster'] != null
|
||||
? WorkCategoryMaster.fromJson(
|
||||
taskJson['workCategoryMaster'])
|
||||
? WorkCategoryMaster.fromJson(taskJson['workCategoryMaster'])
|
||||
: null,
|
||||
plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(),
|
||||
completedWork:
|
||||
(taskJson['completedWork'] as num?)?.toDouble(),
|
||||
todaysAssigned:
|
||||
(taskJson['todaysAssigned'] as num?)?.toDouble(),
|
||||
completedWork: (taskJson['completedWork'] as num?)?.toDouble(),
|
||||
todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(),
|
||||
description: taskJson['description'] as String?,
|
||||
taskDate: taskJson['taskDate'] != null
|
||||
? DateTime.tryParse(taskJson['taskDate'])
|
||||
@ -254,40 +187,11 @@ class DailyTaskPlanningController extends GetxController {
|
||||
level: LogLevel.error, error: e, stackTrace: stack);
|
||||
}
|
||||
}));
|
||||
|
||||
// Merge/replace the building into dailyTasks
|
||||
bool merged = false;
|
||||
for (var t in dailyTasks) {
|
||||
final idx = t.buildings
|
||||
.indexWhere((b) => b.id.toString() == building.id.toString());
|
||||
if (idx != -1) {
|
||||
t.buildings[idx] = building;
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!merged) {
|
||||
// If not present, add a new TaskPlanningDetailsModel wrapper (fallback)
|
||||
dailyTasks.add(TaskPlanningDetailsModel(
|
||||
id: building.id,
|
||||
name: building.name,
|
||||
projectAddress: "",
|
||||
contactPerson: "",
|
||||
startDate: DateTime.now(),
|
||||
endDate: DateTime.now(),
|
||||
projectStatusId: "",
|
||||
buildings: [building],
|
||||
));
|
||||
}
|
||||
|
||||
// Mark as loaded
|
||||
buildingsWithDetails.add(buildingId.toString());
|
||||
} catch (e, stack) {
|
||||
logSafe("Error fetching infra for building $buildingId",
|
||||
logSafe("Error fetching daily task data",
|
||||
level: LogLevel.error, error: e, stackTrace: stack);
|
||||
} finally {
|
||||
buildingLoadingStates.putIfAbsent(buildingId, () => false.obs);
|
||||
buildingLoadingStates[buildingId]!.value = false;
|
||||
isFetchingTasks.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
@ -307,8 +211,7 @@ class DailyTaskPlanningController extends GetxController {
|
||||
);
|
||||
|
||||
if (response != null && response.isNotEmpty) {
|
||||
employees
|
||||
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
|
||||
employees.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
|
||||
|
||||
if (serviceId == null && organizationId == null) {
|
||||
allEmployeesCache = List.from(employees);
|
||||
@ -316,14 +219,12 @@ class DailyTaskPlanningController extends GetxController {
|
||||
|
||||
final currentEmployeeIds = employees.map((e) => e.id).toSet();
|
||||
|
||||
uploadingStates
|
||||
.removeWhere((key, _) => !currentEmployeeIds.contains(key));
|
||||
uploadingStates.removeWhere((key, _) => !currentEmployeeIds.contains(key));
|
||||
employees.forEach((emp) {
|
||||
uploadingStates.putIfAbsent(emp.id, () => false.obs);
|
||||
});
|
||||
|
||||
selectedEmployees
|
||||
.removeWhere((e) => !currentEmployeeIds.contains(e.id));
|
||||
selectedEmployees.removeWhere((e) => !currentEmployeeIds.contains(e.id));
|
||||
|
||||
logSafe("Employees fetched: ${employees.length}", level: LogLevel.info);
|
||||
} else {
|
||||
@ -338,17 +239,13 @@ class DailyTaskPlanningController extends GetxController {
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Error fetching employees",
|
||||
level: LogLevel.error, error: e, stackTrace: stack);
|
||||
logSafe("Error fetching employees", level: LogLevel.error, error: e, stackTrace: stack);
|
||||
|
||||
if (serviceId == null &&
|
||||
organizationId == null &&
|
||||
allEmployeesCache.isNotEmpty) {
|
||||
if (serviceId == null && organizationId == null && allEmployeesCache.isNotEmpty) {
|
||||
employees.assignAll(allEmployeesCache);
|
||||
|
||||
final cachedEmployeeIds = employees.map((e) => e.id).toSet();
|
||||
uploadingStates
|
||||
.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
|
||||
uploadingStates.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
|
||||
employees.forEach((emp) {
|
||||
uploadingStates.putIfAbsent(emp.id, () => false.obs);
|
||||
});
|
||||
|
||||
@ -4,16 +4,16 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/model/dailyTaskPlanning/work_status_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
|
||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
||||
|
||||
enum ApiStatus { idle, loading, success, failure }
|
||||
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
|
||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
||||
|
||||
enum ApiStatus { idle, loading, success, failure }
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/all_organization_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/all_organization_model.dart';
|
||||
|
||||
class AllOrganizationController extends GetxController {
|
||||
RxList<AllOrganization> organizations = <AllOrganization>[].obs;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
|
||||
|
||||
class OrganizationController extends GetxController {
|
||||
/// List of organizations assigned to the selected project
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/model/tenant/tenant_services_model.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/model/tenant/tenant_services_model.dart';
|
||||
|
||||
class ServiceController extends GetxController {
|
||||
List<Service> services = [];
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/tenant_service.dart';
|
||||
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/controller/permission_controller.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/tenant_service.dart';
|
||||
import 'package:marco/model/tenant/tenant_list_model.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
|
||||
class TenantSelectionController extends GetxController {
|
||||
final TenantService _tenantService = TenantService();
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/tenant_service.dart';
|
||||
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/controller/permission_controller.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/tenant_service.dart';
|
||||
import 'package:marco/model/tenant/tenant_list_model.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
|
||||
class TenantSwitchController extends GetxController {
|
||||
final TenantService _tenantService = TenantService();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
|
||||
class ButtonsController extends MyController {
|
||||
List<bool> selected = List.filled(3, false);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:carousel_slider/carousel_controller.dart';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CarouselsController extends MyController {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||
|
||||
class DialogsController extends MyController {
|
||||
List<String> dummyTexts =
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
|
||||
class LoadersController extends MyController {}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/extensions/string.dart';
|
||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_button.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/extensions/string.dart';
|
||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||
import 'package:marco/helpers/widgets/my_button.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TabsController extends MyController {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:on_field_work/controller/my_controller.dart';
|
||||
import 'package:marco/controller/my_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ToastMessageController extends MyController {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||
import 'package:on_field_work/helpers/theme/app_notifier.dart';
|
||||
import 'package:marco/helpers/services/localizations/language.dart';
|
||||
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'dart:ui';
|
||||
import 'package:on_field_work/helpers/services/localizations/translator.dart';
|
||||
import 'package:marco/helpers/services/localizations/translator.dart';
|
||||
|
||||
extension StringUtil on String {
|
||||
Color get toColor {
|
||||
|
||||
@ -2,53 +2,19 @@ class ApiEndpoints {
|
||||
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://api.onfieldwork.com/api";
|
||||
|
||||
|
||||
static const String getMasterCurrencies = "/Master/currencies/list";
|
||||
static const String getMasterExpensesCategories =
|
||||
"/Master/expenses-categories";
|
||||
static const String getExpensePaymentRequestPayee =
|
||||
"/Expense/payment-request/payee";
|
||||
// Dashboard Module API Endpoints
|
||||
static const String getDashboardAttendanceOverview =
|
||||
"/dashboard/attendance-overview";
|
||||
static const String createExpensePaymentRequest =
|
||||
"/expense/payment-request/create";
|
||||
static const String getExpensePaymentRequestList =
|
||||
"/Expense/get/payment-requests/list";
|
||||
static const String getExpensePaymentRequestDetails =
|
||||
"/Expense/get/payment-request/details";
|
||||
static const String getExpensePaymentRequestFilter =
|
||||
"/Expense/payment-request/filter";
|
||||
static const String updateExpensePaymentRequestStatus =
|
||||
"/Expense/payment-request/action";
|
||||
static const String createExpenseforPR = "/expense/payment-request/action";
|
||||
static const String getExpensePaymentRequestEdit =
|
||||
"/expense/payment-request/edit";
|
||||
|
||||
static const String getDashboardProjectProgress = "/dashboard/progression";
|
||||
static const String getDashboardTasks = "/dashboard/tasks";
|
||||
static const String getDashboardTeams = "/dashboard/teams";
|
||||
static const String getDashboardProjects = "/dashboard/projects";
|
||||
static const String getDashboardMonthlyExpenses =
|
||||
"/Dashboard/expense/monthly";
|
||||
static const String getExpenseTypeReport = "/Dashboard/expense/type";
|
||||
static const String getPendingExpenses = "/Dashboard/expense/pendings";
|
||||
static const String getCollectionOverview = "/dashboard/collection-overview";
|
||||
|
||||
static const String getPurchaseInvoiceOverview =
|
||||
"/dashboard/purchase-invoice-overview";
|
||||
|
||||
///// Projects Module API Endpoints
|
||||
static const String createProject = "/project";
|
||||
|
||||
// Attendance Module API Endpoints
|
||||
static const String getProjects = "/project/list";
|
||||
static const String getGlobalProjects = "/project/list/basic";
|
||||
static const String getTodaysAttendance = "/attendance/project/team";
|
||||
static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId";
|
||||
static const String getAttendanceLogs = "/attendance/project/log";
|
||||
static const String getAttendanceLogView = "/attendance/log/attendance";
|
||||
static const String getRegularizationLogs = "/attendance/regularize";
|
||||
@ -104,7 +70,7 @@ class ApiEndpoints {
|
||||
static const String editExpense = "/Expense/edit";
|
||||
static const String getMasterPaymentModes = "/master/payment-modes";
|
||||
static const String getMasterExpenseStatus = "/master/expenses-status";
|
||||
static const String getMasterExpenseCategory = "/master/expenses-categories";
|
||||
static const String getMasterExpenseTypes = "/master/expenses-types";
|
||||
static const String updateExpenseStatus = "/expense/action";
|
||||
static const String deleteExpense = "/expense/delete";
|
||||
|
||||
@ -134,37 +100,15 @@ class ApiEndpoints {
|
||||
static const getAllOrganizations = "/organization/list";
|
||||
|
||||
static const String getAssignedServices = "/Project/get/assigned/services";
|
||||
static const String getAdvancePayments = '/Expense/get/transactions';
|
||||
|
||||
// Organization Hierarchy endpoints
|
||||
static const String getOrganizationHierarchyList =
|
||||
"/organization/hierarchy/list";
|
||||
static const String manageOrganizationHierarchy =
|
||||
"/organization/hierarchy/manage";
|
||||
// Payment Module API Endpoints
|
||||
static const String createOrder = "/payment/create-order";
|
||||
static const String verifyPayment = "/payment/verify-payment";
|
||||
|
||||
|
||||
// Service Project Module API Endpoints
|
||||
static const String getServiceProjectsList = "/serviceproject/list";
|
||||
static const String getServiceProjectDetail = "/serviceproject/details";
|
||||
static const String getServiceProjectJobList = "/serviceproject/job/list";
|
||||
static const String getServiceProjectJobDetail =
|
||||
"/serviceproject/job/details";
|
||||
static const String editServiceProjectJob = "/serviceproject/job/edit";
|
||||
static const String createServiceProjectJob = "/serviceproject/job/create";
|
||||
static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance";
|
||||
static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log";
|
||||
static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list";
|
||||
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation";
|
||||
static const String getTeamRoles = "/master/team-roles/list";
|
||||
static const String getServiceProjectBranches = "/serviceproject/branch/list";
|
||||
|
||||
static const String getMasterJobStatus = "/Master/job-status/list";
|
||||
|
||||
static const String addJobComment = "/ServiceProject/job/add/comment";
|
||||
|
||||
static const String getJobCommentList = "/ServiceProject/job/comment/list";
|
||||
|
||||
// Infra Project Module API Endpoints
|
||||
static const String getInfraProjectsList = "/project/list";
|
||||
static const String getInfraProjectDetail = "/project/details";
|
||||
// Tenant endpoints
|
||||
static const String createTenantSelf = '/Tenant/self/create';
|
||||
static const String tenantSubscribe = '/Tenant/self/subscription';
|
||||
static const String tenantRenewSubscription = '/Tenant/renew/subscription';
|
||||
static const String getIndustries = '/market/industries';
|
||||
static const String getCurrentSubscription = '/subscription/current';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,13 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
|
||||
import 'package:on_field_work/helpers/services/device_info_service.dart';
|
||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart';
|
||||
import 'package:marco/helpers/services/device_info_service.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
|
||||
Future<void> initializeApp() async {
|
||||
try {
|
||||
|
||||
@ -2,7 +2,7 @@ import 'dart:io';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
|
||||
/// Global logger instance
|
||||
Logger? _appLogger;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class AuthService {
|
||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:on_field_work/helpers/services/local_notification_service.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/services/notification_action_handler.dart';
|
||||
import 'package:marco/helpers/services/local_notification_service.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/notification_action_handler.dart';
|
||||
|
||||
/// Firebase Notification Service
|
||||
class FirebaseNotificationService {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Language {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||
import 'package:marco/helpers/services/localizations/language.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get_utils/src/extensions/string_extensions.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart';
|
||||
import 'package:on_field_work/controller/task_planning/daily_task_controller.dart';
|
||||
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||
import 'package:on_field_work/controller/expense/expense_screen_controller.dart';
|
||||
import 'package:on_field_work/controller/expense/expense_detail_controller.dart';
|
||||
import 'package:on_field_work/controller/directory/directory_controller.dart';
|
||||
import 'package:on_field_work/controller/directory/notes_controller.dart';
|
||||
import 'package:on_field_work/controller/document/user_document_controller.dart';
|
||||
import 'package:on_field_work/controller/document/document_details_controller.dart';
|
||||
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
|
||||
import 'package:on_field_work/helpers/utils/permission_constants.dart';
|
||||
import 'package:marco/controller/attendance/attendance_screen_controller.dart';
|
||||
import 'package:marco/controller/task_planning/daily_task_controller.dart';
|
||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
||||
import 'package:marco/controller/expense/expense_detail_controller.dart';
|
||||
import 'package:marco/controller/directory/directory_controller.dart';
|
||||
import 'package:marco/controller/directory/notes_controller.dart';
|
||||
import 'package:marco/controller/document/user_document_controller.dart';
|
||||
import 'package:marco/controller/document/document_details_controller.dart';
|
||||
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||
|
||||
/// Handles incoming FCM notification actions and updates UI/controllers.
|
||||
class NotificationActionHandler {
|
||||
@ -69,6 +69,7 @@ class NotificationActionHandler {
|
||||
}
|
||||
break;
|
||||
case 'Team_Modified':
|
||||
// Call method to handle team modifications and dashboard update
|
||||
_handleDashboardUpdate(data);
|
||||
break;
|
||||
|
||||
@ -128,11 +129,6 @@ class NotificationActionHandler {
|
||||
/// ---------------------- HANDLERS ----------------------
|
||||
|
||||
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
|
||||
if (!_isCurrentProject(data)) {
|
||||
_logger.i("ℹ️ Ignored task planning update from another project.");
|
||||
return;
|
||||
}
|
||||
|
||||
final projectId = data['ProjectId'];
|
||||
if (projectId == null) {
|
||||
_logger.w("⚠️ TaskPlanning update received without ProjectId: $data");
|
||||
@ -163,17 +159,13 @@ class NotificationActionHandler {
|
||||
}
|
||||
|
||||
static void _handleExpenseUpdated(Map<String, dynamic> data) {
|
||||
if (!_isCurrentProject(data)) {
|
||||
_logger.i("ℹ️ Ignored expense update from another project.");
|
||||
return;
|
||||
}
|
||||
|
||||
final expenseId = data['ExpenseId'];
|
||||
if (expenseId == null) {
|
||||
_logger.w("⚠️ Expense update received without ExpenseId: $data");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update Expense List
|
||||
_safeControllerUpdate<ExpenseController>(
|
||||
onFound: (controller) async {
|
||||
await controller.fetchExpenses();
|
||||
@ -183,6 +175,7 @@ class NotificationActionHandler {
|
||||
'✅ ExpenseController refreshed from expense notification.',
|
||||
);
|
||||
|
||||
// Update Expense Detail (if open and matches this expenseId)
|
||||
_safeControllerUpdate<ExpenseDetailController>(
|
||||
onFound: (controller) async {
|
||||
if (controller.expense.value?.id == expenseId) {
|
||||
@ -197,11 +190,6 @@ class NotificationActionHandler {
|
||||
}
|
||||
|
||||
static void _handleAttendanceUpdated(Map<String, dynamic> data) {
|
||||
if (!_isCurrentProject(data)) {
|
||||
_logger.i("ℹ️ Ignored attendance update from another project.");
|
||||
return;
|
||||
}
|
||||
|
||||
_safeControllerUpdate<AttendanceController>(
|
||||
onFound: (controller) => controller.refreshDataFromNotification(
|
||||
projectId: data['ProjectId'],
|
||||
@ -213,11 +201,6 @@ class NotificationActionHandler {
|
||||
|
||||
static void _handleTaskUpdated(Map<String, dynamic> data,
|
||||
{required bool isComment}) {
|
||||
if (!_isCurrentProject(data)) {
|
||||
_logger.i("ℹ️ Ignored task update from another project.");
|
||||
return;
|
||||
}
|
||||
|
||||
_safeControllerUpdate<DailyTaskController>(
|
||||
onFound: (controller) => controller.refreshTasksFromNotification(
|
||||
projectId: data['ProjectId'],
|
||||
@ -230,15 +213,11 @@ class NotificationActionHandler {
|
||||
|
||||
/// ---------------------- DOCUMENT HANDLER ----------------------
|
||||
static void _handleDocumentModified(Map<String, dynamic> data) {
|
||||
if (!_isCurrentProject(data)) {
|
||||
_logger.i("ℹ️ Ignored document update from another project.");
|
||||
return;
|
||||
}
|
||||
|
||||
String entityTypeId;
|
||||
String entityId;
|
||||
String? documentId = data['DocumentId'];
|
||||
|
||||
// Determine entity type and ID
|
||||
if (data['Keyword'] == 'Employee_Document_Modified') {
|
||||
entityTypeId = Permissions.employeeEntity;
|
||||
entityId = data['EmployeeId'] ?? '';
|
||||
@ -258,6 +237,7 @@ class NotificationActionHandler {
|
||||
_logger.i(
|
||||
"🔔 Document notification received: keyword=${data['Keyword']}, entityTypeId=$entityTypeId, entityId=$entityId, documentId=$documentId");
|
||||
|
||||
// Refresh Document List
|
||||
if (Get.isRegistered<DocumentController>()) {
|
||||
_safeControllerUpdate<DocumentController>(
|
||||
onFound: (controller) async {
|
||||
@ -275,9 +255,11 @@ class NotificationActionHandler {
|
||||
_logger.w('⚠️ DocumentController not registered, skipping list refresh.');
|
||||
}
|
||||
|
||||
// Refresh Document Details (if open)
|
||||
if (documentId != null && Get.isRegistered<DocumentDetailsController>()) {
|
||||
_safeControllerUpdate<DocumentDetailsController>(
|
||||
onFound: (controller) async {
|
||||
// Refresh details regardless of current document
|
||||
await controller.fetchDocumentDetails(documentId);
|
||||
_logger.i(
|
||||
"✅ DocumentDetailsController refreshed for Document $documentId");
|
||||
@ -294,10 +276,13 @@ class NotificationActionHandler {
|
||||
|
||||
/// ---------------------- DIRECTORY HANDLERS ----------------------
|
||||
static void _handleContactModified(Map<String, dynamic> data) {
|
||||
final contactId = data['ContactId'];
|
||||
|
||||
// Always refresh the contact list
|
||||
_safeControllerUpdate<DirectoryController>(
|
||||
onFound: (controller) {
|
||||
controller.fetchContacts();
|
||||
final contactId = data['ContactId'];
|
||||
// If a specific contact is provided, refresh its notes as well
|
||||
if (contactId != null) {
|
||||
controller.fetchCommentsForContact(contactId);
|
||||
}
|
||||
@ -308,6 +293,7 @@ class NotificationActionHandler {
|
||||
'✅ Directory contacts (and notes if applicable) refreshed from notification.',
|
||||
);
|
||||
|
||||
// Refresh notes globally as well
|
||||
_safeControllerUpdate<NotesController>(
|
||||
onFound: (controller) => controller.fetchNotes(),
|
||||
notFoundMessage: '⚠️ NotesController not found, cannot refresh notes.',
|
||||
@ -316,6 +302,7 @@ class NotificationActionHandler {
|
||||
}
|
||||
|
||||
static void _handleContactNoteModified(Map<String, dynamic> data) {
|
||||
// Refresh both contacts and notes when a note is modified
|
||||
_handleContactModified(data);
|
||||
}
|
||||
|
||||
@ -337,11 +324,6 @@ class NotificationActionHandler {
|
||||
|
||||
/// ---------------------- DASHBOARD HANDLER ----------------------
|
||||
static void _handleDashboardUpdate(Map<String, dynamic> data) {
|
||||
if (!_isCurrentProject(data)) {
|
||||
_logger.i("ℹ️ Ignored dashboard update from another project.");
|
||||
return;
|
||||
}
|
||||
|
||||
_safeControllerUpdate<DashboardController>(
|
||||
onFound: (controller) async {
|
||||
final type = data['type'] ?? '';
|
||||
@ -365,9 +347,11 @@ class NotificationActionHandler {
|
||||
controller.projectController.selectedProjectId.value;
|
||||
final projectIdsString = data['ProjectIds'] ?? '';
|
||||
|
||||
// Convert comma-separated string to List<String>
|
||||
final notificationProjectIds =
|
||||
projectIdsString.split(',').map((e) => e.trim()).toList();
|
||||
|
||||
// Refresh only if current project ID is in the list
|
||||
if (notificationProjectIds.contains(currentProjectId)) {
|
||||
await controller.fetchDashboardTeams(projectId: currentProjectId);
|
||||
}
|
||||
@ -391,40 +375,17 @@ class NotificationActionHandler {
|
||||
|
||||
/// ---------------------- UTILITY ----------------------
|
||||
|
||||
static bool _isCurrentProject(Map<String, dynamic> data) {
|
||||
try {
|
||||
final dashboard = Get.find<DashboardController>();
|
||||
final currentProjectId =
|
||||
dashboard.projectController.selectedProjectId.value;
|
||||
final notificationProjectId = data['ProjectId']?.toString();
|
||||
|
||||
if (notificationProjectId == null || notificationProjectId.isEmpty) {
|
||||
return true; // No project info → allow global refresh
|
||||
}
|
||||
|
||||
return notificationProjectId == currentProjectId;
|
||||
} catch (e) {
|
||||
_logger.w("⚠️ Could not verify project context: $e");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static void _safeControllerUpdate<T>({
|
||||
required void Function(T controller) onFound,
|
||||
required String notFoundMessage,
|
||||
required String successMessage,
|
||||
}) {
|
||||
if (!Get.isRegistered<T>()) {
|
||||
_logger.w(notFoundMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final controller = Get.find<T>();
|
||||
onFound(controller);
|
||||
_logger.i(successMessage);
|
||||
} catch (e) {
|
||||
_logger.w('⚠️ Error updating controller: $e');
|
||||
_logger.w(notFoundMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
39
lib/helpers/services/payment_service.dart
Normal file
39
lib/helpers/services/payment_service.dart
Normal file
@ -0,0 +1,39 @@
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class PaymentService {
|
||||
/// Create a Razorpay order on backend
|
||||
Future<Map<String, dynamic>?> createOrder(double amount) async {
|
||||
try {
|
||||
logSafe("🟢 Calling createPaymentOrder API with amount: ₹$amount");
|
||||
final response = await ApiService.createPaymentOrder(amount);
|
||||
logSafe("🧩 Raw response in PaymentService: $response");
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
logSafe("❌ Error in createOrder: $e", level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify Razorpay payment signature
|
||||
Future<Map<String, dynamic>?> verifyPayment({
|
||||
required String orderId,
|
||||
required String paymentId,
|
||||
required String signature,
|
||||
}) async {
|
||||
try {
|
||||
logSafe("🟢 Calling verifyPayment API...");
|
||||
final response = await ApiService.verifyPayment(
|
||||
orderId: orderId,
|
||||
paymentId: paymentId,
|
||||
signature: signature,
|
||||
);
|
||||
logSafe("✅ VerifyPayment API response: $response");
|
||||
return response;
|
||||
} catch (e) {
|
||||
logSafe("❌ Error in verifyPayment: $e", level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,13 +2,13 @@ import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/model/user_permission.dart';
|
||||
import 'package:on_field_work/model/employees/employee_info.dart';
|
||||
import 'package:on_field_work/model/projects_model.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/model/user_permission.dart';
|
||||
import 'package:marco/model/employees/employee_info.dart';
|
||||
import 'package:marco/model/projects_model.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||
|
||||
class PermissionService {
|
||||
// In-memory cache keyed by user token
|
||||
|
||||
@ -2,13 +2,13 @@ import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||
import 'package:on_field_work/model/employees/employee_info.dart';
|
||||
import 'package:on_field_work/model/user_permission.dart';
|
||||
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/localizations/language.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/model/employees/employee_info.dart';
|
||||
import 'package:marco/model/user_permission.dart';
|
||||
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
|
||||
|
||||
class LocalStorage {
|
||||
static const String _loggedInUserKey = "user";
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/controller/project_controller.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
||||
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/auth_service.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/tenant/tenant_list_model.dart';
|
||||
|
||||
/// Abstract interface for tenant service functionality
|
||||
abstract class ITenantService {
|
||||
@ -63,39 +64,29 @@ class TenantService implements ITenantService {
|
||||
{bool hasRetried = false}) async {
|
||||
try {
|
||||
final headers = await _authorizedHeaders();
|
||||
logSafe("➡️ GET $_baseUrl/auth/get/user/tenants\nHeaders: $headers",
|
||||
level: LogLevel.info);
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse("$_baseUrl/auth/get/user/tenants"),
|
||||
headers: headers,
|
||||
);
|
||||
final response = await http
|
||||
.get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers);
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
// ✅ Handle empty response BEFORE decoding
|
||||
if (response.body.isEmpty || response.body.trim().isEmpty) {
|
||||
logSafe("❌ Empty tenant response — auto logout");
|
||||
await LocalStorage.logout();
|
||||
return null;
|
||||
}
|
||||
logSafe(
|
||||
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
|
||||
level: LogLevel.info);
|
||||
|
||||
Map<String, dynamic> data;
|
||||
try {
|
||||
data = jsonDecode(response.body);
|
||||
} catch (e) {
|
||||
logSafe("❌ Invalid JSON in tenant response — auto logout");
|
||||
await LocalStorage.logout();
|
||||
return null;
|
||||
}
|
||||
|
||||
// SUCCESS CASE
|
||||
if (response.statusCode == 200 && data['success'] == true) {
|
||||
final list = data['data'];
|
||||
if (list is! List) return null;
|
||||
return List<Map<String, dynamic>>.from(list);
|
||||
logSafe("✅ Tenants fetched successfully.");
|
||||
return List<Map<String, dynamic>>.from(data['data']);
|
||||
}
|
||||
|
||||
// TOKEN EXPIRED
|
||||
if (response.statusCode == 401 && !hasRetried) {
|
||||
logSafe("⚠️ Unauthorized while fetching tenants. Refreshing token...",
|
||||
level: LogLevel.warning);
|
||||
final refreshed = await AuthService.refreshToken();
|
||||
if (refreshed) return getTenants(hasRetried: true);
|
||||
logSafe("❌ Token refresh failed while fetching tenants.",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -170,4 +161,65 @@ class TenantService implements ITenantService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> createTenant(
|
||||
Map<String, dynamic> payload) async {
|
||||
try {
|
||||
logSafe("🟢 Creating tenant: $payload");
|
||||
final resp = await ApiService.createTenantSelf(payload);
|
||||
logSafe("🧩 createTenant response: $resp");
|
||||
return resp;
|
||||
} catch (e, s) {
|
||||
logSafe("❌ Exception in createTenant: $e\n$s", level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> subscribeTenant(
|
||||
Map<String, dynamic> payload) async {
|
||||
try {
|
||||
logSafe("🟢 Subscribing tenant: $payload");
|
||||
final resp = await ApiService.subscribeTenant(payload);
|
||||
logSafe("🧩 subscribeTenant response: $resp");
|
||||
return resp;
|
||||
} catch (e, s) {
|
||||
logSafe("❌ Exception in subscribeTenant: $e\n$s", level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> renewSubscription(
|
||||
Map<String, dynamic> payload) async {
|
||||
try {
|
||||
logSafe("🟢 Renewing subscription: $payload");
|
||||
final resp = await ApiService.renewTenantSubscription(payload);
|
||||
logSafe("🧩 renewSubscription response: $resp");
|
||||
return resp;
|
||||
} catch (e, s) {
|
||||
logSafe("❌ Exception in renewSubscription: $e\n$s",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>?> getIndustries() async {
|
||||
try {
|
||||
logSafe("🟢 Fetching industries via ApiService...");
|
||||
|
||||
// ApiService.getIndustries() directly returns a List<dynamic>
|
||||
final response = await ApiService.getIndustries();
|
||||
|
||||
// No need to dig into response["data"], because response itself is a List
|
||||
if (response == null || response.isEmpty) {
|
||||
logSafe("💡 ! No industries found (empty list)");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Safely cast list of maps
|
||||
return List<Map<String, dynamic>>.from(response);
|
||||
} catch (e, s) {
|
||||
logSafe("❌ Exception in getIndustries: $e\n$s", level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
|
||||
enum LeftBarThemeType { light, dark }
|
||||
enum ContentThemeType { light, dark }
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
||||
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my.dart';
|
||||
import 'package:marco/helpers/services/localizations/language.dart';
|
||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/widgets/my.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
@ -6,13 +6,13 @@
|
||||
* */
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_constant.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_screen_media.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text_style.dart';
|
||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/widgets/my.dart';
|
||||
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||
import 'package:marco/helpers/widgets/my_constant.dart';
|
||||
import 'package:marco/helpers/widgets/my_screen_media.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
@ -230,7 +230,7 @@ class AppStyle {
|
||||
containerRadius: AppStyle.containerRadius.medium,
|
||||
cardRadius: AppStyle.cardRadius.medium,
|
||||
buttonRadius: AppStyle.buttonRadius.medium,
|
||||
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'On Field Work', route: '/client/dashboard'),
|
||||
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'Marco', route: '/client/dashboard'),
|
||||
));
|
||||
bool isMobile = true;
|
||||
try {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:on_field_work/helpers/services/json_decoder.dart';
|
||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
||||
import 'package:on_field_work/helpers/services/localizations/translator.dart';
|
||||
import 'package:on_field_work/helpers/services/navigation_services.dart';
|
||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||
import 'package:on_field_work/helpers/theme/app_notifier.dart';
|
||||
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/services/json_decoder.dart';
|
||||
import 'package:marco/helpers/services/localizations/language.dart';
|
||||
import 'package:marco/helpers/services/localizations/translator.dart';
|
||||
import 'package:marco/helpers/services/navigation_services.dart';
|
||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/helpers/widgets/wave_background.dart';
|
||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/wave_background.dart';
|
||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
|
||||
class ThemeOption {
|
||||
final String label;
|
||||
@ -63,9 +63,6 @@ class ThemeController extends GetxController {
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
showApplied.value = false;
|
||||
|
||||
// Navigate to dashboard after applying theme
|
||||
Get.offAllNamed('/dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
|
||||
class BaseBottomSheet extends StatefulWidget {
|
||||
final String title;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class ContactPickerHelper {
|
||||
static Future<String?> pickIndianPhoneNumber(BuildContext context) async {
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DateTimeUtils {
|
||||
/// Default date format
|
||||
static const String defaultFormat = 'dd MMM yyyy';
|
||||
|
||||
/// Converts a UTC datetime string to local time and formats it.
|
||||
static String convertUtcToLocal(String utcTimeString,
|
||||
{String format = 'dd-MM-yyyy'}) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class LauncherUtils {
|
||||
/// Launches the phone dialer with the provided phone number
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
||||
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_dashed_divider.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_navigation_mixin.dart';
|
||||
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/widgets/my_dashed_divider.dart';
|
||||
import 'package:marco/helpers/widgets/my_navigation_mixin.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
mixin UIMixin {
|
||||
|
||||
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