marco.pms.mobileapp/lib/controller/expense/add_expense_controller.dart

334 lines
11 KiB
Dart

import 'dart:io';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:file_picker/file_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:geocoding/geocoding.dart';
import 'package:mime/mime.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/expense/payment_types_model.dart';
import 'package:marco/model/expense/expense_type_model.dart';
import 'package:marco/model/expense/expense_status_model.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/controller/expense/expense_screen_controller.dart';
class AddExpenseController extends GetxController {
// === Text Controllers ===
final amountController = TextEditingController();
final descriptionController = TextEditingController();
final supplierController = TextEditingController();
final transactionIdController = TextEditingController();
final gstController = TextEditingController();
final locationController = TextEditingController();
final ExpenseController expenseController = Get.find<ExpenseController>();
// === Project Mapping ===
final RxMap<String, String> projectsMap = <String, String>{}.obs;
// === Selected Models ===
final Rx<PaymentModeModel?> selectedPaymentMode = Rx<PaymentModeModel?>(null);
final Rx<ExpenseTypeModel?> selectedExpenseType = Rx<ExpenseTypeModel?>(null);
final Rx<ExpenseStatusModel?> selectedExpenseStatus =
Rx<ExpenseStatusModel?>(null);
final RxString selectedProject = ''.obs;
final Rx<EmployeeModel?> selectedPaidBy = Rx<EmployeeModel?>(null);
// === States ===
final RxBool preApproved = false.obs;
final RxBool isFetchingLocation = false.obs;
final Rx<DateTime?> selectedTransactionDate = Rx<DateTime?>(null);
// === Master Data ===
final RxList<String> projects = <String>[].obs;
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
final RxList<ExpenseStatusModel> expenseStatuses = <ExpenseStatusModel>[].obs;
final RxList<String> globalProjects = <String>[].obs;
// === Attachments ===
final RxList<File> attachments = <File>[].obs;
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs;
final RxBool isSubmitting = false.obs;
@override
void onInit() {
super.onInit();
fetchMasterData();
fetchGlobalProjects();
fetchAllEmployees();
}
@override
void onClose() {
amountController.dispose();
descriptionController.dispose();
supplierController.dispose();
transactionIdController.dispose();
gstController.dispose();
locationController.dispose();
super.onClose();
}
// === Pick Attachments ===
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 && result.paths.isNotEmpty) {
final newFiles =
result.paths.whereType<String>().map((e) => File(e)).toList();
attachments.addAll(newFiles);
}
} catch (e) {
Get.snackbar("Error", "Failed to pick attachments: $e");
}
}
void removeAttachment(File file) {
attachments.remove(file);
}
// === Fetch Master Data ===
Future<void> fetchMasterData() async {
try {
final expenseTypesData = await ApiService.getMasterExpenseTypes();
if (expenseTypesData is List) {
expenseTypes.value =
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
}
final paymentModesData = await ApiService.getMasterPaymentModes();
if (paymentModesData is List) {
paymentModes.value =
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
}
final expenseStatusData = await ApiService.getMasterExpenseStatus();
if (expenseStatusData is List) {
expenseStatuses.value = expenseStatusData
.map((e) => ExpenseStatusModel.fromJson(e))
.toList();
}
} catch (e) {
Get.snackbar("Error", "Failed to fetch master data: $e");
}
}
// === Fetch Current Location ===
Future<void> fetchCurrentLocation() async {
isFetchingLocation.value = true;
try {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
Get.snackbar(
"Error", "Location permission denied. Enable in settings.");
return;
}
}
if (!await Geolocator.isLocationServiceEnabled()) {
Get.snackbar("Error", "Location services are disabled. Enable them.");
return;
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
final placemarks = await placemarkFromCoordinates(
position.latitude,
position.longitude,
);
if (placemarks.isNotEmpty) {
final place = placemarks.first;
final addressParts = [
place.name,
place.street,
place.subLocality,
place.locality,
place.administrativeArea,
place.country,
].where((part) => part != null && part.isNotEmpty).toList();
locationController.text = addressParts.join(", ");
} else {
locationController.text = "${position.latitude}, ${position.longitude}";
}
} catch (e) {
Get.snackbar("Error", "Error fetching location: $e");
} finally {
isFetchingLocation.value = false;
}
}
// === Submit Expense ===
// === Submit Expense ===
Future<void> submitExpense() async {
if (isSubmitting.value) return; // Prevent multiple taps
isSubmitting.value = true;
try {
// === Validation ===
List<String> missingFields = [];
if (selectedProject.value.isEmpty) missingFields.add("Project");
if (selectedExpenseType.value == null) missingFields.add("Expense Type");
if (selectedPaymentMode.value == null) missingFields.add("Payment Mode");
if (selectedPaidBy.value == null) missingFields.add("Paid By");
if (amountController.text.isEmpty) missingFields.add("Amount");
if (supplierController.text.isEmpty) missingFields.add("Supplier Name");
if (descriptionController.text.isEmpty) missingFields.add("Description");
if (attachments.isEmpty) missingFields.add("Attachments");
if (missingFields.isNotEmpty) {
showAppSnackbar(
title: "Missing Fields",
message: "Please provide: ${missingFields.join(', ')}.",
type: SnackbarType.error,
);
return;
}
final double? amount = double.tryParse(amountController.text);
if (amount == null) {
showAppSnackbar(
title: "Error",
message: "Please enter a valid amount.",
type: SnackbarType.error,
);
return;
}
final projectId = projectsMap[selectedProject.value];
if (projectId == null) {
showAppSnackbar(
title: "Error",
message: "Invalid project selection.",
type: SnackbarType.error,
);
return;
}
// === Convert Attachments ===
final attachmentData = await Future.wait(attachments.map((file) async {
final bytes = await file.readAsBytes();
final base64String = base64Encode(bytes);
final mimeType =
lookupMimeType(file.path) ?? 'application/octet-stream';
final fileSize = await file.length();
return {
"fileName": file.path.split('/').last,
"base64Data": base64String,
"contentType": mimeType,
"fileSize": fileSize,
"description": "",
};
}).toList());
// === API Call ===
final success = await ApiService.createExpenseApi(
projectId: projectId,
expensesTypeId: selectedExpenseType.value!.id,
paymentModeId: selectedPaymentMode.value!.id,
paidById: selectedPaidBy.value?.id ?? "",
transactionDate:
(selectedTransactionDate.value ?? DateTime.now()).toUtc(),
transactionId: transactionIdController.text,
description: descriptionController.text,
location: locationController.text,
supplerName: supplierController.text,
amount: amount,
noOfPersons: 0,
billAttachments: attachmentData,
);
if (success) {
await Get.find<ExpenseController>().fetchExpenses(); // 🔄 Refresh list
Get.back();
showAppSnackbar(
title: "Success",
message: "Expense created successfully!",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message: "Failed to create expense. Try again.",
type: SnackbarType.error,
);
}
} catch (e) {
showAppSnackbar(
title: "Error",
message: "Something went wrong: $e",
type: SnackbarType.error,
);
} finally {
isSubmitting.value = false;
}
}
// === Fetch Projects ===
Future<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
if (response != null) {
final names = <String>[];
for (var item in response) {
final name = item['name']?.toString().trim();
final id = item['id']?.toString().trim();
if (name != null && id != null && name.isNotEmpty) {
projectsMap[name] = id;
names.add(name);
}
}
globalProjects.assignAll(names);
logSafe("Fetched ${names.length} global projects");
}
} catch (e) {
logSafe("Failed to fetch global projects: $e", level: LogLevel.error);
}
}
// === Fetch All Employees ===
Future<void> fetchAllEmployees() async {
isLoading.value = true;
try {
final response = await ApiService.getAllEmployees();
if (response != null && response.isNotEmpty) {
allEmployees
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
logSafe(
"All Employees fetched for Manage Bucket: ${allEmployees.length}",
level: LogLevel.info,
);
} else {
allEmployees.clear();
logSafe("No employees found for Manage Bucket.",
level: LogLevel.warning);
}
} catch (e) {
allEmployees.clear();
logSafe("Error fetching employees in Manage Bucket",
level: LogLevel.error, error: e);
}
isLoading.value = false;
update();
}
}