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

370 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/finance/payment_request_details_model.dart';
import 'package:marco/model/expense/payment_types_model.dart';
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:mime/mime.dart';
class PaymentRequestDetailController extends GetxController {
final Rx<PaymentRequestData?> paymentRequest = Rx<PaymentRequestData?>(null);
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
// Employee selection
final Rx<EmployeeModel?> selectedReimbursedBy = Rx<EmployeeModel?>(null);
final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
final RxList<EmployeeModel> employeeSearchResults = <EmployeeModel>[].obs;
final TextEditingController employeeSearchController =
TextEditingController();
final RxBool isSearchingEmployees = false.obs;
// Attachments
final RxList<File> attachments = <File>[].obs;
final RxList<Map<String, dynamic>> existingAttachments =
<Map<String, dynamic>>[].obs;
final isProcessingAttachment = false.obs;
// Payment mode
final selectedPaymentMode = Rxn<PaymentModeModel>();
// Text controllers for form
final TextEditingController locationController = TextEditingController();
final TextEditingController gstNumberController = TextEditingController();
// Form submission state
final RxBool isSubmitting = false.obs;
late String _requestId;
bool _isInitialized = false;
RxBool paymentSheetOpened = false.obs;
final ImagePicker _picker = ImagePicker();
/// Initialize controller
void init(String requestId) {
if (_isInitialized) return;
_isInitialized = true;
_requestId = requestId;
// Fetch payment request details + employees concurrently
Future.wait([
fetchPaymentRequestDetail(),
fetchAllEmployees(),
fetchPaymentModes(),
]);
}
/// Generic API wrapper for error handling
Future<T?> _apiCallWrapper<T>(
Future<T?> Function() apiCall, String operationName) async {
isLoading.value = true;
errorMessage.value = '';
try {
final result = await apiCall();
return result;
} catch (e) {
errorMessage.value = 'Error during $operationName: $e';
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error);
return null;
} finally {
isLoading.value = false;
}
}
/// Fetch payment request details
Future<void> fetchPaymentRequestDetail() async {
isLoading.value = true;
try {
final response =
await ApiService.getExpensePaymentRequestDetailApi(_requestId);
if (response != null) {
paymentRequest.value = response.data;
} else {
errorMessage.value = "Failed to fetch payment request details";
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error,
);
}
} catch (e) {
errorMessage.value = "Error fetching payment request details: $e";
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
/// Pick files from gallery or file picker
Future<void> pickAttachments() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
allowMultiple: true,
);
if (result != null) {
attachments.addAll(
result.paths.whereType<String>().map(File.new),
);
}
} catch (e) {
_errorSnackbar("Attachment error: $e");
}
}
void removeAttachment(File file) => attachments.remove(file);
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;
}
}
// --- Location ---
final RxBool isFetchingLocation = false.obs;
Future<void> fetchCurrentLocation() async {
isFetchingLocation.value = true;
try {
if (!await _ensureLocationPermission()) return;
final position = await Geolocator.getCurrentPosition();
final placemarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
locationController.text = placemarks.isNotEmpty
? [
placemarks.first.name,
placemarks.first.street,
placemarks.first.locality,
placemarks.first.administrativeArea,
placemarks.first.country,
].where((e) => e?.isNotEmpty == true).join(", ")
: "${position.latitude}, ${position.longitude}";
} catch (e) {
_errorSnackbar("Location error: $e");
} finally {
isFetchingLocation.value = false;
}
}
Future<bool> _ensureLocationPermission() async {
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
_errorSnackbar("Location permission denied.");
return false;
}
}
if (!await Geolocator.isLocationServiceEnabled()) {
_errorSnackbar("Location service disabled.");
return false;
}
return true;
}
/// Fetch all employees
Future<void> fetchAllEmployees() async {
final response = await _apiCallWrapper(
() => ApiService.getAllEmployees(), "fetch all employees");
if (response != null && response.isNotEmpty) {
try {
allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e)));
} catch (e) {
errorMessage.value = 'Failed to parse employee data: $e';
showAppSnackbar(
title: 'Error',
message: errorMessage.value,
type: SnackbarType.error);
}
} else {
allEmployees.clear();
}
}
/// Fetch payment modes
Future<void> fetchPaymentModes() async {
isLoading.value = true;
try {
final paymentModesData = await ApiService.getMasterPaymentModes();
if (paymentModesData is List) {
paymentModes.value =
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
} else {
paymentModes.clear();
showAppSnackbar(
title: 'Error',
message: 'Failed to fetch payment modes',
type: SnackbarType.error);
}
} catch (e) {
paymentModes.clear();
showAppSnackbar(
title: 'Error',
message: 'Error fetching payment modes: $e',
type: SnackbarType.error);
} finally {
isLoading.value = false;
}
}
/// Search employees
Future<void> searchEmployees(String query) async {
if (query.trim().isEmpty) {
employeeSearchResults.clear();
return;
}
isSearchingEmployees.value = true;
try {
final data =
await ApiService.searchEmployeesBasic(searchString: query.trim());
employeeSearchResults.assignAll(
(data ?? []).map((e) => EmployeeModel.fromJson(e)),
);
} catch (e) {
employeeSearchResults.clear();
} finally {
isSearchingEmployees.value = false;
}
}
/// Update payment request status
Future<bool> updatePaymentRequestStatus({
required String statusId,
required String comment,
String? paidTransactionId,
String? paidById,
DateTime? paidAt,
double? baseAmount,
double? taxAmount,
String? tdsPercentage,
}) async {
isLoading.value = true;
try {
final success = await ApiService.updateExpensePaymentRequestStatusApi(
paymentRequestId: _requestId,
statusId: statusId,
comment: comment,
paidTransactionId: paidTransactionId,
paidById: paidById,
paidAt: paidAt,
baseAmount: baseAmount,
taxAmount: taxAmount,
tdsPercentage: tdsPercentage,
);
if (success) {
showAppSnackbar(
title: 'Success',
message: 'Payment submitted successfully',
type: SnackbarType.success);
await fetchPaymentRequestDetail();
} else {
showAppSnackbar(
title: 'Error',
message: 'Failed to update status. Please try again.',
type: SnackbarType.error);
}
return success;
} catch (e) {
showAppSnackbar(
title: 'Error',
message: 'Something went wrong: $e',
type: SnackbarType.error);
return false;
} finally {
isLoading.value = false;
}
}
// --- Snackbar Helper ---
void _errorSnackbar(String msg, [String title = "Error"]) {
showAppSnackbar(title: title, message: msg, type: SnackbarType.error);
}
// --- Payment Mode Selection ---
void selectPaymentMode(PaymentModeModel mode) {
selectedPaymentMode.value = mode;
}
// --- Submit Expense ---
Future<bool> submitExpense(
{required String statusId, String? comment}) async {
if (selectedPaymentMode.value == null) return false;
isSubmitting.value = true;
try {
// prepare attachments
final success = await ApiService.createExpenseForPRApi(
paymentModeId: selectedPaymentMode.value!.id,
location: locationController.text,
gstNumber: gstNumberController.text,
paymentRequestId: _requestId,
billAttachments: attachments.map((file) {
final bytes = file.readAsBytesSync();
final mimeType =
lookupMimeType(file.path) ?? 'application/octet-stream';
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(bytes),
"contentType": mimeType,
"description": "",
"fileSize": bytes.length,
"isActive": true,
};
}).toList(),
statusId: statusId,
comment: comment ?? '',
);
if (success) {
// Refresh the payment request details so the UI updates
await fetchPaymentRequestDetail();
}
return success;
} finally {
isSubmitting.value = false;
}
}
}