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:
Vaibhav Surve 2025-08-02 17:39:08 +05:30
parent bba44d4d39
commit 0150400092
4 changed files with 120 additions and 94 deletions

View File

@ -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);

View File

@ -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 {

View File

@ -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(

View File

@ -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,
);
}
}
},