refactor: Replace Get.snackbar with showAppSnackbar for consistent error handling and improve code readability across expense-related controllers and views.
This commit is contained in:
parent
bba44d4d39
commit
0150400092
@ -18,7 +18,6 @@ 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();
|
||||
@ -28,21 +27,17 @@ class AddExpenseController extends GetxController {
|
||||
final transactionDateController = TextEditingController();
|
||||
final TextEditingController noOfPersonsController = 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<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;
|
||||
@ -51,7 +46,6 @@ class AddExpenseController extends GetxController {
|
||||
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>();
|
||||
@ -77,7 +71,6 @@ class AddExpenseController extends GetxController {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// === Pick Attachments ===
|
||||
Future<void> pickAttachments() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
@ -86,12 +79,15 @@ class AddExpenseController extends GetxController {
|
||||
allowMultiple: true,
|
||||
);
|
||||
if (result != null && result.paths.isNotEmpty) {
|
||||
final files =
|
||||
result.paths.whereType<String>().map((e) => File(e)).toList();
|
||||
final files = result.paths.whereType<String>().map((e) => File(e)).toList();
|
||||
attachments.addAll(files);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Failed to pick attachments: $e");
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to pick attachments: $e",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,16 +95,14 @@ class AddExpenseController extends GetxController {
|
||||
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
|
||||
lastDate: now,
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
selectedTransactionDate.value = picked;
|
||||
transactionDateController.text =
|
||||
@ -116,31 +110,33 @@ class AddExpenseController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// === Fetch Current Location ===
|
||||
Future<void> fetchCurrentLocation() async {
|
||||
isFetchingLocation.value = true;
|
||||
try {
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
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.");
|
||||
if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Location permission denied. Enable in settings.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!await Geolocator.isLocationServiceEnabled()) {
|
||||
Get.snackbar("Error", "Location services are disabled. Enable them.");
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Location services are disabled. Enable them.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
final placemarks =
|
||||
await placemarkFromCoordinates(position.latitude, position.longitude);
|
||||
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
||||
final placemarks = await placemarkFromCoordinates(position.latitude, position.longitude);
|
||||
|
||||
if (placemarks.isNotEmpty) {
|
||||
final place = placemarks.first;
|
||||
@ -157,13 +153,16 @@ class AddExpenseController extends GetxController {
|
||||
locationController.text = "${position.latitude}, ${position.longitude}";
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Error fetching location: $e");
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Error fetching location: $e",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
} finally {
|
||||
isFetchingLocation.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// === Submit Expense ===
|
||||
Future<void> submitExpense() async {
|
||||
if (isSubmitting.value) return;
|
||||
isSubmitting.value = true;
|
||||
@ -239,8 +238,7 @@ class AddExpenseController extends GetxController {
|
||||
expensesTypeId: selectedExpenseType.value!.id,
|
||||
paymentModeId: selectedPaymentMode.value!.id,
|
||||
paidById: selectedPaidBy.value!.id,
|
||||
transactionDate:
|
||||
(selectedTransactionDate.value ?? DateTime.now()).toUtc(),
|
||||
transactionDate: selectedTransactionDate.value?.toUtc() ?? DateTime.now().toUtc(),
|
||||
transactionId: transactionIdController.text,
|
||||
description: descriptionController.text,
|
||||
location: locationController.text,
|
||||
@ -278,7 +276,6 @@ class AddExpenseController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// === Fetch Data Methods ===
|
||||
Future<void> fetchMasterData() async {
|
||||
try {
|
||||
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
||||
@ -286,22 +283,22 @@ class AddExpenseController extends GetxController {
|
||||
final expenseStatusData = await ApiService.getMasterExpenseStatus();
|
||||
|
||||
if (expenseTypesData is List) {
|
||||
expenseTypes.value =
|
||||
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||
expenseTypes.value = expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
if (paymentModesData is List) {
|
||||
paymentModes.value =
|
||||
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
|
||||
paymentModes.value = paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
if (expenseStatusData is List) {
|
||||
expenseStatuses.value = expenseStatusData
|
||||
.map((e) => ExpenseStatusModel.fromJson(e))
|
||||
.toList();
|
||||
expenseStatuses.value = expenseStatusData.map((e) => ExpenseStatusModel.fromJson(e)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Failed to fetch master data: $e");
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to fetch master data: $e",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,8 +329,7 @@ class AddExpenseController extends GetxController {
|
||||
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);
|
||||
logSafe("All Employees fetched: ${allEmployees.length}", level: LogLevel.info);
|
||||
} else {
|
||||
allEmployees.clear();
|
||||
logSafe("No employees found.", level: LogLevel.warning);
|
||||
|
@ -7,6 +7,8 @@ import 'package:marco/model/expense/payment_types_model.dart';
|
||||
import 'package:marco/model/expense/expense_type_model.dart';
|
||||
import 'package:marco/model/expense/expense_status_model.dart';
|
||||
import 'package:marco/model/employee_model.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
|
||||
|
||||
class ExpenseController extends GetxController {
|
||||
final RxList<ExpenseModel> expenses = <ExpenseModel>[].obs;
|
||||
@ -68,15 +70,27 @@ class ExpenseController extends GetxController {
|
||||
if (success) {
|
||||
expenses.removeWhere((e) => e.id == expenseId);
|
||||
logSafe("Expense deleted successfully.");
|
||||
Get.snackbar("Deleted", "Expense has been deleted successfully.");
|
||||
showAppSnackbar(
|
||||
title: "Deleted",
|
||||
message: "Expense has been deleted successfully.",
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
} else {
|
||||
logSafe("Failed to delete expense: $expenseId", level: LogLevel.error);
|
||||
Get.snackbar("Failed", "Failed to delete expense.");
|
||||
showAppSnackbar(
|
||||
title: "Failed",
|
||||
message: "Failed to delete expense.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception in deleteExpense: $e", level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
Get.snackbar("Error", "Something went wrong while deleting.");
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Something went wrong while deleting.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,29 +173,32 @@ class ExpenseController extends GetxController {
|
||||
|
||||
/// Fetch master data: expense types, payment modes, and expense status
|
||||
Future<void> fetchMasterData() async {
|
||||
try {
|
||||
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
||||
if (expenseTypesData is List) {
|
||||
expenseTypes.value =
|
||||
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
final paymentModesData = await ApiService.getMasterPaymentModes();
|
||||
if (paymentModesData is List) {
|
||||
paymentModes.value =
|
||||
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
final expenseStatusData = await ApiService.getMasterExpenseStatus();
|
||||
if (expenseStatusData is List) {
|
||||
expenseStatuses.value = expenseStatusData
|
||||
.map((e) => ExpenseStatusModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Failed to fetch master data: $e");
|
||||
try {
|
||||
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
||||
if (expenseTypesData is List) {
|
||||
expenseTypes.value =
|
||||
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
final paymentModesData = await ApiService.getMasterPaymentModes();
|
||||
if (paymentModesData is List) {
|
||||
paymentModes.value =
|
||||
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
final expenseStatusData = await ApiService.getMasterExpenseStatus();
|
||||
if (expenseStatusData is List) {
|
||||
expenseStatuses.value =
|
||||
expenseStatusData.map((e) => ExpenseStatusModel.fromJson(e)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to fetch master data: $e",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch global projects
|
||||
Future<void> fetchGlobalProjects() async {
|
||||
|
@ -7,6 +7,8 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
|
||||
|
||||
class ReimbursementBottomSheet extends StatefulWidget {
|
||||
final String expenseId;
|
||||
@ -34,7 +36,8 @@ class ReimbursementBottomSheet extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ReimbursementBottomSheetState extends State<ReimbursementBottomSheet> {
|
||||
final ExpenseDetailController controller = Get.find<ExpenseDetailController>();
|
||||
final ExpenseDetailController controller =
|
||||
Get.find<ExpenseDetailController>();
|
||||
|
||||
final TextEditingController commentCtrl = TextEditingController();
|
||||
final TextEditingController txnCtrl = TextEditingController();
|
||||
@ -119,7 +122,11 @@ class _ReimbursementBottomSheetState extends State<ReimbursementBottomSheet> {
|
||||
txnCtrl.text.trim().isEmpty ||
|
||||
dateStr.value.isEmpty ||
|
||||
controller.selectedReimbursedBy.value == null) {
|
||||
Get.snackbar("Incomplete", "Please fill all fields");
|
||||
showAppSnackbar(
|
||||
title: "Incomplete",
|
||||
message: "Please fill all fields",
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -133,9 +140,17 @@ class _ReimbursementBottomSheetState extends State<ReimbursementBottomSheet> {
|
||||
|
||||
if (success) {
|
||||
Get.back();
|
||||
Get.snackbar('Success', 'Reimbursement submitted');
|
||||
showAppSnackbar(
|
||||
title: "Success",
|
||||
message: "Reimbursement submitted",
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
} else {
|
||||
Get.snackbar('Error', controller.errorMessage.value);
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: controller.errorMessage.value,
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
@ -148,7 +163,6 @@ class _ReimbursementBottomSheetState extends State<ReimbursementBottomSheet> {
|
||||
decoration: _inputDecoration("Enter comment"),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
|
||||
MyText.labelMedium("Transaction ID"),
|
||||
MySpacing.height(8),
|
||||
TextField(
|
||||
@ -156,7 +170,6 @@ class _ReimbursementBottomSheetState extends State<ReimbursementBottomSheet> {
|
||||
decoration: _inputDecoration("Enter transaction ID"),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
|
||||
MyText.labelMedium("Reimbursement Date"),
|
||||
MySpacing.height(8),
|
||||
GestureDetector(
|
||||
@ -183,7 +196,6 @@ class _ReimbursementBottomSheetState extends State<ReimbursementBottomSheet> {
|
||||
),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
|
||||
MyText.labelMedium("Reimbursed By"),
|
||||
MySpacing.height(8),
|
||||
GestureDetector(
|
||||
|
@ -12,6 +12,7 @@ import 'package:marco/model/expense/expense_detail_model.dart';
|
||||
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:marco/model/expense/reimbursement_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
|
||||
class ExpenseDetailScreen extends StatelessWidget {
|
||||
final String expenseId;
|
||||
@ -224,21 +225,18 @@ class ExpenseDetailScreen extends StatelessWidget {
|
||||
);
|
||||
|
||||
if (success) {
|
||||
Get.snackbar(
|
||||
'Success',
|
||||
'Expense reimbursed successfully.',
|
||||
backgroundColor: Colors.green.withOpacity(0.8),
|
||||
colorText: Colors.white,
|
||||
showAppSnackbar(
|
||||
title: 'Success',
|
||||
message: 'Expense reimbursed successfully.',
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
await controller.fetchExpenseDetails();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Failed to reimburse expense.',
|
||||
backgroundColor: Colors.red.withOpacity(0.8),
|
||||
colorText: Colors.white,
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: 'Failed to reimburse expense.',
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@ -250,19 +248,18 @@ class ExpenseDetailScreen extends StatelessWidget {
|
||||
await controller.updateExpenseStatus(next.id);
|
||||
|
||||
if (success) {
|
||||
Get.snackbar(
|
||||
'Success',
|
||||
'Expense moved to ${next.displayName.isNotEmpty ? next.displayName : next.name}',
|
||||
backgroundColor: Colors.green.withOpacity(0.8),
|
||||
colorText: Colors.white,
|
||||
showAppSnackbar(
|
||||
title: 'Success',
|
||||
message:
|
||||
'Expense moved to ${next.displayName.isNotEmpty ? next.displayName : next.name}',
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
await controller.fetchExpenseDetails();
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Failed to update status.',
|
||||
backgroundColor: Colors.red.withOpacity(0.8),
|
||||
colorText: Colors.white,
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: 'Failed to update status.',
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -491,7 +488,11 @@ class _InvoiceDocuments extends StatelessWidget {
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
Get.snackbar("Error", "Could not open the document.");
|
||||
showAppSnackbar(
|
||||
title: 'Error',
|
||||
message: 'Could not open the document.',
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user