From ebc8996f7167ea28852efc49c9a0aaeb0809efbb Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 3 Nov 2025 15:00:59 +0530 Subject: [PATCH] feat: Enhance monthly expense dashboard with expense type filtering functionality --- .../dashboard/dashboard_controller.dart | 31 ++++++- .../monthly_expense_dashboard_chart.dart | 80 +++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index 42ffbc2..50e801a 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -6,6 +6,7 @@ import 'package:marco/model/dashboard/project_progress_model.dart'; import 'package:marco/model/dashboard/pending_expenses_model.dart'; import 'package:marco/model/dashboard/expense_type_report_model.dart'; import 'package:marco/model/dashboard/monthly_expence_model.dart'; +import 'package:marco/model/expense/expense_type_model.dart'; class DashboardController extends GetxController { // ========================= @@ -78,6 +79,21 @@ class DashboardController extends GetxController { MonthlyExpenseDuration.twelveMonths.obs; final RxInt selectedMonthsCount = 12.obs; + final RxList expenseTypes = [].obs; + final Rx selectedExpenseType = Rx(null); + + void updateSelectedExpenseType(ExpenseTypeModel? type) { + selectedExpenseType.value = type; + + // Debug print to verify + print('Selected: ${type?.name ?? "All Types"}'); + + if (type == null) { + fetchMonthlyExpenses(); + } else { + fetchMonthlyExpenses(categoryId: type.id); + } + } @override void onInit() { @@ -188,6 +204,7 @@ class DashboardController extends GetxController { endDate: expenseReportEndDate.value, ), fetchMonthlyExpenses(), + fetchMasterData() ]); } @@ -209,7 +226,7 @@ class DashboardController extends GetxController { selectedMonthsCount.value = 12; break; case MonthlyExpenseDuration.all: - selectedMonthsCount.value = 0; + selectedMonthsCount.value = 0; // 0 = All months in your API break; } @@ -217,6 +234,18 @@ class DashboardController extends GetxController { fetchMonthlyExpenses(); } + Future fetchMasterData() async { + try { + final expenseTypesData = await ApiService.getMasterExpenseTypes(); + if (expenseTypesData is List) { + expenseTypes.value = + expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList(); + } + } catch (e) { + logSafe('Error fetching master data', level: LogLevel.error, error: e); + } + } + Future fetchMonthlyExpenses({String? categoryId}) async { try { isMonthlyExpenseLoading.value = true; diff --git a/lib/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart b/lib/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart index 48c1288..d3e27cf 100644 --- a/lib/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart +++ b/lib/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart @@ -121,6 +121,7 @@ class MonthlyExpenseDashboardChart extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _ChartHeader( + controller: _controller, // pass controller explicitly selectedDuration: selectedDuration, onDurationChanged: _controller.updateMonthlyExpenseDuration, totalExpense: totalExpense, @@ -172,11 +173,13 @@ class MonthlyExpenseDashboardChart extends StatelessWidget { class _ChartHeader extends StatelessWidget { const _ChartHeader({ Key? key, + required this.controller, // added required this.selectedDuration, required this.onDurationChanged, required this.totalExpense, }) : super(key: key); + final DashboardController controller; // added final MonthlyExpenseDuration selectedDuration; final ValueChanged onDurationChanged; final double totalExpense; @@ -189,6 +192,83 @@ class _ChartHeader extends StatelessWidget { _buildTitle(), const SizedBox(height: 2), _buildSubtitle(), + const SizedBox(height: 8), + // ========================== + // Row with popup menu on the right + // ========================== + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Obx(() { + final selectedType = controller.selectedExpenseType.value; + + return PopupMenuButton( + tooltip: 'Filter by Expense Type', + onSelected: (String value) { + if (value == 'all') { + controller.updateSelectedExpenseType(null); + } else { + final type = controller.expenseTypes + .firstWhere((t) => t.id == value); + controller.updateSelectedExpenseType(type); + } + }, + itemBuilder: (context) { + final types = controller.expenseTypes; + return [ + PopupMenuItem( + value: 'all', + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('All Types'), + if (selectedType == null) + const Icon(Icons.check, + size: 16, color: Colors.blueAccent), + ], + ), + ), + ...types.map((type) => PopupMenuItem( + value: type.id, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(type.name), + if (selectedType?.id == type.id) + const Icon(Icons.check, + size: 16, color: Colors.blueAccent), + ], + ), + )), + ]; + }, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + selectedType?.name ?? 'All Types', + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + ), + const SizedBox(width: 4), + const Icon(Icons.arrow_drop_down, size: 20), + ], + ), + ), + ); + }), + ], + ), + const SizedBox(height: 8), _buildDurationSelector(), ],