Compare commits
No commits in common. "main" and "Dashboard_Charts" have entirely different histories.
main
...
Dashboard_
@ -1,4 +1,4 @@
|
|||||||
# On Field Work
|
# marco
|
||||||
|
|
||||||
A new Flutter project.
|
A new Flutter project.
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
// Define the namespace for your Android application
|
// Define the namespace for your Android application
|
||||||
namespace = "com.marcoonfieldwork.aiot"
|
namespace = "com.marco.aiot"
|
||||||
// Set the compile SDK version based on Flutter's configuration
|
// Set the compile SDK version based on Flutter's configuration
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
// Set the NDK version based on Flutter's configuration
|
// Set the NDK version based on Flutter's configuration
|
||||||
@ -37,7 +37,7 @@ android {
|
|||||||
// Default configuration for your application
|
// Default configuration for your application
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// Specify your unique Application ID. This identifies your app on Google Play.
|
// Specify your unique Application ID. This identifies your app on Google Play.
|
||||||
applicationId = "com.marcoonfieldwork.aiot"
|
applicationId = "com.marco.aiot"
|
||||||
// Set minimum and target SDK versions based on Flutter's configuration
|
// Set minimum and target SDK versions based on Flutter's configuration
|
||||||
minSdk = 23
|
minSdk = 23
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
"client_info": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
|
"mobilesdk_app_id": "1:626581282477:android:8d3cf5009ff92ef67ff024",
|
||||||
"android_client_info": {
|
"android_client_info": {
|
||||||
"package_name": "com.marcoonfieldwork.aiot"
|
"package_name": "com.marco.aiot"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [],
|
"oauth_client": [],
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="On Field Work"
|
android:label="Marco"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.marcoonfieldwork.aiot
|
package com.marco.aiot
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.6.0" apply false
|
id "com.android.application" version "8.6.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "2.2.21" apply false
|
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ===============================
|
|
||||||
# Flutter APK Build Script (AAB Disabled)
|
|
||||||
# ===============================
|
|
||||||
|
|
||||||
# Exit immediately if a command exits with a non-zero status
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Colors for pretty output
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# App info
|
|
||||||
APP_NAME="On Field Work"
|
|
||||||
BUILD_DIR="build/app/outputs"
|
|
||||||
|
|
||||||
echo -e "${CYAN}🚀 Starting Flutter build script for $APP_NAME...${NC}"
|
|
||||||
|
|
||||||
# Step 1: Clean previous builds
|
|
||||||
echo -e "${YELLOW}🧹 Cleaning previous builds...${NC}"
|
|
||||||
flutter clean
|
|
||||||
|
|
||||||
# Step 2: Get dependencies
|
|
||||||
echo -e "${YELLOW}📦 Fetching dependencies...${NC}"
|
|
||||||
flutter pub get
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# Step 3: Build AAB (Commented)
|
|
||||||
# ==============================
|
|
||||||
# echo -e "${CYAN}🏗 Building AAB file...${NC}"
|
|
||||||
# flutter build appbundle --release
|
|
||||||
|
|
||||||
# Step 4: Build APK
|
|
||||||
echo -e "${CYAN}🏗 Building APK file...${NC}"
|
|
||||||
flutter build apk --release
|
|
||||||
|
|
||||||
# Step 5: Show output paths
|
|
||||||
# AAB_PATH="$BUILD_DIR/bundle/release/app-release.aab"
|
|
||||||
APK_PATH="$BUILD_DIR/apk/release/app-release.apk"
|
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Build completed successfully!${NC}"
|
|
||||||
# echo -e "${YELLOW}📍 AAB file: ${CYAN}$AAB_PATH${NC}"
|
|
||||||
echo -e "${YELLOW}📍 APK file: ${CYAN}$APK_PATH${NC}"
|
|
||||||
|
|
||||||
# Optional: open the folder (Mac/Linux)
|
|
||||||
if command -v xdg-open &> /dev/null
|
|
||||||
then
|
|
||||||
xdg-open "$BUILD_DIR"
|
|
||||||
elif command -v open &> /dev/null
|
|
||||||
then
|
|
||||||
open "$BUILD_DIR"
|
|
||||||
fi
|
|
||||||
@ -368,7 +368,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -384,7 +384,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@ -401,7 +401,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@ -416,7 +416,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@ -547,7 +547,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@ -569,7 +569,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.marcoonfieldwork.aiot;
|
PRODUCT_BUNDLE_IDENTIFIER = com.marco.aiot;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>On Field Work</string>
|
<string>Marco</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>on field work</string>
|
<string>marco</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
@ -8,5 +8,5 @@ class AppConstant {
|
|||||||
static int iOSAppVersion = 1;
|
static int iOSAppVersion = 1;
|
||||||
static String version = "1.0.0";
|
static String version = "1.0.0";
|
||||||
|
|
||||||
static String get appName => 'On Field Work';
|
static String get appName => 'Marco';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,62 +1,44 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:on_field_work/controller/dashboard/dashboard_controller.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/controller/project_controller.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
|
||||||
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
|
import 'package:marco/model/attendance/attendance_model.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
import 'package:on_field_work/model/attendance/attendance_log_model.dart';
|
import 'package:marco/model/employees/employee_model.dart';
|
||||||
import 'package:on_field_work/model/attendance/attendance_log_view_model.dart';
|
import 'package:marco/model/attendance/attendance_log_model.dart';
|
||||||
import 'package:on_field_work/model/attendance/attendance_model.dart';
|
import 'package:marco/model/regularization_log_model.dart';
|
||||||
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
|
import 'package:marco/model/attendance/attendance_log_view_model.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
|
||||||
import 'package:on_field_work/model/project_model.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
import 'package:on_field_work/model/regularization_log_model.dart';
|
|
||||||
|
|
||||||
class AttendanceController extends GetxController {
|
class AttendanceController extends GetxController {
|
||||||
// ------------------ Data Models ------------------
|
// Data models
|
||||||
final List<AttendanceModel> attendances = <AttendanceModel>[];
|
List<AttendanceModel> attendances = [];
|
||||||
final List<ProjectModel> projects = <ProjectModel>[];
|
List<ProjectModel> projects = [];
|
||||||
final List<EmployeeModel> employees = <EmployeeModel>[];
|
List<EmployeeModel> employees = [];
|
||||||
final List<AttendanceLogModel> attendanceLogs = <AttendanceLogModel>[];
|
List<AttendanceLogModel> attendanceLogs = [];
|
||||||
final List<RegularizationLogModel> regularizationLogs =
|
List<RegularizationLogModel> regularizationLogs = [];
|
||||||
<RegularizationLogModel>[];
|
List<AttendanceLogViewModel> attendenceLogsView = [];
|
||||||
final List<AttendanceLogViewModel> attendenceLogsView =
|
|
||||||
<AttendanceLogViewModel>[];
|
|
||||||
|
|
||||||
// ------------------ Organizations ------------------
|
// States
|
||||||
final List<Organization> organizations = <Organization>[];
|
String selectedTab = 'Employee List';
|
||||||
Organization? selectedOrganization;
|
DateTime? startDateAttendance;
|
||||||
final RxBool isLoadingOrganizations = false.obs;
|
DateTime? endDateAttendance;
|
||||||
|
|
||||||
// ------------------ States ------------------
|
final isLoading = true.obs;
|
||||||
String selectedTab = 'todaysAttendance';
|
final isLoadingProjects = true.obs;
|
||||||
|
final isLoadingEmployees = true.obs;
|
||||||
// ✅ Reactive date range
|
final isLoadingAttendanceLogs = true.obs;
|
||||||
final Rx<DateTime> startDateAttendance =
|
final isLoadingRegularizationLogs = true.obs;
|
||||||
DateTime.now().subtract(const Duration(days: 7)).obs;
|
final isLoadingLogView = true.obs;
|
||||||
final Rx<DateTime> endDateAttendance =
|
final uploadingStates = <String, RxBool>{}.obs;
|
||||||
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;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -66,169 +48,135 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
void _initializeDefaults() {
|
void _initializeDefaults() {
|
||||||
_setDefaultDateRange();
|
_setDefaultDateRange();
|
||||||
|
fetchProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setDefaultDateRange() {
|
void _setDefaultDateRange() {
|
||||||
final DateTime today = DateTime.now();
|
final today = DateTime.now();
|
||||||
startDateAttendance.value = today.subtract(const Duration(days: 7));
|
startDateAttendance = today.subtract(const Duration(days: 7));
|
||||||
endDateAttendance.value = today.subtract(const Duration(days: 1));
|
endDateAttendance = today.subtract(const Duration(days: 1));
|
||||||
logSafe(
|
logSafe(
|
||||||
'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}',
|
"Default date range set: $startDateAttendance to $endDateAttendance");
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Computed Filters ------------------
|
// ------------------ Project & Employee ------------------
|
||||||
List<EmployeeModel> get filteredEmployees {
|
/// Called when a notification says attendance has been updated
|
||||||
final String query = searchQuery.value.trim().toLowerCase();
|
|
||||||
if (query.isEmpty) return employees;
|
|
||||||
return employees
|
|
||||||
.where(
|
|
||||||
(EmployeeModel e) => e.name.toLowerCase().contains(query),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<AttendanceLogModel> get filteredLogs {
|
|
||||||
final String query = searchQuery.value.trim().toLowerCase();
|
|
||||||
if (query.isEmpty) return attendanceLogs;
|
|
||||||
return attendanceLogs
|
|
||||||
.where(
|
|
||||||
(AttendanceLogModel log) => log.name.toLowerCase().contains(query),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<RegularizationLogModel> get filteredRegularizationLogs {
|
|
||||||
final String query = searchQuery.value.trim().toLowerCase();
|
|
||||||
if (query.isEmpty) return regularizationLogs;
|
|
||||||
return regularizationLogs
|
|
||||||
.where(
|
|
||||||
(RegularizationLogModel log) =>
|
|
||||||
log.name.toLowerCase().contains(query),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------ Project & Employee APIs ------------------
|
|
||||||
Future<void> refreshDataFromNotification({String? projectId}) async {
|
Future<void> refreshDataFromNotification({String? projectId}) async {
|
||||||
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
projectId ??= Get.find<ProjectController>().selectedProject?.id;
|
||||||
if (projectId == null) {
|
if (projectId == null) {
|
||||||
logSafe(
|
logSafe("No project selected for attendance refresh from notification",
|
||||||
'No project selected for attendance refresh from notification',
|
level: LogLevel.warning);
|
||||||
level: LogLevel.warning,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await fetchProjectData(projectId);
|
await fetchProjectData(projectId);
|
||||||
logSafe(
|
logSafe(
|
||||||
'Attendance data refreshed from notification for project $projectId',
|
"Attendance data refreshed from notification for project $projectId");
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchTodaysAttendance(String? projectId) async {
|
Future<void> fetchProjects() async {
|
||||||
|
isLoadingProjects.value = true;
|
||||||
|
|
||||||
|
final response = await ApiService.getProjects();
|
||||||
|
if (response != null && response.isNotEmpty) {
|
||||||
|
projects = response.map((e) => ProjectModel.fromJson(e)).toList();
|
||||||
|
logSafe("Projects fetched: ${projects.length}");
|
||||||
|
} else {
|
||||||
|
projects = [];
|
||||||
|
logSafe("Failed to fetch projects or no projects available.",
|
||||||
|
level: LogLevel.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingProjects.value = false;
|
||||||
|
update(['attendance_dashboard_controller']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoadingEmployees.value = true;
|
isLoadingEmployees.value = true;
|
||||||
|
|
||||||
final List<dynamic>? response = await ApiService.getTodaysAttendance(
|
final response = await ApiService.getEmployeesByProject(projectId);
|
||||||
projectId,
|
|
||||||
organizationId: selectedOrganization?.id,
|
|
||||||
);
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
employees
|
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
|
||||||
..clear()
|
for (var emp in employees) {
|
||||||
..addAll(
|
|
||||||
response
|
|
||||||
.map<EmployeeModel>(
|
|
||||||
(dynamic e) => EmployeeModel.fromJson(
|
|
||||||
e as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final EmployeeModel emp in employees) {
|
|
||||||
uploadingStates[emp.id] = false.obs;
|
uploadingStates[emp.id] = false.obs;
|
||||||
}
|
}
|
||||||
|
logSafe("Employees fetched: ${employees.length} for project $projectId");
|
||||||
logSafe(
|
|
||||||
'Employees fetched: ${employees.length} for project $projectId',
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
logSafe(
|
logSafe("Failed to fetch employees for project $projectId",
|
||||||
'Failed to fetch employees for project $projectId',
|
level: LogLevel.error);
|
||||||
level: LogLevel.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingEmployees.value = false;
|
isLoadingEmployees.value = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchOrganizations(String projectId) async {
|
|
||||||
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}');
|
|
||||||
} else {
|
|
||||||
logSafe(
|
|
||||||
'Failed to fetch organizations for project $projectId',
|
|
||||||
level: LogLevel.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingOrganizations.value = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------ Attendance Capture ------------------
|
// ------------------ Attendance Capture ------------------
|
||||||
|
|
||||||
Future<bool> captureAndUploadAttendance(
|
Future<bool> captureAndUploadAttendance(
|
||||||
String id,
|
String id,
|
||||||
String employeeId,
|
String employeeId,
|
||||||
String projectId, {
|
String projectId, {
|
||||||
String comment = 'Marked via mobile app',
|
String comment = "Marked via mobile app",
|
||||||
required int action,
|
required int action,
|
||||||
bool imageCapture = true,
|
bool imageCapture = true,
|
||||||
String? markTime,
|
String? markTime, // still optional in controller
|
||||||
String? date,
|
String? date, // new optional param
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
_setUploading(employeeId, true);
|
uploadingStates[employeeId]?.value = true;
|
||||||
|
|
||||||
final XFile? image = await _captureAndPrepareImage(
|
XFile? image;
|
||||||
employeeId: employeeId,
|
if (imageCapture) {
|
||||||
imageCapture: imageCapture,
|
image = await ImagePicker()
|
||||||
);
|
.pickImage(source: ImageSource.camera, imageQuality: 80);
|
||||||
if (imageCapture && image == null) {
|
if (image == null) {
|
||||||
return false;
|
logSafe("Image capture cancelled.", level: LogLevel.warning);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final compressedBytes =
|
||||||
|
await compressImageToUnder100KB(File(image.path));
|
||||||
|
if (compressedBytes == null) {
|
||||||
|
logSafe("Image compression failed.", level: LogLevel.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final compressedFile = await saveCompressedImageToFile(compressedBytes);
|
||||||
|
image = XFile(compressedFile.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Position? position = await _getCurrentPositionSafely();
|
if (!await _handleLocationPermission()) return false;
|
||||||
if (position == null) return false;
|
final position = await Geolocator.getCurrentPosition(
|
||||||
|
desiredAccuracy: LocationAccuracy.high);
|
||||||
|
|
||||||
final String imageName = imageCapture
|
final imageName = imageCapture
|
||||||
? ApiService.generateImageName(
|
? ApiService.generateImageName(employeeId, employees.length + 1)
|
||||||
employeeId,
|
: "";
|
||||||
employees.length + 1,
|
|
||||||
)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
final DateTime effectiveDate =
|
// ---------------- DATE / TIME LOGIC ----------------
|
||||||
_resolveEffectiveDateForAction(action, employeeId);
|
final now = DateTime.now();
|
||||||
|
|
||||||
final DateTime now = DateTime.now();
|
// Default effectiveDate = now
|
||||||
final String formattedMarkTime =
|
DateTime effectiveDate = now;
|
||||||
markTime ?? DateFormat('hh:mm a').format(now);
|
|
||||||
final String formattedDate =
|
if (action == 1) {
|
||||||
|
// Checkout
|
||||||
|
// Try to find today's open log for this employee
|
||||||
|
final log = attendanceLogs.firstWhereOrNull(
|
||||||
|
(log) => log.employeeId == employeeId && log.checkOut == null,
|
||||||
|
);
|
||||||
|
if (log?.checkIn != null) {
|
||||||
|
effectiveDate = log!.checkIn!; // use check-in date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now);
|
||||||
|
|
||||||
|
final formattedDate =
|
||||||
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
|
date ?? DateFormat('yyyy-MM-dd').format(effectiveDate);
|
||||||
|
|
||||||
final bool result = await ApiService.uploadAttendanceImage(
|
// ---------------- API CALL ----------------
|
||||||
|
final result = await ApiService.uploadAttendanceImage(
|
||||||
id,
|
id,
|
||||||
employeeId,
|
employeeId,
|
||||||
image,
|
image,
|
||||||
@ -243,99 +191,15 @@ class AttendanceController extends GetxController {
|
|||||||
date: formattedDate,
|
date: formattedDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result) {
|
logSafe(
|
||||||
logSafe(
|
"Attendance uploaded for $employeeId, action: $action, date: $formattedDate");
|
||||||
'Attendance uploaded for $employeeId, action: $action, date: $formattedDate',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Get.isRegistered<DashboardController>()) {
|
|
||||||
final DashboardController dashboardController =
|
|
||||||
Get.find<DashboardController>();
|
|
||||||
await dashboardController.fetchTodaysAttendance(projectId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe(
|
logSafe("Error uploading attendance",
|
||||||
'Error uploading attendance',
|
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
level: LogLevel.error,
|
|
||||||
error: e,
|
|
||||||
stackTrace: stacktrace,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
_setUploading(employeeId, false);
|
uploadingStates[employeeId]?.value = false;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<XFile?> _captureAndPrepareImage({
|
|
||||||
required String employeeId,
|
|
||||||
required bool imageCapture,
|
|
||||||
}) async {
|
|
||||||
if (!imageCapture) return null;
|
|
||||||
|
|
||||||
final XFile? rawImage = await ImagePicker().pickImage(
|
|
||||||
source: ImageSource.camera,
|
|
||||||
imageQuality: 80,
|
|
||||||
);
|
|
||||||
if (rawImage == null) {
|
|
||||||
logSafe(
|
|
||||||
'Image capture cancelled.',
|
|
||||||
level: LogLevel.warning,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final File timestampedFile = await TimestampImageHelper.addTimestamp(
|
|
||||||
imageFile: File(rawImage.path),
|
|
||||||
);
|
|
||||||
|
|
||||||
final List<int>? compressedBytes =
|
|
||||||
await compressImageToUnder100KB(timestampedFile);
|
|
||||||
if (compressedBytes == null) {
|
|
||||||
logSafe(
|
|
||||||
'Image compression failed.',
|
|
||||||
level: LogLevel.error,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIX: convert List<int> -> Uint8List
|
|
||||||
final Uint8List compressedUint8List = Uint8List.fromList(compressedBytes);
|
|
||||||
|
|
||||||
final File compressedFile =
|
|
||||||
await saveCompressedImageToFile(compressedUint8List);
|
|
||||||
return XFile(compressedFile.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Position?> _getCurrentPositionSafely() async {
|
|
||||||
final bool permissionGranted = await _handleLocationPermission();
|
|
||||||
if (!permissionGranted) return null;
|
|
||||||
|
|
||||||
return Geolocator.getCurrentPosition(
|
|
||||||
desiredAccuracy: LocationAccuracy.high,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime _resolveEffectiveDateForAction(int action, String employeeId) {
|
|
||||||
final DateTime now = DateTime.now();
|
|
||||||
if (action != 1) return now;
|
|
||||||
|
|
||||||
final AttendanceLogModel? log = attendanceLogs.firstWhereOrNull(
|
|
||||||
(AttendanceLogModel log) =>
|
|
||||||
log.employeeId == employeeId && log.checkOut == null,
|
|
||||||
);
|
|
||||||
|
|
||||||
return log?.checkIn ?? now;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setUploading(String employeeId, bool value) {
|
|
||||||
final RxBool? state = uploadingStates[employeeId];
|
|
||||||
if (state != null) {
|
|
||||||
state.value = value;
|
|
||||||
} else {
|
|
||||||
uploadingStates[employeeId] = value.obs;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,19 +209,14 @@ class AttendanceController extends GetxController {
|
|||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
logSafe(
|
logSafe('Location permissions are denied', level: LogLevel.warning);
|
||||||
'Location permissions are denied',
|
|
||||||
level: LogLevel.warning,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission == LocationPermission.deniedForever) {
|
if (permission == LocationPermission.deniedForever) {
|
||||||
logSafe(
|
logSafe('Location permissions are permanently denied',
|
||||||
'Location permissions are permanently denied',
|
level: LogLevel.error);
|
||||||
level: LogLevel.error,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,40 +224,22 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Attendance Logs ------------------
|
// ------------------ Attendance Logs ------------------
|
||||||
Future<void> fetchAttendanceLogs(
|
|
||||||
String? projectId, {
|
Future<void> fetchAttendanceLogs(String? projectId,
|
||||||
DateTime? dateFrom,
|
{DateTime? dateFrom, DateTime? dateTo}) async {
|
||||||
DateTime? dateTo,
|
|
||||||
}) async {
|
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoadingAttendanceLogs.value = true;
|
isLoadingAttendanceLogs.value = true;
|
||||||
|
|
||||||
final List<dynamic>? response = await ApiService.getAttendanceLogs(
|
final response = await ApiService.getAttendanceLogs(projectId,
|
||||||
projectId,
|
dateFrom: dateFrom, dateTo: dateTo);
|
||||||
dateFrom: dateFrom,
|
|
||||||
dateTo: dateTo,
|
|
||||||
organizationId: selectedOrganization?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendanceLogs
|
attendanceLogs =
|
||||||
..clear()
|
response.map((e) => AttendanceLogModel.fromJson(e)).toList();
|
||||||
..addAll(
|
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
|
||||||
response
|
|
||||||
.map<AttendanceLogModel>(
|
|
||||||
(dynamic e) => AttendanceLogModel.fromJson(
|
|
||||||
e as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
logSafe('Attendance logs fetched: ${attendanceLogs.length}');
|
|
||||||
} else {
|
} else {
|
||||||
logSafe(
|
logSafe("Failed to fetch attendance logs for project $projectId",
|
||||||
'Failed to fetch attendance logs for project $projectId',
|
level: LogLevel.error);
|
||||||
level: LogLevel.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingAttendanceLogs.value = false;
|
isLoadingAttendanceLogs.value = false;
|
||||||
@ -406,70 +247,42 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
||||||
final Map<String, List<AttendanceLogModel>> groupedLogs =
|
final groupedLogs = <String, List<AttendanceLogModel>>{};
|
||||||
<String, List<AttendanceLogModel>>{};
|
|
||||||
|
|
||||||
for (final AttendanceLogModel logItem in attendanceLogs) {
|
for (var logItem in attendanceLogs) {
|
||||||
final String checkInDate = logItem.checkIn != null
|
final checkInDate = logItem.checkIn != null
|
||||||
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
|
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
|
||||||
: 'Unknown';
|
: 'Unknown';
|
||||||
|
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
|
||||||
groupedLogs.putIfAbsent(
|
|
||||||
checkInDate,
|
|
||||||
() => <AttendanceLogModel>[],
|
|
||||||
)..add(logItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<MapEntry<String, List<AttendanceLogModel>>> sortedEntries =
|
final sortedEntries = groupedLogs.entries.toList()
|
||||||
groupedLogs.entries.toList()
|
..sort((a, b) {
|
||||||
..sort(
|
if (a.key == 'Unknown') return 1;
|
||||||
(MapEntry<String, List<AttendanceLogModel>> a,
|
if (b.key == 'Unknown') return -1;
|
||||||
MapEntry<String, List<AttendanceLogModel>> b) {
|
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
|
||||||
if (a.key == 'Unknown') return 1;
|
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
|
||||||
if (b.key == 'Unknown') return -1;
|
return dateB.compareTo(dateA);
|
||||||
|
});
|
||||||
|
|
||||||
final DateTime dateA = DateFormat('dd MMM yyyy').parse(a.key);
|
return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
|
||||||
final DateTime dateB = DateFormat('dd MMM yyyy').parse(b.key);
|
|
||||||
return dateB.compareTo(dateA);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return Map<String, List<AttendanceLogModel>>.fromEntries(
|
|
||||||
sortedEntries,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Regularization Logs ------------------
|
// ------------------ Regularization Logs ------------------
|
||||||
|
|
||||||
Future<void> fetchRegularizationLogs(String? projectId) async {
|
Future<void> fetchRegularizationLogs(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoadingRegularizationLogs.value = true;
|
isLoadingRegularizationLogs.value = true;
|
||||||
|
|
||||||
final List<dynamic>? response = await ApiService.getRegularizationLogs(
|
final response = await ApiService.getRegularizationLogs(projectId);
|
||||||
projectId,
|
|
||||||
organizationId: selectedOrganization?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
regularizationLogs
|
regularizationLogs =
|
||||||
..clear()
|
response.map((e) => RegularizationLogModel.fromJson(e)).toList();
|
||||||
..addAll(
|
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
|
||||||
response
|
|
||||||
.map<RegularizationLogModel>(
|
|
||||||
(dynamic e) => RegularizationLogModel.fromJson(
|
|
||||||
e as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
logSafe(
|
|
||||||
'Regularization logs fetched: ${regularizationLogs.length}',
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
logSafe(
|
logSafe("Failed to fetch regularization logs for project $projectId",
|
||||||
'Failed to fetch regularization logs for project $projectId',
|
level: LogLevel.error);
|
||||||
level: LogLevel.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingRegularizationLogs.value = false;
|
isLoadingRegularizationLogs.value = false;
|
||||||
@ -477,38 +290,22 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Attendance Log View ------------------
|
// ------------------ Attendance Log View ------------------
|
||||||
|
|
||||||
Future<void> fetchLogsView(String? id) async {
|
Future<void> fetchLogsView(String? id) async {
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
|
|
||||||
isLoadingLogView.value = true;
|
isLoadingLogView.value = true;
|
||||||
|
|
||||||
final List<dynamic>? response = await ApiService.getAttendanceLogView(id);
|
final response = await ApiService.getAttendanceLogView(id);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendenceLogsView
|
attendenceLogsView =
|
||||||
..clear()
|
response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
|
||||||
..addAll(
|
attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000))
|
||||||
response
|
.compareTo(a.activityTime ?? DateTime(2000)));
|
||||||
.map<AttendanceLogViewModel>(
|
logSafe("Attendance log view fetched for ID: $id");
|
||||||
(dynamic e) => AttendanceLogViewModel.fromJson(
|
|
||||||
e as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
attendenceLogsView.sort(
|
|
||||||
(AttendanceLogViewModel a, AttendanceLogViewModel b) =>
|
|
||||||
(b.activityTime ?? DateTime(2000))
|
|
||||||
.compareTo(a.activityTime ?? DateTime(2000)),
|
|
||||||
);
|
|
||||||
|
|
||||||
logSafe('Attendance log view fetched for ID: $id');
|
|
||||||
} else {
|
} else {
|
||||||
logSafe(
|
logSafe("Failed to fetch attendance log view for ID $id",
|
||||||
'Failed to fetch attendance log view for ID $id',
|
level: LogLevel.error);
|
||||||
level: LogLevel.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingLogView.value = false;
|
isLoadingLogView.value = false;
|
||||||
@ -516,6 +313,7 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ Combined Load ------------------
|
// ------------------ Combined Load ------------------
|
||||||
|
|
||||||
Future<void> loadAttendanceData(String projectId) async {
|
Future<void> loadAttendanceData(String projectId) async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
await fetchProjectData(projectId);
|
await fetchProjectData(projectId);
|
||||||
@ -525,54 +323,37 @@ class AttendanceController extends GetxController {
|
|||||||
Future<void> fetchProjectData(String? projectId) async {
|
Future<void> fetchProjectData(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
await fetchOrganizations(projectId);
|
await Future.wait([
|
||||||
|
fetchEmployeesByProject(projectId),
|
||||||
|
fetchAttendanceLogs(projectId,
|
||||||
|
dateFrom: startDateAttendance, dateTo: endDateAttendance),
|
||||||
|
fetchRegularizationLogs(projectId),
|
||||||
|
]);
|
||||||
|
|
||||||
switch (selectedTab) {
|
logSafe("Project data fetched for project ID: $projectId");
|
||||||
case 'todaysAttendance':
|
|
||||||
await fetchTodaysAttendance(projectId);
|
|
||||||
break;
|
|
||||||
case 'attendanceLogs':
|
|
||||||
await fetchAttendanceLogs(
|
|
||||||
projectId,
|
|
||||||
dateFrom: startDateAttendance.value,
|
|
||||||
dateTo: endDateAttendance.value,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'regularizationRequests':
|
|
||||||
await fetchRegularizationLogs(projectId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
logSafe(
|
|
||||||
'Project data fetched for project ID: $projectId, tab: $selectedTab',
|
|
||||||
);
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------ UI Interaction ------------------
|
// ------------------ UI Interaction ------------------
|
||||||
Future<void> selectDateRangeForAttendance(
|
|
||||||
BuildContext context,
|
|
||||||
AttendanceController controller,
|
|
||||||
) async {
|
|
||||||
final DateTime today = DateTime.now();
|
|
||||||
|
|
||||||
final DateTimeRange? picked = await showDateRangePicker(
|
Future<void> selectDateRangeForAttendance(
|
||||||
|
BuildContext context, AttendanceController controller) async {
|
||||||
|
final today = DateTime.now();
|
||||||
|
|
||||||
|
final picked = await showDateRangePicker(
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: DateTime(2022),
|
firstDate: DateTime(2022),
|
||||||
lastDate: today.subtract(const Duration(days: 1)),
|
lastDate: today.subtract(const Duration(days: 1)),
|
||||||
initialDateRange: DateTimeRange(
|
initialDateRange: DateTimeRange(
|
||||||
start: startDateAttendance.value,
|
start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
|
||||||
end: endDateAttendance.value,
|
end: endDateAttendance ?? today.subtract(const Duration(days: 1)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
startDateAttendance.value = picked.start;
|
startDateAttendance = picked.start;
|
||||||
endDateAttendance.value = picked.end;
|
endDateAttendance = picked.end;
|
||||||
|
|
||||||
logSafe(
|
logSafe(
|
||||||
'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}',
|
"Date range selected: $startDateAttendance to $endDateAttendance");
|
||||||
);
|
|
||||||
|
|
||||||
await controller.fetchAttendanceLogs(
|
await controller.fetchAttendanceLogs(
|
||||||
Get.find<ProjectController>().selectedProject?.id,
|
Get.find<ProjectController>().selectedProject?.id,
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
|
|
||||||
class ForgotPasswordController extends MyController {
|
class ForgotPasswordController extends MyController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class LoginController extends MyController {
|
class LoginController extends MyController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
@ -14,7 +14,6 @@ class LoginController extends MyController {
|
|||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
final RxBool showPassword = false.obs;
|
final RxBool showPassword = false.obs;
|
||||||
final RxBool isChecked = false.obs;
|
final RxBool isChecked = false.obs;
|
||||||
final RxBool showSplash = false.obs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -41,14 +40,18 @@ class LoginController extends MyController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChangeCheckBox(bool? value) => isChecked.value = value ?? false;
|
void onChangeCheckBox(bool? value) {
|
||||||
|
isChecked.value = value ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
void onChangeShowPassword() => showPassword.toggle();
|
void onChangeShowPassword() {
|
||||||
|
showPassword.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> onLogin() async {
|
Future<void> onLogin() async {
|
||||||
if (!basicValidator.validateForm()) return;
|
if (!basicValidator.validateForm()) return;
|
||||||
|
|
||||||
showSplash.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final loginData = basicValidator.getData();
|
final loginData = basicValidator.getData();
|
||||||
@ -57,30 +60,48 @@ class LoginController extends MyController {
|
|||||||
final errors = await AuthService.loginUser(loginData);
|
final errors = await AuthService.loginUser(loginData);
|
||||||
|
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
|
logSafe(
|
||||||
|
"Login failed for user: ${loginData['username']} with errors: $errors",
|
||||||
|
level: LogLevel.warning);
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Login Failed",
|
title: "Login Failed",
|
||||||
message: "Username or password is incorrect",
|
message: "Username or password is incorrect",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
|
|
||||||
basicValidator.addErrors(errors);
|
basicValidator.addErrors(errors);
|
||||||
basicValidator.validateForm();
|
basicValidator.validateForm();
|
||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
} else {
|
} else {
|
||||||
await _handleRememberMe();
|
await _handleRememberMe();
|
||||||
enableRemoteLogging();
|
|
||||||
|
// ✅ Commented out FCM token registration after login
|
||||||
|
/*
|
||||||
|
final fcmToken = await LocalStorage.getFcmToken();
|
||||||
|
if (fcmToken?.isNotEmpty ?? false) {
|
||||||
|
final success = await AuthService.registerDeviceToken(fcmToken!);
|
||||||
|
logSafe(
|
||||||
|
success
|
||||||
|
? "✅ FCM token registered after login."
|
||||||
|
: "⚠️ Failed to register FCM token after login.",
|
||||||
|
level: LogLevel.warning);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
logSafe("Login successful for user: ${loginData['username']}");
|
logSafe("Login successful for user: ${loginData['username']}");
|
||||||
Get.offNamed('/select-tenant');
|
Get.toNamed('/home');
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
|
logSafe("Exception during login",
|
||||||
|
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Login Error",
|
title: "Login Error",
|
||||||
message: "An unexpected error occurred",
|
message: "An unexpected error occurred",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
logSafe("Exception during login",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
} finally {
|
} finally {
|
||||||
showSplash.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +133,11 @@ class LoginController extends MyController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void goToForgotPassword() => Get.toNamed('/auth/forgot_password');
|
void goToForgotPassword() {
|
||||||
|
Get.toNamed('/auth/forgot_password');
|
||||||
|
}
|
||||||
|
|
||||||
void gotoRegister() => Get.offAndToNamed('/auth/register_account');
|
void gotoRegister() {
|
||||||
|
Get.offAndToNamed('/auth/register_account');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
||||||
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/controller/permission_controller.dart';
|
// import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; // 🔴 Commented out
|
||||||
import 'package:on_field_work/controller/project_controller.dart';
|
|
||||||
|
|
||||||
class MPINController extends GetxController {
|
class MPINController extends GetxController {
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
@ -139,17 +138,16 @@ class MPINController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Navigate to dashboard
|
/// Navigate to dashboard
|
||||||
/// Navigate to tenant selection after MPIN verification
|
void _navigateToDashboard({String? message}) {
|
||||||
void _navigateToTenantSelection({String? message}) {
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
logSafe("Navigating to Tenant Selection with message: $message");
|
logSafe("Navigating to Dashboard with message: $message");
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: message,
|
message: message,
|
||||||
type: SnackbarType.success,
|
type: SnackbarType.success,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Get.offAllNamed('/select-tenant');
|
Get.offAll(() => const DashboardScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the primary MPIN fields
|
/// Clear the primary MPIN fields
|
||||||
@ -241,12 +239,15 @@ class MPINController extends GetxController {
|
|||||||
logSafe("verifyMPIN triggered");
|
logSafe("verifyMPIN triggered");
|
||||||
|
|
||||||
final enteredMPIN = digitControllers.map((c) => c.text).join();
|
final enteredMPIN = digitControllers.map((c) => c.text).join();
|
||||||
|
logSafe("Entered MPIN: $enteredMPIN");
|
||||||
|
|
||||||
if (enteredMPIN.length < 4) {
|
if (enteredMPIN.length < 4) {
|
||||||
_showError("Please enter all 4 digits.");
|
_showError("Please enter all 4 digits.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final mpinToken = await LocalStorage.getMpinToken();
|
final mpinToken = await LocalStorage.getMpinToken();
|
||||||
|
|
||||||
if (mpinToken == null || mpinToken.isEmpty) {
|
if (mpinToken == null || mpinToken.isEmpty) {
|
||||||
_showError("Missing MPIN token. Please log in again.");
|
_showError("Missing MPIN token. Please log in again.");
|
||||||
return;
|
return;
|
||||||
@ -255,12 +256,14 @@ class MPINController extends GetxController {
|
|||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final fcmToken = await FirebaseNotificationService().getFcmToken();
|
// ✅ Fetch FCM Token here (DISABLED)
|
||||||
|
// final fcmToken = await FirebaseNotificationService().getFcmToken();
|
||||||
|
|
||||||
final response = await AuthService.verifyMpin(
|
final response = await AuthService.verifyMpin(
|
||||||
mpin: enteredMPIN,
|
mpin: enteredMPIN,
|
||||||
mpinToken: mpinToken,
|
mpinToken: mpinToken,
|
||||||
fcmToken: fcmToken ?? '',
|
// fcmToken: fcmToken ?? '', // 🔴 Commented out
|
||||||
|
fcmToken: '', // ✅ Passing empty string instead
|
||||||
);
|
);
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@ -269,25 +272,12 @@ class MPINController extends GetxController {
|
|||||||
logSafe("MPIN verified successfully");
|
logSafe("MPIN verified successfully");
|
||||||
await LocalStorage.setBool('mpin_verified', true);
|
await LocalStorage.setBool('mpin_verified', true);
|
||||||
|
|
||||||
// 🔹 Ensure controllers are injected and loaded
|
|
||||||
final token = await LocalStorage.getJwtToken();
|
|
||||||
if (token != null && token.isNotEmpty) {
|
|
||||||
if (!Get.isRegistered<PermissionController>()) {
|
|
||||||
Get.put(PermissionController());
|
|
||||||
await Get.find<PermissionController>().loadData(token);
|
|
||||||
}
|
|
||||||
if (!Get.isRegistered<ProjectController>()) {
|
|
||||||
Get.put(ProjectController(), permanent: true);
|
|
||||||
await Get.find<ProjectController>().fetchProjects();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "MPIN Verified Successfully",
|
message: "MPIN Verified Successfully",
|
||||||
type: SnackbarType.success,
|
type: SnackbarType.success,
|
||||||
);
|
);
|
||||||
_navigateToTenantSelection();
|
_navigateToDashboard();
|
||||||
} else {
|
} else {
|
||||||
final errorMessage = response["error"] ?? "Invalid MPIN";
|
final errorMessage = response["error"] ?? "Invalid MPIN";
|
||||||
logSafe("MPIN verification failed: $errorMessage",
|
logSafe("MPIN verification failed: $errorMessage",
|
||||||
@ -303,7 +293,11 @@ class MPINController extends GetxController {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
logSafe("Exception in verifyMPIN", level: LogLevel.error, error: e);
|
logSafe("Exception in verifyMPIN", level: LogLevel.error, error: e);
|
||||||
_showError("Something went wrong. Please try again.");
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Something went wrong. Please try again.",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class OTPController extends GetxController {
|
class OTPController extends GetxController {
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
@ -109,8 +109,7 @@ class OTPController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onOTPChanged(String value, int index) {
|
void onOTPChanged(String value, int index) {
|
||||||
logSafe("[OTPController] OTP field changed: index=$index",
|
logSafe("[OTPController] OTP field changed: index=$index", level: LogLevel.debug);
|
||||||
level: LogLevel.debug);
|
|
||||||
if (value.isNotEmpty) {
|
if (value.isNotEmpty) {
|
||||||
if (index < otpControllers.length - 1) {
|
if (index < otpControllers.length - 1) {
|
||||||
focusNodes[index + 1].requestFocus();
|
focusNodes[index + 1].requestFocus();
|
||||||
@ -126,24 +125,30 @@ class OTPController extends GetxController {
|
|||||||
|
|
||||||
Future<void> verifyOTP() async {
|
Future<void> verifyOTP() async {
|
||||||
final enteredOTP = otpControllers.map((c) => c.text).join();
|
final enteredOTP = otpControllers.map((c) => c.text).join();
|
||||||
|
logSafe("[OTPController] Verifying OTP");
|
||||||
|
|
||||||
final result = await AuthService.verifyOtp(
|
final result = await AuthService.verifyOtp(
|
||||||
email: email.value,
|
email: email.value,
|
||||||
otp: enteredOTP,
|
otp: enteredOTP,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
// ✅ Handle remember-me like in LoginController
|
logSafe("[OTPController] OTP verified successfully");
|
||||||
final remember = LocalStorage.getBool('remember_me') ?? false;
|
showAppSnackbar(
|
||||||
if (remember) await LocalStorage.setToken('otp_email', email.value);
|
title: "Success",
|
||||||
|
message: "OTP verified successfully",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
final bool isMpinEnabled = LocalStorage.getIsMpin();
|
||||||
|
logSafe("[OTPController] MPIN Enabled: $isMpinEnabled");
|
||||||
|
|
||||||
// ✅ Enable remote logging
|
Get.offAllNamed('/home');
|
||||||
enableRemoteLogging();
|
|
||||||
|
|
||||||
Get.offAllNamed('/select-tenant');
|
|
||||||
} else {
|
} else {
|
||||||
|
final error = result['error'] ?? "Failed to verify OTP";
|
||||||
|
logSafe("[OTPController] OTP verification failed", level: LogLevel.warning, error: error);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: result['error']!,
|
message: error,
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -210,8 +215,7 @@ class OTPController extends GetxController {
|
|||||||
final savedEmail = LocalStorage.getToken('otp_email') ?? '';
|
final savedEmail = LocalStorage.getToken('otp_email') ?? '';
|
||||||
emailController.text = savedEmail;
|
emailController.text = savedEmail;
|
||||||
email.value = savedEmail;
|
email.value = savedEmail;
|
||||||
logSafe(
|
logSafe("[OTPController] Loaded saved email from local storage: $savedEmail");
|
||||||
"[OTPController] Loaded saved email from local storage: $savedEmail");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class RegisterAccountController extends MyController {
|
class RegisterAccountController extends MyController {
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_validators.dart';
|
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class ResetPasswordController extends MyController {
|
class ResetPasswordController extends MyController {
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
@ -49,8 +49,8 @@ class ResetPasswordController extends MyController {
|
|||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
logSafe("[ResetPasswordController] Navigating to /dashboard");
|
logSafe("[ResetPasswordController] Navigating to /home");
|
||||||
Get.toNamed('/dashboard');
|
Get.toNamed('/home');
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
logSafe("[ResetPasswordController] Form validation failed", level: LogLevel.warning);
|
logSafe("[ResetPasswordController] Form validation failed", level: LogLevel.warning);
|
||||||
|
|||||||
@ -1,370 +1,263 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
import 'package:on_field_work/model/dashboard/project_progress_model.dart';
|
import 'package:marco/model/dashboard/project_progress_model.dart';
|
||||||
import 'package:on_field_work/model/dashboard/pending_expenses_model.dart';
|
|
||||||
import 'package:on_field_work/model/dashboard/expense_type_report_model.dart';
|
|
||||||
import 'package:on_field_work/model/dashboard/monthly_expence_model.dart';
|
|
||||||
import 'package:on_field_work/model/expense/expense_type_model.dart';
|
|
||||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
|
||||||
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
|
|
||||||
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
|
||||||
|
|
||||||
class DashboardController extends GetxController {
|
class DashboardController extends GetxController {
|
||||||
// Dependencies
|
// =========================
|
||||||
final ProjectController projectController = Get.put(ProjectController());
|
// Attendance overview
|
||||||
|
// =========================
|
||||||
|
final RxList<Map<String, dynamic>> roleWiseData =
|
||||||
|
<Map<String, dynamic>>[].obs;
|
||||||
|
final RxString attendanceSelectedRange = '15D'.obs;
|
||||||
|
final RxBool attendanceIsChartView = true.obs;
|
||||||
|
final RxBool isAttendanceLoading = false.obs;
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// 1. STATE VARIABLES
|
// Project progress overview
|
||||||
// =========================
|
// =========================
|
||||||
|
final RxList<ChartTaskData> projectChartData = <ChartTaskData>[].obs;
|
||||||
|
final RxString projectSelectedRange = '15D'.obs;
|
||||||
|
final RxBool projectIsChartView = true.obs;
|
||||||
|
final RxBool isProjectLoading = false.obs;
|
||||||
|
|
||||||
// Attendance
|
// =========================
|
||||||
final roleWiseData = <Map<String, dynamic>>[].obs;
|
// Projects overview
|
||||||
final attendanceSelectedRange = '15D'.obs;
|
// =========================
|
||||||
final attendanceIsChartView = true.obs;
|
final RxInt totalProjects = 0.obs;
|
||||||
final isAttendanceLoading = false.obs;
|
final RxInt ongoingProjects = 0.obs;
|
||||||
|
final RxBool isProjectsLoading = false.obs;
|
||||||
|
|
||||||
// Project Progress
|
// =========================
|
||||||
final projectChartData = <ChartTaskData>[].obs;
|
// Tasks overview
|
||||||
final projectSelectedRange = '15D'.obs;
|
// =========================
|
||||||
final projectIsChartView = true.obs;
|
final RxInt totalTasks = 0.obs;
|
||||||
final isProjectLoading = false.obs;
|
final RxInt completedTasks = 0.obs;
|
||||||
|
final RxBool isTasksLoading = false.obs;
|
||||||
|
|
||||||
// Overview Counts
|
// =========================
|
||||||
final totalProjects = 0.obs;
|
// Teams overview
|
||||||
final ongoingProjects = 0.obs;
|
// =========================
|
||||||
final isProjectsLoading = false.obs;
|
final RxInt totalEmployees = 0.obs;
|
||||||
|
final RxInt inToday = 0.obs;
|
||||||
|
final RxBool isTeamsLoading = false.obs;
|
||||||
|
|
||||||
final totalTasks = 0.obs;
|
// Common ranges
|
||||||
final completedTasks = 0.obs;
|
|
||||||
final isTasksLoading = false.obs;
|
|
||||||
|
|
||||||
final totalEmployees = 0.obs;
|
|
||||||
final inToday = 0.obs;
|
|
||||||
final isTeamsLoading = false.obs;
|
|
||||||
|
|
||||||
// Expenses & Reports
|
|
||||||
final isPendingExpensesLoading = false.obs;
|
|
||||||
final pendingExpensesData = Rx<PendingExpensesData?>(null);
|
|
||||||
|
|
||||||
final isExpenseTypeReportLoading = false.obs;
|
|
||||||
final expenseTypeReportData = Rx<ExpenseTypeReportData?>(null);
|
|
||||||
final expenseReportStartDate =
|
|
||||||
DateTime.now().subtract(const Duration(days: 15)).obs;
|
|
||||||
final expenseReportEndDate = DateTime.now().obs;
|
|
||||||
|
|
||||||
final isMonthlyExpenseLoading = false.obs;
|
|
||||||
final monthlyExpenseList = <MonthlyExpenseData>[].obs;
|
|
||||||
final selectedMonthlyExpenseDuration =
|
|
||||||
MonthlyExpenseDuration.twelveMonths.obs;
|
|
||||||
final selectedMonthsCount = 12.obs;
|
|
||||||
|
|
||||||
final expenseTypes = <ExpenseTypeModel>[].obs;
|
|
||||||
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
|
||||||
|
|
||||||
// Teams/Employees
|
|
||||||
final isLoadingEmployees = true.obs;
|
|
||||||
final employees = <EmployeeModel>[].obs;
|
|
||||||
final uploadingStates = <String, RxBool>{}.obs;
|
|
||||||
|
|
||||||
// Collection
|
|
||||||
final isCollectionOverviewLoading = true.obs;
|
|
||||||
final collectionOverviewData = Rx<CollectionOverviewData?>(null);
|
|
||||||
// =========================
|
|
||||||
// Purchase Invoice Overview
|
|
||||||
// =========================
|
|
||||||
final isPurchaseInvoiceLoading = true.obs;
|
|
||||||
final purchaseInvoiceOverviewData = Rx<PurchaseInvoiceOverviewData?>(null);
|
|
||||||
// Constants
|
|
||||||
final List<String> ranges = ['7D', '15D', '30D'];
|
final List<String> ranges = ['7D', '15D', '30D'];
|
||||||
static const _rangeDaysMap = {
|
|
||||||
'7D': 7,
|
|
||||||
'15D': 15,
|
|
||||||
'30D': 30,
|
|
||||||
'3M': 90,
|
|
||||||
'6M': 180
|
|
||||||
};
|
|
||||||
|
|
||||||
// =========================
|
// Inject ProjectController
|
||||||
// 2. COMPUTED PROPERTIES
|
final ProjectController projectController = Get.find<ProjectController>();
|
||||||
// =========================
|
|
||||||
|
|
||||||
int getAttendanceDays() => _rangeDaysMap[attendanceSelectedRange.value] ?? 7;
|
|
||||||
int getProjectDays() => _rangeDaysMap[projectSelectedRange.value] ?? 7;
|
|
||||||
|
|
||||||
// DSO Calculation Constants
|
|
||||||
static const double _w0_30 = 15.0;
|
|
||||||
static const double _w30_60 = 45.0;
|
|
||||||
static const double _w60_90 = 75.0;
|
|
||||||
static const double _w90_plus = 105.0;
|
|
||||||
|
|
||||||
double get calculatedDSO {
|
|
||||||
final data = collectionOverviewData.value;
|
|
||||||
if (data == null || data.totalDueAmount == 0) return 0.0;
|
|
||||||
|
|
||||||
final double weightedDue = (data.bucket0To30Amount * _w0_30) +
|
|
||||||
(data.bucket30To60Amount * _w30_60) +
|
|
||||||
(data.bucket60To90Amount * _w60_90) +
|
|
||||||
(data.bucket90PlusAmount * _w90_plus);
|
|
||||||
|
|
||||||
return weightedDue / data.totalDueAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// 3. LIFECYCLE
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
logSafe('DashboardController initialized', level: LogLevel.info);
|
|
||||||
|
|
||||||
// Project Selection Listener
|
logSafe(
|
||||||
|
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
|
|
||||||
|
fetchAllDashboardData();
|
||||||
|
|
||||||
|
// React to project change
|
||||||
ever<String>(projectController.selectedProjectId, (id) {
|
ever<String>(projectController.selectedProjectId, (id) {
|
||||||
if (id.isNotEmpty) {
|
fetchAllDashboardData();
|
||||||
fetchAllDashboardData();
|
|
||||||
fetchTodaysAttendance(id);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expense Report Date Listener
|
// React to range changes
|
||||||
everAll([expenseReportStartDate, expenseReportEndDate], (_) {
|
|
||||||
if (projectController.selectedProjectId.value.isNotEmpty) {
|
|
||||||
fetchExpenseTypeReport(
|
|
||||||
startDate: expenseReportStartDate.value,
|
|
||||||
endDate: expenseReportEndDate.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Chart Range Listeners
|
|
||||||
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance());
|
||||||
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
ever(projectSelectedRange, (_) => fetchProjectProgress());
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
/// =========================
|
||||||
// 4. USER ACTIONS
|
/// Helper Methods
|
||||||
// =========================
|
/// =========================
|
||||||
|
int _getDaysFromRange(String range) {
|
||||||
void updateAttendanceRange(String range) =>
|
switch (range) {
|
||||||
attendanceSelectedRange.value = range;
|
case '7D':
|
||||||
void updateProjectRange(String range) => projectSelectedRange.value = range;
|
return 7;
|
||||||
void toggleAttendanceChartView(bool isChart) =>
|
case '15D':
|
||||||
attendanceIsChartView.value = isChart;
|
return 15;
|
||||||
void toggleProjectChartView(bool isChart) =>
|
case '30D':
|
||||||
projectIsChartView.value = isChart;
|
return 30;
|
||||||
|
case '3M':
|
||||||
void updateSelectedExpenseType(ExpenseTypeModel? type) {
|
return 90;
|
||||||
selectedExpenseType.value = type;
|
case '6M':
|
||||||
fetchMonthlyExpenses(categoryId: type?.id);
|
return 180;
|
||||||
}
|
default:
|
||||||
|
return 7;
|
||||||
void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) {
|
|
||||||
selectedMonthlyExpenseDuration.value = duration;
|
|
||||||
|
|
||||||
// Efficient Map lookup instead of Switch
|
|
||||||
const durationMap = {
|
|
||||||
MonthlyExpenseDuration.oneMonth: 1,
|
|
||||||
MonthlyExpenseDuration.threeMonths: 3,
|
|
||||||
MonthlyExpenseDuration.sixMonths: 6,
|
|
||||||
MonthlyExpenseDuration.twelveMonths: 12,
|
|
||||||
MonthlyExpenseDuration.all: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
selectedMonthsCount.value = durationMap[duration] ?? 12;
|
|
||||||
fetchMonthlyExpenses();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refreshDashboard() => fetchAllDashboardData();
|
|
||||||
Future<void> refreshAttendance() => fetchRoleWiseAttendance();
|
|
||||||
Future<void> refreshProjects() => fetchProjectProgress();
|
|
||||||
Future<void> refreshTasks() async {
|
|
||||||
final id = projectController.selectedProjectId.value;
|
|
||||||
if (id.isNotEmpty) await fetchDashboardTasks(projectId: id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// 5. DATA FETCHING (API)
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
/// Wrapper to reduce try-finally boilerplate for loading states
|
|
||||||
Future<void> _executeApiCall(
|
|
||||||
RxBool loader, Future<void> Function() apiLogic) async {
|
|
||||||
loader.value = true;
|
|
||||||
try {
|
|
||||||
await apiLogic();
|
|
||||||
} finally {
|
|
||||||
loader.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value);
|
||||||
|
int getProjectDays() => _getDaysFromRange(projectSelectedRange.value);
|
||||||
|
|
||||||
|
void updateAttendanceRange(String range) {
|
||||||
|
attendanceSelectedRange.value = range;
|
||||||
|
logSafe('Attendance range updated to $range', level: LogLevel.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateProjectRange(String range) {
|
||||||
|
projectSelectedRange.value = range;
|
||||||
|
logSafe('Project range updated to $range', level: LogLevel.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleAttendanceChartView(bool isChart) {
|
||||||
|
attendanceIsChartView.value = isChart;
|
||||||
|
logSafe('Attendance chart view toggled to: $isChart',
|
||||||
|
level: LogLevel.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleProjectChartView(bool isChart) {
|
||||||
|
projectIsChartView.value = isChart;
|
||||||
|
logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// Manual refresh
|
||||||
|
/// =========================
|
||||||
|
Future<void> refreshDashboard() async {
|
||||||
|
logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug);
|
||||||
|
await fetchAllDashboardData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// =========================
|
||||||
|
/// Fetch all dashboard data
|
||||||
|
/// =========================
|
||||||
Future<void> fetchAllDashboardData() async {
|
Future<void> fetchAllDashboardData() async {
|
||||||
final String projectId = projectController.selectedProjectId.value;
|
final String projectId = projectController.selectedProjectId.value;
|
||||||
if (projectId.isEmpty) return;
|
|
||||||
|
// Skip fetching if no project is selected
|
||||||
|
if (projectId.isEmpty) {
|
||||||
|
logSafe('No project selected. Skipping dashboard API calls.',
|
||||||
|
level: LogLevel.warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
fetchRoleWiseAttendance(),
|
fetchRoleWiseAttendance(),
|
||||||
fetchProjectProgress(),
|
fetchProjectProgress(),
|
||||||
fetchDashboardTasks(projectId: projectId),
|
fetchDashboardTasks(projectId: projectId),
|
||||||
fetchDashboardTeams(projectId: projectId),
|
fetchDashboardTeams(projectId: projectId),
|
||||||
fetchPendingExpenses(),
|
|
||||||
fetchExpenseTypeReport(
|
|
||||||
startDate: expenseReportStartDate.value,
|
|
||||||
endDate: expenseReportEndDate.value,
|
|
||||||
),
|
|
||||||
fetchMonthlyExpenses(),
|
|
||||||
fetchMasterData(),
|
|
||||||
fetchCollectionOverview(),
|
|
||||||
fetchPurchaseInvoiceOverview(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchCollectionOverview() async {
|
/// =========================
|
||||||
final projectId = projectController.selectedProjectId.value;
|
/// API Calls
|
||||||
if (projectId.isEmpty) return;
|
/// =========================
|
||||||
|
|
||||||
await _executeApiCall(isCollectionOverviewLoading, () async {
|
|
||||||
final response =
|
|
||||||
await ApiService.getCollectionOverview(projectId: projectId);
|
|
||||||
collectionOverviewData.value =
|
|
||||||
(response?.success == true) ? response!.data : null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchTodaysAttendance(String projectId) async {
|
|
||||||
await _executeApiCall(isLoadingEmployees, () async {
|
|
||||||
final response = await ApiService.getAttendanceForDashboard(projectId);
|
|
||||||
if (response != null) {
|
|
||||||
employees.value = response;
|
|
||||||
for (var emp in employees) {
|
|
||||||
uploadingStates.putIfAbsent(emp.id, () => false.obs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchMasterData() async {
|
|
||||||
try {
|
|
||||||
final data = await ApiService.getMasterExpenseTypes();
|
|
||||||
if (data is List) {
|
|
||||||
expenseTypes.value =
|
|
||||||
data.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchMonthlyExpenses({String? categoryId}) async {
|
|
||||||
await _executeApiCall(isMonthlyExpenseLoading, () async {
|
|
||||||
final response = await ApiService.getDashboardMonthlyExpensesApi(
|
|
||||||
categoryId: categoryId,
|
|
||||||
months: selectedMonthsCount.value,
|
|
||||||
);
|
|
||||||
monthlyExpenseList.value =
|
|
||||||
(response?.success == true) ? response!.data : [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchPurchaseInvoiceOverview() async {
|
|
||||||
final projectId = projectController.selectedProjectId.value;
|
|
||||||
if (projectId.isEmpty) return;
|
|
||||||
|
|
||||||
await _executeApiCall(isPurchaseInvoiceLoading, () async {
|
|
||||||
final response = await ApiService.getPurchaseInvoiceOverview(
|
|
||||||
projectId: projectId,
|
|
||||||
);
|
|
||||||
purchaseInvoiceOverviewData.value =
|
|
||||||
(response?.success == true) ? response!.data : null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchPendingExpenses() async {
|
|
||||||
final id = projectController.selectedProjectId.value;
|
|
||||||
if (id.isEmpty) return;
|
|
||||||
|
|
||||||
await _executeApiCall(isPendingExpensesLoading, () async {
|
|
||||||
final response = await ApiService.getPendingExpensesApi(projectId: id);
|
|
||||||
pendingExpensesData.value =
|
|
||||||
(response?.success == true) ? response!.data : null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchRoleWiseAttendance() async {
|
Future<void> fetchRoleWiseAttendance() async {
|
||||||
final id = projectController.selectedProjectId.value;
|
final String projectId = projectController.selectedProjectId.value;
|
||||||
if (id.isEmpty) return;
|
|
||||||
|
|
||||||
await _executeApiCall(isAttendanceLoading, () async {
|
if (projectId.isEmpty) return;
|
||||||
final response = await ApiService.getDashboardAttendanceOverview(
|
|
||||||
id, getAttendanceDays());
|
|
||||||
roleWiseData.value =
|
|
||||||
response?.map((e) => Map<String, dynamic>.from(e)).toList() ?? [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchExpenseTypeReport(
|
try {
|
||||||
{required DateTime startDate, required DateTime endDate}) async {
|
isAttendanceLoading.value = true;
|
||||||
final id = projectController.selectedProjectId.value;
|
|
||||||
if (id.isEmpty) return;
|
|
||||||
|
|
||||||
await _executeApiCall(isExpenseTypeReportLoading, () async {
|
final List<dynamic>? response =
|
||||||
final response = await ApiService.getExpenseTypeReportApi(
|
await ApiService.getDashboardAttendanceOverview(
|
||||||
projectId: id,
|
projectId, getAttendanceDays());
|
||||||
startDate: startDate,
|
|
||||||
endDate: endDate,
|
if (response != null) {
|
||||||
);
|
roleWiseData.value =
|
||||||
expenseTypeReportData.value =
|
response.map((e) => Map<String, dynamic>.from(e)).toList();
|
||||||
(response?.success == true) ? response!.data : null;
|
logSafe('Attendance overview fetched successfully.',
|
||||||
});
|
level: LogLevel.info);
|
||||||
|
} else {
|
||||||
|
roleWiseData.clear();
|
||||||
|
logSafe('Failed to fetch attendance overview: response is null.',
|
||||||
|
level: LogLevel.error);
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
roleWiseData.clear();
|
||||||
|
logSafe('Error fetching attendance overview',
|
||||||
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
|
} finally {
|
||||||
|
isAttendanceLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchProjectProgress() async {
|
Future<void> fetchProjectProgress() async {
|
||||||
final id = projectController.selectedProjectId.value;
|
final String projectId = projectController.selectedProjectId.value;
|
||||||
if (id.isEmpty) return;
|
|
||||||
|
if (projectId.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isProjectLoading.value = true;
|
||||||
|
|
||||||
await _executeApiCall(isProjectLoading, () async {
|
|
||||||
final response = await ApiService.getProjectProgress(
|
final response = await ApiService.getProjectProgress(
|
||||||
projectId: id, days: getProjectDays());
|
projectId: projectId,
|
||||||
if (response?.success == true) {
|
days: getProjectDays(),
|
||||||
projectChartData.value = response!.data
|
);
|
||||||
.map((d) => ChartTaskData.fromProjectData(d))
|
|
||||||
.toList();
|
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 {
|
} else {
|
||||||
projectChartData.clear();
|
projectChartData.clear();
|
||||||
|
logSafe('Failed to fetch project progress', level: LogLevel.error);
|
||||||
}
|
}
|
||||||
});
|
} catch (e, st) {
|
||||||
|
projectChartData.clear();
|
||||||
|
logSafe('Error fetching project progress',
|
||||||
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
|
} finally {
|
||||||
|
isProjectLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDashboardTasks({required String projectId}) async {
|
Future<void> fetchDashboardTasks({required String projectId}) async {
|
||||||
await _executeApiCall(isTasksLoading, () async {
|
if (projectId.isEmpty) return; // Skip if empty
|
||||||
|
|
||||||
|
try {
|
||||||
|
isTasksLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
final response = await ApiService.getDashboardTasks(projectId: projectId);
|
||||||
if (response?.success == true) {
|
|
||||||
totalTasks.value = response!.data?.totalTasks ?? 0;
|
if (response != null && response.success) {
|
||||||
|
totalTasks.value = response.data?.totalTasks ?? 0;
|
||||||
completedTasks.value = response.data?.completedTasks ?? 0;
|
completedTasks.value = response.data?.completedTasks ?? 0;
|
||||||
|
logSafe('Dashboard tasks fetched', level: LogLevel.info);
|
||||||
} else {
|
} else {
|
||||||
totalTasks.value = 0;
|
totalTasks.value = 0;
|
||||||
completedTasks.value = 0;
|
completedTasks.value = 0;
|
||||||
|
logSafe('Failed to fetch tasks', level: LogLevel.error);
|
||||||
}
|
}
|
||||||
});
|
} catch (e, st) {
|
||||||
|
totalTasks.value = 0;
|
||||||
|
completedTasks.value = 0;
|
||||||
|
logSafe('Error fetching tasks',
|
||||||
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
|
} finally {
|
||||||
|
isTasksLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDashboardTeams({required String projectId}) async {
|
Future<void> fetchDashboardTeams({required String projectId}) async {
|
||||||
await _executeApiCall(isTeamsLoading, () async {
|
if (projectId.isEmpty) return; // Skip if empty
|
||||||
|
|
||||||
|
try {
|
||||||
|
isTeamsLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
final response = await ApiService.getDashboardTeams(projectId: projectId);
|
||||||
if (response?.success == true) {
|
|
||||||
totalEmployees.value = response!.data?.totalEmployees ?? 0;
|
if (response != null && response.success) {
|
||||||
|
totalEmployees.value = response.data?.totalEmployees ?? 0;
|
||||||
inToday.value = response.data?.inToday ?? 0;
|
inToday.value = response.data?.inToday ?? 0;
|
||||||
|
logSafe('Dashboard teams fetched', level: LogLevel.info);
|
||||||
} else {
|
} else {
|
||||||
totalEmployees.value = 0;
|
totalEmployees.value = 0;
|
||||||
inToday.value = 0;
|
inToday.value = 0;
|
||||||
|
logSafe('Failed to fetch teams', level: LogLevel.error);
|
||||||
}
|
}
|
||||||
});
|
} catch (e, st) {
|
||||||
|
totalEmployees.value = 0;
|
||||||
|
inToday.value = 0;
|
||||||
|
logSafe('Error fetching teams',
|
||||||
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
|
} finally {
|
||||||
|
isTeamsLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MonthlyExpenseDuration {
|
|
||||||
oneMonth,
|
|
||||||
threeMonths,
|
|
||||||
sixMonths,
|
|
||||||
twelveMonths,
|
|
||||||
all,
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/controller/directory/directory_controller.dart';
|
import 'package:marco/controller/directory/directory_controller.dart';
|
||||||
import 'package:on_field_work/controller/directory/notes_controller.dart';
|
import 'package:marco/controller/directory/notes_controller.dart';
|
||||||
|
|
||||||
class AddCommentController extends GetxController {
|
class AddCommentController extends GetxController {
|
||||||
final String contactId;
|
final String contactId;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
class AddContactController extends GetxController {
|
class AddContactController extends GetxController {
|
||||||
final RxList<String> categories = <String>[].obs;
|
final RxList<String> categories = <String>[].obs;
|
||||||
@ -10,7 +10,7 @@ class AddContactController extends GetxController {
|
|||||||
final RxList<String> tags = <String>[].obs;
|
final RxList<String> tags = <String>[].obs;
|
||||||
|
|
||||||
final RxString selectedCategory = ''.obs;
|
final RxString selectedCategory = ''.obs;
|
||||||
final RxList<String> selectedBuckets = <String>[].obs;
|
final RxString selectedBucket = ''.obs;
|
||||||
final RxString selectedProject = ''.obs;
|
final RxString selectedProject = ''.obs;
|
||||||
|
|
||||||
final RxList<String> enteredTags = <String>[].obs;
|
final RxList<String> enteredTags = <String>[].obs;
|
||||||
@ -50,7 +50,7 @@ class AddContactController extends GetxController {
|
|||||||
void resetForm() {
|
void resetForm() {
|
||||||
selectedCategory.value = '';
|
selectedCategory.value = '';
|
||||||
selectedProject.value = '';
|
selectedProject.value = '';
|
||||||
selectedBuckets.clear();
|
selectedBucket.value = '';
|
||||||
enteredTags.clear();
|
enteredTags.clear();
|
||||||
filteredSuggestions.clear();
|
filteredSuggestions.clear();
|
||||||
filteredOrgSuggestions.clear();
|
filteredOrgSuggestions.clear();
|
||||||
@ -94,27 +94,12 @@ class AddContactController extends GetxController {
|
|||||||
required List<Map<String, String>> phones,
|
required List<Map<String, String>> phones,
|
||||||
required String address,
|
required String address,
|
||||||
required String description,
|
required String description,
|
||||||
String? designation,
|
|
||||||
}) async {
|
}) async {
|
||||||
if (isSubmitting.value) return;
|
if (isSubmitting.value) return;
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
|
|
||||||
final categoryId = categoriesMap[selectedCategory.value];
|
final categoryId = categoriesMap[selectedCategory.value];
|
||||||
final bucketIds = selectedBuckets
|
final bucketId = bucketsMap[selectedBucket.value];
|
||||||
.map((name) => bucketsMap[name])
|
|
||||||
.whereType<String>()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (bucketIds.isEmpty) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Missing Buckets",
|
|
||||||
message: "Please select at least one bucket.",
|
|
||||||
type: SnackbarType.warning,
|
|
||||||
);
|
|
||||||
isSubmitting.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final projectIds = selectedProjects
|
final projectIds = selectedProjects
|
||||||
.map((name) => projectsMap[name])
|
.map((name) => projectsMap[name])
|
||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
@ -140,10 +125,10 @@ class AddContactController extends GetxController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedBuckets.isEmpty) {
|
if (selectedBucket.value.trim().isEmpty || bucketId == null) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Missing Bucket",
|
title: "Missing Bucket",
|
||||||
message: "Please select at least one bucket.",
|
message: "Please select a bucket.",
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
@ -165,14 +150,12 @@ class AddContactController extends GetxController {
|
|||||||
if (selectedCategory.value.isNotEmpty && categoryId != null)
|
if (selectedCategory.value.isNotEmpty && categoryId != null)
|
||||||
"contactCategoryId": categoryId,
|
"contactCategoryId": categoryId,
|
||||||
if (projectIds.isNotEmpty) "projectIds": projectIds,
|
if (projectIds.isNotEmpty) "projectIds": projectIds,
|
||||||
"bucketIds": bucketIds,
|
"bucketIds": [bucketId],
|
||||||
if (enteredTags.isNotEmpty) "tags": tagObjects,
|
if (enteredTags.isNotEmpty) "tags": tagObjects,
|
||||||
if (emails.isNotEmpty) "contactEmails": emails,
|
if (emails.isNotEmpty) "contactEmails": emails,
|
||||||
if (phones.isNotEmpty) "contactPhones": phones,
|
if (phones.isNotEmpty) "contactPhones": phones,
|
||||||
if (address.trim().isNotEmpty) "address": address.trim(),
|
if (address.trim().isNotEmpty) "address": address.trim(),
|
||||||
if (description.trim().isNotEmpty) "description": description.trim(),
|
if (description.trim().isNotEmpty) "description": description.trim(),
|
||||||
if (designation != null && designation.trim().isNotEmpty)
|
|
||||||
"designation": designation.trim(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logSafe("${id != null ? 'Updating' : 'Creating'} contact");
|
logSafe("${id != null ? 'Updating' : 'Creating'} contact");
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
class BucketController extends GetxController {
|
class BucketController extends GetxController {
|
||||||
RxBool isCreating = false.obs;
|
RxBool isCreating = false.obs;
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/model/directory/contact_model.dart';
|
||||||
import 'package:on_field_work/model/directory/contact_model.dart';
|
import 'package:marco/model/directory/contact_bucket_list_model.dart';
|
||||||
import 'package:on_field_work/model/directory/contact_bucket_list_model.dart';
|
import 'package:marco/model/directory/directory_comment_model.dart';
|
||||||
import 'package:on_field_work/model/directory/directory_comment_model.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
class DirectoryController extends GetxController {
|
class DirectoryController extends GetxController {
|
||||||
// -------------------- CONTACTS --------------------
|
|
||||||
RxList<ContactModel> allContacts = <ContactModel>[].obs;
|
RxList<ContactModel> allContacts = <ContactModel>[].obs;
|
||||||
RxList<ContactModel> filteredContacts = <ContactModel>[].obs;
|
RxList<ContactModel> filteredContacts = <ContactModel>[].obs;
|
||||||
RxList<ContactCategory> contactCategories = <ContactCategory>[].obs;
|
RxList<ContactCategory> contactCategories = <ContactCategory>[].obs;
|
||||||
@ -17,10 +16,16 @@ class DirectoryController extends GetxController {
|
|||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
RxList<ContactBucket> contactBuckets = <ContactBucket>[].obs;
|
RxList<ContactBucket> contactBuckets = <ContactBucket>[].obs;
|
||||||
RxString searchQuery = ''.obs;
|
RxString searchQuery = ''.obs;
|
||||||
|
RxBool showFabMenu = false.obs;
|
||||||
|
final RxBool showFullEditorToolbar = false.obs;
|
||||||
|
final RxBool isEditorFocused = false.obs;
|
||||||
|
RxBool isNotesView = false.obs;
|
||||||
|
|
||||||
|
final Map<String, RxList<DirectoryComment>> contactCommentsMap = {};
|
||||||
|
RxList<DirectoryComment> getCommentsForContact(String contactId) {
|
||||||
|
return contactCommentsMap[contactId] ?? <DirectoryComment>[].obs;
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------- COMMENTS --------------------
|
|
||||||
final Map<String, RxList<DirectoryComment>> activeCommentsMap = {};
|
|
||||||
final Map<String, RxList<DirectoryComment>> inactiveCommentsMap = {};
|
|
||||||
final editingCommentId = Rxn<String>();
|
final editingCommentId = Rxn<String>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -29,75 +34,26 @@ class DirectoryController extends GetxController {
|
|||||||
fetchContacts();
|
fetchContacts();
|
||||||
fetchBuckets();
|
fetchBuckets();
|
||||||
}
|
}
|
||||||
|
// inside DirectoryController
|
||||||
// -------------------- COMMENTS HANDLING --------------------
|
|
||||||
|
|
||||||
RxList<DirectoryComment> getCommentsForContact(String contactId,
|
|
||||||
{bool active = true}) {
|
|
||||||
return active
|
|
||||||
? activeCommentsMap[contactId] ?? <DirectoryComment>[].obs
|
|
||||||
: inactiveCommentsMap[contactId] ?? <DirectoryComment>[].obs;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchCommentsForContact(String contactId,
|
|
||||||
{bool active = true}) async {
|
|
||||||
try {
|
|
||||||
final data =
|
|
||||||
await ApiService.getDirectoryComments(contactId, active: active);
|
|
||||||
var comments =
|
|
||||||
data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? [];
|
|
||||||
|
|
||||||
// ✅ Deduplicate by ID before storing
|
|
||||||
final Map<String, DirectoryComment> uniqueMap = {
|
|
||||||
for (var c in comments) c.id: c,
|
|
||||||
};
|
|
||||||
comments = uniqueMap.values.toList()
|
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
activeCommentsMap[contactId] = <DirectoryComment>[].obs
|
|
||||||
..assignAll(comments);
|
|
||||||
} else {
|
|
||||||
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs
|
|
||||||
..assignAll(comments);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Error fetching ${active ? 'active' : 'inactive'} comments: $e",
|
|
||||||
level: LogLevel.error);
|
|
||||||
logSafe(stack.toString(), level: LogLevel.debug);
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
activeCommentsMap[contactId] = <DirectoryComment>[].obs;
|
|
||||||
} else {
|
|
||||||
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DirectoryComment> combinedComments(String contactId) {
|
|
||||||
final activeList = getCommentsForContact(contactId, active: true);
|
|
||||||
final inactiveList = getCommentsForContact(contactId, active: false);
|
|
||||||
|
|
||||||
// ✅ Deduplicate by ID (active wins)
|
|
||||||
final Map<String, DirectoryComment> byId = {};
|
|
||||||
for (final c in inactiveList) {
|
|
||||||
byId[c.id] = c;
|
|
||||||
}
|
|
||||||
for (final c in activeList) {
|
|
||||||
byId[c.id] = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
final combined = byId.values.toList()
|
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
|
||||||
return combined;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateComment(DirectoryComment comment) async {
|
Future<void> updateComment(DirectoryComment comment) async {
|
||||||
try {
|
try {
|
||||||
final existing = getCommentsForContact(comment.contactId)
|
logSafe(
|
||||||
.firstWhereOrNull((c) => c.id == comment.id);
|
"Attempting to update comment. id: ${comment.id}, contactId: ${comment.contactId}");
|
||||||
|
|
||||||
if (existing != null && existing.note.trim() == comment.note.trim()) {
|
final commentList = contactCommentsMap[comment.contactId];
|
||||||
|
final oldComment =
|
||||||
|
commentList?.firstWhereOrNull((c) => c.id == comment.id);
|
||||||
|
|
||||||
|
if (oldComment == null) {
|
||||||
|
logSafe("Old comment not found. id: ${comment.id}");
|
||||||
|
} else {
|
||||||
|
logSafe("Old comment note: ${oldComment.note}");
|
||||||
|
logSafe("New comment note: ${comment.note}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldComment != null && oldComment.note.trim() == comment.note.trim()) {
|
||||||
|
logSafe("No changes detected in comment. id: ${comment.id}");
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "No Changes",
|
title: "No Changes",
|
||||||
message: "No changes were made to the comment.",
|
message: "No changes were made to the comment.",
|
||||||
@ -107,26 +63,32 @@ class DirectoryController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final success = await ApiService.updateContactComment(
|
final success = await ApiService.updateContactComment(
|
||||||
comment.id, comment.note, comment.contactId);
|
comment.id,
|
||||||
|
comment.note,
|
||||||
|
comment.contactId,
|
||||||
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await fetchCommentsForContact(comment.contactId, active: true);
|
logSafe("Comment updated successfully. id: ${comment.id}");
|
||||||
await fetchCommentsForContact(comment.contactId, active: false);
|
await fetchCommentsForContact(comment.contactId);
|
||||||
|
|
||||||
|
// ✅ Show success message
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "Comment updated successfully.",
|
message: "Comment updated successfully.",
|
||||||
type: SnackbarType.success,
|
type: SnackbarType.success,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
logSafe("Failed to update comment via API. id: ${comment.id}");
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Failed to update comment.",
|
message: "Failed to update comment.",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e, stack) {
|
} catch (e, stackTrace) {
|
||||||
logSafe("Update comment failed: $e", level: LogLevel.error);
|
logSafe("Update comment failed: ${e.toString()}");
|
||||||
logSafe(stack.toString(), level: LogLevel.debug);
|
logSafe("StackTrace: ${stackTrace.toString()}");
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Failed to update comment.",
|
message: "Failed to update comment.",
|
||||||
@ -135,69 +97,29 @@ class DirectoryController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteComment(String commentId, String contactId) async {
|
Future<void> fetchCommentsForContact(String contactId) async {
|
||||||
try {
|
try {
|
||||||
final success = await ApiService.restoreContactComment(commentId, false);
|
final data = await ApiService.getDirectoryComments(contactId);
|
||||||
|
logSafe("Fetched comments for contact $contactId: $data");
|
||||||
|
|
||||||
if (success) {
|
final comments =
|
||||||
if (editingCommentId.value == commentId) editingCommentId.value = null;
|
data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? [];
|
||||||
await fetchCommentsForContact(contactId, active: true);
|
|
||||||
await fetchCommentsForContact(contactId, active: false);
|
if (!contactCommentsMap.containsKey(contactId)) {
|
||||||
showAppSnackbar(
|
contactCommentsMap[contactId] = <DirectoryComment>[].obs;
|
||||||
title: "Deleted",
|
|
||||||
message: "Comment deleted successfully.",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to delete comment.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Delete comment failed: $e", level: LogLevel.error);
|
contactCommentsMap[contactId]!.assignAll(comments);
|
||||||
logSafe(stack.toString(), level: LogLevel.debug);
|
contactCommentsMap[contactId]?.refresh();
|
||||||
showAppSnackbar(
|
} catch (e) {
|
||||||
title: "Error",
|
logSafe("Error fetching comments for contact $contactId: $e",
|
||||||
message: "Something went wrong while deleting comment.",
|
level: LogLevel.error);
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
contactCommentsMap[contactId] ??= <DirectoryComment>[].obs;
|
||||||
|
contactCommentsMap[contactId]!.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restoreComment(String commentId, String contactId) async {
|
|
||||||
try {
|
|
||||||
final success = await ApiService.restoreContactComment(commentId, true);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
await fetchCommentsForContact(contactId, active: true);
|
|
||||||
await fetchCommentsForContact(contactId, active: false);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Restored",
|
|
||||||
message: "Comment restored successfully.",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to restore comment.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Restore comment failed: $e", level: LogLevel.error);
|
|
||||||
logSafe(stack.toString(), level: LogLevel.debug);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Something went wrong while restoring comment.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- CONTACTS HANDLING --------------------
|
|
||||||
|
|
||||||
Future<void> fetchBuckets() async {
|
Future<void> fetchBuckets() async {
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.getContactBucketList();
|
final response = await ApiService.getContactBucketList();
|
||||||
@ -213,71 +135,11 @@ class DirectoryController extends GetxController {
|
|||||||
logSafe("Bucket fetch error: $e", level: LogLevel.error);
|
logSafe("Bucket fetch error: $e", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// -------------------- CONTACT DELETION / RESTORE --------------------
|
|
||||||
|
|
||||||
Future<void> deleteContact(String contactId) async {
|
|
||||||
try {
|
|
||||||
final success = await ApiService.deleteDirectoryContact(contactId);
|
|
||||||
if (success) {
|
|
||||||
// Refresh contacts after deletion
|
|
||||||
await fetchContacts(active: true);
|
|
||||||
await fetchContacts(active: false);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Deleted",
|
|
||||||
message: "Contact deleted successfully.",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to delete contact.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Delete contact failed: $e", level: LogLevel.error);
|
|
||||||
logSafe(stack.toString(), level: LogLevel.debug);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Something went wrong while deleting contact.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> restoreContact(String contactId) async {
|
|
||||||
try {
|
|
||||||
final success = await ApiService.restoreDirectoryContact(contactId);
|
|
||||||
if (success) {
|
|
||||||
// Refresh contacts after restore
|
|
||||||
await fetchContacts(active: true);
|
|
||||||
await fetchContacts(active: false);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Restored",
|
|
||||||
message: "Contact restored successfully.",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to restore contact.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Restore contact failed: $e", level: LogLevel.error);
|
|
||||||
logSafe(stack.toString(), level: LogLevel.debug);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Something went wrong while restoring contact.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchContacts({bool active = true}) async {
|
Future<void> fetchContacts({bool active = true}) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getDirectoryData(isActive: active);
|
final response = await ApiService.getDirectoryData(isActive: active);
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
@ -298,12 +160,14 @@ class DirectoryController extends GetxController {
|
|||||||
|
|
||||||
void extractCategoriesFromContacts() {
|
void extractCategoriesFromContacts() {
|
||||||
final uniqueCategories = <String, ContactCategory>{};
|
final uniqueCategories = <String, ContactCategory>{};
|
||||||
|
|
||||||
for (final contact in allContacts) {
|
for (final contact in allContacts) {
|
||||||
final category = contact.contactCategory;
|
final category = contact.contactCategory;
|
||||||
if (category != null) {
|
if (category != null && !uniqueCategories.containsKey(category.id)) {
|
||||||
uniqueCategories.putIfAbsent(category.id, () => category);
|
uniqueCategories[category.id] = category;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contactCategories.value = uniqueCategories.values.toList();
|
contactCategories.value = uniqueCategories.values.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,7 +192,6 @@ class DirectoryController extends GetxController {
|
|||||||
contact.tags.any((tag) => tag.name.toLowerCase().contains(query));
|
contact.tags.any((tag) => tag.name.toLowerCase().contains(query));
|
||||||
final categoryNameMatch =
|
final categoryNameMatch =
|
||||||
contact.contactCategory?.name.toLowerCase().contains(query) ?? false;
|
contact.contactCategory?.name.toLowerCase().contains(query) ?? false;
|
||||||
|
|
||||||
final bucketNameMatch = contact.bucketIds.any((id) {
|
final bucketNameMatch = contact.bucketIds.any((id) {
|
||||||
final bucketName = contactBuckets
|
final bucketName = contactBuckets
|
||||||
.firstWhereOrNull((b) => b.id == id)
|
.firstWhereOrNull((b) => b.id == id)
|
||||||
@ -350,6 +213,7 @@ class DirectoryController extends GetxController {
|
|||||||
return categoryMatch && bucketMatch && searchMatch;
|
return categoryMatch && bucketMatch && searchMatch;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
// 🔑 Ensure results are always alphabetically sorted
|
||||||
filteredContacts
|
filteredContacts
|
||||||
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
import 'package:marco/model/employees/employee_model.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/controller/directory/directory_controller.dart';
|
import 'package:marco/controller/directory/directory_controller.dart';
|
||||||
|
|
||||||
class ManageBucketController extends GetxController {
|
class ManageBucketController extends GetxController {
|
||||||
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/model/directory/note_list_response_model.dart';
|
import 'package:marco/model/directory/note_list_response_model.dart';
|
||||||
|
|
||||||
class NotesController extends GetxController {
|
class NotesController extends GetxController {
|
||||||
RxList<NoteModel> notesList = <NoteModel>[].obs;
|
RxList<NoteModel> notesList = <NoteModel>[].obs;
|
||||||
@ -107,49 +107,6 @@ class NotesController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restoreOrDeleteNote(NoteModel note,
|
|
||||||
{bool restore = true}) async {
|
|
||||||
final action = restore ? "restore" : "delete";
|
|
||||||
|
|
||||||
try {
|
|
||||||
logSafe("Attempting to $action note id: ${note.id}");
|
|
||||||
|
|
||||||
final success = await ApiService.restoreContactComment(
|
|
||||||
note.id,
|
|
||||||
restore, // true = restore, false = delete
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
final index = notesList.indexWhere((n) => n.id == note.id);
|
|
||||||
if (index != -1) {
|
|
||||||
notesList[index] = note.copyWith(isActive: restore);
|
|
||||||
notesList.refresh();
|
|
||||||
}
|
|
||||||
showAppSnackbar(
|
|
||||||
title: restore ? "Restored" : "Deleted",
|
|
||||||
message: restore
|
|
||||||
? "Note has been restored successfully."
|
|
||||||
: "Note has been deleted successfully.",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message:
|
|
||||||
restore ? "Failed to restore note." : "Failed to delete note.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("$action note failed: $e", error: e, stackTrace: st);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Something went wrong while trying to $action the note.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void addNote(NoteModel note) {
|
void addNote(NoteModel note) {
|
||||||
notesList.insert(0, note);
|
notesList.insert(0, note);
|
||||||
logSafe("Note added to list");
|
logSafe("Note added to list");
|
||||||
|
|||||||
@ -1,82 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
class DocumentDetailsController extends GetxController {
|
|
||||||
/// Observables
|
|
||||||
var isLoading = false.obs;
|
|
||||||
var documentDetails = Rxn<DocumentDetailsResponse>();
|
|
||||||
|
|
||||||
var versions = <DocumentVersionItem>[].obs;
|
|
||||||
var isVersionsLoading = false.obs;
|
|
||||||
|
|
||||||
// Loading states for buttons
|
|
||||||
var isVerifyLoading = false.obs;
|
|
||||||
var isRejectLoading = false.obs;
|
|
||||||
|
|
||||||
/// Fetch document details by id
|
|
||||||
Future<void> fetchDocumentDetails(String documentId) async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
final response = await ApiService.getDocumentDetailsApi(documentId);
|
|
||||||
documentDetails.value = response;
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch document versions by parentAttachmentId
|
|
||||||
Future<void> fetchDocumentVersions(String parentAttachmentId) async {
|
|
||||||
try {
|
|
||||||
isVersionsLoading.value = true;
|
|
||||||
final response = await ApiService.getDocumentVersionsApi(
|
|
||||||
parentAttachmentId: parentAttachmentId,
|
|
||||||
);
|
|
||||||
if (response != null) {
|
|
||||||
versions.assignAll(response.data.data);
|
|
||||||
} else {
|
|
||||||
versions.clear();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isVersionsLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify document
|
|
||||||
Future<bool> verifyDocument(String documentId) async {
|
|
||||||
try {
|
|
||||||
isVerifyLoading.value = true;
|
|
||||||
final result =
|
|
||||||
await ApiService.verifyDocumentApi(id: documentId, isVerify: true);
|
|
||||||
if (result) await fetchDocumentDetails(documentId);
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
isVerifyLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reject document
|
|
||||||
Future<bool> rejectDocument(String documentId) async {
|
|
||||||
try {
|
|
||||||
isRejectLoading.value = true;
|
|
||||||
final result =
|
|
||||||
await ApiService.verifyDocumentApi(id: documentId, isVerify: false);
|
|
||||||
if (result) await fetchDocumentDetails(documentId);
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
isRejectLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch Pre-Signed URL for a given version
|
|
||||||
Future<String?> fetchPresignedUrl(String versionId) async {
|
|
||||||
return await ApiService.getPresignedUrlApi(versionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear data when leaving the screen
|
|
||||||
void clearDetails() {
|
|
||||||
documentDetails.value = null;
|
|
||||||
versions.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
|
||||||
import 'package:on_field_work/model/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';
|
|
||||||
|
|
||||||
class DocumentUploadController extends GetxController {
|
|
||||||
// Observables
|
|
||||||
var isLoading = false.obs;
|
|
||||||
var isUploading = false.obs;
|
|
||||||
|
|
||||||
var categories = <DocumentType>[].obs;
|
|
||||||
var tags = <TagItem>[].obs;
|
|
||||||
|
|
||||||
DocumentType? selectedCategory;
|
|
||||||
|
|
||||||
/// --- FILE HANDLING ---
|
|
||||||
String? selectedFileName;
|
|
||||||
String? selectedFileBase64;
|
|
||||||
String? selectedFileContentType;
|
|
||||||
int? selectedFileSize;
|
|
||||||
|
|
||||||
/// --- TAG HANDLING ---
|
|
||||||
final tagCtrl = TextEditingController();
|
|
||||||
final enteredTags = <String>[].obs;
|
|
||||||
final filteredSuggestions = <String>[].obs;
|
|
||||||
var documentTypes = <DocumentType>[].obs;
|
|
||||||
DocumentType? selectedType;
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
fetchCategories();
|
|
||||||
fetchTags();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch available document categories
|
|
||||||
Future<void> fetchCategories() async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
final response = await ApiService.getMasterDocumentTypesApi();
|
|
||||||
if (response != null && response.data.isNotEmpty) {
|
|
||||||
categories.assignAll(response.data);
|
|
||||||
logSafe("Fetched categories: ${categories.length}");
|
|
||||||
} else {
|
|
||||||
logSafe("No categories fetched", level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchDocumentTypes(String categoryId) async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
final response =
|
|
||||||
await ApiService.getDocumentTypesByCategoryApi(categoryId);
|
|
||||||
if (response != null && response.data.isNotEmpty) {
|
|
||||||
documentTypes.assignAll(response.data);
|
|
||||||
selectedType = null; // reset previous type
|
|
||||||
} else {
|
|
||||||
documentTypes.clear();
|
|
||||||
selectedType = null;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> fetchPresignedUrl(String versionId) async {
|
|
||||||
return await ApiService.getPresignedUrlApi(versionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch available document tags
|
|
||||||
Future<void> fetchTags() async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
final response = await ApiService.getMasterDocumentTagsApi();
|
|
||||||
if (response != null) {
|
|
||||||
tags.assignAll(response.data);
|
|
||||||
logSafe("Fetched tags: ${tags.length}");
|
|
||||||
} else {
|
|
||||||
logSafe("No tags fetched", level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// --- TAG LOGIC ---
|
|
||||||
void filterSuggestions(String query) {
|
|
||||||
if (query.isEmpty) {
|
|
||||||
filteredSuggestions.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
filteredSuggestions.assignAll(
|
|
||||||
tags.map((t) => t.name).where(
|
|
||||||
(tag) => tag.toLowerCase().contains(query.toLowerCase()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addEnteredTag(String tag) {
|
|
||||||
if (tag.trim().isEmpty) return;
|
|
||||||
if (!enteredTags.contains(tag.trim())) {
|
|
||||||
enteredTags.add(tag.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeEnteredTag(String tag) {
|
|
||||||
enteredTags.remove(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearSuggestions() {
|
|
||||||
filteredSuggestions.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload document
|
|
||||||
Future<bool> uploadDocument({
|
|
||||||
required String documentId,
|
|
||||||
required String name,
|
|
||||||
required String entityId,
|
|
||||||
required String documentTypeId,
|
|
||||||
required String fileName,
|
|
||||||
required String base64Data,
|
|
||||||
required String contentType,
|
|
||||||
required int fileSize,
|
|
||||||
String? description,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
isUploading.value = true;
|
|
||||||
|
|
||||||
final payloadTags =
|
|
||||||
enteredTags.map((t) => {"name": t, "isActive": true}).toList();
|
|
||||||
|
|
||||||
final payload = {
|
|
||||||
"documentId": documentId,
|
|
||||||
"name": name,
|
|
||||||
"description": description,
|
|
||||||
"entityId": entityId,
|
|
||||||
"documentTypeId": documentTypeId,
|
|
||||||
"fileName": fileName,
|
|
||||||
"base64Data":
|
|
||||||
base64Data.isNotEmpty ? "<base64-string-truncated>" : null,
|
|
||||||
"contentType": contentType,
|
|
||||||
"fileSize": fileSize,
|
|
||||||
"tags": payloadTags,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Log the payload (hide long base64 string for readability)
|
|
||||||
logSafe("Upload payload: $payload");
|
|
||||||
|
|
||||||
final success = await ApiService.uploadDocumentApi(
|
|
||||||
documentId: documentId,
|
|
||||||
name: name,
|
|
||||||
description: description,
|
|
||||||
entityId: entityId,
|
|
||||||
documentTypeId: documentTypeId,
|
|
||||||
fileName: fileName,
|
|
||||||
base64Data: base64Data,
|
|
||||||
contentType: contentType,
|
|
||||||
fileSize: fileSize,
|
|
||||||
tags: payloadTags,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Success",
|
|
||||||
message: "Document uploaded successfully",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Could not upload document",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Upload error: $e", level: LogLevel.error);
|
|
||||||
logSafe("Stacktrace: $stack", level: LogLevel.debug);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "An unexpected error occurred",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
isUploading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> editDocument(Map<String, dynamic> payload) async {
|
|
||||||
try {
|
|
||||||
isUploading.value = true;
|
|
||||||
|
|
||||||
final attachment = payload["attachment"];
|
|
||||||
|
|
||||||
final success = await ApiService.editDocumentApi(
|
|
||||||
id: payload["id"],
|
|
||||||
name: payload["name"],
|
|
||||||
documentId: payload["documentId"],
|
|
||||||
description: payload["description"],
|
|
||||||
tags: (payload["tags"] as List).cast<Map<String, dynamic>>(),
|
|
||||||
attachment: attachment,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Success",
|
|
||||||
message: "Document updated successfully",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to update document",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Edit error: $e", level: LogLevel.error);
|
|
||||||
logSafe("Stacktrace: $stack", level: LogLevel.debug);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "An unexpected error occurred",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
isUploading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,226 +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/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';
|
|
||||||
|
|
||||||
class DocumentController extends GetxController {
|
|
||||||
// ==================== Observables ====================
|
|
||||||
final isLoading = false.obs;
|
|
||||||
final documents = <DocumentItem>[].obs;
|
|
||||||
final filters = Rxn<DocumentFiltersData>();
|
|
||||||
|
|
||||||
// Selected filters (multi-select)
|
|
||||||
final selectedUploadedBy = <String>[].obs;
|
|
||||||
final selectedCategory = <String>[].obs;
|
|
||||||
final selectedType = <String>[].obs;
|
|
||||||
final selectedTag = <String>[].obs;
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
final pageNumber = 1.obs;
|
|
||||||
final pageSize = 20;
|
|
||||||
final hasMore = true.obs;
|
|
||||||
|
|
||||||
// Error handling
|
|
||||||
final errorMessage = ''.obs;
|
|
||||||
|
|
||||||
// Preferences
|
|
||||||
final showInactive = false.obs;
|
|
||||||
|
|
||||||
// Search
|
|
||||||
final searchQuery = ''.obs;
|
|
||||||
final searchController = TextEditingController();
|
|
||||||
|
|
||||||
// Additional filters
|
|
||||||
final isUploadedAt = true.obs;
|
|
||||||
final isVerified = RxnBool();
|
|
||||||
final startDate = Rxn<DateTime>();
|
|
||||||
final endDate = Rxn<DateTime>();
|
|
||||||
|
|
||||||
// ==================== Lifecycle ====================
|
|
||||||
@override
|
|
||||||
void onClose() {
|
|
||||||
// Don't dispose searchController here - it's managed by the page
|
|
||||||
super.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== API Methods ====================
|
|
||||||
|
|
||||||
/// Fetch document filters for entity
|
|
||||||
Future<void> fetchFilters(String entityTypeId) async {
|
|
||||||
try {
|
|
||||||
final response = await ApiService.getDocumentFilters(entityTypeId);
|
|
||||||
|
|
||||||
if (response != null && response.success) {
|
|
||||||
filters.value = response.data;
|
|
||||||
} else {
|
|
||||||
errorMessage.value = response?.message ?? 'Failed to fetch filters';
|
|
||||||
_showError('Failed to load filters');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
errorMessage.value = 'Error fetching filters: $e';
|
|
||||||
_showError('Error loading filters');
|
|
||||||
debugPrint('❌ Error fetching filters: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle document active/inactive state
|
|
||||||
Future<bool> toggleDocumentActive(
|
|
||||||
String id, {
|
|
||||||
required bool isActive,
|
|
||||||
required String entityTypeId,
|
|
||||||
required String entityId,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
final success = await ApiService.deleteDocumentApi(
|
|
||||||
id: id,
|
|
||||||
isActive: isActive,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// Refresh list after state change
|
|
||||||
await fetchDocuments(
|
|
||||||
entityTypeId: entityTypeId,
|
|
||||||
entityId: entityId,
|
|
||||||
reset: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show success snackbar
|
|
||||||
showAppSnackbar(
|
|
||||||
title: 'Success',
|
|
||||||
message: isActive ? 'Document deactivated' : 'Document activated',
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
errorMessage.value = 'Failed to update document state';
|
|
||||||
_showError('Failed to update document state');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
errorMessage.value = 'Error updating document: $e';
|
|
||||||
_showError('Error updating document: $e');
|
|
||||||
debugPrint('❌ Error toggling document state: $e');
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch documents for entity with pagination
|
|
||||||
Future<void> fetchDocuments({
|
|
||||||
required String entityTypeId,
|
|
||||||
required String entityId,
|
|
||||||
String? filter,
|
|
||||||
String? searchString,
|
|
||||||
bool reset = false,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
if (reset) {
|
|
||||||
pageNumber.value = 1;
|
|
||||||
documents.clear();
|
|
||||||
hasMore.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasMore.value && !reset) return;
|
|
||||||
if (isLoading.value) return;
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
final response = await ApiService.getDocumentListApi(
|
|
||||||
entityTypeId: entityTypeId,
|
|
||||||
entityId: entityId,
|
|
||||||
filter: filter ?? '',
|
|
||||||
searchString: searchString ?? searchQuery.value,
|
|
||||||
pageNumber: pageNumber.value,
|
|
||||||
pageSize: pageSize,
|
|
||||||
isActive: !showInactive.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response != null && response.success) {
|
|
||||||
if (response.data?.data.isNotEmpty ?? false) {
|
|
||||||
documents.addAll(response.data!.data);
|
|
||||||
pageNumber.value++;
|
|
||||||
} else {
|
|
||||||
hasMore.value = false;
|
|
||||||
}
|
|
||||||
errorMessage.value = '';
|
|
||||||
} else {
|
|
||||||
errorMessage.value = response?.message ?? 'Failed to fetch documents';
|
|
||||||
if (documents.isEmpty) {
|
|
||||||
_showError('Failed to load documents');
|
|
||||||
} else {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: 'Warning',
|
|
||||||
message: 'No more documents to load',
|
|
||||||
type: SnackbarType.warning,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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 {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Helper Methods ====================
|
|
||||||
|
|
||||||
/// Clear all selected filters
|
|
||||||
void clearFilters() {
|
|
||||||
selectedUploadedBy.clear();
|
|
||||||
selectedCategory.clear();
|
|
||||||
selectedType.clear();
|
|
||||||
selectedTag.clear();
|
|
||||||
isUploadedAt.value = true;
|
|
||||||
isVerified.value = null;
|
|
||||||
startDate.value = null;
|
|
||||||
endDate.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if any filters are active
|
|
||||||
bool hasActiveFilters() {
|
|
||||||
return selectedUploadedBy.isNotEmpty ||
|
|
||||||
selectedCategory.isNotEmpty ||
|
|
||||||
selectedType.isNotEmpty ||
|
|
||||||
selectedTag.isNotEmpty ||
|
|
||||||
startDate.value != null ||
|
|
||||||
endDate.value != null ||
|
|
||||||
isVerified.value != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show error message via snackbar
|
|
||||||
void _showError(String message) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: 'Error',
|
|
||||||
message: message,
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset controller state
|
|
||||||
void reset() {
|
|
||||||
documents.clear();
|
|
||||||
clearFilters();
|
|
||||||
searchController.clear();
|
|
||||||
searchQuery.value = '';
|
|
||||||
pageNumber.value = 1;
|
|
||||||
hasMore.value = true;
|
|
||||||
showInactive.value = false;
|
|
||||||
errorMessage.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
|
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
|
|
||||||
class DynamicMenuController extends GetxController {
|
class DynamicMenuController extends GetxController {
|
||||||
// UI reactive states
|
// UI reactive states
|
||||||
@ -11,14 +12,20 @@ class DynamicMenuController extends GetxController {
|
|||||||
final RxString errorMessage = ''.obs;
|
final RxString errorMessage = ''.obs;
|
||||||
final RxList<MenuItem> menuItems = <MenuItem>[].obs;
|
final RxList<MenuItem> menuItems = <MenuItem>[].obs;
|
||||||
|
|
||||||
|
Timer? _autoRefreshTimer;
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
// Fetch menus directly from API at startup
|
|
||||||
fetchMenu();
|
fetchMenu();
|
||||||
|
|
||||||
|
/// Auto refresh every 5 minutes (adjust as needed)
|
||||||
|
_autoRefreshTimer = Timer.periodic(
|
||||||
|
const Duration(minutes: 15),
|
||||||
|
(_) => fetchMenu(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch dynamic menu from API (no local cache)
|
/// Fetch dynamic menu from API with error and local storage support
|
||||||
Future<void> fetchMenu() async {
|
Future<void> fetchMenu() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
hasError.value = false;
|
hasError.value = false;
|
||||||
@ -27,36 +34,53 @@ class DynamicMenuController extends GetxController {
|
|||||||
try {
|
try {
|
||||||
final responseData = await ApiService.getMenuApi();
|
final responseData = await ApiService.getMenuApi();
|
||||||
if (responseData != null) {
|
if (responseData != null) {
|
||||||
|
// Directly parse full JSON into MenuResponse
|
||||||
final menuResponse = MenuResponse.fromJson(responseData);
|
final menuResponse = MenuResponse.fromJson(responseData);
|
||||||
|
|
||||||
menuItems.assignAll(menuResponse.data);
|
menuItems.assignAll(menuResponse.data);
|
||||||
|
|
||||||
logSafe("✅ Menu loaded from API with ${menuItems.length} items");
|
// Save menus for offline use
|
||||||
|
await LocalStorage.setMenus(menuItems);
|
||||||
|
|
||||||
|
logSafe("Menu loaded from API with ${menuItems.length} items");
|
||||||
} else {
|
} else {
|
||||||
_handleApiFailure("Menu API returned null response");
|
// If API fails, load from cache
|
||||||
|
final cachedMenus = LocalStorage.getMenus();
|
||||||
|
if (cachedMenus.isNotEmpty) {
|
||||||
|
menuItems.assignAll(cachedMenus);
|
||||||
|
logSafe("Loaded menus from cache: ${menuItems.length} items");
|
||||||
|
} else {
|
||||||
|
hasError.value = true;
|
||||||
|
errorMessage.value = "Failed to fetch menu";
|
||||||
|
menuItems.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_handleApiFailure("Menu fetch exception: $e");
|
logSafe("Menu fetch exception: $e", level: LogLevel.error);
|
||||||
|
|
||||||
|
// On error, load cached menus
|
||||||
|
final cachedMenus = LocalStorage.getMenus();
|
||||||
|
if (cachedMenus.isNotEmpty) {
|
||||||
|
menuItems.assignAll(cachedMenus);
|
||||||
|
logSafe("Loaded menus from cache after error: ${menuItems.length}");
|
||||||
|
} else {
|
||||||
|
hasError.value = true;
|
||||||
|
errorMessage.value = e.toString();
|
||||||
|
menuItems.clear();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleApiFailure(String logMessage) {
|
|
||||||
logSafe(logMessage, level: LogLevel.error);
|
|
||||||
|
|
||||||
// No cache available, show error state
|
|
||||||
hasError.value = true;
|
|
||||||
errorMessage.value = "❌ Unable to load menus. Please try again later.";
|
|
||||||
menuItems.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isMenuAllowed(String menuName) {
|
bool isMenuAllowed(String menuName) {
|
||||||
final menu = menuItems.firstWhereOrNull((m) => m.name == menuName);
|
final menu = menuItems.firstWhereOrNull((m) => m.name == menuName);
|
||||||
return menu?.available ?? false;
|
return menu?.available ?? false; // default false if not found
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
_autoRefreshTimer?.cancel(); // clean up timer
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
enum Gender {
|
enum Gender {
|
||||||
male,
|
male,
|
||||||
@ -18,188 +17,122 @@ enum Gender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AddEmployeeController extends MyController {
|
class AddEmployeeController extends MyController {
|
||||||
Map<String, dynamic>? editingEmployeeData;
|
List<PlatformFile> files = [];
|
||||||
|
|
||||||
// State
|
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
final List<PlatformFile> files = [];
|
|
||||||
final List<String> categories = [];
|
|
||||||
|
|
||||||
Gender? selectedGender;
|
Gender? selectedGender;
|
||||||
List<Map<String, dynamic>> roles = [];
|
List<Map<String, dynamic>> roles = [];
|
||||||
String? selectedRoleId;
|
String? selectedRoleId;
|
||||||
String selectedCountryCode = '+91';
|
String selectedCountryCode = "+91";
|
||||||
bool showOnline = true;
|
bool showOnline = true;
|
||||||
DateTime? joiningDate;
|
final List<String> categories = [];
|
||||||
String? selectedOrganizationId;
|
|
||||||
RxString selectedOrganizationName = RxString('');
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
logSafe('Initializing AddEmployeeController...');
|
logSafe("Initializing AddEmployeeController...");
|
||||||
_initializeFields();
|
_initializeFields();
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
|
|
||||||
if (editingEmployeeData != null) {
|
|
||||||
prefillFields();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeFields() {
|
void _initializeFields() {
|
||||||
basicValidator.addField(
|
basicValidator.addField(
|
||||||
'first_name',
|
'first_name',
|
||||||
label: 'First Name',
|
label: "First Name",
|
||||||
required: true,
|
required: true,
|
||||||
controller: TextEditingController(),
|
controller: TextEditingController(),
|
||||||
);
|
);
|
||||||
basicValidator.addField(
|
basicValidator.addField(
|
||||||
'phone_number',
|
'phone_number',
|
||||||
label: 'Phone Number',
|
label: "Phone Number",
|
||||||
required: true,
|
required: true,
|
||||||
controller: TextEditingController(),
|
controller: TextEditingController(),
|
||||||
);
|
);
|
||||||
basicValidator.addField(
|
basicValidator.addField(
|
||||||
'last_name',
|
'last_name',
|
||||||
label: 'Last Name',
|
label: "Last Name",
|
||||||
required: true,
|
required: true,
|
||||||
controller: TextEditingController(),
|
controller: TextEditingController(),
|
||||||
);
|
);
|
||||||
// Email is optional in controller; UI enforces when application access is checked
|
logSafe("Fields initialized for first_name, phone_number, last_name.");
|
||||||
basicValidator.addField(
|
|
||||||
'email',
|
|
||||||
label: 'Email',
|
|
||||||
required: false,
|
|
||||||
controller: TextEditingController(),
|
|
||||||
);
|
|
||||||
|
|
||||||
logSafe('Fields initialized for first_name, phone_number, last_name, email.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefill fields in edit mode
|
|
||||||
void prefillFields() {
|
|
||||||
logSafe('Prefilling data for editing...');
|
|
||||||
basicValidator.getController('first_name')?.text =
|
|
||||||
editingEmployeeData?['first_name'] ?? '';
|
|
||||||
basicValidator.getController('last_name')?.text =
|
|
||||||
editingEmployeeData?['last_name'] ?? '';
|
|
||||||
basicValidator.getController('phone_number')?.text =
|
|
||||||
editingEmployeeData?['phone_number'] ?? '';
|
|
||||||
|
|
||||||
selectedGender = editingEmployeeData?['gender'] != null
|
|
||||||
? Gender.values.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
|
|
||||||
: null;
|
|
||||||
|
|
||||||
basicValidator.getController('email')?.text =
|
|
||||||
editingEmployeeData?['email'] ?? '';
|
|
||||||
|
|
||||||
selectedRoleId = editingEmployeeData?['job_role_id'];
|
|
||||||
|
|
||||||
if (editingEmployeeData?['joining_date'] != null) {
|
|
||||||
joiningDate = DateTime.tryParse(editingEmployeeData!['joining_date']);
|
|
||||||
}
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setJoiningDate(DateTime date) {
|
|
||||||
joiningDate = date;
|
|
||||||
logSafe('Joining date selected: $date');
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGenderSelected(Gender? gender) {
|
void onGenderSelected(Gender? gender) {
|
||||||
selectedGender = gender;
|
selectedGender = gender;
|
||||||
logSafe('Gender selected: ${gender?.name}');
|
logSafe("Gender selected: ${gender?.name}");
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchRoles() async {
|
Future<void> fetchRoles() async {
|
||||||
logSafe('Fetching roles...');
|
logSafe("Fetching roles...");
|
||||||
try {
|
try {
|
||||||
final result = await ApiService.getRoles();
|
final result = await ApiService.getRoles();
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
roles = List<Map<String, dynamic>>.from(result);
|
roles = List<Map<String, dynamic>>.from(result);
|
||||||
logSafe('Roles fetched successfully.');
|
logSafe("Roles fetched successfully.");
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
logSafe('Failed to fetch roles: null result', level: LogLevel.error);
|
logSafe("Failed to fetch roles: null result", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
logSafe('Error fetching roles', level: LogLevel.error, error: e, stackTrace: st);
|
logSafe("Error fetching roles",
|
||||||
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onRoleSelected(String? roleId) {
|
void onRoleSelected(String? roleId) {
|
||||||
selectedRoleId = roleId;
|
selectedRoleId = roleId;
|
||||||
logSafe('Role selected: $roleId');
|
logSafe("Role selected: $roleId");
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or update employee
|
Future<Map<String, dynamic>?> createEmployees() async {
|
||||||
Future<Map<String, dynamic>?> createOrUpdateEmployee({
|
logSafe("Starting employee creation...");
|
||||||
String? email,
|
|
||||||
bool hasApplicationAccess = false,
|
|
||||||
}) async {
|
|
||||||
logSafe(editingEmployeeData != null
|
|
||||||
? 'Starting employee update...'
|
|
||||||
: 'Starting employee creation...');
|
|
||||||
|
|
||||||
if (selectedGender == null || selectedRoleId == null) {
|
if (selectedGender == null || selectedRoleId == null) {
|
||||||
|
logSafe("Missing gender or role.", level: LogLevel.warning);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: 'Missing Fields',
|
title: "Missing Fields",
|
||||||
message: 'Please select both Gender and Role.',
|
message: "Please select both Gender and Role.",
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final firstName = basicValidator.getController('first_name')?.text.trim();
|
final firstName = basicValidator.getController("first_name")?.text.trim();
|
||||||
final lastName = basicValidator.getController('last_name')?.text.trim();
|
final lastName = basicValidator.getController("last_name")?.text.trim();
|
||||||
final phoneNumber = basicValidator.getController('phone_number')?.text.trim();
|
final phoneNumber =
|
||||||
|
basicValidator.getController("phone_number")?.text.trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// sanitize orgId before sending
|
|
||||||
final String? orgId = (selectedOrganizationId != null &&
|
|
||||||
selectedOrganizationId!.trim().isNotEmpty)
|
|
||||||
? selectedOrganizationId
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final response = await ApiService.createEmployee(
|
final response = await ApiService.createEmployee(
|
||||||
id: editingEmployeeData?['id'],
|
|
||||||
firstName: firstName!,
|
firstName: firstName!,
|
||||||
lastName: lastName!,
|
lastName: lastName!,
|
||||||
phoneNumber: phoneNumber!,
|
phoneNumber: phoneNumber!,
|
||||||
gender: selectedGender!.name,
|
gender: selectedGender!.name,
|
||||||
jobRoleId: selectedRoleId!,
|
jobRoleId: selectedRoleId!,
|
||||||
joiningDate: joiningDate?.toIso8601String() ?? '',
|
|
||||||
organizationId: orgId,
|
|
||||||
email: email,
|
|
||||||
hasApplicationAccess: hasApplicationAccess,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
logSafe('Response: $response');
|
logSafe("Response: $response");
|
||||||
|
|
||||||
if (response != null && response['success'] == true) {
|
if (response != null && response['success'] == true) {
|
||||||
|
logSafe("Employee created successfully.");
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: 'Success',
|
title: "Success",
|
||||||
message: editingEmployeeData != null
|
message: "Employee created successfully!",
|
||||||
? 'Employee updated successfully!'
|
|
||||||
: 'Employee created successfully!',
|
|
||||||
type: SnackbarType.success,
|
type: SnackbarType.success,
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
} else {
|
} else {
|
||||||
logSafe('Failed operation', level: LogLevel.error);
|
logSafe("Failed to create employee (response false)",
|
||||||
|
level: LogLevel.error);
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
logSafe('Error creating/updating employee',
|
logSafe("Error creating employee",
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
}
|
}
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
message: 'Failed to save employee.',
|
message: "Failed to create employee.",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
@ -215,8 +148,9 @@ class AddEmployeeController extends MyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: 'Permission Required',
|
title: "Permission Required",
|
||||||
message: 'Please allow Contacts permission from settings to pick a contact.',
|
message:
|
||||||
|
"Please allow Contacts permission from settings to pick a contact.",
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@ -234,8 +168,8 @@ class AddEmployeeController extends MyController {
|
|||||||
await FlutterContacts.getContact(picked.id, withProperties: true);
|
await FlutterContacts.getContact(picked.id, withProperties: true);
|
||||||
if (contact == null) {
|
if (contact == null) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
message: 'Failed to load contact details.',
|
message: "Failed to load contact details.",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -243,8 +177,8 @@ class AddEmployeeController extends MyController {
|
|||||||
|
|
||||||
if (contact.phones.isEmpty) {
|
if (contact.phones.isEmpty) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: 'No Phone Number',
|
title: "No Phone Number",
|
||||||
message: 'Selected contact has no phone number.',
|
message: "Selected contact has no phone number.",
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -258,8 +192,8 @@ class AddEmployeeController extends MyController {
|
|||||||
|
|
||||||
if (indiaPhones.isEmpty) {
|
if (indiaPhones.isEmpty) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: 'No Indian Number',
|
title: "No Indian Number",
|
||||||
message: 'Selected contact has no Indian (+91) phone number.',
|
message: "Selected contact has no Indian (+91) phone number.",
|
||||||
type: SnackbarType.warning,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -272,20 +206,19 @@ class AddEmployeeController extends MyController {
|
|||||||
selectedPhone = await showDialog<String>(
|
selectedPhone = await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('Choose an Indian number'),
|
title: Text("Choose an Indian number"),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: indiaPhones
|
children: indiaPhones
|
||||||
.map(
|
.map((p) => ListTile(
|
||||||
(p) => ListTile(
|
title: Text(p.number),
|
||||||
title: Text(p.number),
|
onTap: () => Navigator.of(ctx).pop(p.number),
|
||||||
onTap: () => Navigator.of(ctx).pop(p.number),
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedPhone == null) return;
|
if (selectedPhone == null) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,11 +231,11 @@ class AddEmployeeController extends MyController {
|
|||||||
phoneWithoutCountryCode;
|
phoneWithoutCountryCode;
|
||||||
update();
|
update();
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
logSafe('Error fetching contacts',
|
logSafe("Error fetching contacts",
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
message: 'Failed to fetch contacts.',
|
message: "Failed to fetch contacts.",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/model/global_project_model.dart';
|
import 'package:marco/model/global_project_model.dart';
|
||||||
import 'package:on_field_work/model/employees/assigned_projects_model.dart';
|
import 'package:marco/model/employees/assigned_projects_model.dart';
|
||||||
import 'package:on_field_work/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
class AssignProjectController extends GetxController {
|
class AssignProjectController extends GetxController {
|
||||||
final String employeeId;
|
final String employeeId;
|
||||||
|
|||||||
@ -1,59 +1,85 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
import 'package:marco/model/attendance/attendance_model.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_details_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
|
import 'package:marco/model/employees/employee_model.dart';
|
||||||
|
import 'package:marco/model/employees/employee_details_model.dart';
|
||||||
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
class EmployeesScreenController extends GetxController {
|
class EmployeesScreenController extends GetxController {
|
||||||
/// ✅ Data lists
|
List<AttendanceModel> attendances = [];
|
||||||
|
List<ProjectModel> projects = [];
|
||||||
|
String? selectedProjectId;
|
||||||
|
List<EmployeeDetailsModel> employeeDetails = [];
|
||||||
|
RxBool isAllEmployeeSelected = false.obs;
|
||||||
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
|
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
|
||||||
|
|
||||||
|
RxBool isLoading = false.obs;
|
||||||
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
|
Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
|
||||||
Rxn<EmployeeDetailsModel>();
|
Rxn<EmployeeDetailsModel>();
|
||||||
|
|
||||||
/// ✅ Loading states
|
|
||||||
RxBool isLoading = false.obs;
|
|
||||||
RxBool isLoadingEmployeeDetails = false.obs;
|
RxBool isLoadingEmployeeDetails = false.obs;
|
||||||
|
|
||||||
/// ✅ Selection state
|
|
||||||
RxBool isAllEmployeeSelected = false.obs;
|
|
||||||
RxSet<String> selectedEmployeeIds = <String>{}.obs;
|
|
||||||
|
|
||||||
/// ✅ Upload state tracking (if needed later)
|
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
|
||||||
|
|
||||||
RxList<EmployeeModel> selectedEmployeePrimaryManagers = <EmployeeModel>[].obs;
|
|
||||||
RxList<EmployeeModel> selectedEmployeeSecondaryManagers =
|
|
||||||
<EmployeeModel>[].obs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
fetchAllEmployees();
|
isLoading.value = true;
|
||||||
|
fetchAllProjects().then((_) {
|
||||||
|
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
||||||
|
if (projectId != null) {
|
||||||
|
selectedProjectId = projectId;
|
||||||
|
fetchEmployeesByProject(projectId);
|
||||||
|
} else if (isAllEmployeeSelected.value) {
|
||||||
|
fetchAllEmployees();
|
||||||
|
} else {
|
||||||
|
clearEmployees();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔹 Fetch all employees (no project filter)
|
Future<void> fetchAllProjects() async {
|
||||||
Future<void> fetchAllEmployees({String? organizationId}) 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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchAllEmployees() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
update(['employee_screen_controller']);
|
update(['employee_screen_controller']);
|
||||||
|
|
||||||
await _handleApiCall(
|
await _handleApiCall(
|
||||||
() => ApiService.getAllEmployees(organizationId: organizationId),
|
ApiService.getAllEmployees,
|
||||||
onSuccess: (data) {
|
onSuccess: (data) {
|
||||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||||
logSafe(
|
logSafe("All Employees fetched: ${employees.length} employees loaded.",
|
||||||
"All Employees fetched: ${employees.length} employees loaded.",
|
level: LogLevel.info);
|
||||||
level: LogLevel.info,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset selection states when new data arrives
|
|
||||||
selectedEmployeeIds.clear();
|
|
||||||
isAllEmployeeSelected.value = false;
|
|
||||||
},
|
},
|
||||||
onEmpty: () {
|
onEmpty: () {
|
||||||
employees.clear();
|
employees.clear();
|
||||||
selectedEmployeeIds.clear();
|
logSafe("No Employee data found or API call failed.",
|
||||||
isAllEmployeeSelected.value = false;
|
|
||||||
logSafe("No Employee data found or API call failed",
|
|
||||||
level: LogLevel.warning);
|
level: LogLevel.warning);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -62,7 +88,49 @@ class EmployeesScreenController extends GetxController {
|
|||||||
update(['employee_screen_controller']);
|
update(['employee_screen_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔹 Fetch details for a specific employee
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
|
if (projectId == null || projectId.isEmpty) {
|
||||||
|
logSafe("Project ID is required but was null or empty.",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
await _handleApiCall(
|
||||||
|
() => ApiService.getAllEmployeesByProject(projectId),
|
||||||
|
onSuccess: (data) {
|
||||||
|
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||||
|
|
||||||
|
for (var emp in employees) {
|
||||||
|
uploadingStates[emp.id] = false.obs;
|
||||||
|
}
|
||||||
|
|
||||||
|
logSafe(
|
||||||
|
"Employees fetched: ${employees.length} for project $projectId",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onEmpty: () {
|
||||||
|
employees.clear();
|
||||||
|
logSafe(
|
||||||
|
"No employees found for project $projectId.",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (e) {
|
||||||
|
logSafe(
|
||||||
|
"Error fetching employees for project $projectId",
|
||||||
|
level: LogLevel.error,
|
||||||
|
error: e,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
update(['employee_screen_controller']);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> fetchEmployeeDetails(String? employeeId) async {
|
Future<void> fetchEmployeeDetails(String? employeeId) async {
|
||||||
if (employeeId == null || employeeId.isEmpty) return;
|
if (employeeId == null || employeeId.isEmpty) return;
|
||||||
|
|
||||||
@ -72,80 +140,31 @@ class EmployeesScreenController extends GetxController {
|
|||||||
() => ApiService.getEmployeeDetails(employeeId),
|
() => ApiService.getEmployeeDetails(employeeId),
|
||||||
onSuccess: (data) {
|
onSuccess: (data) {
|
||||||
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
|
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
|
||||||
logSafe("Employee details loaded for $employeeId",
|
logSafe(
|
||||||
level: LogLevel.info);
|
"Employee details loaded for $employeeId",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onEmpty: () {
|
onEmpty: () {
|
||||||
selectedEmployeeDetails.value = null;
|
selectedEmployeeDetails.value = null;
|
||||||
logSafe("No employee details found for $employeeId",
|
logSafe(
|
||||||
level: LogLevel.warning);
|
"No employee details found for $employeeId",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError: (e) {
|
onError: (e) {
|
||||||
selectedEmployeeDetails.value = null;
|
selectedEmployeeDetails.value = null;
|
||||||
logSafe("Error fetching employee details for $employeeId",
|
logSafe(
|
||||||
level: LogLevel.error, error: e);
|
"Error fetching employee details for $employeeId",
|
||||||
|
level: LogLevel.error,
|
||||||
|
error: e,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
isLoadingEmployeeDetails.value = false;
|
isLoadingEmployeeDetails.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch reporting managers for a specific employee from /organization/hierarchy/list/:employeeId
|
|
||||||
Future<void> fetchReportingManagers(String? employeeId) async {
|
|
||||||
if (employeeId == null || employeeId.isEmpty) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// ✅ Always clear before new fetch (to avoid mixing old data)
|
|
||||||
selectedEmployeePrimaryManagers.clear();
|
|
||||||
selectedEmployeeSecondaryManagers.clear();
|
|
||||||
|
|
||||||
// Fetch from existing API helper
|
|
||||||
final data = await ApiService.getOrganizationHierarchyList(employeeId);
|
|
||||||
|
|
||||||
if (data == null || data.isEmpty) {
|
|
||||||
update(['employee_screen_controller']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final item in data) {
|
|
||||||
try {
|
|
||||||
final reportTo = item['reportTo'];
|
|
||||||
if (reportTo == null) continue;
|
|
||||||
|
|
||||||
final emp = EmployeeModel.fromJson(reportTo);
|
|
||||||
final isPrimary = item['isPrimary'] == true;
|
|
||||||
|
|
||||||
if (isPrimary) {
|
|
||||||
if (!selectedEmployeePrimaryManagers.any((e) => e.id == emp.id)) {
|
|
||||||
selectedEmployeePrimaryManagers.add(emp);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!selectedEmployeeSecondaryManagers.any((e) => e.id == emp.id)) {
|
|
||||||
selectedEmployeeSecondaryManagers.add(emp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// ignore malformed items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update(['employee_screen_controller']);
|
|
||||||
} catch (e) {
|
|
||||||
logSafe("Error fetching reporting managers for $employeeId",
|
|
||||||
level: LogLevel.error, error: e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 🔹 Clear all employee data
|
|
||||||
void clearEmployees() {
|
|
||||||
employees.clear();
|
|
||||||
selectedEmployeeIds.clear();
|
|
||||||
isAllEmployeeSelected.value = false;
|
|
||||||
logSafe("Employees cleared", level: LogLevel.info);
|
|
||||||
update(['employee_screen_controller']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 🔹 Generic handler for list API responses
|
|
||||||
Future<void> _handleApiCall(
|
Future<void> _handleApiCall(
|
||||||
Future<List<dynamic>?> Function() apiCall, {
|
Future<List<dynamic>?> Function() apiCall, {
|
||||||
required Function(List<dynamic>) onSuccess,
|
required Function(List<dynamic>) onSuccess,
|
||||||
@ -168,7 +187,6 @@ class EmployeesScreenController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔹 Generic handler for single-object API responses
|
|
||||||
Future<void> _handleSingleApiCall(
|
Future<void> _handleSingleApiCall(
|
||||||
Future<Map<String, dynamic>?> Function() apiCall, {
|
Future<Map<String, dynamic>?> Function() apiCall, {
|
||||||
required Function(Map<String, dynamic>) onSuccess,
|
required Function(Map<String, dynamic>) onSuccess,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
|
|
||||||
class ComingSoonController extends MyController {
|
class ComingSoonController extends MyController {
|
||||||
Timer? countdownTimer;
|
Timer? countdownTimer;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
|
|
||||||
class Error404Controller extends MyController {
|
class Error404Controller extends MyController {
|
||||||
void goToDashboardScreen() {
|
void goToDashboardScreen() {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
|
|
||||||
class Error500Controller extends MyController {
|
class Error500Controller extends MyController {
|
||||||
void goToDashboardScreen() {
|
void goToDashboardScreen() {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -8,41 +7,28 @@ import 'package:get/get.dart';
|
|||||||
import 'package:geocoding/geocoding.dart';
|
import 'package:geocoding/geocoding.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:intl/intl.dart';
|
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:marco/controller/expense/expense_screen_controller.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
import 'package:marco/model/employees/employee_model.dart';
|
||||||
import 'package:on_field_work/model/expense/expense_type_model.dart';
|
import 'package:marco/model/expense/expense_type_model.dart';
|
||||||
import 'package:on_field_work/model/expense/payment_types_model.dart';
|
import 'package:marco/model/expense/payment_types_model.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
class AddExpenseController extends GetxController {
|
class AddExpenseController extends GetxController {
|
||||||
// --- Text Controllers ---
|
// --- Text Controllers ---
|
||||||
final controllers = <TextEditingController>[
|
final amountController = TextEditingController();
|
||||||
TextEditingController(), // amount
|
final descriptionController = TextEditingController();
|
||||||
TextEditingController(), // description
|
final supplierController = TextEditingController();
|
||||||
TextEditingController(), // supplier
|
final transactionIdController = TextEditingController();
|
||||||
TextEditingController(), // transactionId
|
final gstController = TextEditingController();
|
||||||
TextEditingController(), // gst
|
final locationController = TextEditingController();
|
||||||
TextEditingController(), // location
|
final transactionDateController = TextEditingController();
|
||||||
TextEditingController(), // transactionDate
|
final noOfPersonsController = TextEditingController();
|
||||||
TextEditingController(), // noOfPersons
|
|
||||||
TextEditingController(), // employeeSearch
|
|
||||||
];
|
|
||||||
|
|
||||||
TextEditingController get amountController => controllers[0];
|
final employeeSearchController = TextEditingController();
|
||||||
TextEditingController get descriptionController => controllers[1];
|
|
||||||
TextEditingController get supplierController => controllers[2];
|
|
||||||
TextEditingController get transactionIdController => controllers[3];
|
|
||||||
TextEditingController get gstController => controllers[4];
|
|
||||||
TextEditingController get locationController => controllers[5];
|
|
||||||
TextEditingController get transactionDateController => controllers[6];
|
|
||||||
TextEditingController get noOfPersonsController => controllers[7];
|
|
||||||
TextEditingController get employeeSearchController => controllers[8];
|
|
||||||
|
|
||||||
// --- Reactive State ---
|
// --- Reactive State ---
|
||||||
final isLoading = false.obs;
|
final isLoading = false.obs;
|
||||||
@ -51,22 +37,10 @@ class AddExpenseController extends GetxController {
|
|||||||
final isEditMode = false.obs;
|
final isEditMode = false.obs;
|
||||||
final isSearchingEmployees = false.obs;
|
final isSearchingEmployees = false.obs;
|
||||||
|
|
||||||
// --- Paid By (Single + Multi Selection Support) ---
|
|
||||||
|
|
||||||
// single selection
|
|
||||||
final selectedPaidBy = Rxn<EmployeeModel>();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// helper setters
|
|
||||||
void setSelectedPaidBy(EmployeeModel? emp) {
|
|
||||||
selectedPaidBy.value = emp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Dropdown Selections & Data ---
|
// --- Dropdown Selections & Data ---
|
||||||
final selectedPaymentMode = Rxn<PaymentModeModel>();
|
final selectedPaymentMode = Rxn<PaymentModeModel>();
|
||||||
final selectedExpenseType = Rxn<ExpenseTypeModel>();
|
final selectedExpenseType = Rxn<ExpenseTypeModel>();
|
||||||
// final selectedPaidBy = Rxn<EmployeeModel>();
|
final selectedPaidBy = Rxn<EmployeeModel>();
|
||||||
final selectedProject = ''.obs;
|
final selectedProject = ''.obs;
|
||||||
final selectedTransactionDate = Rxn<DateTime>();
|
final selectedTransactionDate = Rxn<DateTime>();
|
||||||
|
|
||||||
@ -79,25 +53,34 @@ class AddExpenseController extends GetxController {
|
|||||||
final paymentModes = <PaymentModeModel>[].obs;
|
final paymentModes = <PaymentModeModel>[].obs;
|
||||||
final allEmployees = <EmployeeModel>[].obs;
|
final allEmployees = <EmployeeModel>[].obs;
|
||||||
final employeeSearchResults = <EmployeeModel>[].obs;
|
final employeeSearchResults = <EmployeeModel>[].obs;
|
||||||
final isProcessingAttachment = false.obs;
|
|
||||||
|
|
||||||
String? editingExpenseId;
|
String? editingExpenseId;
|
||||||
|
|
||||||
final expenseController = Get.find<ExpenseController>();
|
final expenseController = Get.find<ExpenseController>();
|
||||||
final ImagePicker _picker = ImagePicker();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
loadMasterData();
|
fetchMasterData();
|
||||||
employeeSearchController.addListener(
|
fetchGlobalProjects();
|
||||||
() => searchEmployees(employeeSearchController.text),
|
employeeSearchController.addListener(() {
|
||||||
);
|
searchEmployees(employeeSearchController.text);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
for (var c in controllers) {
|
for (var c in [
|
||||||
|
amountController,
|
||||||
|
descriptionController,
|
||||||
|
supplierController,
|
||||||
|
transactionIdController,
|
||||||
|
gstController,
|
||||||
|
locationController,
|
||||||
|
transactionDateController,
|
||||||
|
noOfPersonsController,
|
||||||
|
employeeSearchController,
|
||||||
|
]) {
|
||||||
c.dispose();
|
c.dispose();
|
||||||
}
|
}
|
||||||
super.onClose();
|
super.onClose();
|
||||||
@ -108,19 +91,11 @@ class AddExpenseController extends GetxController {
|
|||||||
if (query.trim().isEmpty) return employeeSearchResults.clear();
|
if (query.trim().isEmpty) return employeeSearchResults.clear();
|
||||||
isSearchingEmployees.value = true;
|
isSearchingEmployees.value = true;
|
||||||
try {
|
try {
|
||||||
final data = await ApiService.searchEmployeesBasic(
|
final data =
|
||||||
searchString: query.trim(),
|
await ApiService.searchEmployeesBasic(searchString: query.trim());
|
||||||
|
employeeSearchResults.assignAll(
|
||||||
|
(data ?? []).map((e) => EmployeeModel.fromJson(e)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data is List) {
|
|
||||||
employeeSearchResults.assignAll(
|
|
||||||
data
|
|
||||||
.map((e) => EmployeeModel.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
employeeSearchResults.clear();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("Error searching employees: $e", level: LogLevel.error);
|
logSafe("Error searching employees: $e", level: LogLevel.error);
|
||||||
employeeSearchResults.clear();
|
employeeSearchResults.clear();
|
||||||
@ -129,77 +104,64 @@ class AddExpenseController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Form Population (Edit) ---
|
// --- Form Population: Edit Mode ---
|
||||||
Future<void> populateFieldsForEdit(Map<String, dynamic> data) async {
|
Future<void> populateFieldsForEdit(Map<String, dynamic> data) async {
|
||||||
isEditMode.value = true;
|
isEditMode.value = true;
|
||||||
editingExpenseId = '${data['id']}';
|
editingExpenseId = '${data['id']}';
|
||||||
|
|
||||||
selectedProject.value = data['projectName'] ?? '';
|
selectedProject.value = data['projectName'] ?? '';
|
||||||
amountController.text = '${data['amount'] ?? ''}';
|
amountController.text = data['amount']?.toString() ?? '';
|
||||||
supplierController.text = data['supplerName'] ?? '';
|
supplierController.text = data['supplerName'] ?? '';
|
||||||
descriptionController.text = data['description'] ?? '';
|
descriptionController.text = data['description'] ?? '';
|
||||||
transactionIdController.text = data['transactionId'] ?? '';
|
transactionIdController.text = data['transactionId'] ?? '';
|
||||||
locationController.text = data['location'] ?? '';
|
locationController.text = data['location'] ?? '';
|
||||||
noOfPersonsController.text = '${data['noOfPersons'] ?? 0}';
|
noOfPersonsController.text = (data['noOfPersons'] ?? 0).toString();
|
||||||
|
|
||||||
_setTransactionDate(data['transactionDate']);
|
// Transaction Date
|
||||||
_setDropdowns(data);
|
if (data['transactionDate'] != null) {
|
||||||
await _setPaidBy(data);
|
try {
|
||||||
_setAttachments(data['attachments']);
|
final parsed = DateTime.parse(data['transactionDate']);
|
||||||
|
selectedTransactionDate.value = parsed;
|
||||||
_logPrefilledData();
|
transactionDateController.text =
|
||||||
}
|
DateFormat('dd-MM-yyyy').format(parsed);
|
||||||
|
} catch (_) {
|
||||||
void _setTransactionDate(dynamic dateStr) {
|
selectedTransactionDate.value = null;
|
||||||
if (dateStr == null) {
|
transactionDateController.clear();
|
||||||
selectedTransactionDate.value = null;
|
}
|
||||||
transactionDateController.clear();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
final parsed = DateTime.parse(dateStr);
|
|
||||||
selectedTransactionDate.value = parsed;
|
|
||||||
transactionDateController.text = DateFormat('dd-MM-yyyy').format(parsed);
|
|
||||||
} catch (_) {
|
|
||||||
selectedTransactionDate.value = null;
|
|
||||||
transactionDateController.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setDropdowns(Map<String, dynamic> data) {
|
// Dropdown
|
||||||
selectedExpenseType.value =
|
selectedExpenseType.value =
|
||||||
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
|
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
|
||||||
selectedPaymentMode.value =
|
selectedPaymentMode.value =
|
||||||
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
|
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _setPaidBy(Map<String, dynamic> data) async {
|
// Paid By
|
||||||
final paidById = '${data['paidById']}';
|
final paidById = '${data['paidById']}';
|
||||||
selectedPaidBy.value =
|
selectedPaidBy.value =
|
||||||
allEmployees.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
allEmployees.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
||||||
|
|
||||||
if (selectedPaidBy.value == null && data['paidByFirstName'] != null) {
|
if (selectedPaidBy.value == null && data['paidByFirstName'] != null) {
|
||||||
await searchEmployees(
|
await searchEmployees(
|
||||||
'${data['paidByFirstName']} ${data['paidByLastName']}',
|
'${data['paidByFirstName']} ${data['paidByLastName']}');
|
||||||
);
|
|
||||||
selectedPaidBy.value = employeeSearchResults
|
selectedPaidBy.value = employeeSearchResults
|
||||||
.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void _setAttachments(dynamic attachmentsData) {
|
// Attachments
|
||||||
existingAttachments.clear();
|
existingAttachments.clear();
|
||||||
if (attachmentsData is List) {
|
if (data['attachments'] is List) {
|
||||||
existingAttachments.addAll(
|
existingAttachments.addAll(
|
||||||
List<Map<String, dynamic>>.from(attachmentsData).map(
|
List<Map<String, dynamic>>.from(data['attachments'])
|
||||||
(e) => {...e, 'isActive': true},
|
.map((e) => {...e, 'isActive': true}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logPrefilledData();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _logPrefilledData() {
|
void _logPrefilledData() {
|
||||||
final info = [
|
logSafe('--- Prefilled Expense Data ---', level: LogLevel.info);
|
||||||
|
[
|
||||||
'ID: $editingExpenseId',
|
'ID: $editingExpenseId',
|
||||||
'Project: ${selectedProject.value}',
|
'Project: ${selectedProject.value}',
|
||||||
'Amount: ${amountController.text}',
|
'Amount: ${amountController.text}',
|
||||||
@ -209,15 +171,12 @@ class AddExpenseController extends GetxController {
|
|||||||
'Location: ${locationController.text}',
|
'Location: ${locationController.text}',
|
||||||
'Transaction Date: ${transactionDateController.text}',
|
'Transaction Date: ${transactionDateController.text}',
|
||||||
'No. of Persons: ${noOfPersonsController.text}',
|
'No. of Persons: ${noOfPersonsController.text}',
|
||||||
'Expense Category: ${selectedExpenseType.value?.name}',
|
'Expense Type: ${selectedExpenseType.value?.name}',
|
||||||
'Payment Mode: ${selectedPaymentMode.value?.name}',
|
'Payment Mode: ${selectedPaymentMode.value?.name}',
|
||||||
'Paid By: ${selectedPaidBy.value?.name}',
|
'Paid By: ${selectedPaidBy.value?.name}',
|
||||||
'Attachments: ${attachments.length}',
|
'Attachments: ${attachments.length}',
|
||||||
'Existing Attachments: ${existingAttachments.length}',
|
'Existing Attachments: ${existingAttachments.length}',
|
||||||
];
|
].forEach((str) => logSafe(str, level: LogLevel.info));
|
||||||
for (var line in info) {
|
|
||||||
logSafe(line, level: LogLevel.info);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Pickers ---
|
// --- Pickers ---
|
||||||
@ -230,7 +189,7 @@ class AddExpenseController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (pickedDate != null) {
|
if (pickedDate != null) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final finalDateTime = DateTime(
|
final finalDateTime = DateTime(
|
||||||
pickedDate.year,
|
pickedDate.year,
|
||||||
pickedDate.month,
|
pickedDate.month,
|
||||||
@ -239,6 +198,7 @@ class AddExpenseController extends GetxController {
|
|||||||
now.minute,
|
now.minute,
|
||||||
now.second,
|
now.second,
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedTransactionDate.value = finalDateTime;
|
selectedTransactionDate.value = finalDateTime;
|
||||||
transactionDateController.text =
|
transactionDateController.text =
|
||||||
DateFormat('dd MMM yyyy').format(finalDateTime);
|
DateFormat('dd MMM yyyy').format(finalDateTime);
|
||||||
@ -253,9 +213,8 @@ class AddExpenseController extends GetxController {
|
|||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
attachments.addAll(
|
attachments
|
||||||
result.paths.whereType<String>().map(File.new),
|
.addAll(result.paths.whereType<String>().map((path) => File(path)));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorSnackbar("Attachment error: $e");
|
_errorSnackbar("Attachment error: $e");
|
||||||
@ -264,33 +223,12 @@ class AddExpenseController extends GetxController {
|
|||||||
|
|
||||||
void removeAttachment(File file) => attachments.remove(file);
|
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; // start loading
|
|
||||||
File imageFile = File(pickedFile.path);
|
|
||||||
|
|
||||||
// Add timestamp to the captured image
|
|
||||||
File timestampedFile = await TimestampImageHelper.addTimestamp(
|
|
||||||
imageFile: imageFile,
|
|
||||||
);
|
|
||||||
|
|
||||||
attachments.add(timestampedFile);
|
|
||||||
attachments.refresh(); // refresh UI
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_errorSnackbar("Camera error: $e");
|
|
||||||
} finally {
|
|
||||||
isProcessingAttachment.value = false; // stop loading
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Location ---
|
// --- Location ---
|
||||||
Future<void> fetchCurrentLocation() async {
|
Future<void> fetchCurrentLocation() async {
|
||||||
isFetchingLocation.value = true;
|
isFetchingLocation.value = true;
|
||||||
try {
|
try {
|
||||||
if (!await _ensureLocationPermission()) return;
|
final permission = await _ensureLocationPermission();
|
||||||
|
if (!permission) return;
|
||||||
|
|
||||||
final position = await Geolocator.getCurrentPosition();
|
final position = await Geolocator.getCurrentPosition();
|
||||||
final placemarks =
|
final placemarks =
|
||||||
@ -302,7 +240,7 @@ class AddExpenseController extends GetxController {
|
|||||||
placemarks.first.street,
|
placemarks.first.street,
|
||||||
placemarks.first.locality,
|
placemarks.first.locality,
|
||||||
placemarks.first.administrativeArea,
|
placemarks.first.administrativeArea,
|
||||||
placemarks.first.country,
|
placemarks.first.country
|
||||||
].where((e) => e?.isNotEmpty == true).join(", ")
|
].where((e) => e?.isNotEmpty == true).join(", ")
|
||||||
: "${position.latitude}, ${position.longitude}";
|
: "${position.latitude}, ${position.longitude}";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -332,23 +270,19 @@ class AddExpenseController extends GetxController {
|
|||||||
|
|
||||||
// --- Data Fetching ---
|
// --- Data Fetching ---
|
||||||
Future<void> loadMasterData() async =>
|
Future<void> loadMasterData() async =>
|
||||||
Future.wait([fetchMasterData(), fetchGlobalProjects()]);
|
await Future.wait([fetchMasterData(), fetchGlobalProjects()]);
|
||||||
|
|
||||||
Future<void> fetchMasterData() async {
|
Future<void> fetchMasterData() async {
|
||||||
try {
|
try {
|
||||||
final types = await ApiService.getMasterExpenseTypes();
|
final types = await ApiService.getMasterExpenseTypes();
|
||||||
if (types is List) {
|
if (types is List)
|
||||||
expenseTypes.value = types
|
expenseTypes.value =
|
||||||
.map((e) => ExpenseTypeModel.fromJson(e as Map<String, dynamic>))
|
types.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
final modes = await ApiService.getMasterPaymentModes();
|
final modes = await ApiService.getMasterPaymentModes();
|
||||||
if (modes is List) {
|
if (modes is List)
|
||||||
paymentModes.value = modes
|
paymentModes.value =
|
||||||
.map((e) => PaymentModeModel.fromJson(e as Map<String, dynamic>))
|
modes.map((e) => PaymentModeModel.fromJson(e)).toList();
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
_errorSnackbar("Failed to fetch master data");
|
_errorSnackbar("Failed to fetch master data");
|
||||||
}
|
}
|
||||||
@ -360,8 +294,8 @@ class AddExpenseController extends GetxController {
|
|||||||
if (response != null) {
|
if (response != null) {
|
||||||
final names = <String>[];
|
final names = <String>[];
|
||||||
for (var item in response) {
|
for (var item in response) {
|
||||||
final name = item['name']?.toString().trim();
|
final name = item['name']?.toString().trim(),
|
||||||
final id = item['id']?.toString().trim();
|
id = item['id']?.toString().trim();
|
||||||
if (name != null && id != null) {
|
if (name != null && id != null) {
|
||||||
projectsMap[name] = id;
|
projectsMap[name] = id;
|
||||||
names.add(name);
|
names.add(name);
|
||||||
@ -386,7 +320,24 @@ class AddExpenseController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final payload = await _buildExpensePayload();
|
final payload = await _buildExpensePayload();
|
||||||
final success = await _submitToApi(payload);
|
|
||||||
|
final success = isEditMode.value && editingExpenseId != null
|
||||||
|
? await ApiService.editExpenseApi(
|
||||||
|
expenseId: editingExpenseId!, payload: payload)
|
||||||
|
: await ApiService.createExpenseApi(
|
||||||
|
projectId: payload['projectId'],
|
||||||
|
expensesTypeId: payload['expensesTypeId'],
|
||||||
|
paymentModeId: payload['paymentModeId'],
|
||||||
|
paidById: payload['paidById'],
|
||||||
|
transactionDate: DateTime.parse(payload['transactionDate']),
|
||||||
|
transactionId: payload['transactionId'],
|
||||||
|
description: payload['description'],
|
||||||
|
location: payload['location'],
|
||||||
|
supplerName: payload['supplerName'],
|
||||||
|
amount: payload['amount'],
|
||||||
|
noOfPersons: payload['noOfPersons'],
|
||||||
|
billAttachments: payload['billAttachments'],
|
||||||
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await expenseController.fetchExpenses();
|
await expenseController.fetchExpenses();
|
||||||
@ -407,156 +358,89 @@ class AddExpenseController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _submitToApi(Map<String, dynamic>? payload) async {
|
Future<Map<String, dynamic>> _buildExpensePayload() async {
|
||||||
if (payload == null) {
|
|
||||||
_errorSnackbar("Payload is empty. Cannot submit.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isEditMode.value && editingExpenseId != null) {
|
|
||||||
// Edit existing expense
|
|
||||||
return await ApiService.editExpenseApi(
|
|
||||||
expenseId: editingExpenseId!,
|
|
||||||
payload: payload,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Create new expense
|
|
||||||
return await ApiService.createExpenseApi(
|
|
||||||
projectId: payload['projectId'],
|
|
||||||
expensesTypeId: payload['expenseCategoryId'],
|
|
||||||
paymentModeId: payload['paymentModeId'],
|
|
||||||
paidById: payload['paidById'],
|
|
||||||
transactionDate: DateTime.parse(payload['transactionDate']),
|
|
||||||
transactionId: payload['transactionId'],
|
|
||||||
description: payload['description'],
|
|
||||||
location: payload['location'],
|
|
||||||
supplerName: payload['supplerName'],
|
|
||||||
amount: payload['amount'],
|
|
||||||
noOfPersons: payload['noOfPersons'],
|
|
||||||
billAttachments: payload['billAttachments'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_errorSnackbar("Failed to submit expense: $e");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> _buildExpensePayload() async {
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
final existingAttachmentPayloads = existingAttachments
|
||||||
|
.map((e) => {
|
||||||
|
"documentId": e['documentId'],
|
||||||
|
"fileName": e['fileName'],
|
||||||
|
"contentType": e['contentType'],
|
||||||
|
"fileSize": 0,
|
||||||
|
"description": "",
|
||||||
|
"url": e['url'],
|
||||||
|
"isActive": e['isActive'] ?? true,
|
||||||
|
"base64Data": e['isActive'] == false ? null : e['base64Data'],
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
// --- Get IDs safely ---
|
final newAttachmentPayloads =
|
||||||
final projectId = projectsMap[selectedProject.value];
|
await Future.wait(attachments.map((file) async {
|
||||||
final expenseType = selectedExpenseType.value;
|
final bytes = await file.readAsBytes();
|
||||||
final paymentMode = selectedPaymentMode.value;
|
return {
|
||||||
final paidBy = selectedPaidBy.value;
|
"fileName": file.path.split('/').last,
|
||||||
|
"base64Data": base64Encode(bytes),
|
||||||
|
"contentType": lookupMimeType(file.path) ?? 'application/octet-stream',
|
||||||
|
"fileSize": await file.length(),
|
||||||
|
"description": "",
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
// --- Validate essential fields ---
|
final type = selectedExpenseType.value!;
|
||||||
if (projectId == null) {
|
return {
|
||||||
_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'] ?? "",
|
|
||||||
"fileSize": 0,
|
|
||||||
"description": "",
|
|
||||||
"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();
|
|
||||||
return {
|
|
||||||
"fileName": file.path.split('/').last,
|
|
||||||
"base64Data": base64Encode(bytes),
|
|
||||||
"contentType":
|
|
||||||
lookupMimeType(file.path) ?? 'application/octet-stream',
|
|
||||||
"fileSize": await file.length(),
|
|
||||||
"description": "",
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Build final payload ---
|
|
||||||
final payload = {
|
|
||||||
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
|
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
|
||||||
"projectId": projectId,
|
"projectId": projectsMap[selectedProject.value]!,
|
||||||
"expenseCategoryId": expenseType.id,
|
"expensesTypeId": type.id,
|
||||||
"paymentModeId": paymentMode.id,
|
"paymentModeId": selectedPaymentMode.value!.id,
|
||||||
"paidById": paidBy.id,
|
"paidById": selectedPaidBy.value!.id,
|
||||||
"transactionDate":
|
"transactionDate": (selectedTransactionDate.value?.toUtc() ?? now.toUtc())
|
||||||
(selectedTransactionDate.value ?? now).toUtc().toIso8601String(),
|
.toIso8601String(),
|
||||||
"transactionId": transactionIdController.text.trim(),
|
"transactionId": transactionIdController.text,
|
||||||
"description": descriptionController.text.trim(),
|
"description": descriptionController.text,
|
||||||
"location": locationController.text.trim(),
|
"location": locationController.text,
|
||||||
"supplerName": supplierController.text.trim(),
|
"supplerName": supplierController.text,
|
||||||
"amount": double.tryParse(amountController.text.trim()) ?? 0,
|
"amount": double.parse(amountController.text.trim()),
|
||||||
"noOfPersons": expenseType.noOfPersonsRequired == true
|
"noOfPersons": type.noOfPersonsRequired == true
|
||||||
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
|
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
|
||||||
: 0,
|
: 0,
|
||||||
"billAttachments": [...existingPayload, ...newPayload].isEmpty
|
"billAttachments": [
|
||||||
? null
|
...existingAttachmentPayloads,
|
||||||
: [...existingPayload, ...newPayload],
|
...newAttachmentPayloads
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return payload;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String validateForm() {
|
String validateForm() {
|
||||||
final missing = <String>[];
|
final missing = <String>[];
|
||||||
|
|
||||||
if (selectedProject.value.isEmpty) missing.add("Project");
|
if (selectedProject.value.isEmpty) missing.add("Project");
|
||||||
if (selectedExpenseType.value == null) missing.add("Expense Category");
|
if (selectedExpenseType.value == null) missing.add("Expense Type");
|
||||||
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
|
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
|
||||||
if (selectedPaidBy.value == null) missing.add("Paid By");
|
if (selectedPaidBy.value == null) missing.add("Paid By");
|
||||||
if (amountController.text.trim().isEmpty) missing.add("Amount");
|
if (amountController.text.trim().isEmpty) missing.add("Amount");
|
||||||
if (descriptionController.text.trim().isEmpty) missing.add("Description");
|
if (descriptionController.text.trim().isEmpty) missing.add("Description");
|
||||||
|
|
||||||
if (selectedTransactionDate.value == null) {
|
// Date Required
|
||||||
missing.add("Transaction Date");
|
if (selectedTransactionDate.value == null) missing.add("Transaction Date");
|
||||||
} else if (selectedTransactionDate.value!.isAfter(DateTime.now())) {
|
if (selectedTransactionDate.value != null &&
|
||||||
|
selectedTransactionDate.value!.isAfter(DateTime.now())) {
|
||||||
missing.add("Valid Transaction Date");
|
missing.add("Valid Transaction Date");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (double.tryParse(amountController.text.trim()) == null) {
|
final amount = double.tryParse(amountController.text.trim());
|
||||||
missing.add("Valid Amount");
|
if (amount == null) missing.add("Valid Amount");
|
||||||
}
|
|
||||||
|
|
||||||
final hasActiveExisting =
|
// Attachment: at least one required at all times
|
||||||
|
bool hasActiveExisting =
|
||||||
existingAttachments.any((e) => e['isActive'] != false);
|
existingAttachments.any((e) => e['isActive'] != false);
|
||||||
if (attachments.isEmpty && !hasActiveExisting) {
|
if (attachments.isEmpty && !hasActiveExisting) missing.add("Attachment");
|
||||||
missing.add("Attachment");
|
|
||||||
}
|
|
||||||
|
|
||||||
return missing.isEmpty ? '' : "Please provide: ${missing.join(', ')}.";
|
return missing.isEmpty ? '' : "Please provide: ${missing.join(', ')}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Snackbar Helper ---
|
// --- Snackbar Helper ---
|
||||||
void _errorSnackbar(String msg, [String title = "Error"]) {
|
void _errorSnackbar(String msg, [String title = "Error"]) => showAppSnackbar(
|
||||||
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
|
title: title,
|
||||||
}
|
message: msg,
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/model/expense/expense_detail_model.dart';
|
import 'package:marco/model/expense/expense_detail_model.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
import 'package:marco/model/employees/employee_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ExpenseDetailController extends GetxController {
|
class ExpenseDetailController extends GetxController {
|
||||||
@ -142,10 +142,6 @@ class ExpenseDetailController extends GetxController {
|
|||||||
required String reimburseDate,
|
required String reimburseDate,
|
||||||
required String reimburseById,
|
required String reimburseById,
|
||||||
required String statusId,
|
required String statusId,
|
||||||
double? baseAmount,
|
|
||||||
double? taxAmount,
|
|
||||||
double? tdsPercent,
|
|
||||||
double? netPayable,
|
|
||||||
}) async {
|
}) async {
|
||||||
final success = await _apiCallWrapper(
|
final success = await _apiCallWrapper(
|
||||||
() => ApiService.updateExpenseStatusApi(
|
() => ApiService.updateExpenseStatusApi(
|
||||||
@ -155,16 +151,13 @@ class ExpenseDetailController extends GetxController {
|
|||||||
reimburseTransactionId: reimburseTransactionId,
|
reimburseTransactionId: reimburseTransactionId,
|
||||||
reimburseDate: reimburseDate,
|
reimburseDate: reimburseDate,
|
||||||
reimbursedById: reimburseById,
|
reimbursedById: reimburseById,
|
||||||
baseAmount: baseAmount,
|
|
||||||
taxAmount: taxAmount,
|
|
||||||
tdsPercent: tdsPercent,
|
|
||||||
netPayable: netPayable,
|
|
||||||
),
|
),
|
||||||
"submit reimbursement",
|
"submit reimbursement",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success == true) {
|
if (success == true) {
|
||||||
await fetchExpenseDetails();
|
// Explicitly check for true as _apiCallWrapper returns T?
|
||||||
|
await fetchExpenseDetails(); // Refresh details after successful update
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = "Failed to submit reimbursement.";
|
errorMessage.value = "Failed to submit reimbursement.";
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/model/expense/expense_list_model.dart';
|
import 'package:marco/model/expense/expense_list_model.dart';
|
||||||
import 'package:on_field_work/model/expense/payment_types_model.dart';
|
import 'package:marco/model/expense/payment_types_model.dart';
|
||||||
import 'package:on_field_work/model/expense/expense_type_model.dart';
|
import 'package:marco/model/expense/expense_type_model.dart';
|
||||||
import 'package:on_field_work/model/expense/expense_status_model.dart';
|
import 'package:marco/model/expense/expense_status_model.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
import 'package:marco/model/employees/employee_model.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ExpenseController extends GetxController {
|
class ExpenseController extends GetxController {
|
||||||
@ -213,7 +213,7 @@ class ExpenseController extends GetxController {
|
|||||||
selectedCreatedByEmployees.clear();
|
selectedCreatedByEmployees.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch master data: Expense Categorys, payment modes, and expense status
|
/// Fetch master data: expense types, payment modes, and expense status
|
||||||
Future<void> fetchMasterData() async {
|
Future<void> fetchMasterData() async {
|
||||||
try {
|
try {
|
||||||
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||||
|
|
||||||
class FaqsController extends MyController {
|
class FaqsController extends MyController {
|
||||||
final List<bool> dataExpansionPanel = [true, false, false, false, false, false];
|
final List<bool> dataExpansionPanel = [true, false, false, false, false, false];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
|
|
||||||
class PricingController extends MyController {
|
class PricingController extends MyController {
|
||||||
bool isMonth = false;
|
bool isMonth = false;
|
||||||
|
|||||||
@ -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 {}
|
class AuthLayout2Controller extends MyController {}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AuthLayoutController extends MyController {
|
class AuthLayoutController extends MyController {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:on_field_work/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
|
|
||||||
class LayoutController extends GetxController {
|
class LayoutController extends GetxController {
|
||||||
// Theme Customization
|
// Theme Customization
|
||||||
@ -55,7 +55,7 @@ class LayoutController extends GetxController {
|
|||||||
isLoadingProjects.value = true;
|
isLoadingProjects.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.getGlobalProjects();
|
final response = await ApiService.getProjects();
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
final fetchedProjects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:get/get_state_manager/get_state_manager.dart';
|
import 'package:get/get_state_manager/get_state_manager.dart';
|
||||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
|
|
||||||
abstract class MyController extends GetxController {
|
abstract class MyController extends GetxController {
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -2,21 +2,17 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/permission_service.dart';
|
import 'package:marco/helpers/services/permission_service.dart';
|
||||||
import 'package:on_field_work/model/user_permission.dart';
|
import 'package:marco/model/user_permission.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_info.dart';
|
import 'package:marco/model/employees/employee_info.dart';
|
||||||
import 'package:on_field_work/model/projects_model.dart';
|
import 'package:marco/model/projects_model.dart';
|
||||||
|
|
||||||
class PermissionController extends GetxController {
|
class PermissionController extends GetxController {
|
||||||
var permissions = <UserPermission>[].obs;
|
var permissions = <UserPermission>[].obs;
|
||||||
var employeeInfo = Rxn<EmployeeInfo>();
|
var employeeInfo = Rxn<EmployeeInfo>();
|
||||||
var projectsInfo = <ProjectInfo>[].obs;
|
var projectsInfo = <ProjectInfo>[].obs;
|
||||||
Timer? _refreshTimer;
|
Timer? _refreshTimer;
|
||||||
var isLoading = true.obs;
|
|
||||||
|
|
||||||
/// ← NEW: reactive flag to signal permissions are loaded
|
|
||||||
var permissionsLoaded = false.obs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -30,8 +26,7 @@ class PermissionController extends GetxController {
|
|||||||
await loadData(token!);
|
await loadData(token!);
|
||||||
_startAutoRefresh();
|
_startAutoRefresh();
|
||||||
} else {
|
} else {
|
||||||
logSafe("Token is null or empty. Skipping API load and auto-refresh.",
|
logSafe("Token is null or empty. Skipping API load and auto-refresh.", level: LogLevel.warning);
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,28 +37,19 @@ class PermissionController extends GetxController {
|
|||||||
logSafe("Auth token retrieved: $token", level: LogLevel.debug);
|
logSafe("Auth token retrieved: $token", level: LogLevel.debug);
|
||||||
return token;
|
return token;
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe("Error retrieving auth token",
|
logSafe("Error retrieving auth token", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadData(String token) async {
|
Future<void> loadData(String token) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
|
||||||
final userData = await PermissionService.fetchAllUserData(token);
|
final userData = await PermissionService.fetchAllUserData(token);
|
||||||
_updateState(userData);
|
_updateState(userData);
|
||||||
await _storeData();
|
await _storeData();
|
||||||
logSafe("Data loaded and state updated successfully.");
|
logSafe("Data loaded and state updated successfully.");
|
||||||
|
|
||||||
// ← NEW: mark permissions as loaded
|
|
||||||
permissionsLoaded.value = true;
|
|
||||||
|
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe("Error loading data from API",
|
logSafe("Error loading data from API", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,8 +60,7 @@ class PermissionController extends GetxController {
|
|||||||
projectsInfo.assignAll(userData['projects']);
|
projectsInfo.assignAll(userData['projects']);
|
||||||
logSafe("State updated with user data.");
|
logSafe("State updated with user data.");
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe("Error updating state",
|
logSafe("Error updating state", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,33 +89,31 @@ class PermissionController extends GetxController {
|
|||||||
|
|
||||||
logSafe("User data successfully stored in SharedPreferences.");
|
logSafe("User data successfully stored in SharedPreferences.");
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe("Error storing data",
|
logSafe("Error storing data", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startAutoRefresh() {
|
void _startAutoRefresh() {
|
||||||
_refreshTimer = Timer.periodic(const Duration(minutes: 30), (timer) async {
|
_refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async {
|
||||||
logSafe("Auto-refresh triggered.");
|
logSafe("Auto-refresh triggered.");
|
||||||
final token = await _getAuthToken();
|
final token = await _getAuthToken();
|
||||||
if (token?.isNotEmpty ?? false) {
|
if (token?.isNotEmpty ?? false) {
|
||||||
await loadData(token!);
|
await loadData(token!);
|
||||||
} else {
|
} else {
|
||||||
logSafe("Token missing during auto-refresh. Skipping.",
|
logSafe("Token missing during auto-refresh. Skipping.", level: LogLevel.warning);
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasPermission(String permissionId) {
|
bool hasPermission(String permissionId) {
|
||||||
final hasPerm = permissions.any((p) => p.id == permissionId);
|
final hasPerm = permissions.any((p) => p.id == permissionId);
|
||||||
|
logSafe("Checking permission $permissionId: $hasPerm", level: LogLevel.debug);
|
||||||
return hasPerm;
|
return hasPerm;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isUserAssignedToProject(String projectId) {
|
bool isUserAssignedToProject(String projectId) {
|
||||||
final assigned = projectsInfo.any((project) => project.id == projectId);
|
final assigned = projectsInfo.any((project) => project.id == projectId);
|
||||||
logSafe("Checking project assignment for $projectId: $assigned",
|
logSafe("Checking project assignment for $projectId: $assigned", level: LogLevel.debug);
|
||||||
level: LogLevel.debug);
|
|
||||||
return assigned;
|
return assigned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/model/global_project_model.dart';
|
import 'package:marco/model/global_project_model.dart';
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
|
|
||||||
class ProjectController extends GetxController {
|
class ProjectController extends GetxController {
|
||||||
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
|
RxList<GlobalProjectModel> projects = <GlobalProjectModel>[].obs;
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/model/dailyTaskPlanning/master_work_category_model.dart';
|
import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart';
|
||||||
|
|
||||||
class AddTaskController extends GetxController {
|
class AddTaskController extends GetxController {
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_model.dart';
|
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
|
||||||
import 'package:on_field_work/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
|
|
||||||
|
|
||||||
class DailyTaskController extends GetxController {
|
class DailyTaskController extends GetxController {
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
@ -13,10 +12,6 @@ class DailyTaskController extends GetxController {
|
|||||||
DateTime? startDateTask;
|
DateTime? startDateTask;
|
||||||
DateTime? endDateTask;
|
DateTime? endDateTask;
|
||||||
|
|
||||||
// Rx fields for DateRangePickerWidget
|
|
||||||
Rx<DateTime> startDateTaskRx = DateTime.now().obs;
|
|
||||||
Rx<DateTime> endDateTaskRx = DateTime.now().obs;
|
|
||||||
|
|
||||||
List<TaskModel> dailyTasks = [];
|
List<TaskModel> dailyTasks = [];
|
||||||
final RxSet<String> expandedDates = <String>{}.obs;
|
final RxSet<String> expandedDates = <String>{}.obs;
|
||||||
|
|
||||||
@ -28,28 +23,13 @@ class DailyTaskController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RxSet<String> selectedBuildings = <String>{}.obs;
|
|
||||||
RxSet<String> selectedFloors = <String>{}.obs;
|
|
||||||
RxSet<String> selectedActivities = <String>{}.obs;
|
|
||||||
RxSet<String> selectedServices = <String>{}.obs;
|
|
||||||
|
|
||||||
RxBool isFilterLoading = false.obs;
|
|
||||||
RxBool isLoading = true.obs;
|
RxBool isLoading = true.obs;
|
||||||
RxBool isLoadingMore = false.obs;
|
|
||||||
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
||||||
|
|
||||||
// Pagination
|
|
||||||
int currentPage = 1;
|
|
||||||
int pageSize = 20;
|
|
||||||
bool hasMore = true;
|
|
||||||
|
|
||||||
FilterData? taskFilterData;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
_initializeDefaults();
|
_initializeDefaults();
|
||||||
_initializeRxDates();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeDefaults() {
|
void _initializeDefaults() {
|
||||||
@ -67,126 +47,47 @@ class DailyTaskController extends GetxController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeRxDates() {
|
Future<void> fetchTaskData(String? projectId) async {
|
||||||
startDateTaskRx.value =
|
if (projectId == null) {
|
||||||
startDateTask ?? DateTime.now().subtract(const Duration(days: 7));
|
logSafe("fetchTaskData: Skipped, projectId is null",
|
||||||
endDateTaskRx.value = endDateTask ?? DateTime.now();
|
level: LogLevel.warning);
|
||||||
}
|
return;
|
||||||
|
|
||||||
void clearTaskFilters() {
|
|
||||||
selectedBuildings.clear();
|
|
||||||
selectedFloors.clear();
|
|
||||||
selectedActivities.clear();
|
|
||||||
selectedServices.clear();
|
|
||||||
startDateTask = null;
|
|
||||||
endDateTask = null;
|
|
||||||
|
|
||||||
// reset Rx dates as well
|
|
||||||
startDateTaskRx.value = DateTime.now().subtract(const Duration(days: 7));
|
|
||||||
endDateTaskRx.value = DateTime.now();
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateDateRange(DateTime? start, DateTime? end) {
|
|
||||||
if (start != null && end != null) {
|
|
||||||
startDateTask = start;
|
|
||||||
endDateTask = end;
|
|
||||||
|
|
||||||
startDateTaskRx.value = start;
|
|
||||||
endDateTaskRx.value = end;
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchTaskData(
|
|
||||||
String projectId, {
|
|
||||||
int pageNumber = 1,
|
|
||||||
int pageSize = 20,
|
|
||||||
bool isLoadMore = false,
|
|
||||||
}) async {
|
|
||||||
if (!isLoadMore) {
|
|
||||||
isLoading.value = true;
|
|
||||||
currentPage = 1;
|
|
||||||
hasMore = true;
|
|
||||||
groupedDailyTasks.clear();
|
|
||||||
dailyTasks.clear();
|
|
||||||
} else {
|
|
||||||
isLoadingMore.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the filter object
|
isLoading.value = true;
|
||||||
final filter = {
|
|
||||||
"buildingIds": selectedBuildings.toList(),
|
|
||||||
"floorIds": selectedFloors.toList(),
|
|
||||||
"activityIds": selectedActivities.toList(),
|
|
||||||
"serviceIds": selectedServices.toList(),
|
|
||||||
"dateFrom": startDateTask?.toIso8601String(),
|
|
||||||
"dateTo": endDateTask?.toIso8601String(),
|
|
||||||
};
|
|
||||||
|
|
||||||
final response = await ApiService.getDailyTasks(
|
final response = await ApiService.getDailyTasks(
|
||||||
projectId,
|
projectId,
|
||||||
filter: filter,
|
dateFrom: startDateTask,
|
||||||
pageNumber: pageNumber,
|
dateTo: endDateTask,
|
||||||
pageSize: pageSize,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
isLoading.value = false;
|
||||||
if (!isLoadMore) {
|
|
||||||
groupedDailyTasks.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var task in response) {
|
if (response != null) {
|
||||||
|
groupedDailyTasks.clear();
|
||||||
|
|
||||||
|
for (var taskJson in response) {
|
||||||
|
final task = TaskModel.fromJson(taskJson);
|
||||||
final assignmentDateKey =
|
final assignmentDateKey =
|
||||||
task.assignmentDate.toIso8601String().split('T')[0];
|
task.assignmentDate.toIso8601String().split('T')[0];
|
||||||
|
|
||||||
// Initialize list if not present
|
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
|
||||||
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []);
|
|
||||||
|
|
||||||
// Only add task if it doesn't already exist (avoid duplicates)
|
|
||||||
if (!groupedDailyTasks[assignmentDateKey]!
|
|
||||||
.any((t) => t.id == task.id)) {
|
|
||||||
groupedDailyTasks[assignmentDateKey]!.add(task);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
|
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
|
||||||
currentPage = pageNumber;
|
|
||||||
} else {
|
|
||||||
hasMore = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = false;
|
logSafe(
|
||||||
isLoadingMore.value = false;
|
"Daily tasks fetched and grouped: ${dailyTasks.length} for project $projectId",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchTaskFilter(String projectId) async {
|
|
||||||
isFilterLoading.value = true;
|
|
||||||
try {
|
|
||||||
final filterResponse = await ApiService.getDailyTaskFilter(projectId);
|
|
||||||
|
|
||||||
if (filterResponse != null && filterResponse.success) {
|
|
||||||
taskFilterData = filterResponse.data;
|
|
||||||
logSafe(
|
|
||||||
"Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}",
|
|
||||||
level: LogLevel.info,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logSafe(
|
|
||||||
"Failed to fetch task filter for projectId: $projectId",
|
|
||||||
level: LogLevel.warning,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Exception in fetchTaskFilter: $e", level: LogLevel.error);
|
|
||||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
|
||||||
} finally {
|
|
||||||
isFilterLoading.value = false;
|
|
||||||
update();
|
update();
|
||||||
|
} else {
|
||||||
|
logSafe(
|
||||||
|
"Failed to fetch daily tasks for project $projectId",
|
||||||
|
level: LogLevel.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,29 +114,22 @@ class DailyTaskController extends GetxController {
|
|||||||
startDateTask = picked.start;
|
startDateTask = picked.start;
|
||||||
endDateTask = picked.end;
|
endDateTask = picked.end;
|
||||||
|
|
||||||
// update Rx fields as well
|
|
||||||
startDateTaskRx.value = picked.start;
|
|
||||||
endDateTaskRx.value = picked.end;
|
|
||||||
|
|
||||||
logSafe(
|
logSafe(
|
||||||
"Date range selected: $startDateTask to $endDateTask",
|
"Date range selected: $startDateTask to $endDateTask",
|
||||||
level: LogLevel.info,
|
level: LogLevel.info,
|
||||||
);
|
);
|
||||||
|
|
||||||
final projectId = controller.selectedProjectId;
|
await controller.fetchTaskData(controller.selectedProjectId);
|
||||||
if (projectId != null && projectId.isNotEmpty) {
|
|
||||||
await controller.fetchTaskData(projectId);
|
|
||||||
} else {
|
|
||||||
logSafe("Project ID is null or empty, skipping fetchTaskData",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void refreshTasksFromNotification({
|
void refreshTasksFromNotification({
|
||||||
required String projectId,
|
required String projectId,
|
||||||
required String taskAllocationId,
|
required String taskAllocationId,
|
||||||
}) async {
|
}) async {
|
||||||
await fetchTaskData(projectId);
|
// re-fetch tasks
|
||||||
update();
|
await fetchTaskData(projectId);
|
||||||
}
|
|
||||||
|
update(); // rebuilds UI
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +1,42 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_planning_model.dart';
|
import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
import 'package:marco/model/employees/employee_model.dart';
|
||||||
|
|
||||||
class DailyTaskPlanningController extends GetxController {
|
class DailyTaskPlanningController extends GetxController {
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
|
List<EmployeeModel> employees = [];
|
||||||
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
|
|
||||||
List<EmployeeModel> allEmployeesCache = [];
|
|
||||||
List<TaskPlanningDetailsModel> dailyTasks = [];
|
List<TaskPlanningDetailsModel> dailyTasks = [];
|
||||||
|
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
|
||||||
|
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
List<Map<String, dynamic>> roles = [];
|
List<Map<String, dynamic>> roles = [];
|
||||||
RxBool isAssigningTask = false.obs;
|
RxBool isAssigningTask = false.obs;
|
||||||
RxnString selectedRoleId = RxnString();
|
|
||||||
RxBool isFetchingTasks = true.obs;
|
|
||||||
RxBool isFetchingProjects = true.obs;
|
|
||||||
RxBool isFetchingEmployees = true.obs;
|
|
||||||
|
|
||||||
/// New: track per-building loading and loaded state for lazy infra loading
|
RxnString selectedRoleId = RxnString();
|
||||||
RxMap<String, RxBool> buildingLoadingStates = <String, RxBool>{}.obs;
|
RxBool isLoading = false.obs;
|
||||||
final Set<String> buildingsWithDetails = <String>{};
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
|
_initializeDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeDefaults() {
|
||||||
|
fetchProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
String? formFieldValidator(String? value, {required String fieldType}) {
|
String? formFieldValidator(String? value, {required String fieldType}) {
|
||||||
if (value == null || value.trim().isEmpty) return 'This field is required';
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'This field is required';
|
||||||
|
}
|
||||||
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
|
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
|
||||||
return 'Please enter a valid number';
|
return 'Please enter a valid number';
|
||||||
}
|
}
|
||||||
@ -45,14 +47,21 @@ class DailyTaskPlanningController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateSelectedEmployees() {
|
void updateSelectedEmployees() {
|
||||||
selectedEmployees.value =
|
final selected =
|
||||||
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
|
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
|
||||||
logSafe("Updated selected employees", level: LogLevel.debug);
|
selectedEmployees.value = selected;
|
||||||
|
logSafe(
|
||||||
|
"Updated selected employees",
|
||||||
|
level: LogLevel.debug,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onRoleSelected(String? roleId) {
|
void onRoleSelected(String? roleId) {
|
||||||
selectedRoleId.value = roleId;
|
selectedRoleId.value = roleId;
|
||||||
logSafe("Role selected", level: LogLevel.info);
|
logSafe(
|
||||||
|
"Role selected",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchRoles() async {
|
Future<void> fetchRoles() async {
|
||||||
@ -73,8 +82,6 @@ class DailyTaskPlanningController extends GetxController {
|
|||||||
required String description,
|
required String description,
|
||||||
required List<String> taskTeam,
|
required List<String> taskTeam,
|
||||||
DateTime? assignmentDate,
|
DateTime? assignmentDate,
|
||||||
String? organizationId,
|
|
||||||
String? serviceId,
|
|
||||||
}) async {
|
}) async {
|
||||||
isAssigningTask.value = true;
|
isAssigningTask.value = true;
|
||||||
logSafe("Starting assign task...", level: LogLevel.info);
|
logSafe("Starting assign task...", level: LogLevel.info);
|
||||||
@ -85,11 +92,9 @@ class DailyTaskPlanningController extends GetxController {
|
|||||||
description: description,
|
description: description,
|
||||||
taskTeam: taskTeam,
|
taskTeam: taskTeam,
|
||||||
assignmentDate: assignmentDate,
|
assignmentDate: assignmentDate,
|
||||||
organizationId: organizationId,
|
|
||||||
serviceId: serviceId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
isAssigningTask.value = false;
|
isAssigningTask.value = false;
|
||||||
|
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
logSafe("Task assigned successfully", level: LogLevel.info);
|
logSafe("Task assigned successfully", level: LogLevel.info);
|
||||||
@ -110,257 +115,92 @@ class DailyTaskPlanningController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch buildings list only (no deep area/workItem calls) for initial load.
|
Future<void> fetchProjects() async {
|
||||||
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
|
isLoading.value = true;
|
||||||
if (projectId == null) return;
|
|
||||||
|
|
||||||
isFetchingTasks.value = true;
|
|
||||||
try {
|
try {
|
||||||
final infraResponse = await ApiService.getInfraDetails(
|
final response = await ApiService.getProjects();
|
||||||
projectId,
|
if (response?.isEmpty ?? true) {
|
||||||
serviceId: serviceId,
|
logSafe("No project data found or API call failed",
|
||||||
);
|
|
||||||
final infraData = infraResponse?['data'] as List<dynamic>?;
|
|
||||||
|
|
||||||
if (infraData == null || infraData.isEmpty) {
|
|
||||||
dailyTasks = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter buildings with 0 planned & completed work
|
|
||||||
final filteredBuildings = infraData.where((b) {
|
|
||||||
final planned = (b['plannedWork'] as num?)?.toDouble() ?? 0;
|
|
||||||
final completed = (b['completedWork'] as num?)?.toDouble() ?? 0;
|
|
||||||
return planned > 0 || completed > 0;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
dailyTasks = filteredBuildings.map((buildingJson) {
|
|
||||||
final building = Building(
|
|
||||||
id: buildingJson['id'],
|
|
||||||
name: buildingJson['buildingName'],
|
|
||||||
description: buildingJson['description'],
|
|
||||||
floors: [],
|
|
||||||
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
|
|
||||||
completedWork:
|
|
||||||
(buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return TaskPlanningDetailsModel(
|
|
||||||
id: building.id,
|
|
||||||
name: building.name,
|
|
||||||
projectAddress: "",
|
|
||||||
contactPerson: "",
|
|
||||||
startDate: DateTime.now(),
|
|
||||||
endDate: DateTime.now(),
|
|
||||||
projectStatusId: "",
|
|
||||||
buildings: [building],
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
buildingLoadingStates.clear();
|
|
||||||
buildingsWithDetails.clear();
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Error fetching daily task data",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stack);
|
|
||||||
} finally {
|
|
||||||
isFetchingTasks.value = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch full infra for a single building (floors, workAreas, workItems).
|
|
||||||
/// Called lazily when user expands a building in the UI.
|
|
||||||
Future<void> fetchBuildingInfra(
|
|
||||||
String buildingId, String projectId, String? serviceId) async {
|
|
||||||
if (buildingId.isEmpty) return;
|
|
||||||
|
|
||||||
// mark loading
|
|
||||||
buildingLoadingStates.putIfAbsent(buildingId, () => true.obs);
|
|
||||||
buildingLoadingStates[buildingId]!.value = true;
|
|
||||||
update();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Re-use getInfraDetails and find the building entry for the requested buildingId
|
|
||||||
final infraResponse =
|
|
||||||
await ApiService.getInfraDetails(projectId, serviceId: serviceId);
|
|
||||||
final infraData = infraResponse?['data'] as List<dynamic>? ?? [];
|
|
||||||
|
|
||||||
final buildingJson = infraData.firstWhere(
|
|
||||||
(b) => b['id'].toString() == buildingId.toString(),
|
|
||||||
orElse: () => null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (buildingJson == null) {
|
|
||||||
logSafe("Building $buildingId not found in infra response",
|
|
||||||
level: LogLevel.warning);
|
level: LogLevel.warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build floors & workAreas for this building
|
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
final building = Building(
|
logSafe("Projects fetched: ${projects.length} projects loaded",
|
||||||
id: buildingJson['id'],
|
level: LogLevel.info);
|
||||||
name: buildingJson['buildingName'],
|
update();
|
||||||
description: buildingJson['description'],
|
|
||||||
floors:
|
|
||||||
(buildingJson['floors'] as List<dynamic>? ?? []).map((floorJson) {
|
|
||||||
return Floor(
|
|
||||||
id: floorJson['id'],
|
|
||||||
floorName: floorJson['floorName'],
|
|
||||||
workAreas: (floorJson['workAreas'] as List<dynamic>? ?? [])
|
|
||||||
.map((areaJson) {
|
|
||||||
return WorkArea(
|
|
||||||
id: areaJson['id'],
|
|
||||||
areaName: areaJson['areaName'],
|
|
||||||
workItems: [], // will populate later
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
|
|
||||||
completedWork: (buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
// For each workArea, fetch its work items and populate
|
|
||||||
await Future.wait(
|
|
||||||
building.floors.expand((f) => f.workAreas).map((area) async {
|
|
||||||
try {
|
|
||||||
final taskResponse = await ApiService.getWorkItemsByWorkArea(area.id,
|
|
||||||
serviceId: serviceId);
|
|
||||||
final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
|
|
||||||
area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper(
|
|
||||||
workItemId: taskJson['id'],
|
|
||||||
workItem: WorkItem(
|
|
||||||
id: taskJson['id'],
|
|
||||||
activityMaster: taskJson['activityMaster'] != null
|
|
||||||
? ActivityMaster.fromJson(taskJson['activityMaster'])
|
|
||||||
: null,
|
|
||||||
workCategoryMaster: taskJson['workCategoryMaster'] != null
|
|
||||||
? WorkCategoryMaster.fromJson(
|
|
||||||
taskJson['workCategoryMaster'])
|
|
||||||
: null,
|
|
||||||
plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(),
|
|
||||||
completedWork:
|
|
||||||
(taskJson['completedWork'] as num?)?.toDouble(),
|
|
||||||
todaysAssigned:
|
|
||||||
(taskJson['todaysAssigned'] as num?)?.toDouble(),
|
|
||||||
description: taskJson['description'] as String?,
|
|
||||||
taskDate: taskJson['taskDate'] != null
|
|
||||||
? DateTime.tryParse(taskJson['taskDate'])
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
)));
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Error fetching tasks for work area ${area.id}",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stack);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Merge/replace the building into dailyTasks
|
|
||||||
bool merged = false;
|
|
||||||
for (var t in dailyTasks) {
|
|
||||||
final idx = t.buildings
|
|
||||||
.indexWhere((b) => b.id.toString() == building.id.toString());
|
|
||||||
if (idx != -1) {
|
|
||||||
t.buildings[idx] = building;
|
|
||||||
merged = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!merged) {
|
|
||||||
// If not present, add a new TaskPlanningDetailsModel wrapper (fallback)
|
|
||||||
dailyTasks.add(TaskPlanningDetailsModel(
|
|
||||||
id: building.id,
|
|
||||||
name: building.name,
|
|
||||||
projectAddress: "",
|
|
||||||
contactPerson: "",
|
|
||||||
startDate: DateTime.now(),
|
|
||||||
endDate: DateTime.now(),
|
|
||||||
projectStatusId: "",
|
|
||||||
buildings: [building],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as loaded
|
|
||||||
buildingsWithDetails.add(buildingId.toString());
|
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logSafe("Error fetching infra for building $buildingId",
|
logSafe("Error fetching projects",
|
||||||
level: LogLevel.error, error: e, stackTrace: stack);
|
level: LogLevel.error, error: e, stackTrace: stack);
|
||||||
} finally {
|
} finally {
|
||||||
buildingLoadingStates.putIfAbsent(buildingId, () => false.obs);
|
isLoading.value = false;
|
||||||
buildingLoadingStates[buildingId]!.value = false;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchTaskData(String? projectId) async {
|
||||||
|
if (projectId == null) {
|
||||||
|
logSafe("Project ID is null", level: LogLevel.warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
final response = await ApiService.getDailyTasksDetails(projectId);
|
||||||
|
final data = response?['data'];
|
||||||
|
if (data != null) {
|
||||||
|
dailyTasks = [TaskPlanningDetailsModel.fromJson(data)];
|
||||||
|
logSafe(
|
||||||
|
"Daily task Planning Details fetched",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logSafe("Data field is null", level: LogLevel.warning);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
logSafe("Error fetching daily task data",
|
||||||
|
level: LogLevel.error, error: e, stackTrace: stack);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchEmployeesByProjectService({
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
required String projectId,
|
if (projectId == null || projectId.isEmpty) {
|
||||||
String? serviceId,
|
logSafe("Project ID is required but was null or empty",
|
||||||
String? organizationId,
|
level: LogLevel.error);
|
||||||
}) async {
|
return;
|
||||||
isFetchingEmployees.value = true;
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.getEmployeesByProjectService(
|
final response = await ApiService.getAllEmployeesByProject(projectId);
|
||||||
projectId,
|
|
||||||
serviceId: serviceId ?? '',
|
|
||||||
organizationId: organizationId ?? '',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
employees
|
employees =
|
||||||
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
|
response.map((json) => EmployeeModel.fromJson(json)).toList();
|
||||||
|
for (var emp in employees) {
|
||||||
if (serviceId == null && organizationId == null) {
|
uploadingStates[emp.id] = false.obs;
|
||||||
allEmployeesCache = List.from(employees);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentEmployeeIds = employees.map((e) => e.id).toSet();
|
|
||||||
|
|
||||||
uploadingStates
|
|
||||||
.removeWhere((key, _) => !currentEmployeeIds.contains(key));
|
|
||||||
employees.forEach((emp) {
|
|
||||||
uploadingStates.putIfAbsent(emp.id, () => false.obs);
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedEmployees
|
|
||||||
.removeWhere((e) => !currentEmployeeIds.contains(e.id));
|
|
||||||
|
|
||||||
logSafe("Employees fetched: ${employees.length}", level: LogLevel.info);
|
|
||||||
} else {
|
|
||||||
employees.clear();
|
|
||||||
uploadingStates.clear();
|
|
||||||
selectedEmployees.clear();
|
|
||||||
logSafe(
|
logSafe(
|
||||||
serviceId != null || organizationId != null
|
"Employees fetched: ${employees.length} for project $projectId",
|
||||||
? "Filtered employees empty"
|
level: LogLevel.info,
|
||||||
: "No employees found",
|
);
|
||||||
|
} else {
|
||||||
|
employees = [];
|
||||||
|
logSafe(
|
||||||
|
"No employees found for project $projectId",
|
||||||
level: LogLevel.warning,
|
level: LogLevel.warning,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
logSafe("Error fetching employees",
|
logSafe(
|
||||||
level: LogLevel.error, error: e, stackTrace: stack);
|
"Error fetching employees for project $projectId",
|
||||||
|
level: LogLevel.error,
|
||||||
if (serviceId == null &&
|
error: e,
|
||||||
organizationId == null &&
|
stackTrace: stack,
|
||||||
allEmployeesCache.isNotEmpty) {
|
);
|
||||||
employees.assignAll(allEmployeesCache);
|
|
||||||
|
|
||||||
final cachedEmployeeIds = employees.map((e) => e.id).toSet();
|
|
||||||
uploadingStates
|
|
||||||
.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
|
|
||||||
employees.forEach((emp) {
|
|
||||||
uploadingStates.putIfAbsent(emp.id, () => false.obs);
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedEmployees.removeWhere((e) => !cachedEmployeeIds.contains(e.id));
|
|
||||||
} else {
|
|
||||||
employees.clear();
|
|
||||||
uploadingStates.clear();
|
|
||||||
selectedEmployees.clear();
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
isFetchingEmployees.value = false;
|
isLoading.value = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,16 +4,15 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/model/dailyTaskPlanning/work_status_model.dart';
|
import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
enum ApiStatus { idle, loading, success, failure }
|
||||||
|
|
||||||
@ -33,11 +32,9 @@ class ReportTaskActionController extends MyController {
|
|||||||
final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
|
final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
|
||||||
|
|
||||||
final RxString selectedWorkStatusName = ''.obs;
|
final RxString selectedWorkStatusName = ''.obs;
|
||||||
final RxBool isPickingImage = false.obs;
|
|
||||||
|
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
final DailyTaskPlanningController taskController =
|
final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
|
||||||
Get.put(DailyTaskPlanningController());
|
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
|
|
||||||
final assignedDateController = TextEditingController();
|
final assignedDateController = TextEditingController();
|
||||||
@ -86,31 +83,18 @@ class ReportTaskActionController extends MyController {
|
|||||||
|
|
||||||
void _initializeFormFields() {
|
void _initializeFormFields() {
|
||||||
basicValidator
|
basicValidator
|
||||||
..addField('assigned_date',
|
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
||||||
label: "Assigned Date", controller: assignedDateController)
|
..addField('work_area', label: "Work Area", controller: workAreaController)
|
||||||
..addField('work_area',
|
|
||||||
label: "Work Area", controller: workAreaController)
|
|
||||||
..addField('activity', label: "Activity", controller: activityController)
|
..addField('activity', label: "Activity", controller: activityController)
|
||||||
..addField('team_size',
|
..addField('team_size', label: "Team Size", controller: teamSizeController)
|
||||||
label: "Team Size", controller: teamSizeController)
|
|
||||||
..addField('task_id', label: "Task Id", controller: taskIdController)
|
..addField('task_id', label: "Task Id", controller: taskIdController)
|
||||||
..addField('assigned', label: "Assigned", controller: assignedController)
|
..addField('assigned', label: "Assigned", controller: assignedController)
|
||||||
..addField('completed_work',
|
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
|
||||||
label: "Completed Work",
|
..addField('comment', label: "Comment", required: true, controller: commentController)
|
||||||
required: true,
|
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
|
||||||
controller: completedWorkController)
|
..addField('team_members', label: "Team Members", controller: teamMembersController)
|
||||||
..addField('comment',
|
..addField('planned_work', label: "Planned Work", controller: plannedWorkController)
|
||||||
label: "Comment", required: true, controller: commentController)
|
..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
|
||||||
..addField('assigned_by',
|
|
||||||
label: "Assigned By", controller: assignedByController)
|
|
||||||
..addField('team_members',
|
|
||||||
label: "Team Members", controller: teamMembersController)
|
|
||||||
..addField('planned_work',
|
|
||||||
label: "Planned Work", controller: plannedWorkController)
|
|
||||||
..addField('approved_task',
|
|
||||||
label: "Approved Task",
|
|
||||||
required: true,
|
|
||||||
controller: approvedTaskController);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> approveTask({
|
Future<bool> approveTask({
|
||||||
@ -124,8 +108,7 @@ class ReportTaskActionController extends MyController {
|
|||||||
|
|
||||||
if (projectId.isEmpty || reportActionId.isEmpty) {
|
if (projectId.isEmpty || reportActionId.isEmpty) {
|
||||||
_showError("Project ID and Report Action ID are required.");
|
_showError("Project ID and Report Action ID are required.");
|
||||||
logSafe("Missing required projectId or reportActionId",
|
logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
|
||||||
level: LogLevel.warning);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,15 +117,13 @@ class ReportTaskActionController extends MyController {
|
|||||||
|
|
||||||
if (approvedTaskInt == null) {
|
if (approvedTaskInt == null) {
|
||||||
_showError("Invalid approved task count.");
|
_showError("Invalid approved task count.");
|
||||||
logSafe("Invalid approvedTaskCount: $approvedTaskCount",
|
logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
|
||||||
level: LogLevel.warning);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
|
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
|
||||||
_showError("Approved task count cannot exceed completed work.");
|
_showError("Approved task count cannot exceed completed work.");
|
||||||
logSafe("Validation failed: approved > completed",
|
logSafe("Validation failed: approved > completed", level: LogLevel.warning);
|
||||||
level: LogLevel.warning);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,8 +159,7 @@ class ReportTaskActionController extends MyController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
logSafe("Error in approveTask: $e",
|
logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
_showError("An error occurred.");
|
_showError("An error occurred.");
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@ -227,8 +207,7 @@ class ReportTaskActionController extends MyController {
|
|||||||
_showError("Failed to comment task.");
|
_showError("Failed to comment task.");
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
logSafe("Error in commentTask: $e",
|
logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
_showError("An error occurred while commenting the task.");
|
_showError("An error occurred while commenting the task.");
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@ -245,8 +224,7 @@ class ReportTaskActionController extends MyController {
|
|||||||
workStatus.assignAll(model.data);
|
workStatus.assignAll(model.data);
|
||||||
logSafe("Fetched ${model.data.length} work statuses");
|
logSafe("Fetched ${model.data.length} work statuses");
|
||||||
} else {
|
} else {
|
||||||
logSafe("No work statuses found or API call failed",
|
logSafe("No work statuses found or API call failed", level: LogLevel.warning);
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingWorkStatus.value = false;
|
isLoadingWorkStatus.value = false;
|
||||||
@ -273,8 +251,7 @@ class ReportTaskActionController extends MyController {
|
|||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
logSafe(
|
logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
|
||||||
"_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
|
|
||||||
return results.whereType<Map<String, dynamic>>().toList();
|
return results.whereType<Map<String, dynamic>>().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,40 +267,23 @@ class ReportTaskActionController extends MyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pickImages({required bool fromCamera}) async {
|
Future<void> pickImages({required bool fromCamera}) async {
|
||||||
try {
|
logSafe("Opening image picker...");
|
||||||
isPickingImage.value = true; // start loading
|
if (fromCamera) {
|
||||||
logSafe("Opening image picker...");
|
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
|
||||||
|
if (pickedFile != null) {
|
||||||
if (fromCamera) {
|
selectedImages.add(File(pickedFile.path));
|
||||||
final pickedFile = await _picker.pickImage(
|
logSafe("Image added from camera: ${pickedFile.path}", );
|
||||||
source: ImageSource.camera,
|
|
||||||
imageQuality: 75,
|
|
||||||
);
|
|
||||||
if (pickedFile != null) {
|
|
||||||
final timestampedFile = await TimestampImageHelper.addTimestamp(
|
|
||||||
imageFile: File(pickedFile.path),
|
|
||||||
);
|
|
||||||
selectedImages.add(timestampedFile);
|
|
||||||
logSafe("Image added from camera with timestamp: ${pickedFile.path}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
|
||||||
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
|
||||||
logSafe("${pickedFiles.length} images added from gallery.");
|
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} else {
|
||||||
logSafe("Error picking images: $e",
|
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
||||||
level: LogLevel.error, stackTrace: st);
|
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
||||||
} finally {
|
logSafe("${pickedFiles.length} images added from gallery.", );
|
||||||
isPickingImage.value = false; // stop loading
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeImageAt(int index) {
|
void removeImageAt(int index) {
|
||||||
if (index >= 0 && index < selectedImages.length) {
|
if (index >= 0 && index < selectedImages.length) {
|
||||||
logSafe(
|
logSafe("Removing image at index $index", );
|
||||||
"Removing image at index $index",
|
|
||||||
);
|
|
||||||
selectedImages.removeAt(index);
|
selectedImages.removeAt(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,20 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:on_field_work/helpers/widgets/my_image_compressor.dart';
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart';
|
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
enum ApiStatus { idle, loading, success, failure }
|
||||||
|
|
||||||
final DailyTaskPlanningController taskController =
|
final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
|
||||||
Get.put(DailyTaskPlanningController());
|
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
|
|
||||||
class ReportTaskController extends MyController {
|
class ReportTaskController extends MyController {
|
||||||
@ -25,7 +23,6 @@ class ReportTaskController extends MyController {
|
|||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
|
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
|
||||||
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
|
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
|
||||||
final RxBool isPickingImage = false.obs;
|
|
||||||
|
|
||||||
RxList<File> selectedImages = <File>[].obs;
|
RxList<File> selectedImages = <File>[].obs;
|
||||||
|
|
||||||
@ -46,27 +43,17 @@ class ReportTaskController extends MyController {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
logSafe("Initializing ReportTaskController...");
|
logSafe("Initializing ReportTaskController...");
|
||||||
basicValidator
|
basicValidator
|
||||||
..addField('assigned_date',
|
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
||||||
label: "Assigned Date", controller: assignedDateController)
|
..addField('work_area', label: "Work Area", controller: workAreaController)
|
||||||
..addField('work_area',
|
|
||||||
label: "Work Area", controller: workAreaController)
|
|
||||||
..addField('activity', label: "Activity", controller: activityController)
|
..addField('activity', label: "Activity", controller: activityController)
|
||||||
..addField('team_size',
|
..addField('team_size', label: "Team Size", controller: teamSizeController)
|
||||||
label: "Team Size", controller: teamSizeController)
|
|
||||||
..addField('task_id', label: "Task Id", controller: taskIdController)
|
..addField('task_id', label: "Task Id", controller: taskIdController)
|
||||||
..addField('assigned', label: "Assigned", controller: assignedController)
|
..addField('assigned', label: "Assigned", controller: assignedController)
|
||||||
..addField('completed_work',
|
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
|
||||||
label: "Completed Work",
|
..addField('comment', label: "Comment", required: true, controller: commentController)
|
||||||
required: true,
|
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
|
||||||
controller: completedWorkController)
|
..addField('team_members', label: "Team Members", controller: teamMembersController)
|
||||||
..addField('comment',
|
..addField('planned_work', label: "Planned Work", controller: plannedWorkController);
|
||||||
label: "Comment", required: true, controller: commentController)
|
|
||||||
..addField('assigned_by',
|
|
||||||
label: "Assigned By", controller: assignedByController)
|
|
||||||
..addField('team_members',
|
|
||||||
label: "Team Members", controller: teamMembersController)
|
|
||||||
..addField('planned_work',
|
|
||||||
label: "Planned Work", controller: plannedWorkController);
|
|
||||||
logSafe("Form fields initialized.");
|
logSafe("Form fields initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,13 +83,9 @@ class ReportTaskController extends MyController {
|
|||||||
required DateTime reportedDate,
|
required DateTime reportedDate,
|
||||||
List<File>? images,
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
logSafe(
|
logSafe("Reporting task for projectId", );
|
||||||
"Reporting task for projectId",
|
|
||||||
);
|
|
||||||
final completedWork = completedWorkController.text.trim();
|
final completedWork = completedWorkController.text.trim();
|
||||||
if (completedWork.isEmpty ||
|
if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) {
|
||||||
int.tryParse(completedWork) == null ||
|
|
||||||
int.parse(completedWork) < 0) {
|
|
||||||
_showError("Completed work must be a positive number.");
|
_showError("Completed work must be a positive number.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -138,8 +121,7 @@ class ReportTaskController extends MyController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
logSafe("Exception while reporting task",
|
logSafe("Exception while reporting task", level: LogLevel.error, error: e, stackTrace: s);
|
||||||
level: LogLevel.error, error: e, stackTrace: s);
|
|
||||||
reportStatus.value = ApiStatus.failure;
|
reportStatus.value = ApiStatus.failure;
|
||||||
_showError("An error occurred while reporting the task.");
|
_showError("An error occurred while reporting the task.");
|
||||||
return false;
|
return false;
|
||||||
@ -156,9 +138,7 @@ class ReportTaskController extends MyController {
|
|||||||
required String comment,
|
required String comment,
|
||||||
List<File>? images,
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
logSafe(
|
logSafe("Submitting comment for project", );
|
||||||
"Submitting comment for project",
|
|
||||||
);
|
|
||||||
|
|
||||||
final commentField = commentController.text.trim();
|
final commentField = commentController.text.trim();
|
||||||
if (commentField.isEmpty) {
|
if (commentField.isEmpty) {
|
||||||
@ -186,16 +166,14 @@ class ReportTaskController extends MyController {
|
|||||||
_showError("Failed to comment task.");
|
_showError("Failed to comment task.");
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
logSafe("Exception while commenting task",
|
logSafe("Exception while commenting task", level: LogLevel.error, error: e, stackTrace: s);
|
||||||
level: LogLevel.error, error: e, stackTrace: s);
|
|
||||||
_showError("An error occurred while commenting the task.");
|
_showError("An error occurred while commenting the task.");
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>?> _prepareImages(
|
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images, String context) async {
|
||||||
List<File>? images, String context) async {
|
|
||||||
if (images == null || images.isEmpty) return null;
|
if (images == null || images.isEmpty) return null;
|
||||||
|
|
||||||
logSafe("Preparing images for $context upload...");
|
logSafe("Preparing images for $context upload...");
|
||||||
@ -213,8 +191,7 @@ class ReportTaskController extends MyController {
|
|||||||
"description": "Image uploaded for $context",
|
"description": "Image uploaded for $context",
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("Image processing failed: ${file.path}",
|
logSafe("Image processing failed: ${file.path}", level: LogLevel.warning, error: e);
|
||||||
level: LogLevel.warning, error: e);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -235,31 +212,18 @@ class ReportTaskController extends MyController {
|
|||||||
|
|
||||||
Future<void> pickImages({required bool fromCamera}) async {
|
Future<void> pickImages({required bool fromCamera}) async {
|
||||||
try {
|
try {
|
||||||
isPickingImage.value = true; // Start loading
|
|
||||||
|
|
||||||
if (fromCamera) {
|
if (fromCamera) {
|
||||||
final pickedFile = await _picker.pickImage(
|
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
|
||||||
source: ImageSource.camera,
|
|
||||||
imageQuality: 75,
|
|
||||||
);
|
|
||||||
if (pickedFile != null) {
|
if (pickedFile != null) {
|
||||||
// Only camera images get timestamp
|
selectedImages.add(File(pickedFile.path));
|
||||||
final timestampedFile = await TimestampImageHelper.addTimestamp(
|
|
||||||
imageFile: File(pickedFile.path),
|
|
||||||
);
|
|
||||||
selectedImages.add(timestampedFile);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
||||||
// Gallery images added as-is without timestamp
|
|
||||||
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
||||||
}
|
}
|
||||||
|
logSafe("Images picked: ${selectedImages.length}", );
|
||||||
logSafe("Images picked: ${selectedImages.length}");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("Error picking images", level: LogLevel.warning, error: e);
|
logSafe("Error picking images", level: LogLevel.warning, error: e);
|
||||||
} finally {
|
|
||||||
isPickingImage.value = false; // Stop loading
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
|
||||||
import 'package:on_field_work/model/all_organization_model.dart';
|
|
||||||
|
|
||||||
class AllOrganizationController extends GetxController {
|
|
||||||
RxList<AllOrganization> organizations = <AllOrganization>[].obs;
|
|
||||||
Rxn<AllOrganization> selectedOrganization = Rxn<AllOrganization>();
|
|
||||||
final isLoadingOrganizations = false.obs;
|
|
||||||
|
|
||||||
String? passedOrgId;
|
|
||||||
|
|
||||||
AllOrganizationController({this.passedOrgId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
fetchAllOrganizations();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchAllOrganizations() async {
|
|
||||||
try {
|
|
||||||
isLoadingOrganizations.value = true;
|
|
||||||
|
|
||||||
final response = await ApiService.getAllOrganizations();
|
|
||||||
if (response != null && response.data.data.isNotEmpty) {
|
|
||||||
organizations.value = response.data.data;
|
|
||||||
|
|
||||||
// Select organization based on passed ID, or fallback to first
|
|
||||||
if (passedOrgId != null) {
|
|
||||||
selectedOrganization.value =
|
|
||||||
organizations.firstWhere(
|
|
||||||
(org) => org.id == passedOrgId,
|
|
||||||
orElse: () => organizations.first,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
selectedOrganization.value ??= organizations.first;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
organizations.clear();
|
|
||||||
selectedOrganization.value = null;
|
|
||||||
}
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
logSafe(
|
|
||||||
"Failed to fetch organizations: $e",
|
|
||||||
level: LogLevel.error,
|
|
||||||
error: e,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
);
|
|
||||||
organizations.clear();
|
|
||||||
selectedOrganization.value = null;
|
|
||||||
} finally {
|
|
||||||
isLoadingOrganizations.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectOrganization(AllOrganization? org) {
|
|
||||||
selectedOrganization.value = org;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearSelection() {
|
|
||||||
selectedOrganization.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String get currentSelection => selectedOrganization.value?.name ?? "All Organizations";
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
|
||||||
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
|
|
||||||
|
|
||||||
class OrganizationController extends GetxController {
|
|
||||||
/// List of organizations assigned to the selected project
|
|
||||||
List<Organization> organizations = [];
|
|
||||||
|
|
||||||
/// Currently selected organization (reactive)
|
|
||||||
Rxn<Organization> selectedOrganization = Rxn<Organization>();
|
|
||||||
|
|
||||||
/// Loading state for fetching organizations
|
|
||||||
final isLoadingOrganizations = false.obs;
|
|
||||||
|
|
||||||
/// Fetch organizations assigned to a given project
|
|
||||||
Future<void> fetchOrganizations(String projectId) async {
|
|
||||||
try {
|
|
||||||
isLoadingOrganizations.value = true;
|
|
||||||
|
|
||||||
final response = await ApiService.getAssignedOrganizations(projectId);
|
|
||||||
if (response != null && response.data.isNotEmpty) {
|
|
||||||
organizations = response.data;
|
|
||||||
logSafe("Organizations fetched: ${organizations.length}");
|
|
||||||
} else {
|
|
||||||
organizations = [];
|
|
||||||
logSafe("No organizations found for project $projectId",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
logSafe("Failed to fetch organizations: $e",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stackTrace);
|
|
||||||
organizations = [];
|
|
||||||
} finally {
|
|
||||||
isLoadingOrganizations.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select an organization
|
|
||||||
void selectOrganization(Organization? org) {
|
|
||||||
selectedOrganization.value = org;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the selection (set to "All Organizations")
|
|
||||||
void clearSelection() {
|
|
||||||
selectedOrganization.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current selection name for UI
|
|
||||||
String get currentSelection =>
|
|
||||||
selectedOrganization.value?.name ?? "All Organizations";
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
|
||||||
import 'package:on_field_work/model/tenant/tenant_services_model.dart';
|
|
||||||
|
|
||||||
class ServiceController extends GetxController {
|
|
||||||
List<Service> services = [];
|
|
||||||
Service? selectedService;
|
|
||||||
final isLoadingServices = false.obs;
|
|
||||||
|
|
||||||
/// Fetch services assigned to a project
|
|
||||||
Future<void> fetchServices(String projectId) async {
|
|
||||||
try {
|
|
||||||
isLoadingServices.value = true;
|
|
||||||
final response = await ApiService.getAssignedServices(projectId);
|
|
||||||
if (response != null) {
|
|
||||||
services = response.data;
|
|
||||||
logSafe("Services fetched: ${services.length}");
|
|
||||||
} else {
|
|
||||||
logSafe("Failed to fetch services for project $projectId",
|
|
||||||
level: LogLevel.error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoadingServices.value = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select a service
|
|
||||||
void selectService(Service? service) {
|
|
||||||
selectedService = service;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear selection
|
|
||||||
void clearSelection() {
|
|
||||||
selectedService = null;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current selected name
|
|
||||||
String get currentSelection => selectedService?.name ?? "All Services";
|
|
||||||
}
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/tenant_service.dart';
|
|
||||||
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
|
||||||
import 'package:on_field_work/controller/permission_controller.dart';
|
|
||||||
|
|
||||||
class TenantSelectionController extends GetxController {
|
|
||||||
final TenantService _tenantService = TenantService();
|
|
||||||
|
|
||||||
// Tenant list
|
|
||||||
final tenants = <Tenant>[].obs;
|
|
||||||
|
|
||||||
// Loading state
|
|
||||||
final isLoading = false.obs;
|
|
||||||
|
|
||||||
// Selected tenant ID
|
|
||||||
final selectedTenantId = RxnString();
|
|
||||||
|
|
||||||
// Flag to indicate auto-selection (for splash screen)
|
|
||||||
final isAutoSelecting = false.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
loadTenants();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load tenants and handle auto-selection
|
|
||||||
Future<void> loadTenants() async {
|
|
||||||
isLoading.value = true;
|
|
||||||
isAutoSelecting.value = true; // show splash during auto-selection
|
|
||||||
try {
|
|
||||||
final data = await _tenantService.getTenants();
|
|
||||||
if (data == null || data.isEmpty) {
|
|
||||||
tenants.clear();
|
|
||||||
logSafe("⚠️ No tenants found for the user.", level: LogLevel.warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tenants.value = data.map((e) => Tenant.fromJson(e)).toList();
|
|
||||||
|
|
||||||
final recentTenantId = LocalStorage.getRecentTenantId();
|
|
||||||
|
|
||||||
// Auto-select if only one tenant
|
|
||||||
if (tenants.length == 1) {
|
|
||||||
await _selectTenant(tenants.first.id);
|
|
||||||
}
|
|
||||||
// Auto-select recent tenant if available
|
|
||||||
else if (recentTenantId != null) {
|
|
||||||
final recentTenant =
|
|
||||||
tenants.firstWhereOrNull((t) => t.id == recentTenantId);
|
|
||||||
if (recentTenant != null) {
|
|
||||||
await _selectTenant(recentTenant.id);
|
|
||||||
} else {
|
|
||||||
_clearSelection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No auto-selection
|
|
||||||
else {
|
|
||||||
_clearSelection();
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("❌ Exception in loadTenants",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to load organizations. Please try again.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
isAutoSelecting.value = false; // hide splash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User manually selects a tenant
|
|
||||||
Future<void> onTenantSelected(String tenantId) async {
|
|
||||||
isAutoSelecting.value = true;
|
|
||||||
await _selectTenant(tenantId);
|
|
||||||
isAutoSelecting.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Internal tenant selection logic
|
|
||||||
Future<void> _selectTenant(String tenantId) async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
final success = await _tenantService.selectTenant(tenantId);
|
|
||||||
if (!success) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Unable to select organization. Please try again.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update tenant & persist
|
|
||||||
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
|
|
||||||
TenantService.setSelectedTenant(selectedTenant);
|
|
||||||
selectedTenantId.value = tenantId;
|
|
||||||
await LocalStorage.setRecentTenantId(tenantId);
|
|
||||||
|
|
||||||
// Load permissions if token exists
|
|
||||||
final token = LocalStorage.getJwtToken();
|
|
||||||
if (token != null && token.isNotEmpty) {
|
|
||||||
if (!Get.isRegistered<PermissionController>()) {
|
|
||||||
Get.put(PermissionController());
|
|
||||||
}
|
|
||||||
await Get.find<PermissionController>().loadData(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate **before changing isAutoSelecting**
|
|
||||||
await Get.offAllNamed('/dashboard');
|
|
||||||
|
|
||||||
// Then hide splash
|
|
||||||
isAutoSelecting.value = false;
|
|
||||||
} catch (e) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "An unexpected error occurred while selecting organization.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear tenant selection
|
|
||||||
void _clearSelection() {
|
|
||||||
selectedTenantId.value = null;
|
|
||||||
TenantService.currentTenant = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/tenant_service.dart';
|
|
||||||
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
|
||||||
import 'package:on_field_work/controller/permission_controller.dart';
|
|
||||||
|
|
||||||
class TenantSwitchController extends GetxController {
|
|
||||||
final TenantService _tenantService = TenantService();
|
|
||||||
|
|
||||||
final tenants = <Tenant>[].obs;
|
|
||||||
final isLoading = false.obs;
|
|
||||||
final selectedTenantId = RxnString();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
loadTenants();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load all tenants for switching (does not auto-select)
|
|
||||||
Future<void> loadTenants() async {
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
final data = await _tenantService.getTenants();
|
|
||||||
if (data == null || data.isEmpty) {
|
|
||||||
tenants.clear();
|
|
||||||
logSafe("⚠️ No tenants available for switching.", level: LogLevel.warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tenants.value = data.map((e) => Tenant.fromJson(e)).toList();
|
|
||||||
|
|
||||||
// Keep current tenant as selected
|
|
||||||
selectedTenantId.value = TenantService.currentTenant?.id;
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("❌ Exception in loadTenants", level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to load organizations for switching.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Switch to a different tenant and navigate fully
|
|
||||||
Future<void> switchTenant(String tenantId) async {
|
|
||||||
if (TenantService.currentTenant?.id == tenantId) return;
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
final success = await _tenantService.selectTenant(tenantId);
|
|
||||||
if (!success) {
|
|
||||||
logSafe("❌ Tenant switch failed: $tenantId", level: LogLevel.warning);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Unable to switch organization. Try again.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
|
|
||||||
TenantService.setSelectedTenant(selectedTenant);
|
|
||||||
selectedTenantId.value = tenantId;
|
|
||||||
|
|
||||||
// Persist recent tenant
|
|
||||||
await LocalStorage.setRecentTenantId(tenantId);
|
|
||||||
|
|
||||||
logSafe("✅ Tenant switched successfully: $tenantId");
|
|
||||||
|
|
||||||
// 🔹 Load permissions after tenant switch (null-safe)
|
|
||||||
final token = await LocalStorage.getJwtToken();
|
|
||||||
if (token != null && token.isNotEmpty) {
|
|
||||||
if (!Get.isRegistered<PermissionController>()) {
|
|
||||||
Get.put(PermissionController());
|
|
||||||
logSafe("✅ PermissionController injected after tenant switch.");
|
|
||||||
}
|
|
||||||
await Get.find<PermissionController>().loadData(token);
|
|
||||||
} else {
|
|
||||||
logSafe("⚠️ JWT token is null. Cannot load permissions.", level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FULL NAVIGATION: reload app/dashboard
|
|
||||||
Get.offAllNamed('/dashboard');
|
|
||||||
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Success",
|
|
||||||
message: "Switched to organization: ${selectedTenant.name}",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("❌ Exception in switchTenant", level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "An unexpected error occurred while switching organization.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
|
|
||||||
class ButtonsController extends MyController {
|
class ButtonsController extends MyController {
|
||||||
List<bool> selected = List.filled(3, false);
|
List<bool> selected = List.filled(3, false);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:carousel_slider/carousel_controller.dart';
|
import 'package:carousel_slider/carousel_controller.dart';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class CarouselsController extends MyController {
|
class CarouselsController extends MyController {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||||
|
|
||||||
class DialogsController extends MyController {
|
class DialogsController extends MyController {
|
||||||
List<String> dummyTexts =
|
List<String> dummyTexts =
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
|
|
||||||
class LoadersController extends MyController {}
|
class LoadersController extends MyController {}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||||
import 'package:flutter/animation.dart';
|
import 'package:flutter/animation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/extensions/string.dart';
|
import 'package:marco/helpers/extensions/string.dart';
|
||||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_button.dart';
|
import 'package:marco/helpers/widgets/my_button.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_text_utils.dart';
|
import 'package:marco/helpers/widgets/my_text_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class TabsController extends MyController {
|
class TabsController extends MyController {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import 'package:on_field_work/controller/my_controller.dart';
|
import 'package:marco/controller/my_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ToastMessageController extends MyController {
|
class ToastMessageController extends MyController {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
import 'package:marco/helpers/services/localizations/language.dart';
|
||||||
import 'package:on_field_work/helpers/theme/app_notifier.dart';
|
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:on_field_work/helpers/services/localizations/translator.dart';
|
import 'package:marco/helpers/services/localizations/translator.dart';
|
||||||
|
|
||||||
extension StringUtil on String {
|
extension StringUtil on String {
|
||||||
Color get toColor {
|
Color get toColor {
|
||||||
|
|||||||
@ -1,54 +1,19 @@
|
|||||||
class ApiEndpoints {
|
class ApiEndpoints {
|
||||||
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
// static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
|
||||||
// static const String baseUrl = "https://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";
|
|
||||||
|
|
||||||
|
// Dashboard Module API Endpoints
|
||||||
|
static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview";
|
||||||
static const String getDashboardProjectProgress = "/dashboard/progression";
|
static const String getDashboardProjectProgress = "/dashboard/progression";
|
||||||
static const String getDashboardTasks = "/dashboard/tasks";
|
static const String getDashboardTasks = "/dashboard/tasks";
|
||||||
static const String getDashboardTeams = "/dashboard/teams";
|
static const String getDashboardTeams = "/dashboard/teams";
|
||||||
static const String getDashboardProjects = "/dashboard/projects";
|
static const String getDashboardProjects = "/dashboard/projects";
|
||||||
static const String getDashboardMonthlyExpenses =
|
|
||||||
"/Dashboard/expense/monthly";
|
|
||||||
static const String getExpenseTypeReport = "/Dashboard/expense/type";
|
|
||||||
static const String getPendingExpenses = "/Dashboard/expense/pendings";
|
|
||||||
static const String getCollectionOverview = "/dashboard/collection-overview";
|
|
||||||
|
|
||||||
static const String getPurchaseInvoiceOverview =
|
|
||||||
"/dashboard/purchase-invoice-overview";
|
|
||||||
|
|
||||||
///// Projects Module API Endpoints
|
|
||||||
static const String createProject = "/project";
|
|
||||||
|
|
||||||
// Attendance Module API Endpoints
|
// Attendance Module API Endpoints
|
||||||
static const String getProjects = "/project/list";
|
static const String getProjects = "/project/list";
|
||||||
static const String getGlobalProjects = "/project/list/basic";
|
static const String getGlobalProjects = "/project/list/basic";
|
||||||
static const String getTodaysAttendance = "/attendance/project/team";
|
static const String getEmployeesByProject = "/attendance/project/team";
|
||||||
static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId";
|
|
||||||
static const String getAttendanceLogs = "/attendance/project/log";
|
static const String getAttendanceLogs = "/attendance/project/log";
|
||||||
static const String getAttendanceLogView = "/attendance/log/attendance";
|
static const String getAttendanceLogView = "/attendance/log/attendance";
|
||||||
static const String getRegularizationLogs = "/attendance/regularize";
|
static const String getRegularizationLogs = "/attendance/regularize";
|
||||||
@ -56,11 +21,10 @@ class ApiEndpoints {
|
|||||||
|
|
||||||
// Employee Screen API Endpoints
|
// Employee Screen API Endpoints
|
||||||
static const String getAllEmployeesByProject = "/employee/list";
|
static const String getAllEmployeesByProject = "/employee/list";
|
||||||
static const String getAllEmployeesByOrganization = "/project/get/task/team";
|
|
||||||
static const String getAllEmployees = "/employee/list";
|
static const String getAllEmployees = "/employee/list";
|
||||||
static const String getEmployeesWithoutPermission = "/employee/basic";
|
static const String getEmployeesWithoutPermission = "/employee/basic";
|
||||||
static const String getRoles = "/roles/jobrole";
|
static const String getRoles = "/roles/jobrole";
|
||||||
static const String createEmployee = "/employee/app/manage";
|
static const String createEmployee = "/employee/manage-mobile";
|
||||||
static const String getEmployeeInfo = "/employee/profile/get";
|
static const String getEmployeeInfo = "/employee/profile/get";
|
||||||
static const String assignEmployee = "/employee/profile/get";
|
static const String assignEmployee = "/employee/profile/get";
|
||||||
static const String getAssignedProjects = "/project/assigned-projects";
|
static const String getAssignedProjects = "/project/assigned-projects";
|
||||||
@ -76,20 +40,16 @@ class ApiEndpoints {
|
|||||||
static const String approveReportAction = "/task/approve";
|
static const String approveReportAction = "/task/approve";
|
||||||
static const String assignTask = "/project/task";
|
static const String assignTask = "/project/task";
|
||||||
static const String getmasterWorkCategories = "/Master/work-categories";
|
static const String getmasterWorkCategories = "/Master/work-categories";
|
||||||
static const String getDailyTaskProjectProgressFilter = "/task/filter";
|
|
||||||
|
|
||||||
////// Directory Module API Endpoints ///////
|
////// Directory Module API Endpoints ///////
|
||||||
static const String getDirectoryContacts = "/directory";
|
static const String getDirectoryContacts = "/directory";
|
||||||
static const String getDirectoryBucketList = "/directory/buckets";
|
static const String getDirectoryBucketList = "/directory/buckets";
|
||||||
static const String getDirectoryContactDetail = "/directory/notes";
|
static const String getDirectoryContactDetail = "/directory/notes";
|
||||||
static const String getDirectoryContactCategory =
|
static const String getDirectoryContactCategory = "/master/contact-categories";
|
||||||
"/master/contact-categories";
|
|
||||||
static const String getDirectoryContactTags = "/master/contact-tags";
|
static const String getDirectoryContactTags = "/master/contact-tags";
|
||||||
static const String getDirectoryOrganization = "/directory/organization";
|
static const String getDirectoryOrganization = "/directory/organization";
|
||||||
static const String createContact = "/directory";
|
static const String createContact = "/directory";
|
||||||
static const String updateContact = "/directory";
|
static const String updateContact = "/directory";
|
||||||
static const String deleteContact = "/directory";
|
|
||||||
static const String restoreContact = "/directory/note";
|
|
||||||
static const String getDirectoryNotes = "/directory/notes";
|
static const String getDirectoryNotes = "/directory/notes";
|
||||||
static const String updateDirectoryNotes = "/directory/note";
|
static const String updateDirectoryNotes = "/directory/note";
|
||||||
static const String createBucket = "/directory/bucket";
|
static const String createBucket = "/directory/bucket";
|
||||||
@ -104,67 +64,10 @@ class ApiEndpoints {
|
|||||||
static const String editExpense = "/Expense/edit";
|
static const String editExpense = "/Expense/edit";
|
||||||
static const String getMasterPaymentModes = "/master/payment-modes";
|
static const String getMasterPaymentModes = "/master/payment-modes";
|
||||||
static const String getMasterExpenseStatus = "/master/expenses-status";
|
static const String getMasterExpenseStatus = "/master/expenses-status";
|
||||||
static const String getMasterExpenseCategory = "/master/expenses-categories";
|
static const String getMasterExpenseTypes = "/master/expenses-types";
|
||||||
static const String updateExpenseStatus = "/expense/action";
|
static const String updateExpenseStatus = "/expense/action";
|
||||||
static const String deleteExpense = "/expense/delete";
|
static const String deleteExpense = "/expense/delete";
|
||||||
|
|
||||||
////// Dynamic Menu Module API Endpoints
|
////// Dynamic Menu Module API Endpoints
|
||||||
static const String getDynamicMenu = "/appmenu/get/menu-mobile";
|
static const String getDynamicMenu = "/appmenu/get/menu-mobile";
|
||||||
|
|
||||||
///// Document Module API Endpoints
|
|
||||||
static const String getMasterDocumentCategories =
|
|
||||||
"/master/document-category/list";
|
|
||||||
static const String getMasterDocumentTags = "/document/get/tags";
|
|
||||||
static const String getDocumentList = "/document/list";
|
|
||||||
static const String getDocumentDetails = "/document/get/details";
|
|
||||||
static const String uploadDocument = "/document/upload";
|
|
||||||
static const String deleteDocument = "/document/delete";
|
|
||||||
static const String getDocumentFilter = "/document/get/filter";
|
|
||||||
static const String getDocumentTypesByCategory = "/master/document-type/list";
|
|
||||||
static const String getDocumentVersion = "/document/get/version";
|
|
||||||
static const String getDocumentVersions = "/document/list/versions";
|
|
||||||
static const String editDocument = "/document/edit";
|
|
||||||
static const String verifyDocument = "/document/verify";
|
|
||||||
|
|
||||||
/// Logs Module API Endpoints
|
|
||||||
static const String uploadLogs = "/log";
|
|
||||||
|
|
||||||
static const String getAssignedOrganizations =
|
|
||||||
"/project/get/assigned/organization";
|
|
||||||
static const getAllOrganizations = "/organization/list";
|
|
||||||
|
|
||||||
static const String getAssignedServices = "/Project/get/assigned/services";
|
|
||||||
static const String getAdvancePayments = '/Expense/get/transactions';
|
|
||||||
|
|
||||||
// Organization Hierarchy endpoints
|
|
||||||
static const String getOrganizationHierarchyList =
|
|
||||||
"/organization/hierarchy/list";
|
|
||||||
static const String manageOrganizationHierarchy =
|
|
||||||
"/organization/hierarchy/manage";
|
|
||||||
|
|
||||||
|
|
||||||
// Service Project Module API Endpoints
|
|
||||||
static const String getServiceProjectsList = "/serviceproject/list";
|
|
||||||
static const String getServiceProjectDetail = "/serviceproject/details";
|
|
||||||
static const String getServiceProjectJobList = "/serviceproject/job/list";
|
|
||||||
static const String getServiceProjectJobDetail =
|
|
||||||
"/serviceproject/job/details";
|
|
||||||
static const String editServiceProjectJob = "/serviceproject/job/edit";
|
|
||||||
static const String createServiceProjectJob = "/serviceproject/job/create";
|
|
||||||
static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance";
|
|
||||||
static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log";
|
|
||||||
static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list";
|
|
||||||
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation";
|
|
||||||
static const String getTeamRoles = "/master/team-roles/list";
|
|
||||||
static const String getServiceProjectBranches = "/serviceproject/branch/list";
|
|
||||||
|
|
||||||
static const String getMasterJobStatus = "/Master/job-status/list";
|
|
||||||
|
|
||||||
static const String addJobComment = "/ServiceProject/job/add/comment";
|
|
||||||
|
|
||||||
static const String getJobCommentList = "/ServiceProject/job/comment/list";
|
|
||||||
|
|
||||||
// Infra Project Module API Endpoints
|
|
||||||
static const String getInfraProjectsList = "/project/list";
|
|
||||||
static const String getInfraProjectDetail = "/project/details";
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,18 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:url_strategy/url_strategy.dart';
|
import 'package:url_strategy/url_strategy.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
// import 'package:firebase_core/firebase_core.dart'; // ❌ Commented out Firebase
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
import 'package:on_field_work/helpers/services/firebase/firebase_messaging_service.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:on_field_work/helpers/services/device_info_service.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
// import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; // ❌ Commented out FCM
|
||||||
|
import 'package:marco/helpers/services/device_info_service.dart';
|
||||||
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
|
|
||||||
Future<void> initializeApp() async {
|
Future<void> initializeApp() async {
|
||||||
try {
|
try {
|
||||||
@ -15,14 +20,15 @@ Future<void> initializeApp() async {
|
|||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
_setupUI(),
|
_setupUI(),
|
||||||
_setupFirebase(),
|
// _setupFirebase(), // ❌ Commented out Firebase init
|
||||||
_setupLocalStorage(),
|
_setupLocalStorage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await _setupDeviceInfo();
|
await _setupDeviceInfo();
|
||||||
await _handleAuthTokens();
|
await _handleAuthTokens();
|
||||||
await _setupTheme();
|
await _setupTheme();
|
||||||
await _setupFirebaseMessaging();
|
await _setupControllers();
|
||||||
|
// await _setupFirebaseMessaging(); // ❌ Commented out FCM init
|
||||||
|
|
||||||
_finalizeAppStyle();
|
_finalizeAppStyle();
|
||||||
|
|
||||||
@ -38,37 +44,29 @@ Future<void> initializeApp() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleAuthTokens() async {
|
|
||||||
final refreshToken = await LocalStorage.getRefreshToken();
|
|
||||||
if (refreshToken?.isNotEmpty ?? false) {
|
|
||||||
logSafe("🔁 Refresh token found. Attempting to refresh JWT...");
|
|
||||||
final success = await AuthService.refreshToken();
|
|
||||||
if (!success) {
|
|
||||||
logSafe("⚠️ Refresh token invalid or expired. User must login again.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logSafe("❌ No refresh token found. Skipping refresh.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _setupUI() async {
|
Future<void> _setupUI() async {
|
||||||
setPathUrlStrategy();
|
setPathUrlStrategy();
|
||||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
logSafe("💡 UI setup completed with default system behavior.");
|
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
systemNavigationBarColor: Colors.transparent,
|
||||||
|
statusBarIconBrightness: Brightness.light,
|
||||||
|
systemNavigationBarIconBrightness: Brightness.dark,
|
||||||
|
));
|
||||||
|
logSafe("💡 UI setup completed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ❌ Commented out Firebase setup
|
||||||
|
/*
|
||||||
Future<void> _setupFirebase() async {
|
Future<void> _setupFirebase() async {
|
||||||
await Firebase.initializeApp();
|
await Firebase.initializeApp();
|
||||||
logSafe("💡 Firebase initialized.");
|
logSafe("💡 Firebase initialized.");
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
Future<void> _setupLocalStorage() async {
|
Future<void> _setupLocalStorage() async {
|
||||||
if (!LocalStorage.isInitialized) {
|
await LocalStorage.init();
|
||||||
await LocalStorage.init();
|
logSafe("💡 Local storage initialized.");
|
||||||
logSafe("💡 Local storage initialized.");
|
|
||||||
} else {
|
|
||||||
logSafe("ℹ️ Local storage already initialized, skipping.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setupDeviceInfo() async {
|
Future<void> _setupDeviceInfo() async {
|
||||||
@ -77,15 +75,55 @@ Future<void> _setupDeviceInfo() async {
|
|||||||
logSafe("📱 Device Info: ${deviceInfoService.deviceData}");
|
logSafe("📱 Device Info: ${deviceInfoService.deviceData}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleAuthTokens() async {
|
||||||
|
final refreshToken = await LocalStorage.getRefreshToken();
|
||||||
|
if (refreshToken?.isNotEmpty ?? false) {
|
||||||
|
logSafe("🔁 Refresh token found. Attempting to refresh JWT...");
|
||||||
|
final success = await AuthService.refreshToken();
|
||||||
|
if (!success) {
|
||||||
|
logSafe(
|
||||||
|
"⚠️ Refresh token invalid or expired. Skipping controller injection.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logSafe("❌ No refresh token found. Skipping refresh.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _setupTheme() async {
|
Future<void> _setupTheme() async {
|
||||||
await ThemeCustomizer.init();
|
await ThemeCustomizer.init();
|
||||||
logSafe("💡 Theme customizer initialized.");
|
logSafe("💡 Theme customizer initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _setupControllers() async {
|
||||||
|
final token = LocalStorage.getString('jwt_token');
|
||||||
|
if (token?.isEmpty ?? true) {
|
||||||
|
logSafe("⚠️ No valid JWT token found. Skipping controller initialization.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Get.isRegistered<PermissionController>()) {
|
||||||
|
Get.put(PermissionController());
|
||||||
|
logSafe("💡 PermissionController injected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Get.isRegistered<ProjectController>()) {
|
||||||
|
Get.put(ProjectController(), permanent: true);
|
||||||
|
logSafe("💡 ProjectController injected as permanent.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
Get.find<PermissionController>().loadData(token!),
|
||||||
|
Get.find<ProjectController>().fetchProjects(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Commented out Firebase Messaging setup
|
||||||
|
/*
|
||||||
Future<void> _setupFirebaseMessaging() async {
|
Future<void> _setupFirebaseMessaging() async {
|
||||||
await FirebaseNotificationService().initialize();
|
await FirebaseNotificationService().initialize();
|
||||||
logSafe("💡 Firebase Messaging initialized.");
|
logSafe("💡 Firebase Messaging initialized.");
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
void _finalizeAppStyle() {
|
void _finalizeAppStyle() {
|
||||||
AppStyle.init();
|
AppStyle.init();
|
||||||
|
|||||||
@ -2,41 +2,16 @@ import 'dart:io';
|
|||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
|
||||||
|
|
||||||
/// Global logger instance
|
/// Global logger instance
|
||||||
Logger? _appLogger;
|
late final Logger appLogger;
|
||||||
late final FileLogOutput _fileLogOutput;
|
late final FileLogOutput fileLogOutput;
|
||||||
|
|
||||||
/// Store logs temporarily for API posting
|
|
||||||
final List<Map<String, dynamic>> _logBuffer = [];
|
|
||||||
|
|
||||||
/// Lock flag to prevent concurrent posting
|
|
||||||
bool _isPosting = false;
|
|
||||||
|
|
||||||
/// Flag to allow API posting only after login
|
|
||||||
bool _canPostLogs = false;
|
|
||||||
|
|
||||||
/// Maximum number of logs before triggering API post
|
|
||||||
const int _maxLogsBeforePost = 100;
|
|
||||||
|
|
||||||
/// Maximum logs in memory buffer
|
|
||||||
const int _maxBufferSize = 500;
|
|
||||||
|
|
||||||
/// Enum → logger level mapping
|
|
||||||
const _levelMap = {
|
|
||||||
LogLevel.debug: Level.debug,
|
|
||||||
LogLevel.info: Level.info,
|
|
||||||
LogLevel.warning: Level.warning,
|
|
||||||
LogLevel.error: Level.error,
|
|
||||||
LogLevel.verbose: Level.verbose,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Initialize logging
|
/// Initialize logging
|
||||||
Future<void> initLogging() async {
|
Future<void> initLogging() async {
|
||||||
_fileLogOutput = FileLogOutput();
|
fileLogOutput = FileLogOutput();
|
||||||
|
|
||||||
_appLogger = Logger(
|
appLogger = Logger(
|
||||||
printer: PrettyPrinter(
|
printer: PrettyPrinter(
|
||||||
methodCount: 0,
|
methodCount: 0,
|
||||||
printTime: true,
|
printTime: true,
|
||||||
@ -45,18 +20,12 @@ Future<void> initLogging() async {
|
|||||||
),
|
),
|
||||||
output: MultiOutput([
|
output: MultiOutput([
|
||||||
ConsoleOutput(),
|
ConsoleOutput(),
|
||||||
_fileLogOutput,
|
fileLogOutput,
|
||||||
]),
|
]),
|
||||||
level: Level.debug,
|
level: Level.debug,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable API posting after login
|
|
||||||
void enableRemoteLogging() {
|
|
||||||
_canPostLogs = true;
|
|
||||||
_postBufferedLogs(); // flush logs if any
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Safe logger wrapper
|
/// Safe logger wrapper
|
||||||
void logSafe(
|
void logSafe(
|
||||||
String message, {
|
String message, {
|
||||||
@ -65,60 +34,27 @@ void logSafe(
|
|||||||
StackTrace? stackTrace,
|
StackTrace? stackTrace,
|
||||||
bool sensitive = false,
|
bool sensitive = false,
|
||||||
}) {
|
}) {
|
||||||
if (sensitive || _appLogger == null) return;
|
if (sensitive) return;
|
||||||
|
|
||||||
final loggerLevel = _levelMap[level] ?? Level.info;
|
switch (level) {
|
||||||
_appLogger!.log(loggerLevel, message, error: error, stackTrace: stackTrace);
|
case LogLevel.debug:
|
||||||
|
appLogger.d(message, error: error, stackTrace: stackTrace);
|
||||||
// Buffer logs for API posting
|
break;
|
||||||
_logBuffer.add({
|
case LogLevel.warning:
|
||||||
"logLevel": level.name,
|
appLogger.w(message, error: error, stackTrace: stackTrace);
|
||||||
"message": message,
|
break;
|
||||||
"timeStamp": DateTime.now().toUtc().toIso8601String(),
|
case LogLevel.error:
|
||||||
"ipAddress": "this is test IP", // TODO: real IP
|
appLogger.e(message, error: error, stackTrace: stackTrace);
|
||||||
"userAgent": "FlutterApp/1.0", // TODO: device_info_plus
|
break;
|
||||||
"details": error?.toString() ?? stackTrace?.toString(),
|
case LogLevel.verbose:
|
||||||
});
|
appLogger.v(message, error: error, stackTrace: stackTrace);
|
||||||
|
break;
|
||||||
if (_logBuffer.length >= _maxLogsBeforePost) {
|
default:
|
||||||
_postBufferedLogs();
|
appLogger.i(message, error: error, stackTrace: stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Post buffered logs to API
|
/// Log output to file (safe path, no permission required)
|
||||||
Future<void> _postBufferedLogs() async {
|
|
||||||
if (!_canPostLogs) return; // 🚫 skip if not logged in
|
|
||||||
if (_isPosting || _logBuffer.isEmpty) return;
|
|
||||||
|
|
||||||
_isPosting = true;
|
|
||||||
final logsToSend = List<Map<String, dynamic>>.from(_logBuffer);
|
|
||||||
_logBuffer.clear();
|
|
||||||
|
|
||||||
try {
|
|
||||||
final success = await ApiService.postLogsApi(logsToSend);
|
|
||||||
if (!success) {
|
|
||||||
_reinsertLogs(logsToSend, reason: "API call returned false");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_reinsertLogs(logsToSend, reason: "API exception: $e");
|
|
||||||
} finally {
|
|
||||||
_isPosting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reinsert logs into buffer if posting fails
|
|
||||||
void _reinsertLogs(List<Map<String, dynamic>> logs, {required String reason}) {
|
|
||||||
_appLogger?.w("Failed to post logs, re-queuing. Reason: $reason");
|
|
||||||
|
|
||||||
if (_logBuffer.length + logs.length > _maxBufferSize) {
|
|
||||||
_appLogger?.e("Buffer full. Dropping ${logs.length} logs to prevent crash.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logBuffer.insertAll(0, logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// File-based log output (safe storage)
|
|
||||||
class FileLogOutput extends LogOutput {
|
class FileLogOutput extends LogOutput {
|
||||||
File? _logFile;
|
File? _logFile;
|
||||||
|
|
||||||
@ -145,6 +81,7 @@ class FileLogOutput extends LogOutput {
|
|||||||
@override
|
@override
|
||||||
void output(OutputEvent event) async {
|
void output(OutputEvent event) async {
|
||||||
await _init();
|
await _init();
|
||||||
|
|
||||||
if (event.lines.isEmpty) return;
|
if (event.lines.isEmpty) return;
|
||||||
|
|
||||||
final logMessage = event.lines.join('\n') + '\n';
|
final logMessage = event.lines.join('\n') + '\n';
|
||||||
@ -185,5 +122,22 @@ class FileLogOutput extends LogOutput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom log levels
|
/// Simple log printer for file output
|
||||||
|
class SimpleFileLogPrinter extends LogPrinter {
|
||||||
|
@override
|
||||||
|
List<String> log(LogEvent event) {
|
||||||
|
final message = event.message.toString();
|
||||||
|
|
||||||
|
if (message.contains('[SENSITIVE]')) return [];
|
||||||
|
|
||||||
|
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
|
||||||
|
final level = event.level.name.toUpperCase();
|
||||||
|
final error = event.error != null ? ' | ERROR: ${event.error}' : '';
|
||||||
|
final stack =
|
||||||
|
event.stackTrace != null ? '\nSTACKTRACE:\n${event.stackTrace}' : '';
|
||||||
|
return ['[$timestamp] [$level] $message$error$stack'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional enum for log levels
|
||||||
enum LogLevel { debug, info, warning, error, verbose }
|
enum LogLevel { debug, info, warning, error, verbose }
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||||
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||||
@ -50,20 +54,7 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final body = {"fcmToken": fcmToken};
|
final body = {"fcmToken": fcmToken};
|
||||||
final headers = {
|
|
||||||
..._headers,
|
|
||||||
'Authorization': 'Bearer $token',
|
|
||||||
};
|
|
||||||
final endpoint = "$_baseUrl/auth/set/device-token";
|
|
||||||
|
|
||||||
// 🔹 Log request details
|
|
||||||
logSafe("📡 Device Token API Request");
|
|
||||||
logSafe("➡️ Endpoint: $endpoint");
|
|
||||||
logSafe("➡️ Headers: ${jsonEncode(headers)}");
|
|
||||||
logSafe("➡️ Payload: ${jsonEncode(body)}");
|
|
||||||
|
|
||||||
final data = await _post("/auth/set/device-token", body, authToken: token);
|
final data = await _post("/auth/set/device-token", body, authToken: token);
|
||||||
|
|
||||||
if (data != null && data['success'] == true) {
|
if (data != null && data['success'] == true) {
|
||||||
logSafe("✅ Device token registered successfully.");
|
logSafe("✅ Device token registered successfully.");
|
||||||
return true;
|
return true;
|
||||||
@ -79,7 +70,7 @@ class AuthService {
|
|||||||
logSafe("Login payload (raw): $data");
|
logSafe("Login payload (raw): $data");
|
||||||
logSafe("Login payload (JSON): ${jsonEncode(data)}");
|
logSafe("Login payload (JSON): ${jsonEncode(data)}");
|
||||||
|
|
||||||
final responseData = await _post("/auth/app/login", data);
|
final responseData = await _post("/auth/login-mobile", data);
|
||||||
if (responseData == null)
|
if (responseData == null)
|
||||||
return {"error": "Network error. Please check your connection."};
|
return {"error": "Network error. Please check your connection."};
|
||||||
|
|
||||||
@ -94,8 +85,8 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> refreshToken() async {
|
static Future<bool> refreshToken() async {
|
||||||
final accessToken = LocalStorage.getJwtToken();
|
final accessToken = await LocalStorage.getJwtToken();
|
||||||
final refreshToken = LocalStorage.getRefreshToken();
|
final refreshToken = await LocalStorage.getRefreshToken();
|
||||||
|
|
||||||
if ([accessToken, refreshToken].any((t) => t == null || t.isEmpty)) {
|
if ([accessToken, refreshToken].any((t) => t == null || t.isEmpty)) {
|
||||||
logSafe("Missing access or refresh token.", level: LogLevel.warning);
|
logSafe("Missing access or refresh token.", level: LogLevel.warning);
|
||||||
@ -111,7 +102,7 @@ class AuthService {
|
|||||||
logSafe("Token refreshed successfully.");
|
logSafe("Token refreshed successfully.");
|
||||||
|
|
||||||
// 🔹 Retry FCM token registration after token refresh
|
// 🔹 Retry FCM token registration after token refresh
|
||||||
final newFcmToken = LocalStorage.getFcmToken();
|
final newFcmToken = await LocalStorage.getFcmToken();
|
||||||
if (newFcmToken?.isNotEmpty ?? false) {
|
if (newFcmToken?.isNotEmpty ?? false) {
|
||||||
final success = await registerDeviceToken(newFcmToken!);
|
final success = await registerDeviceToken(newFcmToken!);
|
||||||
logSafe(
|
logSafe(
|
||||||
@ -153,7 +144,7 @@ class AuthService {
|
|||||||
}) =>
|
}) =>
|
||||||
_wrapErrorHandling(
|
_wrapErrorHandling(
|
||||||
() async {
|
() async {
|
||||||
final token = LocalStorage.getJwtToken();
|
final token = await LocalStorage.getJwtToken();
|
||||||
return _post(
|
return _post(
|
||||||
"/auth/generate-mpin",
|
"/auth/generate-mpin",
|
||||||
{"employeeId": employeeId, "mpin": mpin},
|
{"employeeId": employeeId, "mpin": mpin},
|
||||||
@ -286,6 +277,30 @@ class AuthService {
|
|||||||
await LocalStorage.setIsMpin(false);
|
await LocalStorage.setIsMpin(false);
|
||||||
await LocalStorage.removeMpinToken();
|
await LocalStorage.removeMpinToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Get.isRegistered<PermissionController>()) {
|
||||||
|
Get.put(PermissionController());
|
||||||
|
logSafe("✅ PermissionController injected after login.");
|
||||||
|
}
|
||||||
|
if (!Get.isRegistered<ProjectController>()) {
|
||||||
|
Get.put(ProjectController(), permanent: true);
|
||||||
|
logSafe("✅ ProjectController injected after login.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Get.find<PermissionController>().loadData(data['token']);
|
||||||
|
await Get.find<ProjectController>().fetchProjects();
|
||||||
|
|
||||||
|
// 🔹 Always try to register FCM token after login
|
||||||
|
final fcmToken = await LocalStorage.getFcmToken();
|
||||||
|
if (fcmToken?.isNotEmpty ?? false) {
|
||||||
|
final success = await registerDeviceToken(fcmToken!);
|
||||||
|
logSafe(
|
||||||
|
success
|
||||||
|
? "✅ FCM token registered after login."
|
||||||
|
: "⚠️ Failed to register FCM token after login.",
|
||||||
|
level: success ? LogLevel.info : LogLevel.warning);
|
||||||
|
}
|
||||||
|
|
||||||
isLoggedIn = true;
|
isLoggedIn = true;
|
||||||
logSafe("✅ Login flow completed and controllers initialized.");
|
logSafe("✅ Login flow completed and controllers initialized.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,24 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
import 'package:googleapis_auth/auth_io.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
|
||||||
import 'package:on_field_work/helpers/services/local_notification_service.dart';
|
import 'package:marco/helpers/services/local_notification_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/notification_action_handler.dart';
|
import 'package:marco/helpers/services/notification_action_handler.dart';
|
||||||
|
|
||||||
/// Firebase Notification Service
|
/// Firebase Notification Service
|
||||||
class FirebaseNotificationService {
|
class FirebaseNotificationService {
|
||||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
||||||
final Logger _logger = Logger();
|
final Logger _logger = Logger();
|
||||||
|
|
||||||
|
static const _fcmScopes = [
|
||||||
|
'https://www.googleapis.com/auth/firebase.messaging',
|
||||||
|
];
|
||||||
|
|
||||||
/// Initialize FCM (Firebase.initializeApp() should be called once globally)
|
/// Initialize FCM (Firebase.initializeApp() should be called once globally)
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
_logger.i('✅ FirebaseMessaging initializing...');
|
_logger.i('✅ FirebaseMessaging initializing...');
|
||||||
@ -19,7 +27,7 @@ class FirebaseNotificationService {
|
|||||||
_registerMessageListeners();
|
_registerMessageListeners();
|
||||||
_registerTokenRefreshListener();
|
_registerTokenRefreshListener();
|
||||||
|
|
||||||
// Fetch token on app start (and register with server if JWT available)
|
// Fetch token on app start (but only register with server if JWT available)
|
||||||
await getFcmToken(registerOnServer: true);
|
await getFcmToken(registerOnServer: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +57,6 @@ class FirebaseNotificationService {
|
|||||||
|
|
||||||
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap);
|
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap);
|
||||||
|
|
||||||
// Background messages
|
|
||||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +119,80 @@ class FirebaseNotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a test notification using FCM v1 API
|
||||||
|
Future<void> sendTestNotification(String deviceToken) async {
|
||||||
|
try {
|
||||||
|
final client = await _getAuthenticatedHttpClient();
|
||||||
|
if (client == null) return;
|
||||||
|
|
||||||
|
final projectId = await _getProjectId();
|
||||||
|
if (projectId == null) return;
|
||||||
|
|
||||||
|
_logger.i('🏗 Firebase Project ID: $projectId');
|
||||||
|
|
||||||
|
final url = Uri.parse(
|
||||||
|
'https://fcm.googleapis.com/v1/projects/$projectId/messages:send');
|
||||||
|
final payload = _buildNotificationPayload(deviceToken);
|
||||||
|
|
||||||
|
final response = await client.post(
|
||||||
|
url,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode(payload),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
_logger.i('✅ Test notification sent successfully');
|
||||||
|
} else {
|
||||||
|
_logger.e('❌ Send failed: ${response.statusCode} ${response.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.e('❌ Error sending notification', error: e, stackTrace: s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticated HTTP client using service account
|
||||||
|
Future<http.Client?> _getAuthenticatedHttpClient() async {
|
||||||
|
try {
|
||||||
|
final credentials = ServiceAccountCredentials.fromJson(
|
||||||
|
json.decode(await rootBundle.loadString('assets/service-account.json')),
|
||||||
|
);
|
||||||
|
return clientViaServiceAccount(credentials, _fcmScopes);
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.e('❌ Failed to authenticate', error: e, stackTrace: s);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Project ID from service account
|
||||||
|
Future<String?> _getProjectId() async {
|
||||||
|
try {
|
||||||
|
final jsonMap = json
|
||||||
|
.decode(await rootBundle.loadString('assets/service-account.json'));
|
||||||
|
return jsonMap['project_id'];
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('❌ Failed to load project_id: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build FCM v1 payload
|
||||||
|
Map<String, dynamic> _buildNotificationPayload(String token) => {
|
||||||
|
"message": {
|
||||||
|
"token": token,
|
||||||
|
"notification": {
|
||||||
|
"title": "Test Notification",
|
||||||
|
"body": "This is a test message from Flutter (v1 API)"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"click_action": "FLUTTER_NOTIFICATION_CLICK",
|
||||||
|
"type": "expense_updated", // Example
|
||||||
|
"expense_id": "1234"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// Handle tap on notification
|
/// Handle tap on notification
|
||||||
void _handleNotificationTap(RemoteMessage message) {
|
void _handleNotificationTap(RemoteMessage message) {
|
||||||
_logger.i('📌 Notification tapped: ${message.data}');
|
_logger.i('📌 Notification tapped: ${message.data}');
|
||||||
@ -128,9 +209,7 @@ class FirebaseNotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔹 Background handler (required by Firebase)
|
/// Background handler (required by Firebase)
|
||||||
/// Must be a top-level function and annotated for AOT
|
|
||||||
@pragma('vm:entry-point')
|
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
final logger = Logger();
|
final logger = Logger();
|
||||||
logger
|
logger
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Language {
|
class Language {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
import 'package:marco/helpers/services/localizations/language.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get_utils/src/extensions/string_extensions.dart';
|
import 'package:get/get_utils/src/extensions/string_extensions.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|||||||
@ -1,17 +1,11 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
import 'package:on_field_work/controller/attendance/attendance_screen_controller.dart';
|
import 'package:marco/controller/attendance/attendance_screen_controller.dart';
|
||||||
import 'package:on_field_work/controller/task_planning/daily_task_controller.dart';
|
import 'package:marco/controller/task_planning/daily_task_controller.dart';
|
||||||
import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart';
|
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
||||||
import 'package:on_field_work/controller/expense/expense_screen_controller.dart';
|
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
||||||
import 'package:on_field_work/controller/expense/expense_detail_controller.dart';
|
import 'package:marco/controller/expense/expense_detail_controller.dart';
|
||||||
import 'package:on_field_work/controller/directory/directory_controller.dart';
|
|
||||||
import 'package: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';
|
|
||||||
|
|
||||||
/// Handles incoming FCM notification actions and updates UI/controllers.
|
/// Handles incoming FCM notification actions and updates UI/controllers.
|
||||||
class NotificationActionHandler {
|
class NotificationActionHandler {
|
||||||
@ -43,14 +37,10 @@ class NotificationActionHandler {
|
|||||||
static void _handleByType(String type, Map<String, dynamic> data) {
|
static void _handleByType(String type, Map<String, dynamic> data) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'expense_updated':
|
case 'expense_updated':
|
||||||
_handleExpenseUpdated(data);
|
// No specific handler yet
|
||||||
break;
|
break;
|
||||||
case 'attendance_updated':
|
case 'attendance_updated':
|
||||||
_handleAttendanceUpdated(data);
|
_handleAttendanceUpdated(data);
|
||||||
_handleDashboardUpdate(data); // refresh dashboard attendance
|
|
||||||
break;
|
|
||||||
case 'dashboard_update':
|
|
||||||
_handleDashboardUpdate(data); // full dashboard refresh
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
_logger.w('⚠️ Unknown notification type: $type');
|
_logger.w('⚠️ Unknown notification type: $type');
|
||||||
@ -61,63 +51,29 @@ class NotificationActionHandler {
|
|||||||
static void _handleByKeyword(
|
static void _handleByKeyword(
|
||||||
String keyword, String? action, Map<String, dynamic> data) {
|
String keyword, String? action, Map<String, dynamic> data) {
|
||||||
switch (keyword) {
|
switch (keyword) {
|
||||||
/// 🔹 Attendance
|
|
||||||
case 'Attendance':
|
case 'Attendance':
|
||||||
if (_isAttendanceAction(action)) {
|
if (_isAttendanceAction(action)) {
|
||||||
_handleAttendanceUpdated(data);
|
_handleAttendanceUpdated(data);
|
||||||
_handleDashboardUpdate(data);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Team_Modified':
|
|
||||||
_handleDashboardUpdate(data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
/// 🔹 Tasks
|
|
||||||
case 'Report_Task':
|
case 'Report_Task':
|
||||||
_handleTaskUpdated(data, isComment: false);
|
_handleTaskUpdated(data, isComment: false);
|
||||||
_handleDashboardUpdate(data);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Task_Comment':
|
case 'Task_Comment':
|
||||||
_handleTaskUpdated(data, isComment: true);
|
_handleTaskUpdated(data, isComment: true);
|
||||||
_handleDashboardUpdate(data);
|
break;
|
||||||
|
case 'Expenses_Modified':
|
||||||
|
_handleExpenseUpdated(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ✅ New cases
|
||||||
case 'Task_Modified':
|
case 'Task_Modified':
|
||||||
case 'WorkArea_Modified':
|
case 'WorkArea_Modified':
|
||||||
case 'Floor_Modified':
|
case 'Floor_Modified':
|
||||||
case 'Building_Modified':
|
case 'Building_Modified':
|
||||||
_handleTaskPlanningUpdated(data);
|
_handleTaskPlanningUpdated(data);
|
||||||
_handleDashboardUpdate(data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
/// 🔹 Expenses
|
|
||||||
case 'Expenses_Modified':
|
|
||||||
_handleExpenseUpdated(data);
|
|
||||||
_handleDashboardUpdate(data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
/// 🔹 Documents
|
|
||||||
case 'Employee_Document_Modified':
|
|
||||||
case 'Project_Document_Modified':
|
|
||||||
_handleDocumentModified(data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
/// 🔹 Directory / Contacts
|
|
||||||
case 'Contact_Modified':
|
|
||||||
_handleContactModified(data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Contact_Note_Modified':
|
|
||||||
_handleContactNoteModified(data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Bucket_Modified':
|
|
||||||
_handleBucketModified(data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Bucket_Assigned':
|
|
||||||
_handleBucketAssigned(data);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -125,14 +81,7 @@ class NotificationActionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ---------------------- HANDLERS ----------------------
|
|
||||||
|
|
||||||
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
|
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
|
||||||
if (!_isCurrentProject(data)) {
|
|
||||||
_logger.i("ℹ️ Ignored task planning update from another project.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final projectId = data['ProjectId'];
|
final projectId = data['ProjectId'];
|
||||||
if (projectId == null) {
|
if (projectId == null) {
|
||||||
_logger.w("⚠️ TaskPlanning update received without ProjectId: $data");
|
_logger.w("⚠️ TaskPlanning update received without ProjectId: $data");
|
||||||
@ -150,6 +99,7 @@ class NotificationActionHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates the set of allowed Attendance actions
|
||||||
static bool _isAttendanceAction(String? action) {
|
static bool _isAttendanceAction(String? action) {
|
||||||
const validActions = {
|
const validActions = {
|
||||||
'CHECK_IN',
|
'CHECK_IN',
|
||||||
@ -163,17 +113,13 @@ class NotificationActionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void _handleExpenseUpdated(Map<String, dynamic> data) {
|
static void _handleExpenseUpdated(Map<String, dynamic> data) {
|
||||||
if (!_isCurrentProject(data)) {
|
|
||||||
_logger.i("ℹ️ Ignored expense update from another project.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final expenseId = data['ExpenseId'];
|
final expenseId = data['ExpenseId'];
|
||||||
if (expenseId == null) {
|
if (expenseId == null) {
|
||||||
_logger.w("⚠️ Expense update received without ExpenseId: $data");
|
_logger.w("⚠️ Expense update received without ExpenseId: $data");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Expense List
|
||||||
_safeControllerUpdate<ExpenseController>(
|
_safeControllerUpdate<ExpenseController>(
|
||||||
onFound: (controller) async {
|
onFound: (controller) async {
|
||||||
await controller.fetchExpenses();
|
await controller.fetchExpenses();
|
||||||
@ -183,8 +129,10 @@ class NotificationActionHandler {
|
|||||||
'✅ ExpenseController refreshed from expense notification.',
|
'✅ ExpenseController refreshed from expense notification.',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update Expense Detail (if open and matches this expenseId)
|
||||||
_safeControllerUpdate<ExpenseDetailController>(
|
_safeControllerUpdate<ExpenseDetailController>(
|
||||||
onFound: (controller) async {
|
onFound: (controller) async {
|
||||||
|
// only refresh if the open screen is for this expense
|
||||||
if (controller.expense.value?.id == expenseId) {
|
if (controller.expense.value?.id == expenseId) {
|
||||||
await controller.fetchExpenseDetails();
|
await controller.fetchExpenseDetails();
|
||||||
_logger
|
_logger
|
||||||
@ -197,11 +145,6 @@ class NotificationActionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void _handleAttendanceUpdated(Map<String, dynamic> data) {
|
static void _handleAttendanceUpdated(Map<String, dynamic> data) {
|
||||||
if (!_isCurrentProject(data)) {
|
|
||||||
_logger.i("ℹ️ Ignored attendance update from another project.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_safeControllerUpdate<AttendanceController>(
|
_safeControllerUpdate<AttendanceController>(
|
||||||
onFound: (controller) => controller.refreshDataFromNotification(
|
onFound: (controller) => controller.refreshDataFromNotification(
|
||||||
projectId: data['ProjectId'],
|
projectId: data['ProjectId'],
|
||||||
@ -213,11 +156,6 @@ class NotificationActionHandler {
|
|||||||
|
|
||||||
static void _handleTaskUpdated(Map<String, dynamic> data,
|
static void _handleTaskUpdated(Map<String, dynamic> data,
|
||||||
{required bool isComment}) {
|
{required bool isComment}) {
|
||||||
if (!_isCurrentProject(data)) {
|
|
||||||
_logger.i("ℹ️ Ignored task update from another project.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_safeControllerUpdate<DailyTaskController>(
|
_safeControllerUpdate<DailyTaskController>(
|
||||||
onFound: (controller) => controller.refreshTasksFromNotification(
|
onFound: (controller) => controller.refreshTasksFromNotification(
|
||||||
projectId: data['ProjectId'],
|
projectId: data['ProjectId'],
|
||||||
@ -228,203 +166,18 @@ class NotificationActionHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ---------------------- DOCUMENT HANDLER ----------------------
|
/// Generic reusable method for safe GetX controller access + log handling
|
||||||
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'];
|
|
||||||
|
|
||||||
if (data['Keyword'] == 'Employee_Document_Modified') {
|
|
||||||
entityTypeId = Permissions.employeeEntity;
|
|
||||||
entityId = data['EmployeeId'] ?? '';
|
|
||||||
} else if (data['Keyword'] == 'Project_Document_Modified') {
|
|
||||||
entityTypeId = Permissions.projectEntity;
|
|
||||||
entityId = data['ProjectId'] ?? '';
|
|
||||||
} else {
|
|
||||||
_logger.w("⚠️ Document update received with unknown keyword: $data");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entityId.isEmpty) {
|
|
||||||
_logger.w("⚠️ Document update missing entityId: $data");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.i(
|
|
||||||
"🔔 Document notification received: keyword=${data['Keyword']}, entityTypeId=$entityTypeId, entityId=$entityId, documentId=$documentId");
|
|
||||||
|
|
||||||
if (Get.isRegistered<DocumentController>()) {
|
|
||||||
_safeControllerUpdate<DocumentController>(
|
|
||||||
onFound: (controller) async {
|
|
||||||
await controller.fetchDocuments(
|
|
||||||
entityTypeId: entityTypeId,
|
|
||||||
entityId: entityId,
|
|
||||||
reset: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
notFoundMessage:
|
|
||||||
'⚠️ DocumentController not found, cannot refresh list.',
|
|
||||||
successMessage: '✅ DocumentController refreshed from notification.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_logger.w('⚠️ DocumentController not registered, skipping list refresh.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (documentId != null && Get.isRegistered<DocumentDetailsController>()) {
|
|
||||||
_safeControllerUpdate<DocumentDetailsController>(
|
|
||||||
onFound: (controller) async {
|
|
||||||
await controller.fetchDocumentDetails(documentId);
|
|
||||||
_logger.i(
|
|
||||||
"✅ DocumentDetailsController refreshed for Document $documentId");
|
|
||||||
},
|
|
||||||
notFoundMessage:
|
|
||||||
'ℹ️ DocumentDetailsController not active, skipping details refresh.',
|
|
||||||
successMessage: '✅ DocumentDetailsController checked for refresh.',
|
|
||||||
);
|
|
||||||
} else if (documentId != null) {
|
|
||||||
_logger.w(
|
|
||||||
'⚠️ DocumentDetailsController not registered, cannot refresh document details.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ---------------------- DIRECTORY HANDLERS ----------------------
|
|
||||||
static void _handleContactModified(Map<String, dynamic> data) {
|
|
||||||
_safeControllerUpdate<DirectoryController>(
|
|
||||||
onFound: (controller) {
|
|
||||||
controller.fetchContacts();
|
|
||||||
final contactId = data['ContactId'];
|
|
||||||
if (contactId != null) {
|
|
||||||
controller.fetchCommentsForContact(contactId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notFoundMessage:
|
|
||||||
'⚠️ DirectoryController not found, cannot refresh contacts.',
|
|
||||||
successMessage:
|
|
||||||
'✅ Directory contacts (and notes if applicable) refreshed from notification.',
|
|
||||||
);
|
|
||||||
|
|
||||||
_safeControllerUpdate<NotesController>(
|
|
||||||
onFound: (controller) => controller.fetchNotes(),
|
|
||||||
notFoundMessage: '⚠️ NotesController not found, cannot refresh notes.',
|
|
||||||
successMessage: '✅ Notes refreshed from notification.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _handleContactNoteModified(Map<String, dynamic> data) {
|
|
||||||
_handleContactModified(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _handleBucketModified(Map<String, dynamic> data) {
|
|
||||||
_safeControllerUpdate<DirectoryController>(
|
|
||||||
onFound: (controller) => controller.fetchBuckets(),
|
|
||||||
notFoundMessage: '⚠️ DirectoryController not found, cannot refresh.',
|
|
||||||
successMessage: '✅ Buckets refreshed from notification.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _handleBucketAssigned(Map<String, dynamic> data) {
|
|
||||||
_safeControllerUpdate<DirectoryController>(
|
|
||||||
onFound: (controller) => controller.fetchBuckets(),
|
|
||||||
notFoundMessage: '⚠️ DirectoryController not found, cannot refresh.',
|
|
||||||
successMessage: '✅ Bucket assignments refreshed from notification.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ---------------------- 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'] ?? '';
|
|
||||||
switch (type) {
|
|
||||||
case 'attendance_updated':
|
|
||||||
await controller.fetchRoleWiseAttendance();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'task_updated':
|
|
||||||
await controller.fetchDashboardTasks(
|
|
||||||
projectId: controller.projectController.selectedProjectId.value,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'project_progress_update':
|
|
||||||
await controller.fetchProjectProgress();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Employee_Suspend':
|
|
||||||
final currentProjectId =
|
|
||||||
controller.projectController.selectedProjectId.value;
|
|
||||||
final projectIdsString = data['ProjectIds'] ?? '';
|
|
||||||
|
|
||||||
final notificationProjectIds =
|
|
||||||
projectIdsString.split(',').map((e) => e.trim()).toList();
|
|
||||||
|
|
||||||
if (notificationProjectIds.contains(currentProjectId)) {
|
|
||||||
await controller.fetchDashboardTeams(projectId: currentProjectId);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Team_Modified':
|
|
||||||
final projectId = data['ProjectId'] ??
|
|
||||||
controller.projectController.selectedProjectId.value;
|
|
||||||
await controller.fetchDashboardTeams(projectId: projectId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'full_dashboard_refresh':
|
|
||||||
default:
|
|
||||||
await controller.refreshDashboard();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notFoundMessage: '⚠️ DashboardController not found, cannot refresh.',
|
|
||||||
successMessage: '✅ DashboardController refreshed from notification.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ---------------------- UTILITY ----------------------
|
|
||||||
|
|
||||||
static bool _isCurrentProject(Map<String, dynamic> data) {
|
|
||||||
try {
|
|
||||||
final dashboard = Get.find<DashboardController>();
|
|
||||||
final currentProjectId =
|
|
||||||
dashboard.projectController.selectedProjectId.value;
|
|
||||||
final notificationProjectId = data['ProjectId']?.toString();
|
|
||||||
|
|
||||||
if (notificationProjectId == null || notificationProjectId.isEmpty) {
|
|
||||||
return true; // No project info → allow global refresh
|
|
||||||
}
|
|
||||||
|
|
||||||
return notificationProjectId == currentProjectId;
|
|
||||||
} catch (e) {
|
|
||||||
_logger.w("⚠️ Could not verify project context: $e");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _safeControllerUpdate<T>({
|
static void _safeControllerUpdate<T>({
|
||||||
required void Function(T controller) onFound,
|
required void Function(T controller) onFound,
|
||||||
required String notFoundMessage,
|
required String notFoundMessage,
|
||||||
required String successMessage,
|
required String successMessage,
|
||||||
}) {
|
}) {
|
||||||
if (!Get.isRegistered<T>()) {
|
|
||||||
_logger.w(notFoundMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final controller = Get.find<T>();
|
final controller = Get.find<T>();
|
||||||
onFound(controller);
|
onFound(controller);
|
||||||
_logger.i(successMessage);
|
_logger.i(successMessage);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.w('⚠️ Error updating controller: $e');
|
_logger.w(notFoundMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,32 +2,28 @@ import 'dart:convert';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:on_field_work/model/user_permission.dart';
|
import 'package:marco/model/user_permission.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_info.dart';
|
import 'package:marco/model/employees/employee_info.dart';
|
||||||
import 'package:on_field_work/model/projects_model.dart';
|
import 'package:marco/model/projects_model.dart';
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||||
|
|
||||||
class PermissionService {
|
class PermissionService {
|
||||||
// In-memory cache keyed by user token
|
|
||||||
static final Map<String, Map<String, dynamic>> _userDataCache = {};
|
static final Map<String, Map<String, dynamic>> _userDataCache = {};
|
||||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
static const String _baseUrl = ApiEndpoints.baseUrl;
|
||||||
|
|
||||||
/// Fetches all user-related data (permissions, employee info, projects).
|
/// Fetches all user-related data (permissions, employee info, projects)
|
||||||
/// Uses in-memory cache for repeated token queries during session.
|
|
||||||
static Future<Map<String, dynamic>> fetchAllUserData(
|
static Future<Map<String, dynamic>> fetchAllUserData(
|
||||||
String token, {
|
String token, {
|
||||||
bool hasRetried = false,
|
bool hasRetried = false,
|
||||||
}) async {
|
}) async {
|
||||||
logSafe("Fetching user data...");
|
logSafe("Fetching user data...", );
|
||||||
|
|
||||||
// Check for cached data before network request
|
if (_userDataCache.containsKey(token)) {
|
||||||
final cached = _userDataCache[token];
|
logSafe("User data cache hit.", );
|
||||||
if (cached != null) {
|
return _userDataCache[token]!;
|
||||||
logSafe("User data cache hit.");
|
|
||||||
return cached;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final uri = Uri.parse("$_baseUrl/user/profile");
|
final uri = Uri.parse("$_baseUrl/user/profile");
|
||||||
@ -38,8 +34,8 @@ class PermissionService {
|
|||||||
final statusCode = response.statusCode;
|
final statusCode = response.statusCode;
|
||||||
|
|
||||||
if (statusCode == 200) {
|
if (statusCode == 200) {
|
||||||
final raw = json.decode(response.body);
|
logSafe("User data fetched successfully.");
|
||||||
final data = raw['data'] as Map<String, dynamic>;
|
final data = json.decode(response.body)['data'];
|
||||||
|
|
||||||
final result = {
|
final result = {
|
||||||
'permissions': _parsePermissions(data['featurePermissions']),
|
'permissions': _parsePermissions(data['featurePermissions']),
|
||||||
@ -47,12 +43,10 @@ class PermissionService {
|
|||||||
'projects': _parseProjectsInfo(data['projects']),
|
'projects': _parseProjectsInfo(data['projects']),
|
||||||
};
|
};
|
||||||
|
|
||||||
_userDataCache[token] = result; // Cache it for future use
|
_userDataCache[token] = result;
|
||||||
logSafe("User data fetched successfully.");
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token expired, try refresh once then redirect on failure
|
|
||||||
if (statusCode == 401 && !hasRetried) {
|
if (statusCode == 401 && !hasRetried) {
|
||||||
logSafe("Unauthorized. Attempting token refresh...", level: LogLevel.warning);
|
logSafe("Unauthorized. Attempting token refresh...", level: LogLevel.warning);
|
||||||
|
|
||||||
@ -69,43 +63,42 @@ class PermissionService {
|
|||||||
throw Exception('Unauthorized. Token refresh failed.');
|
throw Exception('Unauthorized. Token refresh failed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
final errorMsg = json.decode(response.body)['message'] ?? 'Unknown error';
|
final error = json.decode(response.body)['message'] ?? 'Unknown error';
|
||||||
logSafe("Failed to fetch user data: $errorMsg", level: LogLevel.warning);
|
logSafe("Failed to fetch user data: $error", level: LogLevel.warning);
|
||||||
throw Exception('Failed to fetch user data: $errorMsg');
|
throw Exception('Failed to fetch user data: $error');
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe("Exception while fetching user data", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
logSafe("Exception while fetching user data", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
rethrow; // Let the caller handle or report
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles unauthorized/user sign out flow
|
/// Clears auth data and redirects to login
|
||||||
static Future<void> _handleUnauthorized() async {
|
static Future<void> _handleUnauthorized() async {
|
||||||
logSafe("Clearing tokens and redirecting to login due to unauthorized access.", level: LogLevel.warning);
|
logSafe("Clearing tokens and redirecting to login due to unauthorized access.", level: LogLevel.warning);
|
||||||
|
|
||||||
await LocalStorage.removeToken('jwt_token');
|
await LocalStorage.removeToken('jwt_token');
|
||||||
await LocalStorage.removeToken('refresh_token');
|
await LocalStorage.removeToken('refresh_token');
|
||||||
await LocalStorage.setLoggedInUser(false);
|
await LocalStorage.setLoggedInUser(false);
|
||||||
Get.offAllNamed('/auth/login-option');
|
Get.offAllNamed('/auth/login-option');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Robust model parsing for permissions
|
/// Converts raw permission data into list of `UserPermission`
|
||||||
static List<UserPermission> _parsePermissions(List<dynamic> permissions) {
|
static List<UserPermission> _parsePermissions(List<dynamic> permissions) {
|
||||||
logSafe("Parsing user permissions...");
|
logSafe("Parsing user permissions...");
|
||||||
return permissions
|
return permissions
|
||||||
.map((perm) => UserPermission.fromJson({'id': perm}))
|
.map((id) => UserPermission.fromJson({'id': id}))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Robust model parsing for employee info
|
/// Converts raw employee JSON into `EmployeeInfo`
|
||||||
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic>? data) {
|
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> data) {
|
||||||
logSafe("Parsing employee info...");
|
logSafe("Parsing employee info...");
|
||||||
if (data == null) throw Exception("Employee data missing");
|
|
||||||
return EmployeeInfo.fromJson(data);
|
return EmployeeInfo.fromJson(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Robust model parsing for projects list
|
/// Converts raw projects JSON into list of `ProjectInfo`
|
||||||
static List<ProjectInfo> _parseProjectsInfo(List<dynamic>? projects) {
|
static List<ProjectInfo> _parseProjectsInfo(List<dynamic> projects) {
|
||||||
logSafe("Parsing projects info...");
|
logSafe("Parsing projects info...");
|
||||||
if (projects == null) return [];
|
|
||||||
return projects.map((proj) => ProjectInfo.fromJson(proj)).toList();
|
return projects.map((proj) => ProjectInfo.fromJson(proj)).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:marco/controller/project_controller.dart';
|
||||||
import 'package:on_field_work/controller/project_controller.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/localizations/language.dart';
|
||||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
import 'package:marco/model/employees/employee_info.dart';
|
||||||
import 'package:on_field_work/model/employees/employee_info.dart';
|
import 'package:marco/model/user_permission.dart';
|
||||||
import 'package:on_field_work/model/user_permission.dart';
|
import 'package:marco/model/dynamicMenu/dynamic_menu_model.dart';
|
||||||
import 'package:on_field_work/model/dynamicMenu/dynamic_menu_model.dart';
|
|
||||||
|
|
||||||
class LocalStorage {
|
class LocalStorage {
|
||||||
static const String _loggedInUserKey = "user";
|
static const String _loggedInUserKey = "user";
|
||||||
@ -20,24 +19,10 @@ class LocalStorage {
|
|||||||
static const String _employeeInfoKey = "employee_info";
|
static const String _employeeInfoKey = "employee_info";
|
||||||
static const String _mpinTokenKey = "mpinToken";
|
static const String _mpinTokenKey = "mpinToken";
|
||||||
static const String _isMpinKey = "isMpin";
|
static const String _isMpinKey = "isMpin";
|
||||||
static const String _fcmTokenKey = "fcm_token";
|
static const String _fcmTokenKey = 'fcm_token';
|
||||||
static const String _menuStorageKey = "dynamic_menus";
|
static const String _menuStorageKey = "dynamic_menus";
|
||||||
// In LocalStorage
|
|
||||||
static const String _recentTenantKey = "recent_tenant_id";
|
|
||||||
|
|
||||||
static Future<bool> setRecentTenantId(String tenantId) =>
|
|
||||||
preferences.setString(_recentTenantKey, tenantId);
|
|
||||||
|
|
||||||
static String? getRecentTenantId() =>
|
|
||||||
_initialized ? preferences.getString(_recentTenantKey) : null;
|
|
||||||
|
|
||||||
static Future<bool> removeRecentTenantId() =>
|
|
||||||
preferences.remove(_recentTenantKey);
|
|
||||||
|
|
||||||
static SharedPreferences? _preferencesInstance;
|
static SharedPreferences? _preferencesInstance;
|
||||||
static bool _initialized = false;
|
|
||||||
|
|
||||||
static bool get isInitialized => _initialized;
|
|
||||||
|
|
||||||
static SharedPreferences get preferences {
|
static SharedPreferences get preferences {
|
||||||
if (_preferencesInstance == null) {
|
if (_preferencesInstance == null) {
|
||||||
@ -46,47 +31,42 @@ class LocalStorage {
|
|||||||
return _preferencesInstance!;
|
return _preferencesInstance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialization (idempotent)
|
/// Initialization
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
if (_initialized) return;
|
|
||||||
_preferencesInstance = await SharedPreferences.getInstance();
|
_preferencesInstance = await SharedPreferences.getInstance();
|
||||||
await _initData();
|
await initData();
|
||||||
_initialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _initData() async {
|
static Future<void> initData() async {
|
||||||
AuthService.isLoggedIn = preferences.getBool(_loggedInUserKey) ?? false;
|
AuthService.isLoggedIn = preferences.getBool(_loggedInUserKey) ?? false;
|
||||||
ThemeCustomizer.fromJSON(preferences.getString(_themeCustomizerKey));
|
ThemeCustomizer.fromJSON(preferences.getString(_themeCustomizerKey));
|
||||||
}
|
}
|
||||||
|
/// ================== Sidebar Menu ==================
|
||||||
// ================== Sidebar Menu ==================
|
static Future<bool> setMenus(List<MenuItem> menus) async {
|
||||||
static Future<bool> setMenus(List<MenuItem> menus) async {
|
try {
|
||||||
try {
|
final jsonList = menus.map((e) => e.toJson()).toList();
|
||||||
final jsonList = menus.map((e) => e.toJson()).toList();
|
return preferences.setString(_menuStorageKey, jsonEncode(jsonList));
|
||||||
return preferences.setString(_menuStorageKey, jsonEncode(jsonList));
|
} catch (e) {
|
||||||
} catch (e) {
|
print("Error saving menus: $e");
|
||||||
print("Error saving menus: $e");
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static List<MenuItem> getMenus() {
|
static List<MenuItem> getMenus() {
|
||||||
if (!_initialized) return [];
|
final storedJson = preferences.getString(_menuStorageKey);
|
||||||
final storedJson = preferences.getString(_menuStorageKey);
|
if (storedJson == null) return [];
|
||||||
if (storedJson == null) return [];
|
try {
|
||||||
try {
|
return (jsonDecode(storedJson) as List)
|
||||||
return (jsonDecode(storedJson) as List)
|
.map((e) => MenuItem.fromJson(e as Map<String, dynamic>))
|
||||||
.map((e) => MenuItem.fromJson(e as Map<String, dynamic>))
|
.toList();
|
||||||
.toList();
|
} catch (e) {
|
||||||
} catch (e) {
|
print("Error loading menus: $e");
|
||||||
print("Error loading menus: $e");
|
return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
||||||
|
|
||||||
static Future<bool> removeMenus() => preferences.remove(_menuStorageKey);
|
/// ================== User Permissions ==================
|
||||||
|
|
||||||
// ================== User Permissions ==================
|
|
||||||
static Future<bool> setUserPermissions(
|
static Future<bool> setUserPermissions(
|
||||||
List<UserPermission> permissions) async {
|
List<UserPermission> permissions) async {
|
||||||
final jsonList = permissions.map((e) => e.toJson()).toList();
|
final jsonList = permissions.map((e) => e.toJson()).toList();
|
||||||
@ -94,7 +74,6 @@ class LocalStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static List<UserPermission> getUserPermissions() {
|
static List<UserPermission> getUserPermissions() {
|
||||||
if (!_initialized) return [];
|
|
||||||
final storedJson = preferences.getString(_userPermissionsKey);
|
final storedJson = preferences.getString(_userPermissionsKey);
|
||||||
if (storedJson == null) return [];
|
if (storedJson == null) return [];
|
||||||
return (jsonDecode(storedJson) as List)
|
return (jsonDecode(storedJson) as List)
|
||||||
@ -105,12 +84,11 @@ class LocalStorage {
|
|||||||
static Future<bool> removeUserPermissions() =>
|
static Future<bool> removeUserPermissions() =>
|
||||||
preferences.remove(_userPermissionsKey);
|
preferences.remove(_userPermissionsKey);
|
||||||
|
|
||||||
// ================== Employee Info ==================
|
/// ================== Employee Info ==================
|
||||||
static Future<bool> setEmployeeInfo(EmployeeInfo employeeInfo) => preferences
|
static Future<bool> setEmployeeInfo(EmployeeInfo employeeInfo) =>
|
||||||
.setString(_employeeInfoKey, jsonEncode(employeeInfo.toJson()));
|
preferences.setString(_employeeInfoKey, jsonEncode(employeeInfo.toJson()));
|
||||||
|
|
||||||
static EmployeeInfo? getEmployeeInfo() {
|
static EmployeeInfo? getEmployeeInfo() {
|
||||||
if (!_initialized) return null;
|
|
||||||
final storedJson = preferences.getString(_employeeInfoKey);
|
final storedJson = preferences.getString(_employeeInfoKey);
|
||||||
return storedJson == null
|
return storedJson == null
|
||||||
? null
|
? null
|
||||||
@ -120,7 +98,7 @@ class LocalStorage {
|
|||||||
static Future<bool> removeEmployeeInfo() =>
|
static Future<bool> removeEmployeeInfo() =>
|
||||||
preferences.remove(_employeeInfoKey);
|
preferences.remove(_employeeInfoKey);
|
||||||
|
|
||||||
// ================== Login / Logout ==================
|
/// ================== Login / Logout ==================
|
||||||
static Future<bool> setLoggedInUser(bool loggedIn) =>
|
static Future<bool> setLoggedInUser(bool loggedIn) =>
|
||||||
preferences.setBool(_loggedInUserKey, loggedIn);
|
preferences.setBool(_loggedInUserKey, loggedIn);
|
||||||
|
|
||||||
@ -132,6 +110,7 @@ class LocalStorage {
|
|||||||
final refreshToken = getRefreshToken();
|
final refreshToken = getRefreshToken();
|
||||||
final fcmToken = getFcmToken();
|
final fcmToken = getFcmToken();
|
||||||
|
|
||||||
|
// Call API only if both tokens exist
|
||||||
if (refreshToken != null && fcmToken != null) {
|
if (refreshToken != null && fcmToken != null) {
|
||||||
await AuthService.logoutApi(refreshToken, fcmToken);
|
await AuthService.logoutApi(refreshToken, fcmToken);
|
||||||
}
|
}
|
||||||
@ -139,6 +118,7 @@ class LocalStorage {
|
|||||||
print("Logout API error: $e");
|
print("Logout API error: $e");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ===== Local Cleanup =====
|
||||||
await removeLoggedInUser();
|
await removeLoggedInUser();
|
||||||
await removeToken(_jwtTokenKey);
|
await removeToken(_jwtTokenKey);
|
||||||
await removeToken(_refreshTokenKey);
|
await removeToken(_refreshTokenKey);
|
||||||
@ -146,8 +126,7 @@ class LocalStorage {
|
|||||||
await removeEmployeeInfo();
|
await removeEmployeeInfo();
|
||||||
await removeMpinToken();
|
await removeMpinToken();
|
||||||
await removeIsMpin();
|
await removeIsMpin();
|
||||||
await removeMenus();
|
await removeMenus(); // clear menus on logout
|
||||||
await removeRecentTenantId();
|
|
||||||
await preferences.remove("mpin_verified");
|
await preferences.remove("mpin_verified");
|
||||||
await preferences.remove(_languageKey);
|
await preferences.remove(_languageKey);
|
||||||
await preferences.remove(_themeCustomizerKey);
|
await preferences.remove(_themeCustomizerKey);
|
||||||
@ -160,22 +139,20 @@ class LocalStorage {
|
|||||||
Get.offAllNamed('/auth/login-option');
|
Get.offAllNamed('/auth/login-option');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Theme & Language ==================
|
/// ================== Theme & Language ==================
|
||||||
static Future<bool> setCustomizer(ThemeCustomizer themeCustomizer) =>
|
static Future<bool> setCustomizer(ThemeCustomizer themeCustomizer) =>
|
||||||
preferences.setString(_themeCustomizerKey, themeCustomizer.toJSON());
|
preferences.setString(_themeCustomizerKey, themeCustomizer.toJSON());
|
||||||
|
|
||||||
static Future<bool> setLanguage(Language language) =>
|
static Future<bool> setLanguage(Language language) =>
|
||||||
preferences.setString(_languageKey, language.locale.languageCode);
|
preferences.setString(_languageKey, language.locale.languageCode);
|
||||||
|
|
||||||
static String? getLanguage() =>
|
static String? getLanguage() => preferences.getString(_languageKey);
|
||||||
_initialized ? preferences.getString(_languageKey) : null;
|
|
||||||
|
|
||||||
// ================== Tokens ==================
|
/// ================== Tokens ==================
|
||||||
static Future<bool> setToken(String key, String token) =>
|
static Future<bool> setToken(String key, String token) =>
|
||||||
preferences.setString(key, token);
|
preferences.setString(key, token);
|
||||||
|
|
||||||
static String? getToken(String key) =>
|
static String? getToken(String key) => preferences.getString(key);
|
||||||
_initialized ? preferences.getString(key) : null;
|
|
||||||
|
|
||||||
static Future<bool> removeToken(String key) => preferences.remove(key);
|
static Future<bool> removeToken(String key) => preferences.remove(key);
|
||||||
|
|
||||||
@ -189,39 +166,34 @@ class LocalStorage {
|
|||||||
|
|
||||||
static String? getRefreshToken() => getToken(_refreshTokenKey);
|
static String? getRefreshToken() => getToken(_refreshTokenKey);
|
||||||
|
|
||||||
// ================== FCM Token ==================
|
/// ================== FCM Token ==================
|
||||||
static Future<void> setFcmToken(String token) =>
|
static Future<void> setFcmToken(String token) =>
|
||||||
preferences.setString(_fcmTokenKey, token);
|
preferences.setString(_fcmTokenKey, token);
|
||||||
|
|
||||||
static String? getFcmToken() =>
|
static String? getFcmToken() => preferences.getString(_fcmTokenKey);
|
||||||
_initialized ? preferences.getString(_fcmTokenKey) : null;
|
|
||||||
|
|
||||||
// ================== MPIN ==================
|
/// ================== MPIN ==================
|
||||||
static Future<bool> setMpinToken(String token) =>
|
static Future<bool> setMpinToken(String token) =>
|
||||||
preferences.setString(_mpinTokenKey, token);
|
preferences.setString(_mpinTokenKey, token);
|
||||||
|
|
||||||
static String? getMpinToken() =>
|
static String? getMpinToken() => preferences.getString(_mpinTokenKey);
|
||||||
_initialized ? preferences.getString(_mpinTokenKey) : null;
|
|
||||||
|
|
||||||
static Future<bool> removeMpinToken() => preferences.remove(_mpinTokenKey);
|
static Future<bool> removeMpinToken() => preferences.remove(_mpinTokenKey);
|
||||||
|
|
||||||
static Future<bool> setIsMpin(bool value) =>
|
static Future<bool> setIsMpin(bool value) =>
|
||||||
preferences.setBool(_isMpinKey, value);
|
preferences.setBool(_isMpinKey, value);
|
||||||
|
|
||||||
static bool getIsMpin() =>
|
static bool getIsMpin() => preferences.getBool(_isMpinKey) ?? false;
|
||||||
_initialized ? preferences.getBool(_isMpinKey) ?? false : false;
|
|
||||||
|
|
||||||
static Future<bool> removeIsMpin() => preferences.remove(_isMpinKey);
|
static Future<bool> removeIsMpin() => preferences.remove(_isMpinKey);
|
||||||
|
|
||||||
// ================== Generic Set/Get ==================
|
/// ================== Generic Set/Get ==================
|
||||||
static Future<bool> setBool(String key, bool value) =>
|
static Future<bool> setBool(String key, bool value) =>
|
||||||
preferences.setBool(key, value);
|
preferences.setBool(key, value);
|
||||||
|
|
||||||
static bool? getBool(String key) =>
|
static bool? getBool(String key) => preferences.getBool(key);
|
||||||
_initialized ? preferences.getBool(key) : null;
|
|
||||||
|
|
||||||
static String? getString(String key) =>
|
static String? getString(String key) => preferences.getString(key);
|
||||||
_initialized ? preferences.getString(key) : null;
|
|
||||||
|
|
||||||
static Future<bool> saveString(String key, String value) =>
|
static Future<bool> saveString(String key, String value) =>
|
||||||
preferences.setString(key, value);
|
preferences.setString(key, value);
|
||||||
|
|||||||
@ -1,173 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:on_field_work/controller/project_controller.dart';
|
|
||||||
|
|
||||||
import 'package:on_field_work/helpers/services/api_endpoints.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/auth_service.dart';
|
|
||||||
import 'package:on_field_work/model/tenant/tenant_list_model.dart';
|
|
||||||
|
|
||||||
/// Abstract interface for tenant service functionality
|
|
||||||
abstract class ITenantService {
|
|
||||||
Future<List<Map<String, dynamic>>?> getTenants({bool hasRetried = false});
|
|
||||||
Future<bool> selectTenant(String tenantId, {bool hasRetried = false});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tenant API service
|
|
||||||
class TenantService implements ITenantService {
|
|
||||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
|
||||||
static const Map<String, String> _headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Currently selected tenant
|
|
||||||
static Tenant? currentTenant;
|
|
||||||
|
|
||||||
/// Set the selected tenant
|
|
||||||
static void setSelectedTenant(Tenant tenant) {
|
|
||||||
currentTenant = tenant;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if tenant is selected
|
|
||||||
static bool get isTenantSelected => currentTenant != null;
|
|
||||||
|
|
||||||
/// Build authorized headers
|
|
||||||
static Future<Map<String, String>> _authorizedHeaders() async {
|
|
||||||
final token = await LocalStorage.getJwtToken();
|
|
||||||
if (token == null || token.isEmpty) {
|
|
||||||
throw Exception('Missing JWT token');
|
|
||||||
}
|
|
||||||
return {..._headers, 'Authorization': 'Bearer $token'};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle API errors
|
|
||||||
static void _handleApiError(
|
|
||||||
http.Response response, dynamic data, String context) {
|
|
||||||
final message = data['message'] ?? 'Unknown error';
|
|
||||||
final level =
|
|
||||||
response.statusCode >= 500 ? LogLevel.error : LogLevel.warning;
|
|
||||||
logSafe("❌ $context failed: $message [Status: ${response.statusCode}]",
|
|
||||||
level: level);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log exceptions
|
|
||||||
static void _logException(dynamic e, dynamic st, String context) {
|
|
||||||
logSafe("❌ $context exception",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<Map<String, dynamic>>?> getTenants(
|
|
||||||
{bool hasRetried = false}) async {
|
|
||||||
try {
|
|
||||||
final headers = await _authorizedHeaders();
|
|
||||||
|
|
||||||
final response = await http.get(
|
|
||||||
Uri.parse("$_baseUrl/auth/get/user/tenants"),
|
|
||||||
headers: headers,
|
|
||||||
);
|
|
||||||
|
|
||||||
// ✅ Handle empty response BEFORE decoding
|
|
||||||
if (response.body.isEmpty || response.body.trim().isEmpty) {
|
|
||||||
logSafe("❌ Empty tenant response — auto logout");
|
|
||||||
await LocalStorage.logout();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> data;
|
|
||||||
try {
|
|
||||||
data = jsonDecode(response.body);
|
|
||||||
} catch (e) {
|
|
||||||
logSafe("❌ Invalid JSON in tenant response — auto logout");
|
|
||||||
await LocalStorage.logout();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SUCCESS CASE
|
|
||||||
if (response.statusCode == 200 && data['success'] == true) {
|
|
||||||
final list = data['data'];
|
|
||||||
if (list is! List) return null;
|
|
||||||
return List<Map<String, dynamic>>.from(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TOKEN EXPIRED
|
|
||||||
if (response.statusCode == 401 && !hasRetried) {
|
|
||||||
final refreshed = await AuthService.refreshToken();
|
|
||||||
if (refreshed) return getTenants(hasRetried: true);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleApiError(response, data, "Fetching tenants");
|
|
||||||
return null;
|
|
||||||
} catch (e, st) {
|
|
||||||
_logException(e, st, "Get Tenants API");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> selectTenant(String tenantId, {bool hasRetried = false}) async {
|
|
||||||
try {
|
|
||||||
final headers = await _authorizedHeaders();
|
|
||||||
logSafe(
|
|
||||||
"➡️ POST $_baseUrl/auth/select-tenant/$tenantId\nHeaders: $headers",
|
|
||||||
level: LogLevel.info);
|
|
||||||
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse("$_baseUrl/auth/select-tenant/$tenantId"),
|
|
||||||
headers: headers,
|
|
||||||
);
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
|
|
||||||
logSafe(
|
|
||||||
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
|
|
||||||
level: LogLevel.info);
|
|
||||||
|
|
||||||
if (response.statusCode == 200 && data['success'] == true) {
|
|
||||||
await LocalStorage.setJwtToken(data['data']['token']);
|
|
||||||
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
|
|
||||||
logSafe("✅ Tenant selected successfully. Tokens updated.");
|
|
||||||
|
|
||||||
// 🔥 Refresh projects when tenant changes
|
|
||||||
try {
|
|
||||||
final projectController = Get.find<ProjectController>();
|
|
||||||
projectController.clearProjects();
|
|
||||||
projectController.fetchProjects();
|
|
||||||
} catch (_) {
|
|
||||||
logSafe("⚠️ ProjectController not found while refreshing projects");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔹 Register FCM token after tenant selection
|
|
||||||
final fcmToken = LocalStorage.getFcmToken();
|
|
||||||
if (fcmToken?.isNotEmpty ?? false) {
|
|
||||||
final success = await AuthService.registerDeviceToken(fcmToken!);
|
|
||||||
logSafe(
|
|
||||||
success
|
|
||||||
? "✅ FCM token registered after tenant selection."
|
|
||||||
: "⚠️ Failed to register FCM token after tenant selection.",
|
|
||||||
level: success ? LogLevel.info : LogLevel.warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode == 401 && !hasRetried) {
|
|
||||||
logSafe("⚠️ Unauthorized while selecting tenant. Refreshing token...",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
final refreshed = await AuthService.refreshToken();
|
|
||||||
if (refreshed) return selectTenant(tenantId, hasRetried: true);
|
|
||||||
logSafe("❌ Token refresh failed while selecting tenant.",
|
|
||||||
level: LogLevel.error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleApiError(response, data, "Selecting tenant");
|
|
||||||
return false;
|
|
||||||
} catch (e, st) {
|
|
||||||
_logException(e, st, "Select Tenant API");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +1,35 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
|
|
||||||
enum LeftBarThemeType { light, dark }
|
enum LeftBarThemeType { light, dark }
|
||||||
|
|
||||||
enum ContentThemeType { light, dark }
|
enum ContentThemeType { light, dark }
|
||||||
|
|
||||||
enum RightBarThemeType { light, dark }
|
enum RightBarThemeType { light, dark }
|
||||||
|
|
||||||
|
enum ContentThemeColor {
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
success,
|
||||||
|
info,
|
||||||
|
warning,
|
||||||
|
danger,
|
||||||
|
light,
|
||||||
|
dark,
|
||||||
|
pink,
|
||||||
|
green,
|
||||||
|
red,
|
||||||
|
brandRed;
|
||||||
|
|
||||||
|
Color get color {
|
||||||
|
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get onColor {
|
||||||
|
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class LeftBarTheme {
|
class LeftBarTheme {
|
||||||
final Color background, onBackground;
|
final Color background, onBackground;
|
||||||
final Color labelColor;
|
final Color labelColor;
|
||||||
@ -18,15 +43,16 @@ class LeftBarTheme {
|
|||||||
this.activeItemBackground = const Color(0x15663399),
|
this.activeItemBackground = const Color(0x15663399),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//-------------------------------------- Left Bar Theme ----------------------------------------//
|
||||||
|
|
||||||
static final LeftBarTheme lightLeftBarTheme = LeftBarTheme();
|
static final LeftBarTheme lightLeftBarTheme = LeftBarTheme();
|
||||||
|
|
||||||
static final LeftBarTheme darkLeftBarTheme = LeftBarTheme(
|
static final LeftBarTheme darkLeftBarTheme = LeftBarTheme(
|
||||||
background: const Color(0xff282c32),
|
background: const Color(0xff282c32),
|
||||||
onBackground: const Color(0xffdcdcdc),
|
onBackground: const Color(0xffdcdcdc),
|
||||||
labelColor: const Color(0xff32BFAE),
|
labelColor: const Color(0xff32BFAE),
|
||||||
activeItemBackground: const Color(0x1532BFAE),
|
activeItemBackground: const Color(0x1532BFAE),
|
||||||
activeItemColor: const Color(0xff32BFAE),
|
activeItemColor: const Color(0xff32BFAE));
|
||||||
);
|
|
||||||
|
|
||||||
static LeftBarTheme getThemeFromType(LeftBarThemeType leftBarThemeType) {
|
static LeftBarTheme getThemeFromType(LeftBarThemeType leftBarThemeType) {
|
||||||
switch (leftBarThemeType) {
|
switch (leftBarThemeType) {
|
||||||
@ -47,12 +73,11 @@ class TopBarTheme {
|
|||||||
this.onBackground = const Color(0xff313a46),
|
this.onBackground = const Color(0xff313a46),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//-------------------------------------- Left Bar Theme ----------------------------------------//
|
||||||
|
|
||||||
static final TopBarTheme lightTopBarTheme = TopBarTheme();
|
static final TopBarTheme lightTopBarTheme = TopBarTheme();
|
||||||
|
|
||||||
static final TopBarTheme darkTopBarTheme = TopBarTheme(
|
static final TopBarTheme darkTopBarTheme = TopBarTheme(background: const Color(0xff2c3036), onBackground: const Color(0xffdcdcdc));
|
||||||
background: const Color(0xff2c3036),
|
|
||||||
onBackground: const Color(0xffdcdcdc),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RightBarTheme {
|
class RightBarTheme {
|
||||||
@ -66,41 +91,19 @@ class RightBarTheme {
|
|||||||
this.onDisabled = const Color(0xff313a46),
|
this.onDisabled = const Color(0xff313a46),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//-------------------------------------- Left Bar Theme ----------------------------------------//
|
||||||
|
|
||||||
static final RightBarTheme lightRightBarTheme = RightBarTheme(
|
static final RightBarTheme lightRightBarTheme = RightBarTheme(
|
||||||
disabled: const Color(0xffffffff),
|
disabled: const Color(0xffffffff),
|
||||||
onDisabled: const Color(0xffdee2e6),
|
onDisabled: const Color(0xffdee2e6),
|
||||||
activeSwitchBorderColor: const Color(0xff727cf5),
|
activeSwitchBorderColor: const Color(0xff727cf5),
|
||||||
inactiveSwitchBorderColor: const Color(0xffdee2e6),
|
inactiveSwitchBorderColor: const Color(0xffdee2e6));
|
||||||
);
|
|
||||||
|
|
||||||
static final RightBarTheme darkRightBarTheme = RightBarTheme(
|
static final RightBarTheme darkRightBarTheme = RightBarTheme(
|
||||||
disabled: const Color(0xff444d57),
|
disabled: const Color(0xff444d57),
|
||||||
activeSwitchBorderColor: const Color(0xff727cf5),
|
activeSwitchBorderColor: const Color(0xff727cf5),
|
||||||
inactiveSwitchBorderColor: const Color(0xffdee2e6),
|
inactiveSwitchBorderColor: const Color(0xffdee2e6),
|
||||||
onDisabled: const Color(0xff515a65),
|
onDisabled: const Color(0xff515a65));
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ContentThemeColor {
|
|
||||||
primary,
|
|
||||||
secondary,
|
|
||||||
success,
|
|
||||||
info,
|
|
||||||
warning,
|
|
||||||
danger,
|
|
||||||
light,
|
|
||||||
dark,
|
|
||||||
pink,
|
|
||||||
green,
|
|
||||||
red;
|
|
||||||
|
|
||||||
Color get color {
|
|
||||||
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color get onColor {
|
|
||||||
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContentTheme {
|
class ContentTheme {
|
||||||
@ -117,11 +120,29 @@ class ContentTheme {
|
|||||||
final Color purple, onPurple;
|
final Color purple, onPurple;
|
||||||
final Color pink, onPink;
|
final Color pink, onPink;
|
||||||
final Color red, onRed;
|
final Color red, onRed;
|
||||||
|
final Color brandRed, onBrandRed;
|
||||||
|
|
||||||
final Color cardBackground, cardShadow, cardBorder, cardText, cardTextMuted;
|
final Color cardBackground, cardShadow, cardBorder, cardText, cardTextMuted;
|
||||||
final Color title;
|
final Color title;
|
||||||
final Color disabled, onDisabled;
|
final Color disabled, onDisabled;
|
||||||
|
|
||||||
|
Map<ContentThemeColor, Map<String, Color>> get getMappedIntoThemeColor {
|
||||||
|
var c = AdminTheme.theme.contentTheme;
|
||||||
|
return {
|
||||||
|
ContentThemeColor.primary: {'color': c.primary, 'onColor': c.onPrimary},
|
||||||
|
ContentThemeColor.secondary: {'color': c.secondary, 'onColor': c.onSecondary},
|
||||||
|
ContentThemeColor.success: {'color': c.success, 'onColor': c.onSuccess},
|
||||||
|
ContentThemeColor.info: {'color': c.info, 'onColor': c.onInfo},
|
||||||
|
ContentThemeColor.warning: {'color': c.warning, 'onColor': c.onWarning},
|
||||||
|
ContentThemeColor.danger: {'color': c.danger, 'onColor': c.onDanger},
|
||||||
|
ContentThemeColor.light: {'color': c.light, 'onColor': c.onLight},
|
||||||
|
ContentThemeColor.dark: {'color': c.dark, 'onColor': c.onDark},
|
||||||
|
ContentThemeColor.pink: {'color': c.pink, 'onColor': c.onPink},
|
||||||
|
ContentThemeColor.red: {'color': c.red, 'onColor': c.onRed},
|
||||||
|
ContentThemeColor.brandRed: {'color': c.brandRed, 'onColor': c.onBrandRed},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
ContentTheme({
|
ContentTheme({
|
||||||
this.background = const Color(0xfffafbfe),
|
this.background = const Color(0xfffafbfe),
|
||||||
this.onBackground = const Color(0xffF1F1F2),
|
this.onBackground = const Color(0xffF1F1F2),
|
||||||
@ -142,11 +163,13 @@ class ContentTheme {
|
|||||||
this.dark = const Color(0xff313a46),
|
this.dark = const Color(0xff313a46),
|
||||||
this.onDark = const Color(0xffffffff),
|
this.onDark = const Color(0xffffffff),
|
||||||
this.purple = const Color(0xff800080),
|
this.purple = const Color(0xff800080),
|
||||||
this.onPurple = const Color(0xffffffff),
|
this.onPurple = const Color(0xffFF0000),
|
||||||
this.pink = const Color(0xffff1087),
|
this.pink = const Color(0xffFF1087),
|
||||||
this.onPink = const Color(0xffffffff),
|
this.onPink = const Color(0xffffffff),
|
||||||
this.red = const Color(0xffff0000),
|
this.red = const Color(0xffFF0000),
|
||||||
this.onRed = const Color(0xffffffff),
|
this.onRed = const Color(0xffffffff),
|
||||||
|
this.brandRed = const Color.fromARGB(255, 255, 0, 0),
|
||||||
|
this.onBrandRed = const Color(0xffffffff),
|
||||||
this.cardBackground = const Color(0xffffffff),
|
this.cardBackground = const Color(0xffffffff),
|
||||||
this.cardShadow = const Color(0xffffffff),
|
this.cardShadow = const Color(0xffffffff),
|
||||||
this.cardBorder = const Color(0xffffffff),
|
this.cardBorder = const Color(0xffffffff),
|
||||||
@ -157,103 +180,44 @@ class ContentTheme {
|
|||||||
this.onDisabled = const Color(0xffffffff),
|
this.onDisabled = const Color(0xffffffff),
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<ContentThemeColor, Map<String, Color>> get getMappedIntoThemeColor {
|
//-------------------------------------- Left Bar Theme ----------------------------------------//
|
||||||
return {
|
|
||||||
ContentThemeColor.primary: {'color': primary, 'onColor': onPrimary},
|
|
||||||
ContentThemeColor.secondary: {'color': secondary, 'onColor': onSecondary},
|
|
||||||
ContentThemeColor.success: {'color': success, 'onColor': onSuccess},
|
|
||||||
ContentThemeColor.info: {'color': info, 'onColor': onInfo},
|
|
||||||
ContentThemeColor.warning: {'color': warning, 'onColor': onWarning},
|
|
||||||
ContentThemeColor.danger: {'color': danger, 'onColor': onDanger},
|
|
||||||
ContentThemeColor.light: {'color': light, 'onColor': onLight},
|
|
||||||
ContentThemeColor.dark: {'color': dark, 'onColor': onDark},
|
|
||||||
ContentThemeColor.pink: {'color': pink, 'onColor': onPink},
|
|
||||||
ContentThemeColor.red: {'color': red, 'onColor': onRed},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentTheme copyWith({
|
static final ContentTheme lightContentTheme = ContentTheme(
|
||||||
Color? primary,
|
primary: Color(0xff663399),
|
||||||
Color? onPrimary,
|
background: const Color(0xfffafbfe),
|
||||||
Color? secondary,
|
onBackground: const Color(0xff313a46),
|
||||||
Color? onSecondary,
|
cardBorder: const Color(0xffe8ecf1),
|
||||||
Color? background,
|
cardBackground: const Color(0xffffffff),
|
||||||
Color? onBackground,
|
cardShadow: const Color(0xff9aa1ab),
|
||||||
}) {
|
cardText: const Color(0xff6c757d),
|
||||||
return ContentTheme(
|
title: const Color(0xff6c757d),
|
||||||
primary: primary ?? this.primary,
|
cardTextMuted: const Color(0xff98a6ad),
|
||||||
onPrimary: onPrimary ?? this.onPrimary,
|
brandRed: const Color.fromARGB(255, 255, 0, 0),
|
||||||
secondary: secondary ?? this.secondary,
|
onBrandRed: const Color(0xffffffff),
|
||||||
onSecondary: onSecondary ?? this.onSecondary,
|
);
|
||||||
background: background ?? this.background,
|
|
||||||
onBackground: onBackground ?? this.onBackground,
|
|
||||||
success: success,
|
|
||||||
onSuccess: onSuccess,
|
|
||||||
danger: danger,
|
|
||||||
onDanger: onDanger,
|
|
||||||
warning: warning,
|
|
||||||
onWarning: onWarning,
|
|
||||||
info: info,
|
|
||||||
onInfo: onInfo,
|
|
||||||
light: light,
|
|
||||||
onLight: onLight,
|
|
||||||
dark: dark,
|
|
||||||
onDark: onDark,
|
|
||||||
purple: purple,
|
|
||||||
onPurple: onPurple,
|
|
||||||
pink: pink,
|
|
||||||
onPink: onPink,
|
|
||||||
red: red,
|
|
||||||
onRed: onRed,
|
|
||||||
cardBackground: cardBackground,
|
|
||||||
cardShadow: cardShadow,
|
|
||||||
cardBorder: cardBorder,
|
|
||||||
cardText: cardText,
|
|
||||||
cardTextMuted: cardTextMuted,
|
|
||||||
title: title,
|
|
||||||
disabled: disabled,
|
|
||||||
onDisabled: onDisabled,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ContentTheme withColorTheme(
|
static final ContentTheme darkContentTheme = ContentTheme(
|
||||||
ColorThemeType colorTheme, {
|
primary: Color(0xff32BFAE),
|
||||||
ThemeMode mode = ThemeMode.light,
|
background: const Color(0xff343a40),
|
||||||
}) {
|
onBackground: const Color(0xffF1F1F2),
|
||||||
final baseTheme = mode == ThemeMode.light
|
disabled: const Color(0xff444d57),
|
||||||
? ContentTheme()
|
onDisabled: const Color(0xff515a65),
|
||||||
: ContentTheme(
|
cardBorder: const Color(0xff464f5b),
|
||||||
primary: const Color(0xff32BFAE),
|
cardBackground: const Color(0xff37404a),
|
||||||
background: const Color(0xff343a40),
|
cardShadow: const Color(0xff01030E),
|
||||||
onBackground: const Color(0xffF1F1F2),
|
cardText: const Color(0xffaab8c5),
|
||||||
cardBorder: const Color(0xff464f5b),
|
title: const Color(0xffaab8c5),
|
||||||
cardBackground: const Color(0xff37404a),
|
cardTextMuted: const Color(0xff8391a2),
|
||||||
cardShadow: const Color(0xff01030E),
|
brandRed: const Color.fromARGB(255, 255, 0, 0),
|
||||||
cardText: const Color(0xffaab8c5),
|
onBrandRed: const Color(0xffffffff),
|
||||||
title: const Color(0xffaab8c5),
|
);
|
||||||
cardTextMuted: const Color(0xff8391a2),
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (colorTheme) {
|
|
||||||
case ColorThemeType.purple:
|
|
||||||
return baseTheme.copyWith(primary: const Color(0xff663399), onPrimary: Colors.white);
|
|
||||||
case ColorThemeType.red:
|
|
||||||
return baseTheme.copyWith(primary: const Color(0xffff0000), onPrimary: Colors.white);
|
|
||||||
case ColorThemeType.green:
|
|
||||||
return baseTheme.copyWith(primary: const Color(0xff49BF3C), onPrimary: Colors.white);
|
|
||||||
case ColorThemeType.blue:
|
|
||||||
return baseTheme.copyWith(primary: const Color(0xff007bff), onPrimary: Colors.white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ColorThemeType { purple, red, green, blue }
|
|
||||||
|
|
||||||
class AdminTheme {
|
class AdminTheme {
|
||||||
final ContentTheme contentTheme;
|
|
||||||
final LeftBarTheme leftBarTheme;
|
final LeftBarTheme leftBarTheme;
|
||||||
final RightBarTheme rightBarTheme;
|
final RightBarTheme rightBarTheme;
|
||||||
final TopBarTheme topBarTheme;
|
final TopBarTheme topBarTheme;
|
||||||
|
final ContentTheme contentTheme;
|
||||||
|
|
||||||
AdminTheme({
|
AdminTheme({
|
||||||
required this.leftBarTheme,
|
required this.leftBarTheme,
|
||||||
@ -262,22 +226,19 @@ class AdminTheme {
|
|||||||
required this.contentTheme,
|
required this.contentTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//-------------------------------------- Left Bar Theme ----------------------------------------//
|
||||||
|
|
||||||
static AdminTheme theme = AdminTheme(
|
static AdminTheme theme = AdminTheme(
|
||||||
leftBarTheme: LeftBarTheme.lightLeftBarTheme,
|
leftBarTheme: LeftBarTheme.lightLeftBarTheme,
|
||||||
topBarTheme: TopBarTheme.lightTopBarTheme,
|
topBarTheme: TopBarTheme.lightTopBarTheme,
|
||||||
rightBarTheme: RightBarTheme.lightRightBarTheme,
|
rightBarTheme: RightBarTheme.lightRightBarTheme,
|
||||||
contentTheme: ContentTheme.withColorTheme(ColorThemeType.purple, mode: ThemeMode.light),
|
contentTheme: ContentTheme.lightContentTheme);
|
||||||
);
|
|
||||||
|
|
||||||
static void setTheme() {
|
static void setTheme() {
|
||||||
final themeMode = ThemeCustomizer.instance.theme;
|
|
||||||
final colorTheme = ThemeCustomizer.instance.colorTheme;
|
|
||||||
|
|
||||||
theme = AdminTheme(
|
theme = AdminTheme(
|
||||||
leftBarTheme: themeMode == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme,
|
leftBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme,
|
||||||
topBarTheme: themeMode == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme,
|
topBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme,
|
||||||
rightBarTheme: themeMode == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme,
|
rightBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme,
|
||||||
contentTheme: ContentTheme.withColorTheme(colorTheme, mode: themeMode),
|
contentTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? ContentTheme.darkContentTheme : ContentTheme.lightContentTheme);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
import 'package:marco/helpers/services/localizations/language.dart';
|
||||||
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my.dart';
|
import 'package:marco/helpers/widgets/my.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,13 @@
|
|||||||
* */
|
* */
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my.dart';
|
import 'package:marco/helpers/widgets/my.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_breadcrumb_item.dart';
|
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_constant.dart';
|
import 'package:marco/helpers/widgets/my_constant.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_screen_media.dart';
|
import 'package:marco/helpers/widgets/my_screen_media.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
@ -230,7 +230,7 @@ class AppStyle {
|
|||||||
containerRadius: AppStyle.containerRadius.medium,
|
containerRadius: AppStyle.containerRadius.medium,
|
||||||
cardRadius: AppStyle.cardRadius.medium,
|
cardRadius: AppStyle.cardRadius.medium,
|
||||||
buttonRadius: AppStyle.buttonRadius.medium,
|
buttonRadius: AppStyle.buttonRadius.medium,
|
||||||
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'On Field Work', route: '/client/dashboard'),
|
defaultBreadCrumbItem: MyBreadcrumbItem(name: 'Marco', route: '/client/home'),
|
||||||
));
|
));
|
||||||
bool isMobile = true;
|
bool isMobile = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:marco/helpers/services/json_decoder.dart';
|
||||||
|
import 'package:marco/helpers/services/localizations/language.dart';
|
||||||
|
import 'package:marco/helpers/services/localizations/translator.dart';
|
||||||
|
import 'package:marco/helpers/services/navigation_services.dart';
|
||||||
|
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||||
|
import 'package:marco/helpers/theme/app_notifier.dart';
|
||||||
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:on_field_work/helpers/services/json_decoder.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/localizations/language.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/localizations/translator.dart';
|
|
||||||
import 'package:on_field_work/helpers/services/navigation_services.dart';
|
|
||||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
|
||||||
import 'package:on_field_work/helpers/theme/app_notifier.dart';
|
|
||||||
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
typedef ThemeChangeCallback = void Function(
|
typedef ThemeChangeCallback = void Function(
|
||||||
ThemeCustomizer oldVal, ThemeCustomizer newVal);
|
ThemeCustomizer oldVal, ThemeCustomizer newVal);
|
||||||
@ -24,7 +24,7 @@ class ThemeCustomizer {
|
|||||||
ThemeMode leftBarTheme = ThemeMode.light;
|
ThemeMode leftBarTheme = ThemeMode.light;
|
||||||
ThemeMode rightBarTheme = ThemeMode.light;
|
ThemeMode rightBarTheme = ThemeMode.light;
|
||||||
ThemeMode topBarTheme = ThemeMode.light;
|
ThemeMode topBarTheme = ThemeMode.light;
|
||||||
ColorThemeType colorTheme = ColorThemeType.red;
|
|
||||||
bool rightBarOpen = false;
|
bool rightBarOpen = false;
|
||||||
bool leftBarCondensed = false;
|
bool leftBarCondensed = false;
|
||||||
|
|
||||||
@ -33,8 +33,6 @@ class ThemeCustomizer {
|
|||||||
|
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
await initLanguage();
|
await initLanguage();
|
||||||
await _loadColorTheme();
|
|
||||||
_notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static initLanguage() async {
|
static initLanguage() async {
|
||||||
@ -42,7 +40,7 @@ class ThemeCustomizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String toJSON() {
|
String toJSON() {
|
||||||
return jsonEncode({'theme': theme.name, 'colorTheme': colorTheme.name});
|
return jsonEncode({'theme': theme.name});
|
||||||
}
|
}
|
||||||
|
|
||||||
static ThemeCustomizer fromJSON(String? json) {
|
static ThemeCustomizer fromJSON(String? json) {
|
||||||
@ -51,8 +49,6 @@ class ThemeCustomizer {
|
|||||||
JSONDecoder decoder = JSONDecoder(json);
|
JSONDecoder decoder = JSONDecoder(json);
|
||||||
instance.theme =
|
instance.theme =
|
||||||
decoder.getEnum('theme', ThemeMode.values, ThemeMode.light);
|
decoder.getEnum('theme', ThemeMode.values, ThemeMode.light);
|
||||||
instance.colorTheme = decoder.getEnum(
|
|
||||||
'colorTheme', ColorThemeType.values, ColorThemeType.red);
|
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@ -77,11 +73,6 @@ class ThemeCustomizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Public method to trigger theme updates externally
|
|
||||||
static void applyThemeChange() {
|
|
||||||
_notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void notify() {
|
static void notify() {
|
||||||
for (var value in _notifier) {
|
for (var value in _notifier) {
|
||||||
value(oldInstance, instance);
|
value(oldInstance, instance);
|
||||||
@ -121,46 +112,12 @@ class ThemeCustomizer {
|
|||||||
tc.topBarTheme = topBarTheme;
|
tc.topBarTheme = topBarTheme;
|
||||||
tc.rightBarOpen = rightBarOpen;
|
tc.rightBarOpen = rightBarOpen;
|
||||||
tc.leftBarCondensed = leftBarCondensed;
|
tc.leftBarCondensed = leftBarCondensed;
|
||||||
tc.colorTheme = colorTheme;
|
|
||||||
tc.currentLanguage = currentLanguage.clone();
|
tc.currentLanguage = currentLanguage.clone();
|
||||||
return tc;
|
return tc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ThemeCustomizer{theme: $theme, colorTheme: $colorTheme}';
|
return 'ThemeCustomizer{theme: $theme}';
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 🟢 Color Theme Persistence
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
static const _colorThemeKey = 'color_theme_type';
|
|
||||||
|
|
||||||
/// Save selected color theme
|
|
||||||
static Future<void> saveColorTheme(ColorThemeType type) async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.setString(_colorThemeKey, type.name);
|
|
||||||
instance.colorTheme = type;
|
|
||||||
_notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load saved color theme (called at startup)
|
|
||||||
static Future<void> _loadColorTheme() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
final savedType = prefs.getString(_colorThemeKey);
|
|
||||||
if (savedType != null) {
|
|
||||||
instance.colorTheme = ColorThemeType.values.firstWhere(
|
|
||||||
(e) => e.name == savedType,
|
|
||||||
orElse: () => ColorThemeType.red,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change color theme & persist
|
|
||||||
static Future<void> changeColorTheme(ColorThemeType type) async {
|
|
||||||
oldInstance = instance.clone();
|
|
||||||
instance.colorTheme = type;
|
|
||||||
await saveColorTheme(type);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,273 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:on_field_work/helpers/widgets/wave_background.dart';
|
|
||||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
|
||||||
import 'package:on_field_work/helpers/theme/theme_customizer.dart';
|
|
||||||
|
|
||||||
class ThemeOption {
|
|
||||||
final String label;
|
|
||||||
final Color primary;
|
|
||||||
final Color button;
|
|
||||||
final Color brand;
|
|
||||||
final ColorThemeType colorThemeType;
|
|
||||||
|
|
||||||
ThemeOption(
|
|
||||||
this.label, this.primary, this.button, this.brand, this.colorThemeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<ThemeOption> themeOptions = [
|
|
||||||
ThemeOption(
|
|
||||||
"Theme 1", Colors.red, Colors.red, Colors.red, ColorThemeType.red),
|
|
||||||
ThemeOption(
|
|
||||||
"Theme 2",
|
|
||||||
const Color(0xFF49BF3C),
|
|
||||||
const Color(0xFF49BF3C),
|
|
||||||
const Color(0xFF49BF3C),
|
|
||||||
ColorThemeType.green,
|
|
||||||
),
|
|
||||||
ThemeOption(
|
|
||||||
"Theme 3",
|
|
||||||
const Color(0xFF3F51B5),
|
|
||||||
const Color(0xFF3F51B5),
|
|
||||||
const Color(0xFF3F51B5),
|
|
||||||
ColorThemeType.blue,
|
|
||||||
),
|
|
||||||
ThemeOption(
|
|
||||||
"Theme 4",
|
|
||||||
const Color(0xFF663399),
|
|
||||||
const Color(0xFF663399),
|
|
||||||
const Color(0xFF663399),
|
|
||||||
ColorThemeType.purple,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
class ThemeController extends GetxController {
|
|
||||||
RxInt selectedIndex = 0.obs;
|
|
||||||
RxBool showApplied = false.obs;
|
|
||||||
|
|
||||||
void init() {
|
|
||||||
final currentPrimary = AdminTheme.theme.contentTheme.primary;
|
|
||||||
int index = themeOptions
|
|
||||||
.indexWhere((opt) => opt.primary.value == currentPrimary.value);
|
|
||||||
selectedIndex.value = index == -1 ? 0 : index;
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyTheme(int index) async {
|
|
||||||
selectedIndex.value = index;
|
|
||||||
showApplied.value = true;
|
|
||||||
|
|
||||||
ThemeCustomizer.instance.colorTheme = themeOptions[index].colorThemeType;
|
|
||||||
|
|
||||||
ThemeCustomizer.applyThemeChange();
|
|
||||||
|
|
||||||
await Future.delayed(const Duration(milliseconds: 600));
|
|
||||||
showApplied.value = false;
|
|
||||||
|
|
||||||
// Navigate to dashboard after applying theme
|
|
||||||
Get.offAllNamed('/dashboard');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ThemeEditorWidget extends StatefulWidget {
|
|
||||||
final VoidCallback onClose;
|
|
||||||
|
|
||||||
const ThemeEditorWidget({super.key, required this.onClose});
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ThemeEditorWidgetState createState() => _ThemeEditorWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ThemeEditorWidgetState extends State<ThemeEditorWidget> {
|
|
||||||
final ThemeController themeController = Get.put(ThemeController());
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
themeController.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Header row with title and close button
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
MyText.bodyLarge("Theme Customization", fontWeight: 600),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: widget.onClose,
|
|
||||||
tooltip: "Back",
|
|
||||||
iconSize: 20,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
// Theme cards wrapped in reactive Obx widget
|
|
||||||
Center(
|
|
||||||
child: Obx(
|
|
||||||
() => Wrap(
|
|
||||||
spacing: 12,
|
|
||||||
runSpacing: 12,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
children: List.generate(themeOptions.length, (i) {
|
|
||||||
return ThemeCard(
|
|
||||||
themeOption: themeOptions[i],
|
|
||||||
isSelected: themeController.selectedIndex.value == i,
|
|
||||||
onTap: () => themeController.applyTheme(i),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// Applied indicator reactive widget
|
|
||||||
Obx(
|
|
||||||
() => themeController.showApplied.value
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
color:
|
|
||||||
themeOptions[themeController.selectedIndex.value]
|
|
||||||
.brand,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
"Theme Applied!",
|
|
||||||
style: TextStyle(
|
|
||||||
color: themeOptions[
|
|
||||||
themeController.selectedIndex.value]
|
|
||||||
.brand,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox(),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
const Text(
|
|
||||||
"Preview and select a theme. You can change this anytime.",
|
|
||||||
style: TextStyle(fontSize: 13, color: Colors.black54),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ThemeCard extends StatelessWidget {
|
|
||||||
final ThemeOption themeOption;
|
|
||||||
final bool isSelected;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
const ThemeCard({
|
|
||||||
Key? key,
|
|
||||||
required this.themeOption,
|
|
||||||
required this.isSelected,
|
|
||||||
required this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
width: 80,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
elevation: isSelected ? 4 : 1,
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
onTap: onTap,
|
|
||||||
child: AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 250),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
padding: const EdgeInsets.all(6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: isSelected ? themeOption.brand : Colors.transparent,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 80,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
CustomPaint(
|
|
||||||
painter: RedWavePainter(themeOption.brand, 0.15)),
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Hello, User!",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: themeOption.primary,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
SizedBox(
|
|
||||||
height: 18,
|
|
||||||
child: ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: themeOption.button,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8, vertical: 0),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
elevation: 1,
|
|
||||||
textStyle: const TextStyle(fontSize: 10),
|
|
||||||
),
|
|
||||||
onPressed: () {},
|
|
||||||
child: const Text("Welcome"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
|
||||||
themeOption.label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -24,8 +24,8 @@ class AttendanceActionColors {
|
|||||||
ButtonActions.rejected: Colors.orange,
|
ButtonActions.rejected: Colors.orange,
|
||||||
ButtonActions.approved: Colors.green,
|
ButtonActions.approved: Colors.green,
|
||||||
ButtonActions.requested: Colors.yellow,
|
ButtonActions.requested: Colors.yellow,
|
||||||
ButtonActions.approve: Colors.green,
|
ButtonActions.approve: Colors.blueAccent,
|
||||||
ButtonActions.reject: Colors.red,
|
ButtonActions.reject: Colors.pink,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ class AttendanceButtonHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Color getprimary({
|
static Color getButtonColor({
|
||||||
required bool isYesterday,
|
required bool isYesterday,
|
||||||
required bool isTodayApproved,
|
required bool isTodayApproved,
|
||||||
required int activity,
|
required int activity,
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
|
|
||||||
|
|
||||||
class BaseBottomSheet extends StatefulWidget {
|
class BaseBottomSheet extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String? subtitle;
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final VoidCallback onCancel;
|
final VoidCallback onCancel;
|
||||||
final VoidCallback onSubmit;
|
final VoidCallback onSubmit;
|
||||||
final bool isSubmitting;
|
final bool isSubmitting;
|
||||||
final String submitText;
|
final String submitText;
|
||||||
final Color? submitColor;
|
final Color submitColor;
|
||||||
final IconData submitIcon;
|
final IconData submitIcon;
|
||||||
final bool showButtons;
|
final bool showButtons;
|
||||||
final Widget? bottomContent;
|
final Widget? bottomContent;
|
||||||
@ -22,26 +20,18 @@ class BaseBottomSheet extends StatefulWidget {
|
|||||||
required this.child,
|
required this.child,
|
||||||
required this.onCancel,
|
required this.onCancel,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
this.subtitle,
|
|
||||||
this.isSubmitting = false,
|
this.isSubmitting = false,
|
||||||
this.submitText = 'Submit',
|
this.submitText = 'Submit',
|
||||||
this.submitColor,
|
this.submitColor = Colors.indigo,
|
||||||
this.submitIcon = Icons.check_circle_outline,
|
this.submitIcon = Icons.check_circle_outline,
|
||||||
this.showButtons = true,
|
this.showButtons = true,
|
||||||
this.bottomContent,
|
this.bottomContent,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
State<BaseBottomSheet> createState() => _BaseBottomSheetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BaseBottomSheetState extends State<BaseBottomSheet> with UIMixin {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final effectiveSubmitColor =
|
|
||||||
widget.submitColor ?? contentTheme.primary;
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: mediaQuery.viewInsets,
|
padding: mediaQuery.viewInsets,
|
||||||
@ -60,50 +50,33 @@ class _BaseBottomSheetState extends State<BaseBottomSheet> with UIMixin {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
|
// 👈 prevents overlap with nav bar
|
||||||
top: false,
|
top: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
MySpacing.height(5),
|
MySpacing.height(5),
|
||||||
Center(
|
Container(
|
||||||
child: Container(
|
width: 40,
|
||||||
width: 40,
|
height: 5,
|
||||||
height: 5,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: Colors.grey.shade300,
|
||||||
color: Colors.grey.shade300,
|
borderRadius: BorderRadius.circular(10),
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
Center(
|
MyText.titleLarge(title, fontWeight: 700),
|
||||||
child: MyText.titleLarge(
|
|
||||||
widget.title,
|
|
||||||
fontWeight: 700,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.subtitle != null &&
|
|
||||||
widget.subtitle!.isNotEmpty) ...[
|
|
||||||
MySpacing.height(4),
|
|
||||||
MyText.bodySmall(
|
|
||||||
widget.subtitle!,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.grey[700],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
widget.child,
|
child,
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
if (widget.showButtons) ...[
|
if (showButtons) ...[
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: widget.onCancel,
|
onPressed: onCancel,
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
label: MyText.bodyMedium(
|
label: MyText.bodyMedium(
|
||||||
"Cancel",
|
"Cancel",
|
||||||
@ -115,40 +88,34 @@ class _BaseBottomSheetState extends State<BaseBottomSheet> with UIMixin {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed:
|
onPressed: isSubmitting ? null : onSubmit,
|
||||||
widget.isSubmitting ? null : widget.onSubmit,
|
icon: Icon(submitIcon, color: Colors.white),
|
||||||
icon:
|
|
||||||
Icon(widget.submitIcon, color: Colors.white),
|
|
||||||
label: MyText.bodyMedium(
|
label: MyText.bodyMedium(
|
||||||
widget.isSubmitting
|
isSubmitting ? "Submitting..." : submitText,
|
||||||
? "Submitting..."
|
|
||||||
: widget.submitText,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: effectiveSubmitColor,
|
backgroundColor: submitColor,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (widget.bottomContent != null) ...[
|
if (bottomContent != null) ...[
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
widget.bottomContent!,
|
bottomContent!,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class ContactPickerHelper {
|
class ContactPickerHelper {
|
||||||
static Future<String?> pickIndianPhoneNumber(BuildContext context) async {
|
static Future<String?> pickIndianPhoneNumber(BuildContext context) async {
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class DateTimeUtils {
|
class DateTimeUtils {
|
||||||
/// Default date format
|
|
||||||
static const String defaultFormat = 'dd MMM yyyy';
|
|
||||||
|
|
||||||
/// Converts a UTC datetime string to local time and formats it.
|
/// Converts a UTC datetime string to local time and formats it.
|
||||||
static String convertUtcToLocal(String utcTimeString,
|
static String convertUtcToLocal(String utcTimeString, {String format = 'dd-MM-yyyy'}) {
|
||||||
{String format = 'dd-MM-yyyy'}) {
|
|
||||||
try {
|
try {
|
||||||
|
logSafe('Received UTC string: $utcTimeString'); // 🔹 Log input
|
||||||
|
|
||||||
final parsed = DateTime.parse(utcTimeString);
|
final parsed = DateTime.parse(utcTimeString);
|
||||||
final utcDateTime = DateTime.utc(
|
final utcDateTime = DateTime.utc(
|
||||||
parsed.year,
|
parsed.year,
|
||||||
@ -21,8 +20,16 @@ class DateTimeUtils {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final localDateTime = utcDateTime.toLocal();
|
final localDateTime = utcDateTime.toLocal();
|
||||||
return _formatDateTime(localDateTime, format: format);
|
|
||||||
} catch (e) {
|
final formatted = _formatDateTime(localDateTime, format: format);
|
||||||
|
|
||||||
|
logSafe('Converted Local DateTime: $localDateTime'); // 🔹 Log raw local datetime
|
||||||
|
logSafe('Formatted Local DateTime: $formatted'); // 🔹 Log formatted string
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
logSafe('DateTime conversion failed: $e',
|
||||||
|
error: e, stackTrace: stackTrace);
|
||||||
return 'Invalid Date';
|
return 'Invalid Date';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -30,24 +37,17 @@ class DateTimeUtils {
|
|||||||
/// Public utility for formatting any DateTime.
|
/// Public utility for formatting any DateTime.
|
||||||
static String formatDate(DateTime date, String format) {
|
static String formatDate(DateTime date, String format) {
|
||||||
try {
|
try {
|
||||||
return DateFormat(format).format(date);
|
final formatted = DateFormat(format).format(date);
|
||||||
} catch (e) {
|
logSafe('Formatted DateTime ($date) => $formatted'); // 🔹 Log input/output
|
||||||
|
return formatted;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
logSafe('formatDate failed: $e', error: e, stackTrace: stackTrace);
|
||||||
return 'Invalid Date';
|
return 'Invalid Date';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a date string using the given format.
|
|
||||||
static DateTime? parseDate(String dateString, String format) {
|
|
||||||
try {
|
|
||||||
return DateFormat(format).parse(dateString);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Internal formatter with default format.
|
/// Internal formatter with default format.
|
||||||
static String _formatDateTime(DateTime dateTime,
|
static String _formatDateTime(DateTime dateTime, {String format = 'dd-MM-yyyy'}) {
|
||||||
{String format = 'dd-MM-yyyy'}) {
|
|
||||||
return DateFormat(format).format(dateTime);
|
return DateFormat(format).format(dateTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:on_field_work/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class LauncherUtils {
|
class LauncherUtils {
|
||||||
/// Launches the phone dialer with the provided phone number
|
/// Launches the phone dialer with the provided phone number
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:on_field_work/helpers/theme/admin_theme.dart';
|
import 'package:marco/helpers/theme/admin_theme.dart';
|
||||||
import 'package:on_field_work/helpers/theme/app_theme.dart';
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_dashed_divider.dart';
|
import 'package:marco/helpers/widgets/my_dashed_divider.dart';
|
||||||
import 'package:on_field_work/helpers/widgets/my_navigation_mixin.dart';
|
import 'package:marco/helpers/widgets/my_navigation_mixin.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
mixin UIMixin {
|
mixin UIMixin {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/// Contains all role, permission, and entity UUIDs used for access control across the application.
|
/// Contains all role and permission UUIDs used for access control across the application.
|
||||||
class Permissions {
|
class Permissions {
|
||||||
// ------------------- Project Management ------------------------------
|
// ------------------- Project Management ------------------------------
|
||||||
/// Permission to manage master data (like dropdowns, configurations)
|
/// Permission to manage master data (like dropdowns, configurations)
|
||||||
@ -25,16 +25,14 @@ class Permissions {
|
|||||||
|
|
||||||
// ------------------- Project Infrastructure --------------------------
|
// ------------------- Project Infrastructure --------------------------
|
||||||
/// Permission to manage project infrastructure (e.g., site details)
|
/// Permission to manage project infrastructure (e.g., site details)
|
||||||
static const String manageProjectInfra =
|
static const String manageProjectInfra = "cf2825ad-453b-46aa-91d9-27c124d63373";
|
||||||
"cf2825ad-453b-46aa-91d9-27c124d63373";
|
|
||||||
|
|
||||||
/// Permission to view infrastructure-related details
|
/// Permission to view infrastructure-related details
|
||||||
static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4";
|
static const String viewProjectInfra = "8d7cc6e3-9147-41f7-aaa7-fa507e450bd4";
|
||||||
|
|
||||||
// ------------------- Attendance Management ---------------------------
|
// ------------------- Attendance Management ---------------------------
|
||||||
/// Permission to regularize (edit/update) attendance records
|
/// Permission to regularize (edit/update) attendance records
|
||||||
static const String regularizeAttendance =
|
static const String regularizeAttendance = "57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
|
||||||
"57802c4a-00aa-4a1f-a048-fd2f70dd44b6";
|
|
||||||
|
|
||||||
// ------------------- Task Management ---------------------------------
|
// ------------------- Task Management ---------------------------------
|
||||||
/// Permission to create and manage tasks
|
/// Permission to create and manage tasks
|
||||||
@ -92,102 +90,5 @@ class Permissions {
|
|||||||
|
|
||||||
// ------------------- Application Roles -------------------------------
|
// ------------------- Application Roles -------------------------------
|
||||||
/// Application role ID for users with full expense management rights
|
/// Application role ID for users with full expense management rights
|
||||||
static const String expenseManagement =
|
static const String expenseManagement = "a4e25142-449b-4334-a6e5-22f70e4732d7";
|
||||||
"a4e25142-449b-4334-a6e5-22f70e4732d7";
|
|
||||||
|
|
||||||
// ------------------- Document Entities -------------------------------
|
|
||||||
/// Entity ID for project documents
|
|
||||||
static const String projectEntity = "c8fe7115-aa27-43bc-99f4-7b05fabe436e";
|
|
||||||
|
|
||||||
/// Entity ID for employee documents
|
|
||||||
static const String employeeEntity = "dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7";
|
|
||||||
|
|
||||||
// ------------------- Document Permissions ----------------------------
|
|
||||||
/// Permission to view documents
|
|
||||||
static const String viewDocument = "71189504-f1c8-4ca5-8db6-810497be2854";
|
|
||||||
|
|
||||||
/// Permission to upload documents
|
|
||||||
static const String uploadDocument = "3f6d1f67-6fa5-4b7c-b17b-018d4fe4aab8";
|
|
||||||
|
|
||||||
/// Permission to modify documents
|
|
||||||
static const String modifyDocument = "c423fd81-6273-4b9d-bb5e-76a0fb343833";
|
|
||||||
|
|
||||||
/// Permission to delete documents
|
|
||||||
static const String deleteDocument = "40863a13-5a66-469d-9b48-135bc5dbf486";
|
|
||||||
|
|
||||||
/// Permission to download documents
|
|
||||||
static const String downloadDocument = "404373d0-860f-490e-a575-1c086ffbce1d";
|
|
||||||
|
|
||||||
/// Permission to verify documents
|
|
||||||
static const String verifyDocument = "13a1f30f-38d1-41bf-8e7a-b75189aab8e0";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains constants for menu item IDs fetched from the sidebar menu API.
|
|
||||||
class MenuItems {
|
|
||||||
/// Dashboard menu
|
|
||||||
static const String dashboard = "29e03eda-03e8-4714-92fa-67ae0dc53202";
|
|
||||||
|
|
||||||
/// Daily Task Planning menu
|
|
||||||
static const String dailyTaskPlanning =
|
|
||||||
"77ac5205-f823-442e-b9e4-2420d658aa02";
|
|
||||||
|
|
||||||
/// Daily Progress Report menu
|
|
||||||
static const String dailyProgressReport =
|
|
||||||
"299e3cf5-d034-4403-b4a1-ea46d2714832";
|
|
||||||
|
|
||||||
/// Employees menu
|
|
||||||
static const String employees = "78f0206d-c6cc-44d0-832a-2031ed203018";
|
|
||||||
|
|
||||||
/// Attendance menu
|
|
||||||
static const String attendance = "2f212030-f36b-456c-8e7c-11f00f9ba42b";
|
|
||||||
|
|
||||||
/// Directory menu
|
|
||||||
static const String directory = "31bc367b-7c58-4604-95eb-da059a384103";
|
|
||||||
|
|
||||||
/// Expense & Reimbursement menu
|
|
||||||
static const String expenseReimbursement =
|
|
||||||
"0f0dc1a7-1aca-4cdb-9d7a-8a769ce40728";
|
|
||||||
|
|
||||||
/// Payment Requests menu
|
|
||||||
static const String paymentRequests = "b350a59f-2372-4f68-8dcf-f7cfc44523ca";
|
|
||||||
|
|
||||||
/// Advance Payment Statements menu
|
|
||||||
static const String advancePaymentStatements =
|
|
||||||
"e0251cc1-e6d9-417a-9c76-489cc4b6c347";
|
|
||||||
|
|
||||||
/// Finance menu
|
|
||||||
static const String finance = "5ac409dd-bbe0-4d56-bcb9-229bd3a6353c";
|
|
||||||
|
|
||||||
/// Documents menu
|
|
||||||
static const String documents = "92d2cc39-9e6a-46b2-ae50-84fbf83c95d3";
|
|
||||||
|
|
||||||
/// Service Projects
|
|
||||||
static const String serviceProjects = "7faddfe7-994b-4712-91c2-32ba44129d9b";
|
|
||||||
|
|
||||||
/// Infrastructure Projects
|
|
||||||
static const String infraProjects = "5fab4b88-c9a0-417b-aca2-130980fdb0cf";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains all job status IDs used across the application.
|
|
||||||
class JobStatus {
|
|
||||||
/// Level 1 - New
|
|
||||||
static const String newStatus = "32d76a02-8f44-4aa0-9b66-c3716c45a918";
|
|
||||||
|
|
||||||
/// Level 2 - Assigned
|
|
||||||
static const String assigned = "cfa1886d-055f-4ded-84c6-42a2a8a14a66";
|
|
||||||
|
|
||||||
/// Level 3 - In Progress
|
|
||||||
static const String inProgress = "5a6873a5-fed7-4745-a52f-8f61bf3bd72d";
|
|
||||||
|
|
||||||
/// Level 4 - Work Done
|
|
||||||
static const String workDone = "aab71020-2fb8-44d9-9430-c9a7e9bf33b0";
|
|
||||||
|
|
||||||
/// Level 5 - Review Done
|
|
||||||
static const String reviewDone = "ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7";
|
|
||||||
|
|
||||||
/// Level 6 - Closed
|
|
||||||
static const String closed = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69";
|
|
||||||
|
|
||||||
/// Level 7 - On Hold
|
|
||||||
static const String onHold = "75a0c8b8-9c6a-41af-80bf-b35bab722eb2";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:marco/helpers/extensions/date_time_extension.dart';
|
||||||
import 'package:on_field_work/helpers/extensions/date_time_extension.dart';
|
|
||||||
|
|
||||||
class Utils {
|
class Utils {
|
||||||
static getDateStringFromDateTime(DateTime dateTime,
|
static getDateStringFromDateTime(DateTime dateTime,
|
||||||
@ -45,10 +44,6 @@ class Utils {
|
|||||||
return "$hour:$minute${showSecond ? ":" : ""}$second$meridian";
|
return "$hour:$minute${showSecond ? ":" : ""}$second$meridian";
|
||||||
}
|
}
|
||||||
|
|
||||||
static String formatDate(DateTime date) {
|
|
||||||
return DateFormat('d MMM yyyy').format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getDateTimeStringFromDateTime(DateTime dateTime,
|
static String getDateTimeStringFromDateTime(DateTime dateTime,
|
||||||
{bool showSecond = true,
|
{bool showSecond = true,
|
||||||
bool showDate = true,
|
bool showDate = true,
|
||||||
@ -81,12 +76,4 @@ class Utils {
|
|||||||
return "${b.toStringAsFixed(2)} Bytes";
|
return "${b.toStringAsFixed(2)} Bytes";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String formatCurrency(num amount,
|
|
||||||
{String currency = "INR", String locale = "en_US"}) {
|
|
||||||
// Use en_US for standard K, M, B formatting
|
|
||||||
final symbol = NumberFormat.simpleCurrency(name: currency).currencySymbol;
|
|
||||||
final formatter = NumberFormat.compact(locale: 'en_US');
|
|
||||||
return "$symbol${formatter.format(amount)}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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