// 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 = [].obs; final categories = [].obs; final currencies = [].obs; final globalProjects = >[].obs; // Selected Values final selectedProject = Rx?>(null); final selectedCategory = Rx(null); final selectedPayee = Rx(null); final selectedCurrency = Rx(null); final isAdvancePayment = false.obs; final selectedDueDate = Rx(null); // Text Controllers final titleController = TextEditingController(); final dueDateController = TextEditingController(); final amountController = TextEditingController(); final descriptionController = TextEditingController(); final removedAttachments = >[].obs; // Attachments final attachments = [].obs; final existingAttachments = >[].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 fetchAllMasterData() async { await Future.wait([ _fetchData( payees, ApiService.getExpensePaymentRequestPayeeApi, isLoadingPayees), _fetchData(categories, ApiService.getMasterExpenseCategoriesApi, isLoadingCategories), _fetchData( currencies, ApiService.getMasterCurrenciesApi, isLoadingCurrencies), ]); } /// Generic fetch handler Future _fetchData( RxList list, Future 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 fetchGlobalProjects() async { try { final response = await ApiService.getGlobalProjects(); globalProjects.value = (response ?? []) .map>((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 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 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().map(File.new)); } attachments.refresh(); } catch (e) { _errorSnackbar("Attachment error: $e"); } finally { isProcessingAttachment.value = false; } } Future 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 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 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>> 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 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 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(); } }