import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/finance/payment_request_details_model.dart'; import 'package:marco/model/expense/payment_types_model.dart'; import 'package:marco/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'; class PaymentRequestDetailController extends GetxController { final Rx paymentRequest = Rx(null); final RxBool isLoading = false.obs; final RxString errorMessage = ''.obs; final RxList paymentModes = [].obs; // Employee selection final Rx selectedReimbursedBy = Rx(null); final RxList allEmployees = [].obs; final RxList employeeSearchResults = [].obs; final TextEditingController employeeSearchController = TextEditingController(); final RxBool isSearchingEmployees = false.obs; // Attachments final RxList attachments = [].obs; final RxList> existingAttachments = >[].obs; final isProcessingAttachment = false.obs; // Payment mode final selectedPaymentMode = Rxn(); // 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 _apiCallWrapper( Future 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 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 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(File.new), ); } } catch (e) { _errorSnackbar("Attachment error: $e"); } } void removeAttachment(File file) => attachments.remove(file); Future 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 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 _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 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 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 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 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) { showAppSnackbar( title: 'Success', message: 'Payment submitted successfully', type: SnackbarType.success); await fetchPaymentRequestDetail(); } else { showAppSnackbar( title: 'Error', message: 'Failed to update status. Please try again.', type: SnackbarType.error); } return success; } catch (e) { showAppSnackbar( title: 'Error', message: 'Something went wrong: $e', type: SnackbarType.error); 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 submitExpense() async { if (selectedPaymentMode.value == null) return false; isSubmitting.value = true; try { // Prepare attachments with all required fields final attachmentsPayload = 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(); // Call API return await ApiService.createExpenseForPRApi( paymentModeId: selectedPaymentMode.value!.id, location: locationController.text, gstNumber: gstNumberController.text, paymentRequestId: _requestId, billAttachments: attachmentsPayload, ); } finally { isSubmitting.value = false; } } }