261 lines
10 KiB
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);
|
|
}
|
|
}
|