Vaibhav_Feature-#768 #59
@ -7,19 +7,18 @@ import 'package:get/get.dart';
|
|||||||
import 'package:geocoding/geocoding.dart';
|
import 'package:geocoding/geocoding.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:mime/mime.dart';
|
|
||||||
import 'package:marco/model/employee_model.dart';
|
|
||||||
|
|
||||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
import 'package:marco/model/employee_model.dart';
|
||||||
import 'package:marco/model/expense/expense_type_model.dart';
|
import 'package:marco/model/expense/expense_type_model.dart';
|
||||||
import 'package:marco/model/expense/payment_types_model.dart';
|
import 'package:marco/model/expense/payment_types_model.dart';
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
class AddExpenseController extends GetxController {
|
class AddExpenseController extends GetxController {
|
||||||
// Text Controllers
|
// --- Text Controllers ---
|
||||||
final amountController = TextEditingController();
|
final amountController = TextEditingController();
|
||||||
final descriptionController = TextEditingController();
|
final descriptionController = TextEditingController();
|
||||||
final supplierController = TextEditingController();
|
final supplierController = TextEditingController();
|
||||||
@ -29,32 +28,32 @@ class AddExpenseController extends GetxController {
|
|||||||
final transactionDateController = TextEditingController();
|
final transactionDateController = TextEditingController();
|
||||||
final noOfPersonsController = TextEditingController();
|
final noOfPersonsController = TextEditingController();
|
||||||
|
|
||||||
// State
|
final employeeSearchController = TextEditingController();
|
||||||
|
|
||||||
|
// --- Reactive State ---
|
||||||
final isLoading = false.obs;
|
final isLoading = false.obs;
|
||||||
final isSubmitting = false.obs;
|
final isSubmitting = false.obs;
|
||||||
final isFetchingLocation = false.obs;
|
final isFetchingLocation = false.obs;
|
||||||
final isEditMode = false.obs;
|
final isEditMode = false.obs;
|
||||||
|
final isSearchingEmployees = false.obs;
|
||||||
|
|
||||||
// Dropdown Selections
|
// --- Dropdown Selections & Data ---
|
||||||
final selectedPaymentMode = Rx<PaymentModeModel?>(null);
|
final selectedPaymentMode = Rxn<PaymentModeModel>();
|
||||||
final selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
final selectedExpenseType = Rxn<ExpenseTypeModel>();
|
||||||
final selectedPaidBy = Rx<EmployeeModel?>(null);
|
final selectedPaidBy = Rxn<EmployeeModel>();
|
||||||
final selectedProject = ''.obs;
|
final selectedProject = ''.obs;
|
||||||
final selectedTransactionDate = Rx<DateTime?>(null);
|
final selectedTransactionDate = Rxn<DateTime>();
|
||||||
|
|
||||||
// Data Lists
|
|
||||||
final attachments = <File>[].obs;
|
final attachments = <File>[].obs;
|
||||||
|
final existingAttachments = <Map<String, dynamic>>[].obs;
|
||||||
final globalProjects = <String>[].obs;
|
final globalProjects = <String>[].obs;
|
||||||
final projectsMap = <String, String>{}.obs;
|
final projectsMap = <String, String>{}.obs;
|
||||||
|
|
||||||
final expenseTypes = <ExpenseTypeModel>[].obs;
|
final expenseTypes = <ExpenseTypeModel>[].obs;
|
||||||
final paymentModes = <PaymentModeModel>[].obs;
|
final paymentModes = <PaymentModeModel>[].obs;
|
||||||
final allEmployees = <EmployeeModel>[].obs;
|
final allEmployees = <EmployeeModel>[].obs;
|
||||||
final existingAttachments = <Map<String, dynamic>>[].obs;
|
|
||||||
final employeeSearchController = TextEditingController();
|
|
||||||
final isSearchingEmployees = false.obs;
|
|
||||||
final employeeSearchResults = <EmployeeModel>[].obs;
|
final employeeSearchResults = <EmployeeModel>[].obs;
|
||||||
|
|
||||||
// Editing
|
|
||||||
String? editingExpenseId;
|
String? editingExpenseId;
|
||||||
|
|
||||||
final expenseController = Get.find<ExpenseController>();
|
final expenseController = Get.find<ExpenseController>();
|
||||||
@ -64,7 +63,6 @@ class AddExpenseController extends GetxController {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
fetchMasterData();
|
fetchMasterData();
|
||||||
fetchGlobalProjects();
|
fetchGlobalProjects();
|
||||||
|
|
||||||
employeeSearchController.addListener(() {
|
employeeSearchController.addListener(() {
|
||||||
searchEmployees(employeeSearchController.text);
|
searchEmployees(employeeSearchController.text);
|
||||||
});
|
});
|
||||||
@ -72,36 +70,32 @@ class AddExpenseController extends GetxController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
amountController.dispose();
|
for (var c in [
|
||||||
descriptionController.dispose();
|
amountController,
|
||||||
supplierController.dispose();
|
descriptionController,
|
||||||
transactionIdController.dispose();
|
supplierController,
|
||||||
gstController.dispose();
|
transactionIdController,
|
||||||
locationController.dispose();
|
gstController,
|
||||||
transactionDateController.dispose();
|
locationController,
|
||||||
noOfPersonsController.dispose();
|
transactionDateController,
|
||||||
|
noOfPersonsController,
|
||||||
|
employeeSearchController,
|
||||||
|
]) {
|
||||||
|
c.dispose();
|
||||||
|
}
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> searchEmployees(String searchQuery) async {
|
// --- Employee Search ---
|
||||||
if (searchQuery.trim().isEmpty) {
|
Future<void> searchEmployees(String query) async {
|
||||||
employeeSearchResults.clear();
|
if (query.trim().isEmpty) return employeeSearchResults.clear();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSearchingEmployees.value = true;
|
isSearchingEmployees.value = true;
|
||||||
try {
|
try {
|
||||||
final results = await ApiService.searchEmployeesBasic(
|
final data =
|
||||||
searchString: searchQuery.trim(),
|
await ApiService.searchEmployeesBasic(searchString: query.trim());
|
||||||
|
employeeSearchResults.assignAll(
|
||||||
|
(data ?? []).map((e) => EmployeeModel.fromJson(e)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results != null) {
|
|
||||||
employeeSearchResults.assignAll(
|
|
||||||
results.map((e) => EmployeeModel.fromJson(e)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
employeeSearchResults.clear();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("Error searching employees: $e", level: LogLevel.error);
|
logSafe("Error searching employees: $e", level: LogLevel.error);
|
||||||
employeeSearchResults.clear();
|
employeeSearchResults.clear();
|
||||||
@ -110,73 +104,55 @@ class AddExpenseController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Form Population for Edit ----------
|
// --- Form Population: Edit Mode ---
|
||||||
Future<void> populateFieldsForEdit(Map<String, dynamic> data) async {
|
Future<void> populateFieldsForEdit(Map<String, dynamic> data) async {
|
||||||
isEditMode.value = true;
|
isEditMode.value = true;
|
||||||
editingExpenseId = data['id'];
|
editingExpenseId = '${data['id']}';
|
||||||
|
|
||||||
// --- Fetch all Paid By variables up front ---
|
|
||||||
final paidById = (data['paidById'] ?? '').toString();
|
|
||||||
final paidByFirstName = (data['paidByFirstName'] ?? '').toString().trim();
|
|
||||||
final paidByLastName = (data['paidByLastName'] ?? '').toString().trim();
|
|
||||||
|
|
||||||
// --- Standard Fields ---
|
|
||||||
selectedProject.value = data['projectName'] ?? '';
|
selectedProject.value = data['projectName'] ?? '';
|
||||||
amountController.text = data['amount']?.toString() ?? '';
|
amountController.text = data['amount']?.toString() ?? '';
|
||||||
supplierController.text = data['supplerName'] ?? '';
|
supplierController.text = data['supplerName'] ?? '';
|
||||||
descriptionController.text = data['description'] ?? '';
|
descriptionController.text = data['description'] ?? '';
|
||||||
transactionIdController.text = data['transactionId'] ?? '';
|
transactionIdController.text = data['transactionId'] ?? '';
|
||||||
locationController.text = data['location'] ?? '';
|
locationController.text = data['location'] ?? '';
|
||||||
|
noOfPersonsController.text = (data['noOfPersons'] ?? 0).toString();
|
||||||
|
|
||||||
// --- Transaction Date ---
|
// Transaction Date
|
||||||
if (data['transactionDate'] != null) {
|
if (data['transactionDate'] != null) {
|
||||||
try {
|
try {
|
||||||
final parsedDate = DateTime.parse(data['transactionDate']);
|
final parsed = DateTime.parse(data['transactionDate']);
|
||||||
selectedTransactionDate.value = parsedDate;
|
selectedTransactionDate.value = parsed;
|
||||||
transactionDateController.text =
|
transactionDateController.text =
|
||||||
DateFormat('dd-MM-yyyy').format(parsedDate);
|
DateFormat('dd-MM-yyyy').format(parsed);
|
||||||
} catch (e) {
|
} catch (_) {
|
||||||
logSafe('Error parsing transactionDate: $e', level: LogLevel.warning);
|
|
||||||
selectedTransactionDate.value = null;
|
selectedTransactionDate.value = null;
|
||||||
transactionDateController.clear();
|
transactionDateController.clear();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
selectedTransactionDate.value = null;
|
|
||||||
transactionDateController.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- No of Persons ---
|
// Dropdown
|
||||||
noOfPersonsController.text = (data['noOfPersons'] ?? 0).toString();
|
|
||||||
|
|
||||||
// --- Dropdown selections ---
|
|
||||||
selectedExpenseType.value =
|
selectedExpenseType.value =
|
||||||
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
|
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
|
||||||
selectedPaymentMode.value =
|
selectedPaymentMode.value =
|
||||||
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
|
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
|
||||||
|
|
||||||
// --- Paid By select ---
|
// Paid By
|
||||||
// 1. By ID
|
final paidById = '${data['paidById']}';
|
||||||
// --- Paid By select ---
|
|
||||||
selectedPaidBy.value =
|
selectedPaidBy.value =
|
||||||
allEmployees.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
allEmployees.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
||||||
|
if (selectedPaidBy.value == null && data['paidByFirstName'] != null) {
|
||||||
if (selectedPaidBy.value == null) {
|
await searchEmployees(
|
||||||
final fullName = '$paidByFirstName $paidByLastName';
|
'${data['paidByFirstName']} ${data['paidByLastName']}');
|
||||||
await searchEmployees(fullName);
|
|
||||||
selectedPaidBy.value = employeeSearchResults
|
selectedPaidBy.value = employeeSearchResults
|
||||||
.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Existing Attachments ---
|
// Attachments
|
||||||
existingAttachments.clear();
|
existingAttachments.clear();
|
||||||
if (data['attachments'] != null && data['attachments'] is List) {
|
if (data['attachments'] is List) {
|
||||||
existingAttachments.addAll(
|
existingAttachments.addAll(
|
||||||
List<Map<String, dynamic>>.from(data['attachments']).map((e) {
|
List<Map<String, dynamic>>.from(data['attachments'])
|
||||||
return {
|
.map((e) => {...e, 'isActive': true}),
|
||||||
...e,
|
|
||||||
'isActive': true, // default
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,29 +161,25 @@ class AddExpenseController extends GetxController {
|
|||||||
|
|
||||||
void _logPrefilledData() {
|
void _logPrefilledData() {
|
||||||
logSafe('--- Prefilled Expense Data ---', level: LogLevel.info);
|
logSafe('--- Prefilled Expense Data ---', level: LogLevel.info);
|
||||||
logSafe('ID: $editingExpenseId', level: LogLevel.info);
|
[
|
||||||
logSafe('Project: ${selectedProject.value}', level: LogLevel.info);
|
'ID: $editingExpenseId',
|
||||||
logSafe('Amount: ${amountController.text}', level: LogLevel.info);
|
'Project: ${selectedProject.value}',
|
||||||
logSafe('Supplier: ${supplierController.text}', level: LogLevel.info);
|
'Amount: ${amountController.text}',
|
||||||
logSafe('Description: ${descriptionController.text}', level: LogLevel.info);
|
'Supplier: ${supplierController.text}',
|
||||||
logSafe('Transaction ID: ${transactionIdController.text}',
|
'Description: ${descriptionController.text}',
|
||||||
level: LogLevel.info);
|
'Transaction ID: ${transactionIdController.text}',
|
||||||
logSafe('Location: ${locationController.text}', level: LogLevel.info);
|
'Location: ${locationController.text}',
|
||||||
logSafe('Transaction Date: ${transactionDateController.text}',
|
'Transaction Date: ${transactionDateController.text}',
|
||||||
level: LogLevel.info);
|
'No. of Persons: ${noOfPersonsController.text}',
|
||||||
logSafe('No. of Persons: ${noOfPersonsController.text}',
|
'Expense Type: ${selectedExpenseType.value?.name}',
|
||||||
level: LogLevel.info);
|
'Payment Mode: ${selectedPaymentMode.value?.name}',
|
||||||
logSafe('Expense Type: ${selectedExpenseType.value?.name}',
|
'Paid By: ${selectedPaidBy.value?.name}',
|
||||||
level: LogLevel.info);
|
'Attachments: ${attachments.length}',
|
||||||
logSafe('Payment Mode: ${selectedPaymentMode.value?.name}',
|
'Existing Attachments: ${existingAttachments.length}',
|
||||||
level: LogLevel.info);
|
].forEach((str) => logSafe(str, level: LogLevel.info));
|
||||||
logSafe('Paid By: ${selectedPaidBy.value?.name}', level: LogLevel.info);
|
|
||||||
logSafe('Attachments: ${attachments.length}', level: LogLevel.info);
|
|
||||||
logSafe('Existing Attachments: ${existingAttachments.length}',
|
|
||||||
level: LogLevel.info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Form Actions ----------
|
// --- Pickers ---
|
||||||
Future<void> pickTransactionDate(BuildContext context) async {
|
Future<void> pickTransactionDate(BuildContext context) async {
|
||||||
final picked = await showDatePicker(
|
final picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
@ -215,20 +187,12 @@ class AddExpenseController extends GetxController {
|
|||||||
firstDate: DateTime(DateTime.now().year - 5),
|
firstDate: DateTime(DateTime.now().year - 5),
|
||||||
lastDate: DateTime.now(),
|
lastDate: DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
selectedTransactionDate.value = picked;
|
selectedTransactionDate.value = picked;
|
||||||
transactionDateController.text = DateFormat('dd-MM-yyyy').format(picked);
|
transactionDateController.text = DateFormat('dd-MM-yyyy').format(picked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadMasterData() async {
|
|
||||||
await Future.wait([
|
|
||||||
fetchMasterData(),
|
|
||||||
fetchGlobalProjects(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pickAttachments() async {
|
Future<void> pickAttachments() async {
|
||||||
try {
|
try {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
@ -237,89 +201,109 @@ class AddExpenseController extends GetxController {
|
|||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
attachments.addAll(
|
attachments
|
||||||
result.paths.whereType<String>().map((path) => File(path)),
|
.addAll(result.paths.whereType<String>().map((path) => File(path)));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showAppSnackbar(
|
_errorSnackbar("Attachment error: $e");
|
||||||
title: "Error",
|
|
||||||
message: "Attachment error: $e",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAttachment(File file) => attachments.remove(file);
|
void removeAttachment(File file) => attachments.remove(file);
|
||||||
|
|
||||||
|
// --- Location ---
|
||||||
Future<void> fetchCurrentLocation() async {
|
Future<void> fetchCurrentLocation() async {
|
||||||
isFetchingLocation.value = true;
|
isFetchingLocation.value = true;
|
||||||
try {
|
try {
|
||||||
var permission = await Geolocator.checkPermission();
|
final permission = await _ensureLocationPermission();
|
||||||
if (permission == LocationPermission.denied ||
|
if (!permission) return;
|
||||||
permission == LocationPermission.deniedForever) {
|
|
||||||
permission = await Geolocator.requestPermission();
|
|
||||||
if (permission == LocationPermission.denied ||
|
|
||||||
permission == LocationPermission.deniedForever) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Location permission denied.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await Geolocator.isLocationServiceEnabled()) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Location service disabled.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final position = await Geolocator.getCurrentPosition();
|
final position = await Geolocator.getCurrentPosition();
|
||||||
final placemarks =
|
final placemarks =
|
||||||
await placemarkFromCoordinates(position.latitude, position.longitude);
|
await placemarkFromCoordinates(position.latitude, position.longitude);
|
||||||
|
|
||||||
if (placemarks.isNotEmpty) {
|
locationController.text = placemarks.isNotEmpty
|
||||||
final place = placemarks.first;
|
? [
|
||||||
final address = [
|
placemarks.first.name,
|
||||||
place.name,
|
placemarks.first.street,
|
||||||
place.street,
|
placemarks.first.locality,
|
||||||
place.locality,
|
placemarks.first.administrativeArea,
|
||||||
place.administrativeArea,
|
placemarks.first.country
|
||||||
place.country
|
].where((e) => e?.isNotEmpty == true).join(", ")
|
||||||
].where((e) => e != null && e.isNotEmpty).join(", ");
|
: "${position.latitude}, ${position.longitude}";
|
||||||
locationController.text = address;
|
|
||||||
} else {
|
|
||||||
locationController.text = "${position.latitude}, ${position.longitude}";
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showAppSnackbar(
|
_errorSnackbar("Location error: $e");
|
||||||
title: "Error",
|
|
||||||
message: "Location error: $e",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
isFetchingLocation.value = false;
|
isFetchingLocation.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Submission ----------
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Data Fetching ---
|
||||||
|
Future<void> loadMasterData() async =>
|
||||||
|
await Future.wait([fetchMasterData(), fetchGlobalProjects()]);
|
||||||
|
|
||||||
|
Future<void> fetchMasterData() async {
|
||||||
|
try {
|
||||||
|
final types = await ApiService.getMasterExpenseTypes();
|
||||||
|
if (types is List)
|
||||||
|
expenseTypes.value =
|
||||||
|
types.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||||
|
|
||||||
|
final modes = await ApiService.getMasterPaymentModes();
|
||||||
|
if (modes is List)
|
||||||
|
paymentModes.value =
|
||||||
|
modes.map((e) => PaymentModeModel.fromJson(e)).toList();
|
||||||
|
} catch (_) {
|
||||||
|
_errorSnackbar("Failed to fetch master data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchGlobalProjects() async {
|
||||||
|
try {
|
||||||
|
final response = await ApiService.getGlobalProjects();
|
||||||
|
if (response != null) {
|
||||||
|
final names = <String>[];
|
||||||
|
for (var item in response) {
|
||||||
|
final name = item['name']?.toString().trim(),
|
||||||
|
id = item['id']?.toString().trim();
|
||||||
|
if (name != null && id != null) {
|
||||||
|
projectsMap[name] = id;
|
||||||
|
names.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalProjects.assignAll(names);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("Error fetching projects: $e", level: LogLevel.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Submission ---
|
||||||
Future<void> submitOrUpdateExpense() async {
|
Future<void> submitOrUpdateExpense() async {
|
||||||
if (isSubmitting.value) return;
|
if (isSubmitting.value) return;
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final validation = validateForm();
|
final validationMsg = validateForm();
|
||||||
if (validation.isNotEmpty) {
|
if (validationMsg.isNotEmpty) {
|
||||||
showAppSnackbar(
|
_errorSnackbar(validationMsg, "Missing Fields");
|
||||||
title: "Missing Fields",
|
|
||||||
message: validation,
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,42 +337,29 @@ class AddExpenseController extends GetxController {
|
|||||||
type: SnackbarType.success,
|
type: SnackbarType.success,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showAppSnackbar(
|
_errorSnackbar("Operation failed. Try again.");
|
||||||
title: "Error",
|
|
||||||
message: "Operation failed. Try again.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showAppSnackbar(
|
_errorSnackbar("Unexpected error: $e");
|
||||||
title: "Error",
|
|
||||||
message: "Unexpected error: $e",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _buildExpensePayload() async {
|
Future<Map<String, dynamic>> _buildExpensePayload() async {
|
||||||
final amount = double.parse(amountController.text.trim());
|
final now = DateTime.now();
|
||||||
final projectId = projectsMap[selectedProject.value]!;
|
final existingAttachmentPayloads = existingAttachments
|
||||||
final selectedDate =
|
.map((e) => {
|
||||||
selectedTransactionDate.value?.toUtc() ?? DateTime.now().toUtc();
|
"documentId": e['documentId'],
|
||||||
final existingAttachmentPayloads = existingAttachments.map((e) {
|
"fileName": e['fileName'],
|
||||||
final isActive = e['isActive'] ?? true;
|
"contentType": e['contentType'],
|
||||||
|
"fileSize": 0,
|
||||||
return {
|
"description": "",
|
||||||
"documentId": e['documentId'],
|
"url": e['url'],
|
||||||
"fileName": e['fileName'],
|
"isActive": e['isActive'] ?? true,
|
||||||
"contentType": e['contentType'],
|
"base64Data": e['isActive'] == false ? null : e['base64Data'],
|
||||||
"fileSize": 0,
|
})
|
||||||
"description": "",
|
.toList();
|
||||||
"url": e['url'],
|
|
||||||
"isActive": isActive,
|
|
||||||
"base64Data": isActive ? e['base64Data'] : null,
|
|
||||||
};
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final newAttachmentPayloads =
|
final newAttachmentPayloads =
|
||||||
await Future.wait(attachments.map((file) async {
|
await Future.wait(attachments.map((file) async {
|
||||||
@ -401,34 +372,29 @@ class AddExpenseController extends GetxController {
|
|||||||
"description": "",
|
"description": "",
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
final billAttachments = [
|
|
||||||
...existingAttachmentPayloads,
|
|
||||||
...newAttachmentPayloads
|
|
||||||
];
|
|
||||||
|
|
||||||
final Map<String, dynamic> payload = {
|
final type = selectedExpenseType.value!;
|
||||||
"projectId": projectId,
|
return {
|
||||||
"expensesTypeId": selectedExpenseType.value!.id,
|
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
|
||||||
|
"projectId": projectsMap[selectedProject.value]!,
|
||||||
|
"expensesTypeId": type.id,
|
||||||
"paymentModeId": selectedPaymentMode.value!.id,
|
"paymentModeId": selectedPaymentMode.value!.id,
|
||||||
"paidById": selectedPaidBy.value!.id,
|
"paidById": selectedPaidBy.value!.id,
|
||||||
"transactionDate": selectedDate.toIso8601String(),
|
"transactionDate": (selectedTransactionDate.value?.toUtc() ?? now.toUtc())
|
||||||
|
.toIso8601String(),
|
||||||
"transactionId": transactionIdController.text,
|
"transactionId": transactionIdController.text,
|
||||||
"description": descriptionController.text,
|
"description": descriptionController.text,
|
||||||
"location": locationController.text,
|
"location": locationController.text,
|
||||||
"supplerName": supplierController.text,
|
"supplerName": supplierController.text,
|
||||||
"amount": amount,
|
"amount": double.parse(amountController.text.trim()),
|
||||||
"noOfPersons": selectedExpenseType.value?.noOfPersonsRequired == true
|
"noOfPersons": type.noOfPersonsRequired == true
|
||||||
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
|
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
|
||||||
: 0,
|
: 0,
|
||||||
"billAttachments": billAttachments,
|
"billAttachments": [
|
||||||
|
...existingAttachmentPayloads,
|
||||||
|
...newAttachmentPayloads
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ Add expense ID if in edit mode
|
|
||||||
if (isEditMode.value && editingExpenseId != null) {
|
|
||||||
payload['id'] = editingExpenseId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String validateForm() {
|
String validateForm() {
|
||||||
@ -439,62 +405,30 @@ class AddExpenseController extends GetxController {
|
|||||||
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
|
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
|
||||||
if (selectedPaidBy.value == null) missing.add("Paid By");
|
if (selectedPaidBy.value == null) missing.add("Paid By");
|
||||||
if (amountController.text.trim().isEmpty) missing.add("Amount");
|
if (amountController.text.trim().isEmpty) missing.add("Amount");
|
||||||
if (supplierController.text.trim().isEmpty) missing.add("Supplier Name");
|
|
||||||
if (descriptionController.text.trim().isEmpty) missing.add("Description");
|
if (descriptionController.text.trim().isEmpty) missing.add("Description");
|
||||||
if (!isEditMode.value && attachments.isEmpty) missing.add("Attachments");
|
|
||||||
|
// Date Required
|
||||||
|
if (selectedTransactionDate.value == null) missing.add("Transaction Date");
|
||||||
|
if (selectedTransactionDate.value != null &&
|
||||||
|
selectedTransactionDate.value!.isAfter(DateTime.now())) {
|
||||||
|
missing.add("Valid Transaction Date");
|
||||||
|
}
|
||||||
|
|
||||||
final amount = double.tryParse(amountController.text.trim());
|
final amount = double.tryParse(amountController.text.trim());
|
||||||
if (amount == null) missing.add("Valid Amount");
|
if (amount == null) missing.add("Valid Amount");
|
||||||
|
|
||||||
final selectedDate = selectedTransactionDate.value;
|
// Attachment: at least one required at all times
|
||||||
if (selectedDate != null && selectedDate.isAfter(DateTime.now())) {
|
bool hasActiveExisting =
|
||||||
missing.add("Valid Transaction Date");
|
existingAttachments.any((e) => e['isActive'] != false);
|
||||||
}
|
if (attachments.isEmpty && !hasActiveExisting) missing.add("Attachment");
|
||||||
|
|
||||||
return missing.isEmpty ? '' : "Please provide: ${missing.join(', ')}.";
|
return missing.isEmpty ? '' : "Please provide: ${missing.join(', ')}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Data Fetching ----------
|
// --- Snackbar Helper ---
|
||||||
Future<void> fetchMasterData() async {
|
void _errorSnackbar(String msg, [String title = "Error"]) => showAppSnackbar(
|
||||||
try {
|
title: title,
|
||||||
final types = await ApiService.getMasterExpenseTypes();
|
message: msg,
|
||||||
final modes = await ApiService.getMasterPaymentModes();
|
|
||||||
|
|
||||||
if (types is List) {
|
|
||||||
expenseTypes.value =
|
|
||||||
types.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modes is List) {
|
|
||||||
paymentModes.value =
|
|
||||||
modes.map((e) => PaymentModeModel.fromJson(e)).toList();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to fetch master data",
|
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchGlobalProjects() async {
|
|
||||||
try {
|
|
||||||
final response = await ApiService.getGlobalProjects();
|
|
||||||
if (response != null) {
|
|
||||||
final names = <String>[];
|
|
||||||
for (var item in response) {
|
|
||||||
final name = item['name']?.toString().trim();
|
|
||||||
final id = item['id']?.toString().trim();
|
|
||||||
if (name != null && id != null) {
|
|
||||||
projectsMap[name] = id;
|
|
||||||
names.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
globalProjects.assignAll(names);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logSafe("Error fetching projects: $e", level: LogLevel.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user