297 lines
9.8 KiB
Dart
297 lines
9.8 KiB
Dart
// 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: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 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 = <String>[].obs;
|
|
final categories = <ExpenseCategory>[].obs;
|
|
final currencies = <Currency>[].obs;
|
|
final globalProjects = <Map<String, dynamic>>[].obs;
|
|
|
|
// Selected Values
|
|
final selectedProject = Rx<Map<String, dynamic>?>(null);
|
|
final selectedCategory = Rx<ExpenseCategory?>(null);
|
|
final selectedPayee = ''.obs;
|
|
final selectedCurrency = Rx<Currency?>(null);
|
|
final isAdvancePayment = false.obs;
|
|
final selectedDueDate = Rx<DateTime?>(null);
|
|
|
|
// 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();
|
|
|
|
@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<void> fetchAllMasterData() async {
|
|
await Future.wait([
|
|
_fetchData(
|
|
payees, ApiService.getExpensePaymentRequestPayeeApi, isLoadingPayees),
|
|
_fetchData(categories, ApiService.getMasterExpenseCategoriesApi,
|
|
isLoadingCategories),
|
|
_fetchData(
|
|
currencies, ApiService.getMasterCurrenciesApi, isLoadingCurrencies),
|
|
]);
|
|
}
|
|
|
|
/// Generic fetch handler
|
|
Future<void> _fetchData<T>(
|
|
RxList<T> list, Future<dynamic> 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<void> fetchGlobalProjects() async {
|
|
try {
|
|
final response = await ApiService.getGlobalProjects();
|
|
globalProjects.value = (response ?? [])
|
|
.map<Map<String, dynamic>>((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<void> 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<void> 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<String>().map(File.new));
|
|
}
|
|
attachments.refresh();
|
|
} catch (e) {
|
|
_errorSnackbar("Attachment error: $e");
|
|
} finally {
|
|
isProcessingAttachment.value = false;
|
|
}
|
|
}
|
|
|
|
/// Selection handlers
|
|
void selectProject(Map<String, dynamic> 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;
|
|
|
|
void addAttachment(File file) => attachments.add(file);
|
|
void removeAttachment(File file) => attachments.remove(file);
|
|
|
|
/// Build attachment payload
|
|
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];
|
|
}
|
|
|
|
/// Submit payment request (Project API style)
|
|
Future<bool> 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,
|
|
"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.isEmpty)
|
|
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 = '';
|
|
selectedCurrency.value = null;
|
|
isAdvancePayment.value = false;
|
|
attachments.clear();
|
|
existingAttachments.clear();
|
|
}
|
|
}
|