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