feat(expense): improve expense submission validation and UI feedback
This commit is contained in:
parent
a7bb24ee29
commit
ff01c05a73
@ -173,6 +173,7 @@ class AddExpenseController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Submit Expense ===
|
||||||
// === Submit Expense ===
|
// === Submit Expense ===
|
||||||
Future<void> submitExpense() async {
|
Future<void> submitExpense() async {
|
||||||
if (isSubmitting.value) return; // Prevent multiple taps
|
if (isSubmitting.value) return; // Prevent multiple taps
|
||||||
@ -180,17 +181,21 @@ class AddExpenseController extends GetxController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// === Validation ===
|
// === Validation ===
|
||||||
if (selectedProject.value.isEmpty ||
|
List<String> missingFields = [];
|
||||||
selectedExpenseType.value == null ||
|
|
||||||
selectedPaymentMode.value == null ||
|
if (selectedProject.value.isEmpty) missingFields.add("Project");
|
||||||
descriptionController.text.isEmpty ||
|
if (selectedExpenseType.value == null) missingFields.add("Expense Type");
|
||||||
supplierController.text.isEmpty ||
|
if (selectedPaymentMode.value == null) missingFields.add("Payment Mode");
|
||||||
amountController.text.isEmpty ||
|
if (selectedPaidBy.value == null) missingFields.add("Paid By");
|
||||||
selectedExpenseStatus.value == null ||
|
if (amountController.text.isEmpty) missingFields.add("Amount");
|
||||||
attachments.isEmpty) {
|
if (supplierController.text.isEmpty) missingFields.add("Supplier Name");
|
||||||
|
if (descriptionController.text.isEmpty) missingFields.add("Description");
|
||||||
|
if (attachments.isEmpty) missingFields.add("Attachments");
|
||||||
|
|
||||||
|
if (missingFields.isNotEmpty) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Missing Fields",
|
||||||
message: "Please fill all required fields.",
|
message: "Please provide: ${missingFields.join(', ')}.",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -247,7 +252,6 @@ class AddExpenseController extends GetxController {
|
|||||||
supplerName: supplierController.text,
|
supplerName: supplierController.text,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
noOfPersons: 0,
|
noOfPersons: 0,
|
||||||
statusId: selectedExpenseStatus.value!.id,
|
|
||||||
billAttachments: attachmentData,
|
billAttachments: attachmentData,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -295,7 +295,6 @@ class ApiService {
|
|||||||
required String supplerName,
|
required String supplerName,
|
||||||
required double amount,
|
required double amount,
|
||||||
required int noOfPersons,
|
required int noOfPersons,
|
||||||
required String statusId,
|
|
||||||
required List<Map<String, dynamic>> billAttachments,
|
required List<Map<String, dynamic>> billAttachments,
|
||||||
}) async {
|
}) async {
|
||||||
final payload = {
|
final payload = {
|
||||||
@ -310,7 +309,6 @@ class ApiService {
|
|||||||
"supplerName": supplerName,
|
"supplerName": supplerName,
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
"noOfPersons": noOfPersons,
|
"noOfPersons": noOfPersons,
|
||||||
"statusId": statusId,
|
|
||||||
"billAttachments": billAttachments,
|
"billAttachments": billAttachments,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import 'package:marco/controller/expense/add_expense_controller.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/model/expense/payment_types_model.dart';
|
import 'package:marco/model/expense/payment_types_model.dart';
|
||||||
import 'package:marco/model/expense/expense_type_model.dart';
|
import 'package:marco/model/expense/expense_type_model.dart';
|
||||||
import 'package:marco/model/expense/expense_status_model.dart';
|
|
||||||
|
|
||||||
void showAddExpenseBottomSheet() {
|
void showAddExpenseBottomSheet() {
|
||||||
Get.bottomSheet(
|
Get.bottomSheet(
|
||||||
@ -23,9 +22,9 @@ class _AddExpenseBottomSheet extends StatefulWidget {
|
|||||||
class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
||||||
final AddExpenseController controller = Get.put(AddExpenseController());
|
final AddExpenseController controller = Get.put(AddExpenseController());
|
||||||
final RxBool isProjectExpanded = false.obs;
|
final RxBool isProjectExpanded = false.obs;
|
||||||
|
|
||||||
void _showEmployeeList(BuildContext context) {
|
void _showEmployeeList(BuildContext context) {
|
||||||
final employees = controller.allEmployees;
|
final employees = controller.allEmployees;
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
@ -84,7 +83,10 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
|
|
||||||
// Project Dropdown
|
// Project Dropdown
|
||||||
const _SectionTitle(
|
const _SectionTitle(
|
||||||
icon: Icons.work_outline, title: "Project"),
|
icon: Icons.work_outline,
|
||||||
|
title: "Project",
|
||||||
|
requiredField: true,
|
||||||
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return _DropdownTile(
|
return _DropdownTile(
|
||||||
@ -105,6 +107,7 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
const _SectionTitle(
|
const _SectionTitle(
|
||||||
icon: Icons.category_outlined,
|
icon: Icons.category_outlined,
|
||||||
title: "Expense Type & GST No.",
|
title: "Expense Type & GST No.",
|
||||||
|
requiredField: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
@ -128,7 +131,10 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
|
|
||||||
// Payment Mode
|
// Payment Mode
|
||||||
const _SectionTitle(
|
const _SectionTitle(
|
||||||
icon: Icons.payment, title: "Payment Mode"),
|
icon: Icons.payment,
|
||||||
|
title: "Payment Mode",
|
||||||
|
requiredField: true,
|
||||||
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return _DropdownTile(
|
return _DropdownTile(
|
||||||
@ -143,6 +149,14 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Paid By
|
||||||
|
const _SectionTitle(
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
title: "Paid By",
|
||||||
|
requiredField: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final selected = controller.selectedPaidBy.value;
|
final selected = controller.selectedPaidBy.value;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@ -171,28 +185,12 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Expense Status
|
|
||||||
const _SectionTitle(
|
|
||||||
icon: Icons.flag_outlined, title: "Status"),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Obx(() {
|
|
||||||
return _DropdownTile(
|
|
||||||
title: controller.selectedExpenseStatus.value?.name ??
|
|
||||||
"Select Status",
|
|
||||||
onTap: () => _showOptionList<ExpenseStatusModel>(
|
|
||||||
context,
|
|
||||||
controller.expenseStatuses.toList(),
|
|
||||||
(s) => s.name,
|
|
||||||
(val) =>
|
|
||||||
controller.selectedExpenseStatus.value = val,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Amount
|
// Amount
|
||||||
const _SectionTitle(
|
const _SectionTitle(
|
||||||
icon: Icons.currency_rupee, title: "Amount"),
|
icon: Icons.currency_rupee,
|
||||||
|
title: "Amount",
|
||||||
|
requiredField: true,
|
||||||
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_CustomTextField(
|
_CustomTextField(
|
||||||
controller: controller.amountController,
|
controller: controller.amountController,
|
||||||
@ -200,10 +198,12 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Supplier Name
|
// Supplier Name
|
||||||
const _SectionTitle(
|
const _SectionTitle(
|
||||||
icon: Icons.store_mall_directory_outlined,
|
icon: Icons.store_mall_directory_outlined,
|
||||||
title: "Supplier Name",
|
title: "Supplier Name",
|
||||||
|
requiredField: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_CustomTextField(
|
_CustomTextField(
|
||||||
@ -211,10 +211,12 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
hint: "Enter Supplier Name",
|
hint: "Enter Supplier Name",
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Transaction ID
|
// Transaction ID
|
||||||
const _SectionTitle(
|
const _SectionTitle(
|
||||||
icon: Icons.confirmation_number_outlined,
|
icon: Icons.confirmation_number_outlined,
|
||||||
title: "Transaction ID"),
|
title: "Transaction ID",
|
||||||
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_CustomTextField(
|
_CustomTextField(
|
||||||
controller: controller.transactionIdController,
|
controller: controller.transactionIdController,
|
||||||
@ -256,13 +258,15 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Attachments Section
|
// Attachments Section
|
||||||
const _SectionTitle(
|
const _SectionTitle(
|
||||||
icon: Icons.attach_file, title: "Attachments"),
|
icon: Icons.attach_file,
|
||||||
|
title: "Attachments",
|
||||||
|
requiredField: true,
|
||||||
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@ -376,6 +380,7 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
const _SectionTitle(
|
const _SectionTitle(
|
||||||
icon: Icons.description_outlined,
|
icon: Icons.description_outlined,
|
||||||
title: "Description",
|
title: "Description",
|
||||||
|
requiredField: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_CustomTextField(
|
_CustomTextField(
|
||||||
@ -439,28 +444,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Project Selection List
|
|
||||||
Obx(() {
|
|
||||||
if (!isProjectExpanded.value) return const SizedBox.shrink();
|
|
||||||
return Positioned(
|
|
||||||
top: 110,
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
child: Material(
|
|
||||||
elevation: 4,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: _buildProjectSelectionList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -468,44 +451,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProjectSelectionList() {
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 300),
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: controller.globalProjects.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final project = controller.globalProjects[index];
|
|
||||||
final isSelected = project == controller.selectedProject.value;
|
|
||||||
|
|
||||||
return RadioListTile<String>(
|
|
||||||
value: project,
|
|
||||||
groupValue: controller.selectedProject.value,
|
|
||||||
onChanged: (val) {
|
|
||||||
controller.selectedProject.value = val!;
|
|
||||||
isProjectExpanded.value = false;
|
|
||||||
},
|
|
||||||
title: Text(
|
|
||||||
project,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
||||||
color: isSelected ? Colors.blueAccent : Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
activeColor: Colors.blueAccent,
|
|
||||||
tileColor: isSelected
|
|
||||||
? Colors.blueAccent.withOpacity(0.1)
|
|
||||||
: Colors.transparent,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
visualDensity: const VisualDensity(vertical: -4),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showOptionList<T>(
|
Future<void> _showOptionList<T>(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<T> options,
|
List<T> options,
|
||||||
@ -554,16 +499,38 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
class _SectionTitle extends StatelessWidget {
|
class _SectionTitle extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
const _SectionTitle({required this.icon, required this.title});
|
final bool requiredField;
|
||||||
|
|
||||||
|
const _SectionTitle({
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
this.requiredField = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final color = Colors.grey[700];
|
final color = Colors.grey[700];
|
||||||
return Row(
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: color, size: 18),
|
Icon(icon, color: color, size: 18),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
MyText.bodyMedium(title, fontWeight: 600),
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: DefaultTextStyle.of(context).style.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(text: title),
|
||||||
|
if (requiredField)
|
||||||
|
const TextSpan(
|
||||||
|
text: ' *',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -51,18 +51,17 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
|
|
||||||
if (expenseController.errorMessage.isNotEmpty) {
|
if (expenseController.errorMessage.isNotEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: MyText.bodyMedium(
|
||||||
expenseController.errorMessage.value,
|
expenseController.errorMessage.value,
|
||||||
style: const TextStyle(color: Colors.red),
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expenseController.expenses.isEmpty) {
|
if (expenseController.expenses.isEmpty) {
|
||||||
return const Center(child: Text("No expenses found."));
|
return Center(child: MyText.bodyMedium("No expenses found."));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply search filter
|
|
||||||
final filteredList =
|
final filteredList =
|
||||||
expenseController.expenses.where((expense) {
|
expenseController.expenses.where((expense) {
|
||||||
final query = searchQuery.value.toLowerCase();
|
final query = searchQuery.value.toLowerCase();
|
||||||
@ -76,7 +75,6 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
filteredList.sort(
|
filteredList.sort(
|
||||||
(a, b) => b.transactionDate.compareTo(a.transactionDate));
|
(a, b) => b.transactionDate.compareTo(a.transactionDate));
|
||||||
|
|
||||||
// Split into current month and history
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final currentMonthList = filteredList
|
final currentMonthList = filteredList
|
||||||
.where((e) =>
|
.where((e) =>
|
||||||
@ -117,23 +115,23 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
child: Wrap(
|
child: Wrap(
|
||||||
runSpacing: 10,
|
runSpacing: 10,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
MyText.bodyLarge(
|
||||||
'Filter Expenses',
|
'Filter Expenses',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
fontWeight: 700,
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.date_range),
|
leading: const Icon(Icons.date_range),
|
||||||
title: const Text('Date Range'),
|
title: MyText.bodyMedium('Date Range'),
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.work_outline),
|
leading: const Icon(Icons.work_outline),
|
||||||
title: const Text('Project'),
|
title: MyText.bodyMedium('Project'),
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.check_circle_outline),
|
leading: const Icon(Icons.check_circle_outline),
|
||||||
title: const Text('Status'),
|
title: MyText.bodyMedium('Status'),
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -264,7 +262,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: null,
|
onPressed: null, // Disabled as per request
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -349,13 +347,11 @@ class _ToggleButton extends StatelessWidget {
|
|||||||
Icon(icon,
|
Icon(icon,
|
||||||
size: 16, color: selected ? Colors.white : Colors.grey),
|
size: 16, color: selected ? Colors.white : Colors.grey),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
MyText.bodyMedium(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
color: selected ? Colors.white : Colors.grey,
|
||||||
color: selected ? Colors.white : Colors.grey,
|
fontWeight: 600,
|
||||||
fontWeight: FontWeight.w600,
|
fontSize: 13,
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -371,45 +367,27 @@ class _ExpenseList extends StatelessWidget {
|
|||||||
|
|
||||||
const _ExpenseList({required this.expenseList});
|
const _ExpenseList({required this.expenseList});
|
||||||
|
|
||||||
static Color _getStatusColor(String status) {
|
|
||||||
switch (status) {
|
|
||||||
case 'Requested':
|
|
||||||
return Colors.blue;
|
|
||||||
case 'Review':
|
|
||||||
return Colors.orange;
|
|
||||||
case 'Approved':
|
|
||||||
return Colors.green;
|
|
||||||
case 'Paid':
|
|
||||||
return Colors.purple;
|
|
||||||
case 'Closed':
|
|
||||||
return Colors.grey;
|
|
||||||
default:
|
|
||||||
return Colors.black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (expenseList.isEmpty) {
|
if (expenseList.isEmpty) {
|
||||||
return const Center(child: Text('No expenses found.'));
|
return Center(child: MyText.bodyMedium('No expenses found.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
|
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),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final expense = expenseList[index];
|
final expense = expenseList[index];
|
||||||
final statusColor = _getStatusColor(expense.status.name);
|
|
||||||
|
|
||||||
// Convert UTC date to local formatted string
|
|
||||||
final formattedDate = DateTimeUtils.convertUtcToLocal(
|
final formattedDate = DateTimeUtils.convertUtcToLocal(
|
||||||
expense.transactionDate.toIso8601String(),
|
expense.transactionDate.toIso8601String(),
|
||||||
format: 'dd MMM yyyy, hh:mm a',
|
format: 'dd MMM yyyy, hh:mm a',
|
||||||
);
|
);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () => Get.to(
|
onTap: () => Get.to(
|
||||||
() => const ExpenseDetailScreen(),
|
() => const ExpenseDetailScreen(),
|
||||||
arguments: {'expense': expense},
|
arguments: {'expense': expense},
|
||||||
@ -419,51 +397,35 @@ class _ExpenseList extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Title + Amount row
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.receipt_long,
|
MyText.bodyMedium(
|
||||||
size: 20, color: Colors.red),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
expense.expensesType.name,
|
expense.expensesType.name,
|
||||||
style: const TextStyle(
|
fontWeight: 700,
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
MyText.bodyMedium(
|
||||||
'₹ ${expense.amount.toStringAsFixed(2)}',
|
'₹ ${expense.amount.toStringAsFixed(2)}',
|
||||||
style: const TextStyle(
|
fontWeight: 600,
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
// Date + Status
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
MyText.bodySmall(
|
||||||
formattedDate,
|
formattedDate,
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
MyText.bodySmall(
|
||||||
expense.status.name,
|
expense.status.name,
|
||||||
style: TextStyle(
|
fontWeight: 600,
|
||||||
color: statusColor,
|
color: Colors.black,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user