import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; import 'package:intl/intl.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_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 noOfPersonsController = TextEditingController(); // State final isLoading = false.obs; final isSubmitting = false.obs; final isFetchingLocation = false.obs; final isEditMode = false.obs; // Dropdown Selections final selectedPaymentMode = Rx(null); final selectedExpenseType = Rx(null); final selectedPaidBy = Rx(null); final selectedProject = ''.obs; final selectedTransactionDate = Rx(null); // Data Lists final attachments = [].obs; final globalProjects = [].obs; final projectsMap = {}.obs; final expenseTypes = [].obs; final paymentModes = [].obs; final allEmployees = [].obs; final existingAttachments = >[].obs; // Editing String? editingExpenseId; final 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(); } // ---------- Form Population for Edit ---------- void populateFieldsForEdit(Map data) { isEditMode.value = true; editingExpenseId = data['id']; // Basic fields selectedProject.value = data['projectName'] ?? ''; amountController.text = data['amount']?.toString() ?? ''; supplierController.text = data['supplerName'] ?? ''; descriptionController.text = data['description'] ?? ''; transactionIdController.text = data['transactionId'] ?? ''; locationController.text = data['location'] ?? ''; // Transaction Date if (data['transactionDate'] != null) { try { final parsedDate = DateTime.parse(data['transactionDate']); selectedTransactionDate.value = parsedDate; transactionDateController.text = DateFormat('dd-MM-yyyy').format(parsedDate); } catch (e) { logSafe('Error parsing transactionDate: $e', level: LogLevel.warning); selectedTransactionDate.value = null; transactionDateController.clear(); } } else { selectedTransactionDate.value = null; transactionDateController.clear(); } // No of Persons noOfPersonsController.text = (data['noOfPersons'] ?? 0).toString(); // Select Expense Type and Payment Mode by matching IDs selectedExpenseType.value = expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']); selectedPaymentMode.value = paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']); // Select Paid By employee matching id (case insensitive, trimmed) final paidById = data['paidById']?.toString().trim().toLowerCase() ?? ''; selectedPaidBy.value = allEmployees .firstWhereOrNull((e) => e.id.trim().toLowerCase() == paidById); if (selectedPaidBy.value == null && paidById.isNotEmpty) { logSafe('⚠️ Could not match paidById: "$paidById"', level: LogLevel.warning); for (var emp in allEmployees) { logSafe( 'Employee ID: "${emp.id.trim().toLowerCase()}", Name: "${emp.name}"', level: LogLevel.warning); } } // Populate existing attachments if present existingAttachments.clear(); if (data['attachments'] != null && data['attachments'] is List) { existingAttachments .addAll(List>.from(data['attachments'])); } _logPrefilledData(); } void _logPrefilledData() { logSafe('--- Prefilled Expense Data ---', level: LogLevel.info); logSafe('ID: $editingExpenseId', level: LogLevel.info); logSafe('Project: ${selectedProject.value}', level: LogLevel.info); logSafe('Amount: ${amountController.text}', level: LogLevel.info); logSafe('Supplier: ${supplierController.text}', level: LogLevel.info); logSafe('Description: ${descriptionController.text}', level: LogLevel.info); logSafe('Transaction ID: ${transactionIdController.text}', level: LogLevel.info); logSafe('Location: ${locationController.text}', level: LogLevel.info); logSafe('Transaction Date: ${transactionDateController.text}', level: LogLevel.info); logSafe('No. of Persons: ${noOfPersonsController.text}', level: LogLevel.info); logSafe('Expense Type: ${selectedExpenseType.value?.name}', level: LogLevel.info); logSafe('Payment Mode: ${selectedPaymentMode.value?.name}', level: LogLevel.info); logSafe('Paid By: ${selectedPaidBy.value?.name}', level: LogLevel.info); logSafe('Attachments: ${attachments.length}', level: LogLevel.info); logSafe('Existing Attachments: ${existingAttachments.length}', level: LogLevel.info); } // ---------- Form Actions ---------- Future pickTransactionDate(BuildContext context) async { final picked = await showDatePicker( context: context, initialDate: selectedTransactionDate.value ?? DateTime.now(), firstDate: DateTime(DateTime.now().year - 5), lastDate: DateTime.now(), ); if (picked != null) { selectedTransactionDate.value = picked; transactionDateController.text = DateFormat('dd-MM-yyyy').format(picked); } } Future loadMasterData() async { await Future.wait([ fetchMasterData(), fetchGlobalProjects(), fetchAllEmployees(), ]); } 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) { attachments.addAll( result.paths.whereType().map((path) => File(path)), ); } } catch (e) { showAppSnackbar( title: "Error", message: "Attachment error: $e", type: SnackbarType.error, ); } } void removeAttachment(File file) => attachments.remove(file); Future fetchCurrentLocation() async { isFetchingLocation.value = true; try { var permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { showAppSnackbar( title: "Error", message: "Location permission denied.", type: SnackbarType.error, ); return; } } if (!await Geolocator.isLocationServiceEnabled()) { showAppSnackbar( title: "Error", message: "Location service disabled.", type: SnackbarType.error, ); return; } final position = await Geolocator.getCurrentPosition(); final placemarks = await placemarkFromCoordinates(position.latitude, position.longitude); if (placemarks.isNotEmpty) { final place = placemarks.first; final address = [ place.name, place.street, 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) { showAppSnackbar( title: "Error", message: "Location error: $e", type: SnackbarType.error, ); } finally { isFetchingLocation.value = false; } } // ---------- Submission ---------- Future submitOrUpdateExpense() async { if (isSubmitting.value) return; isSubmitting.value = true; try { final validation = validateForm(); if (validation.isNotEmpty) { showAppSnackbar( title: "Missing Fields", message: validation, type: SnackbarType.error, ); return; } final payload = await _buildExpensePayload(); 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) { await expenseController.fetchExpenses(); Get.back(); showAppSnackbar( title: "Success", message: "Expense ${isEditMode.value ? 'updated' : 'created'} successfully!", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", message: "Operation failed. Try again.", type: SnackbarType.error, ); } } catch (e) { showAppSnackbar( title: "Error", message: "Unexpected error: $e", type: SnackbarType.error, ); } finally { isSubmitting.value = false; } } Future> _buildExpensePayload() async { final amount = double.parse(amountController.text.trim()); final projectId = projectsMap[selectedProject.value]!; final selectedDate = selectedTransactionDate.value?.toUtc() ?? DateTime.now().toUtc(); final existingAttachmentPayloads = existingAttachments .map((e) => { "fileName": e['fileName'], "contentType": e['contentType'], "fileSize": 0, // optional or populate if known "description": "", "url": e['url'], // custom field if your backend accepts }) .toList(); final newAttachmentPayloads = 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": "", }; })); final billAttachments = [ ...existingAttachmentPayloads, ...newAttachmentPayloads ]; final Map payload = { "projectId": projectId, "expensesTypeId": selectedExpenseType.value!.id, "paymentModeId": selectedPaymentMode.value!.id, "paidById": selectedPaidBy.value!.id, "transactionDate": selectedDate.toIso8601String(), "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, }; // ✅ Add expense ID if in edit mode if (isEditMode.value && editingExpenseId != null) { payload['id'] = editingExpenseId; } return payload; } String validateForm() { final 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.trim().isEmpty) missing.add("Amount"); if (supplierController.text.trim().isEmpty) missing.add("Supplier Name"); if (descriptionController.text.trim().isEmpty) missing.add("Description"); if (!isEditMode.value && attachments.isEmpty) missing.add("Attachments"); final amount = double.tryParse(amountController.text.trim()); if (amount == null) missing.add("Valid Amount"); final selectedDate = selectedTransactionDate.value; if (selectedDate != null && selectedDate.isAfter(DateTime.now())) { missing.add("Valid Transaction Date"); } return missing.isEmpty ? '' : "Please provide: ${missing.join(', ')}."; } // ---------- Data Fetching ---------- Future fetchMasterData() async { try { final types = await ApiService.getMasterExpenseTypes(); final modes = await ApiService.getMasterPaymentModes(); if (types is List) { expenseTypes.value = types.map((e) => ExpenseTypeModel.fromJson(e)).toList(); } if (modes is List) { paymentModes.value = modes.map((e) => PaymentModeModel.fromJson(e)).toList(); } } catch (e) { showAppSnackbar( title: "Error", message: "Failed to fetch master data", type: SnackbarType.error, ); } } 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) { projectsMap[name] = id; names.add(name); } } globalProjects.assignAll(names); } } catch (e) { logSafe("Error fetching projects: $e", level: LogLevel.error); } } Future fetchAllEmployees() async { isLoading.value = true; try { final response = await ApiService.getAllEmployees(); if (response != null) { allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e))); } } catch (e) { logSafe("Error fetching employees: $e", level: LogLevel.error); } finally { isLoading.value = false; } } }