346 lines
11 KiB
Dart
346 lines
11 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:geocoding/geocoding.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:mime/mime.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_status_model.dart';
|
|
import 'package:marco/model/expense/expense_type_model.dart';
|
|
import 'package:marco/model/expense/payment_types_model.dart';
|
|
|
|
class AddExpenseController extends GetxController {
|
|
// === Text Controllers ===
|
|
final amountController = TextEditingController();
|
|
final descriptionController = TextEditingController();
|
|
final supplierController = TextEditingController();
|
|
final transactionIdController = TextEditingController();
|
|
final gstController = TextEditingController();
|
|
final locationController = TextEditingController();
|
|
final transactionDateController = TextEditingController();
|
|
|
|
// === State Controllers ===
|
|
final RxBool isLoading = false.obs;
|
|
final RxBool isSubmitting = false.obs;
|
|
final RxBool isFetchingLocation = false.obs;
|
|
|
|
// === Selected Models ===
|
|
final Rx<PaymentModeModel?> selectedPaymentMode = Rx<PaymentModeModel?>(null);
|
|
final Rx<ExpenseTypeModel?> selectedExpenseType = Rx<ExpenseTypeModel?>(null);
|
|
final Rx<ExpenseStatusModel?> selectedExpenseStatus =
|
|
Rx<ExpenseStatusModel?>(null);
|
|
final Rx<EmployeeModel?> selectedPaidBy = Rx<EmployeeModel?>(null);
|
|
final RxString selectedProject = ''.obs;
|
|
final Rx<DateTime?> selectedTransactionDate = Rx<DateTime?>(null);
|
|
|
|
// === Lists ===
|
|
final RxList<File> attachments = <File>[].obs;
|
|
final RxList<String> globalProjects = <String>[].obs;
|
|
final RxList<String> projects = <String>[].obs;
|
|
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
|
|
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
|
|
final RxList<ExpenseStatusModel> expenseStatuses = <ExpenseStatusModel>[].obs;
|
|
final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
|
|
|
// === Mappings ===
|
|
final RxMap<String, String> projectsMap = <String, String>{}.obs;
|
|
|
|
final ExpenseController expenseController = Get.find<ExpenseController>();
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
fetchMasterData();
|
|
fetchGlobalProjects();
|
|
fetchAllEmployees();
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
amountController.dispose();
|
|
descriptionController.dispose();
|
|
supplierController.dispose();
|
|
transactionIdController.dispose();
|
|
gstController.dispose();
|
|
locationController.dispose();
|
|
transactionDateController.dispose();
|
|
super.onClose();
|
|
}
|
|
|
|
// === Pick Attachments ===
|
|
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 && result.paths.isNotEmpty) {
|
|
final files =
|
|
result.paths.whereType<String>().map((e) => File(e)).toList();
|
|
attachments.addAll(files);
|
|
}
|
|
} catch (e) {
|
|
Get.snackbar("Error", "Failed to pick attachments: $e");
|
|
}
|
|
}
|
|
|
|
void removeAttachment(File file) {
|
|
attachments.remove(file);
|
|
}
|
|
|
|
// === Date Picker ===
|
|
void pickTransactionDate(BuildContext context) async {
|
|
final now = DateTime.now();
|
|
final picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: selectedTransactionDate.value ?? now,
|
|
firstDate: DateTime(now.year - 5),
|
|
lastDate: now, // ✅ Restrict future dates
|
|
);
|
|
|
|
if (picked != null) {
|
|
selectedTransactionDate.value = picked;
|
|
transactionDateController.text =
|
|
"${picked.day.toString().padLeft(2, '0')}-${picked.month.toString().padLeft(2, '0')}-${picked.year}";
|
|
}
|
|
}
|
|
|
|
// === Fetch Current Location ===
|
|
Future<void> fetchCurrentLocation() async {
|
|
isFetchingLocation.value = true;
|
|
try {
|
|
LocationPermission permission = await Geolocator.checkPermission();
|
|
if (permission == LocationPermission.denied ||
|
|
permission == LocationPermission.deniedForever) {
|
|
permission = await Geolocator.requestPermission();
|
|
if (permission == LocationPermission.denied ||
|
|
permission == LocationPermission.deniedForever) {
|
|
Get.snackbar(
|
|
"Error", "Location permission denied. Enable in settings.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!await Geolocator.isLocationServiceEnabled()) {
|
|
Get.snackbar("Error", "Location services are disabled. Enable them.");
|
|
return;
|
|
}
|
|
|
|
final position = await Geolocator.getCurrentPosition(
|
|
desiredAccuracy: LocationAccuracy.high);
|
|
final placemarks =
|
|
await placemarkFromCoordinates(position.latitude, position.longitude);
|
|
|
|
if (placemarks.isNotEmpty) {
|
|
final place = placemarks.first;
|
|
final address = [
|
|
place.name,
|
|
place.street,
|
|
place.subLocality,
|
|
place.locality,
|
|
place.administrativeArea,
|
|
place.country,
|
|
].where((e) => e != null && e.isNotEmpty).join(", ");
|
|
locationController.text = address;
|
|
} else {
|
|
locationController.text = "${position.latitude}, ${position.longitude}";
|
|
}
|
|
} catch (e) {
|
|
Get.snackbar("Error", "Error fetching location: $e");
|
|
} finally {
|
|
isFetchingLocation.value = false;
|
|
}
|
|
}
|
|
|
|
// === Submit Expense ===
|
|
Future<void> submitExpense() async {
|
|
if (isSubmitting.value) return;
|
|
isSubmitting.value = true;
|
|
|
|
try {
|
|
List<String> missing = [];
|
|
|
|
if (selectedProject.value.isEmpty) missing.add("Project");
|
|
if (selectedExpenseType.value == null) missing.add("Expense Type");
|
|
if (selectedPaymentMode.value == null) missing.add("Payment Mode");
|
|
if (selectedPaidBy.value == null) missing.add("Paid By");
|
|
if (amountController.text.isEmpty) missing.add("Amount");
|
|
if (supplierController.text.isEmpty) missing.add("Supplier Name");
|
|
if (descriptionController.text.isEmpty) missing.add("Description");
|
|
if (attachments.isEmpty) missing.add("Attachments");
|
|
|
|
if (missing.isNotEmpty) {
|
|
showAppSnackbar(
|
|
title: "Missing Fields",
|
|
message: "Please provide: ${missing.join(', ')}.",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final amount = double.tryParse(amountController.text);
|
|
if (amount == null) {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "Please enter a valid amount.",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final selectedDate = selectedTransactionDate.value ?? DateTime.now();
|
|
if (selectedDate.isAfter(DateTime.now())) {
|
|
showAppSnackbar(
|
|
title: "Invalid Date",
|
|
message: "Transaction date cannot be in the future.",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final projectId = projectsMap[selectedProject.value];
|
|
if (projectId == null) {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "Invalid project selected.",
|
|
type: SnackbarType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final billAttachments = await Future.wait(attachments.map((file) async {
|
|
final bytes = await file.readAsBytes();
|
|
final base64 = base64Encode(bytes);
|
|
final mime = lookupMimeType(file.path) ?? 'application/octet-stream';
|
|
final size = await file.length();
|
|
|
|
return {
|
|
"fileName": file.path.split('/').last,
|
|
"base64Data": base64,
|
|
"contentType": mime,
|
|
"fileSize": size,
|
|
"description": "",
|
|
};
|
|
}));
|
|
|
|
final success = await ApiService.createExpenseApi(
|
|
projectId: projectId,
|
|
expensesTypeId: selectedExpenseType.value!.id,
|
|
paymentModeId: selectedPaymentMode.value!.id,
|
|
paidById: selectedPaidBy.value!.id,
|
|
transactionDate:
|
|
(selectedTransactionDate.value ?? DateTime.now()).toUtc(),
|
|
transactionId: transactionIdController.text,
|
|
description: descriptionController.text,
|
|
location: locationController.text,
|
|
supplerName: supplierController.text,
|
|
amount: amount,
|
|
noOfPersons: 0,
|
|
billAttachments: billAttachments,
|
|
);
|
|
|
|
if (success) {
|
|
await expenseController.fetchExpenses();
|
|
Get.back();
|
|
showAppSnackbar(
|
|
title: "Success",
|
|
message: "Expense created successfully!",
|
|
type: SnackbarType.success,
|
|
);
|
|
} else {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "Failed to create expense. Try again.",
|
|
type: SnackbarType.error,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
showAppSnackbar(
|
|
title: "Error",
|
|
message: "Something went wrong: $e",
|
|
type: SnackbarType.error,
|
|
);
|
|
} finally {
|
|
isSubmitting.value = false;
|
|
}
|
|
}
|
|
|
|
// === Fetch Data Methods ===
|
|
Future<void> fetchMasterData() async {
|
|
try {
|
|
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
|
final paymentModesData = await ApiService.getMasterPaymentModes();
|
|
final expenseStatusData = await ApiService.getMasterExpenseStatus();
|
|
|
|
if (expenseTypesData is List) {
|
|
expenseTypes.value =
|
|
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
|
}
|
|
|
|
if (paymentModesData is List) {
|
|
paymentModes.value =
|
|
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
|
|
}
|
|
|
|
if (expenseStatusData is List) {
|
|
expenseStatuses.value = expenseStatusData
|
|
.map((e) => ExpenseStatusModel.fromJson(e))
|
|
.toList();
|
|
}
|
|
} catch (e) {
|
|
Get.snackbar("Error", "Failed to fetch master data: $e");
|
|
}
|
|
}
|
|
|
|
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 && name.isNotEmpty) {
|
|
projectsMap[name] = id;
|
|
names.add(name);
|
|
}
|
|
}
|
|
globalProjects.assignAll(names);
|
|
logSafe("Fetched ${names.length} global projects");
|
|
}
|
|
} catch (e) {
|
|
logSafe("Failed to fetch global projects: $e", level: LogLevel.error);
|
|
}
|
|
}
|
|
|
|
Future<void> fetchAllEmployees() async {
|
|
isLoading.value = true;
|
|
try {
|
|
final response = await ApiService.getAllEmployees();
|
|
if (response != null && response.isNotEmpty) {
|
|
allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e)));
|
|
logSafe("All Employees fetched: ${allEmployees.length}",
|
|
level: LogLevel.info);
|
|
} else {
|
|
allEmployees.clear();
|
|
logSafe("No employees found.", level: LogLevel.warning);
|
|
}
|
|
} catch (e) {
|
|
allEmployees.clear();
|
|
logSafe("Error fetching employees", level: LogLevel.error, error: e);
|
|
} finally {
|
|
isLoading.value = false;
|
|
update();
|
|
}
|
|
}
|
|
}
|