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: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/helpers/widgets/time_stamp_image_helper.dart'; import 'package:marco/model/finance/expense_category_model.dart'; import 'package:marco/model/finance/currency_list_model.dart'; class PaymentRequestController extends GetxController { // ───────────────────────────────────────────── // 🔹 Loading States // ───────────────────────────────────────────── final isLoadingPayees = false.obs; final isLoadingCategories = false.obs; final isLoadingCurrencies = false.obs; final isProcessingAttachment = false.obs; // ───────────────────────────────────────────── // 🔹 Data Lists // ───────────────────────────────────────────── final payees = [].obs; final categories = [].obs; final currencies = [].obs; final globalProjects = [].obs; // ───────────────────────────────────────────── // 🔹 Selected Values // ───────────────────────────────────────────── final selectedCategory = Rx(null); final selectedPayee = ''.obs; final selectedCurrency = Rx(null); final selectedProject = ''.obs; final isAdvancePayment = false.obs; // ───────────────────────────────────────────── // 🔹 Text Controllers // ───────────────────────────────────────────── final titleController = TextEditingController(); final dueDateController = TextEditingController(); final amountController = TextEditingController(); final descriptionController = TextEditingController(); // ───────────────────────────────────────────── // 🔹 Attachments // ───────────────────────────────────────────── final attachments = [].obs; final existingAttachments = >[].obs; final ImagePicker _picker = ImagePicker(); // ───────────────────────────────────────────── // 🔹 Lifecycle // ───────────────────────────────────────────── @override void onInit() { super.onInit(); fetchAllMasterData(); fetchGlobalProjects(); } @override void onClose() { titleController.dispose(); dueDateController.dispose(); amountController.dispose(); descriptionController.dispose(); super.onClose(); } // ───────────────────────────────────────────── // 🔹 Master Data Fetch // ───────────────────────────────────────────── Future fetchAllMasterData() async { await Future.wait([ fetchPayees(), fetchExpenseCategories(), fetchCurrencies(), ]); } Future fetchPayees() async { try { isLoadingPayees.value = true; final response = await ApiService.getExpensePaymentRequestPayeeApi(); if (response != null && response.data.isNotEmpty) { payees.value = response.data; } else { payees.clear(); } } catch (e) { logSafe("Error fetching payees: $e", level: LogLevel.error); } finally { isLoadingPayees.value = false; } } Future fetchExpenseCategories() async { try { isLoadingCategories.value = true; final response = await ApiService.getMasterExpenseCategoriesApi(); if (response != null && response.data.isNotEmpty) { categories.value = response.data; } else { categories.clear(); } } catch (e) { logSafe("Error fetching categories: $e", level: LogLevel.error); } finally { isLoadingCategories.value = false; } } Future fetchCurrencies() async { try { isLoadingCurrencies.value = true; final response = await ApiService.getMasterCurrenciesApi(); if (response != null && response.data.isNotEmpty) { currencies.value = response.data; } else { currencies.clear(); } } catch (e) { logSafe("Error fetching currencies: $e", level: LogLevel.error); } finally { isLoadingCurrencies.value = false; } } Future fetchGlobalProjects() async { try { final response = await ApiService.getGlobalProjects(); if (response != null && response.isNotEmpty) { globalProjects.value = response .map((e) => e['name']?.toString().trim() ?? '') .where((name) => name.isNotEmpty) .toList(); } else { globalProjects.clear(); } } catch (e) { logSafe("Error fetching projects: $e", level: LogLevel.error); globalProjects.clear(); } } // ───────────────────────────────────────────── // 🔹 File / Image Pickers // ───────────────────────────────────────────── /// 📂 Pick **any type of attachment** (no extension restriction) Future pickAttachments() async { try { final result = await FilePicker.platform.pickFiles( type: FileType.any, // ✅ No restriction on type allowMultiple: true, ); if (result != null && result.paths.isNotEmpty) { attachments.addAll(result.paths.whereType().map(File.new)); } } catch (e) { _errorSnackbar("Attachment error: $e"); } } /// 📸 Pick from camera and auto add timestamp 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; } } /// 🖼️ Pick from gallery Future pickFromGallery() async { try { final pickedFile = await _picker.pickImage(source: ImageSource.gallery); if (pickedFile != null) { attachments.add(File(pickedFile.path)); attachments.refresh(); } } catch (e) { _errorSnackbar("Gallery error: $e"); } } // ───────────────────────────────────────────── // 🔹 Value Selectors // ───────────────────────────────────────────── void selectProject(String project) => selectedProject.value = project; void selectCategory(ExpenseCategory category) => selectedCategory.value = category; void selectPayee(String payee) => selectedPayee.value = payee; void selectCurrency(Currency currency) => selectedCurrency.value = currency; // ───────────────────────────────────────────── // 🔹 Attachment Helpers // ───────────────────────────────────────────── void addAttachment(File file) => attachments.add(file); void removeAttachment(File file) => attachments.remove(file); void removeExistingAttachment(Map file) => existingAttachments.remove(file); // ───────────────────────────────────────────── // 🔹 Payload Builder (for upload) // ───────────────────────────────────────────── Future>> buildAttachmentPayload() async { final existingPayload = existingAttachments .map((e) => { "documentId": e['documentId'], "fileName": e['fileName'], "contentType": e['contentType'] ?? 'application/octet-stream', "fileSize": e['fileSize'] ?? 0, "description": "", "url": e['url'], "isActive": e['isActive'] ?? true, "base64Data": "", }) .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": "", }; }), ); return [...existingPayload, ...newPayload]; } // ───────────────────────────────────────────── // 🔹 Snackbar Helper // ───────────────────────────────────────────── void _errorSnackbar(String msg, [String title = "Error"]) { showAppSnackbar(title: title, message: msg, type: SnackbarType.error); } }