import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:mime/mime.dart'; import 'package:marco/controller/expense/expense_screen_controller.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/employee_model.dart'; import 'package:marco/model/expense/expense_status_model.dart'; import 'package:marco/model/expense/expense_type_model.dart'; import 'package:marco/model/expense/payment_types_model.dart'; class AddExpenseController extends GetxController { // === Text Controllers === final amountController = TextEditingController(); final descriptionController = TextEditingController(); final supplierController = TextEditingController(); final transactionIdController = TextEditingController(); final gstController = TextEditingController(); final locationController = TextEditingController(); final transactionDateController = TextEditingController(); final TextEditingController noOfPersonsController = TextEditingController(); // === State Controllers === final RxBool isLoading = false.obs; final RxBool isSubmitting = false.obs; final RxBool isFetchingLocation = false.obs; // === Selected Models === final Rx selectedPaymentMode = Rx(null); final Rx selectedExpenseType = Rx(null); final Rx selectedExpenseStatus = Rx(null); final Rx selectedPaidBy = Rx(null); final RxString selectedProject = ''.obs; final Rx selectedTransactionDate = Rx(null); // === Lists === final RxList attachments = [].obs; final RxList globalProjects = [].obs; final RxList projects = [].obs; final RxList expenseTypes = [].obs; final RxList paymentModes = [].obs; final RxList expenseStatuses = [].obs; final RxList allEmployees = [].obs; // === Mappings === final RxMap projectsMap = {}.obs; final ExpenseController expenseController = Get.find(); @override void onInit() { super.onInit(); fetchMasterData(); fetchGlobalProjects(); fetchAllEmployees(); } @override void onClose() { amountController.dispose(); descriptionController.dispose(); supplierController.dispose(); transactionIdController.dispose(); gstController.dispose(); locationController.dispose(); transactionDateController.dispose(); noOfPersonsController.dispose(); super.onClose(); } // === Pick Attachments === Future 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 files = result.paths.whereType().map((e) => File(e)).toList(); attachments.addAll(files); } } catch (e) { Get.snackbar("Error", "Failed to pick attachments: $e"); } } void removeAttachment(File file) { attachments.remove(file); } // === Date Picker === void pickTransactionDate(BuildContext context) async { final now = DateTime.now(); final picked = await showDatePicker( context: context, initialDate: selectedTransactionDate.value ?? now, firstDate: DateTime(now.year - 5), lastDate: now, // ✅ Restrict future dates ); if (picked != null) { selectedTransactionDate.value = picked; transactionDateController.text = "${picked.day.toString().padLeft(2, '0')}-${picked.month.toString().padLeft(2, '0')}-${picked.year}"; } } // === Fetch Current Location === Future 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 address = [ place.name, place.street, place.subLocality, place.locality, place.administrativeArea, place.country, ].where((e) => e != null && e.isNotEmpty).join(", "); locationController.text = address; } else { locationController.text = "${position.latitude}, ${position.longitude}"; } } catch (e) { Get.snackbar("Error", "Error fetching location: $e"); } finally { isFetchingLocation.value = false; } } // === Submit Expense === Future submitExpense() async { if (isSubmitting.value) return; isSubmitting.value = true; try { List missing = []; if (selectedProject.value.isEmpty) missing.add("Project"); if (selectedExpenseType.value == null) missing.add("Expense Type"); if (selectedPaymentMode.value == null) missing.add("Payment Mode"); if (selectedPaidBy.value == null) missing.add("Paid By"); if (amountController.text.isEmpty) missing.add("Amount"); if (supplierController.text.isEmpty) missing.add("Supplier Name"); if (descriptionController.text.isEmpty) missing.add("Description"); if (attachments.isEmpty) missing.add("Attachments"); if (missing.isNotEmpty) { showAppSnackbar( title: "Missing Fields", message: "Please provide: ${missing.join(', ')}.", type: SnackbarType.error, ); return; } final amount = double.tryParse(amountController.text); if (amount == null) { showAppSnackbar( title: "Error", message: "Please enter a valid amount.", type: SnackbarType.error, ); return; } final selectedDate = selectedTransactionDate.value ?? DateTime.now(); if (selectedDate.isAfter(DateTime.now())) { showAppSnackbar( title: "Invalid Date", message: "Transaction date cannot be in the future.", type: SnackbarType.error, ); return; } final projectId = projectsMap[selectedProject.value]; if (projectId == null) { showAppSnackbar( title: "Error", message: "Invalid project selected.", type: SnackbarType.error, ); return; } final billAttachments = await Future.wait(attachments.map((file) async { final bytes = await file.readAsBytes(); final base64 = base64Encode(bytes); final mime = lookupMimeType(file.path) ?? 'application/octet-stream'; final size = await file.length(); return { "fileName": file.path.split('/').last, "base64Data": base64, "contentType": mime, "fileSize": size, "description": "", }; })); 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: selectedExpenseType.value?.noOfPersonsRequired == true ? int.tryParse(noOfPersonsController.text.trim()) ?? 0 : 0, billAttachments: billAttachments, ); if (success) { await expenseController.fetchExpenses(); 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 Data Methods === Future fetchMasterData() async { try { final expenseTypesData = await ApiService.getMasterExpenseTypes(); final paymentModesData = await ApiService.getMasterPaymentModes(); final expenseStatusData = await ApiService.getMasterExpenseStatus(); if (expenseTypesData is List) { expenseTypes.value = expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList(); } if (paymentModesData is List) { paymentModes.value = paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList(); } if (expenseStatusData is List) { expenseStatuses.value = expenseStatusData .map((e) => ExpenseStatusModel.fromJson(e)) .toList(); } } catch (e) { Get.snackbar("Error", "Failed to fetch master data: $e"); } } Future fetchGlobalProjects() async { try { final response = await ApiService.getGlobalProjects(); if (response != null) { final names = []; 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); } } Future fetchAllEmployees() async { isLoading.value = true; try { final response = await ApiService.getAllEmployees(); if (response != null && response.isNotEmpty) { allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e))); logSafe("All Employees fetched: ${allEmployees.length}", level: LogLevel.info); } else { allEmployees.clear(); logSafe("No employees found.", level: LogLevel.warning); } } catch (e) { allEmployees.clear(); logSafe("Error fetching employees", level: LogLevel.error, error: e); } finally { isLoading.value = false; update(); } } }