marco.pms.mobileapp/lib/controller/finance/add_payment_request_controller.dart

261 lines
10 KiB
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: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 = <String>[].obs;
final categories = <ExpenseCategory>[].obs;
final currencies = <Currency>[].obs;
final globalProjects = <String>[].obs;
// ─────────────────────────────────────────────
// 🔹 Selected Values
// ─────────────────────────────────────────────
final selectedCategory = Rx<ExpenseCategory?>(null);
final selectedPayee = ''.obs;
final selectedCurrency = Rx<Currency?>(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 = <File>[].obs;
final existingAttachments = <Map<String, dynamic>>[].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<void> fetchAllMasterData() async {
await Future.wait([
fetchPayees(),
fetchExpenseCategories(),
fetchCurrencies(),
]);
}
Future<void> 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<void> 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<void> 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<void> fetchGlobalProjects() async {
try {
final response = await ApiService.getGlobalProjects();
if (response != null && response.isNotEmpty) {
globalProjects.value = response
.map<String>((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<void> 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<String>().map(File.new));
}
} catch (e) {
_errorSnackbar("Attachment error: $e");
}
}
/// 📸 Pick from camera and auto add timestamp
Future<void> 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<void> 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<String, dynamic> file) =>
existingAttachments.remove(file);
// ─────────────────────────────────────────────
// 🔹 Payload Builder (for upload)
// ─────────────────────────────────────────────
Future<List<Map<String, dynamic>>> 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);
}
}