Vaibhav_Feature-#768 #59
@ -13,6 +13,7 @@ import 'package:marco/model/expense/expense_status_model.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/employee_model.dart';
|
||||||
|
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
||||||
|
|
||||||
class AddExpenseController extends GetxController {
|
class AddExpenseController extends GetxController {
|
||||||
// === Text Controllers ===
|
// === Text Controllers ===
|
||||||
@ -22,6 +23,7 @@ class AddExpenseController extends GetxController {
|
|||||||
final transactionIdController = TextEditingController();
|
final transactionIdController = TextEditingController();
|
||||||
final gstController = TextEditingController();
|
final gstController = TextEditingController();
|
||||||
final locationController = TextEditingController();
|
final locationController = TextEditingController();
|
||||||
|
final ExpenseController expenseController = Get.find<ExpenseController>();
|
||||||
|
|
||||||
// === Project Mapping ===
|
// === Project Mapping ===
|
||||||
final RxMap<String, String> projectsMap = <String, String>{}.obs;
|
final RxMap<String, String> projectsMap = <String, String>{}.obs;
|
||||||
@ -49,6 +51,7 @@ class AddExpenseController extends GetxController {
|
|||||||
final RxList<File> attachments = <File>[].obs;
|
final RxList<File> attachments = <File>[].obs;
|
||||||
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
|
final RxBool isSubmitting = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -172,7 +175,11 @@ class AddExpenseController extends GetxController {
|
|||||||
|
|
||||||
// === Submit Expense ===
|
// === Submit Expense ===
|
||||||
Future<void> submitExpense() async {
|
Future<void> submitExpense() async {
|
||||||
// Validation for required fields
|
if (isSubmitting.value) return; // Prevent multiple taps
|
||||||
|
isSubmitting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// === Validation ===
|
||||||
if (selectedProject.value.isEmpty ||
|
if (selectedProject.value.isEmpty ||
|
||||||
selectedExpenseType.value == null ||
|
selectedExpenseType.value == null ||
|
||||||
selectedPaymentMode.value == null ||
|
selectedPaymentMode.value == null ||
|
||||||
@ -209,11 +216,12 @@ class AddExpenseController extends GetxController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert attachments to base64 + meta
|
// === Convert Attachments ===
|
||||||
final attachmentData = await Future.wait(attachments.map((file) async {
|
final attachmentData = await Future.wait(attachments.map((file) async {
|
||||||
final bytes = await file.readAsBytes();
|
final bytes = await file.readAsBytes();
|
||||||
final base64String = base64Encode(bytes);
|
final base64String = base64Encode(bytes);
|
||||||
final mimeType = lookupMimeType(file.path) ?? 'application/octet-stream';
|
final mimeType =
|
||||||
|
lookupMimeType(file.path) ?? 'application/octet-stream';
|
||||||
final fileSize = await file.length();
|
final fileSize = await file.length();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -225,13 +233,14 @@ class AddExpenseController extends GetxController {
|
|||||||
};
|
};
|
||||||
}).toList());
|
}).toList());
|
||||||
|
|
||||||
// Submit API call
|
// === API Call ===
|
||||||
final success = await ApiService.createExpenseApi(
|
final success = await ApiService.createExpenseApi(
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
expensesTypeId: selectedExpenseType.value!.id,
|
expensesTypeId: selectedExpenseType.value!.id,
|
||||||
paymentModeId: selectedPaymentMode.value!.id,
|
paymentModeId: selectedPaymentMode.value!.id,
|
||||||
paidById: selectedPaidBy.value?.id ?? "",
|
paidById: selectedPaidBy.value?.id ?? "",
|
||||||
transactionDate:(selectedTransactionDate.value ?? DateTime.now()).toUtc(),
|
transactionDate:
|
||||||
|
(selectedTransactionDate.value ?? DateTime.now()).toUtc(),
|
||||||
transactionId: transactionIdController.text,
|
transactionId: transactionIdController.text,
|
||||||
description: descriptionController.text,
|
description: descriptionController.text,
|
||||||
location: locationController.text,
|
location: locationController.text,
|
||||||
@ -243,6 +252,7 @@ class AddExpenseController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
await Get.find<ExpenseController>().fetchExpenses(); // 🔄 Refresh list
|
||||||
Get.back();
|
Get.back();
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@ -256,6 +266,15 @@ class AddExpenseController extends GetxController {
|
|||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Something went wrong: $e",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Fetch Projects ===
|
// === Fetch Projects ===
|
||||||
|
@ -395,18 +395,29 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
label:
|
label:
|
||||||
MyText.bodyMedium("Cancel", fontWeight: 600),
|
MyText.bodyMedium("Cancel", fontWeight: 600),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
minimumSize:
|
minimumSize: const Size.fromHeight(48),
|
||||||
const Size.fromHeight(48),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: Obx(() {
|
||||||
onPressed: controller.submitExpense,
|
final isLoading = controller.isSubmitting.value;
|
||||||
icon: const Icon(Icons.check, size: 18),
|
return ElevatedButton.icon(
|
||||||
|
onPressed:
|
||||||
|
isLoading ? null : controller.submitExpense,
|
||||||
|
icon: isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.check, size: 18),
|
||||||
label: MyText.bodyMedium(
|
label: MyText.bodyMedium(
|
||||||
"Submit",
|
isLoading ? "Submitting..." : "Submit",
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
),
|
),
|
||||||
@ -417,10 +428,10 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
),
|
),
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 14),
|
const EdgeInsets.symmetric(vertical: 14),
|
||||||
minimumSize:
|
minimumSize: const Size.fromHeight(48),
|
||||||
const Size.fromHeight(48),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -63,7 +63,8 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply search filter
|
// Apply search filter
|
||||||
final filteredList = expenseController.expenses.where((expense) {
|
final filteredList =
|
||||||
|
expenseController.expenses.where((expense) {
|
||||||
final query = searchQuery.value.toLowerCase();
|
final query = searchQuery.value.toLowerCase();
|
||||||
return query.isEmpty ||
|
return query.isEmpty ||
|
||||||
expense.expensesType.name.toLowerCase().contains(query) ||
|
expense.expensesType.name.toLowerCase().contains(query) ||
|
||||||
@ -71,15 +72,22 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
expense.paymentMode.name.toLowerCase().contains(query);
|
expense.paymentMode.name.toLowerCase().contains(query);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
// Sort by latest transaction date first
|
||||||
|
filteredList.sort(
|
||||||
|
(a, b) => b.transactionDate.compareTo(a.transactionDate));
|
||||||
|
|
||||||
// Split into current month and history
|
// Split into current month and history
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final currentMonthList = filteredList.where((e) =>
|
final currentMonthList = filteredList
|
||||||
|
.where((e) =>
|
||||||
e.transactionDate.month == now.month &&
|
e.transactionDate.month == now.month &&
|
||||||
e.transactionDate.year == now.year).toList();
|
e.transactionDate.year == now.year)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final historyList = filteredList.where((e) =>
|
final historyList = filteredList
|
||||||
e.transactionDate.isBefore(
|
.where((e) => e.transactionDate
|
||||||
DateTime(now.year, now.month, 1))).toList();
|
.isBefore(DateTime(now.year, now.month, 1)))
|
||||||
|
.toList();
|
||||||
|
|
||||||
final listToShow =
|
final listToShow =
|
||||||
isHistoryView.value ? historyList : currentMonthList;
|
isHistoryView.value ? historyList : currentMonthList;
|
||||||
@ -235,8 +243,7 @@ class _SearchAndFilter extends StatelessWidget {
|
|||||||
controller: searchController,
|
controller: searchController,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
prefixIcon:
|
prefixIcon:
|
||||||
const Icon(Icons.search, size: 20, color: Colors.grey),
|
const Icon(Icons.search, size: 20, color: Colors.grey),
|
||||||
hintText: 'Search expenses...',
|
hintText: 'Search expenses...',
|
||||||
@ -257,7 +264,7 @@ class _SearchAndFilter extends StatelessWidget {
|
|||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.tune, color: Colors.black),
|
icon: const Icon(Icons.tune, color: Colors.black),
|
||||||
onPressed: onFilterTap,
|
onPressed: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -339,7 +346,8 @@ class _ToggleButton extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 16, color: selected ? Colors.white : Colors.grey),
|
Icon(icon,
|
||||||
|
size: 16, color: selected ? Colors.white : Colors.grey),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
@ -365,12 +373,18 @@ class _ExpenseList extends StatelessWidget {
|
|||||||
|
|
||||||
static Color _getStatusColor(String status) {
|
static Color _getStatusColor(String status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'Requested': return Colors.blue;
|
case 'Requested':
|
||||||
case 'Review': return Colors.orange;
|
return Colors.blue;
|
||||||
case 'Approved': return Colors.green;
|
case 'Review':
|
||||||
case 'Paid': return Colors.purple;
|
return Colors.orange;
|
||||||
case 'Closed': return Colors.grey;
|
case 'Approved':
|
||||||
default: return Colors.black;
|
return Colors.green;
|
||||||
|
case 'Paid':
|
||||||
|
return Colors.purple;
|
||||||
|
case 'Closed':
|
||||||
|
return Colors.grey;
|
||||||
|
default:
|
||||||
|
return Colors.black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,7 +395,7 @@ class _ExpenseList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
|
||||||
itemCount: expenseList.length,
|
itemCount: expenseList.length,
|
||||||
separatorBuilder: (_, __) =>
|
separatorBuilder: (_, __) =>
|
||||||
Divider(color: Colors.grey.shade300, height: 20),
|
Divider(color: Colors.grey.shade300, height: 20),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user