From 3c95583a236075a89f1d885a89703159fef7205d Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 3 Nov 2025 14:24:39 +0530 Subject: [PATCH] feat: Add monthly expense reporting functionality with dashboard integration --- .../dashboard/dashboard_controller.dart | 90 +++- lib/helpers/services/api_endpoints.dart | 4 +- lib/helpers/services/api_service.dart | 43 ++ .../dashbaord/attendance_overview_chart.dart | 13 +- .../monthly_expense_dashboard_chart.dart | 440 ++++++++++++++++++ .../dashbaord/project_progress_chart.dart | 79 ++-- .../dashboard/monthly_expence_model.dart | 70 +++ lib/view/dashboard/dashboard_screen.dart | 10 +- 8 files changed, 707 insertions(+), 42 deletions(-) create mode 100644 lib/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart create mode 100644 lib/model/dashboard/monthly_expence_model.dart diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index 2a7a24b..42ffbc2 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -5,6 +5,7 @@ import 'package:marco/controller/project_controller.dart'; 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'; class DashboardController extends GetxController { // ========================= @@ -61,10 +62,22 @@ class DashboardController extends GetxController { final RxBool isExpenseTypeReportLoading = false.obs; final Rx expenseTypeReportData = Rx(null); - final Rx expenseReportStartDate = - DateTime.now().subtract(const Duration(days: 15)).obs; -final Rx expenseReportEndDate = DateTime.now().obs; + final Rx expenseReportStartDate = + DateTime.now().subtract(const Duration(days: 15)).obs; + final Rx expenseReportEndDate = DateTime.now().obs; + // ========================= + // Monthly Expense Report + // ========================= + final RxBool isMonthlyExpenseLoading = false.obs; + final RxList monthlyExpenseList = + [].obs; + // ========================= + // Monthly Expense Report Filters + // ========================= + final Rx selectedMonthlyExpenseDuration = + MonthlyExpenseDuration.twelveMonths.obs; + final RxInt selectedMonthsCount = 12.obs; @override void onInit() { @@ -173,10 +186,71 @@ final Rx expenseReportEndDate = DateTime.now().obs; fetchExpenseTypeReport( startDate: expenseReportStartDate.value, endDate: expenseReportEndDate.value, - ) + ), + fetchMonthlyExpenses(), ]); } + void updateMonthlyExpenseDuration(MonthlyExpenseDuration duration) { + selectedMonthlyExpenseDuration.value = duration; + + // Set months count based on selection + switch (duration) { + case MonthlyExpenseDuration.oneMonth: + selectedMonthsCount.value = 1; + break; + case MonthlyExpenseDuration.threeMonths: + selectedMonthsCount.value = 3; + break; + case MonthlyExpenseDuration.sixMonths: + selectedMonthsCount.value = 6; + break; + case MonthlyExpenseDuration.twelveMonths: + selectedMonthsCount.value = 12; + break; + case MonthlyExpenseDuration.all: + selectedMonthsCount.value = 0; + break; + } + + // Re-fetch updated data + fetchMonthlyExpenses(); + } + + Future fetchMonthlyExpenses({String? categoryId}) async { + try { + isMonthlyExpenseLoading.value = true; + + int months = selectedMonthsCount.value; + logSafe( + 'Fetching Monthly Expense Report for last $months months' + '${categoryId != null ? ' (categoryId: $categoryId)' : ''}', + level: LogLevel.info, + ); + + final response = await ApiService.getDashboardMonthlyExpensesApi( + categoryId: categoryId, + months: months, + ); + + if (response != null && response.success) { + monthlyExpenseList.value = response.data; + logSafe('Monthly Expense Report fetched successfully.', + level: LogLevel.info); + } else { + monthlyExpenseList.clear(); + logSafe('Failed to fetch Monthly Expense Report.', + level: LogLevel.error); + } + } catch (e, st) { + monthlyExpenseList.clear(); + logSafe('Error fetching Monthly Expense Report', + level: LogLevel.error, error: e, stackTrace: st); + } finally { + isMonthlyExpenseLoading.value = false; + } + } + Future fetchPendingExpenses() async { final String projectId = projectController.selectedProjectId.value; if (projectId.isEmpty) return; @@ -345,3 +419,11 @@ final Rx expenseReportEndDate = DateTime.now().obs; } } } + +enum MonthlyExpenseDuration { + oneMonth, + threeMonths, + sixMonths, + twelveMonths, + all, +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index d4ad614..906e33e 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,6 +1,6 @@ class ApiEndpoints { - // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; - static const String baseUrl = "https://api.marcoaiot.com/api"; + static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // Dashboard Module API Endpoints diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 2f426ab..86b3e8d 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -25,6 +25,7 @@ import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_respo import 'package:marco/model/all_organization_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'; class ApiService { static const bool enableLogs = true; @@ -294,6 +295,48 @@ class ApiService { } } + /// Get Monthly Expense Report (categoryId is optional) + static Future + getDashboardMonthlyExpensesApi({ + String? categoryId, + int months = 12, + }) async { + const endpoint = ApiEndpoints.getDashboardMonthlyExpenses; + logSafe("Fetching Dashboard Monthly Expenses for last $months months"); + + try { + final queryParams = { + 'months': months.toString(), + if (categoryId != null && categoryId.isNotEmpty) + 'categoryId': categoryId, + }; + + final response = await _getRequest( + endpoint, + queryParams: queryParams, + ); + + if (response == null) { + logSafe("Monthly Expense request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData(response, + label: "Dashboard Monthly Expenses"); + + if (jsonResponse != null) { + return DashboardMonthlyExpenseResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getDashboardMonthlyExpensesApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + /// Get Expense Type Report static Future getExpenseTypeReportApi({ required String projectId, diff --git a/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart index a777767..3435b20 100644 --- a/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart +++ b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart @@ -302,8 +302,17 @@ class _AttendanceChart extends StatelessWidget { child: SfCartesianChart( tooltipBehavior: TooltipBehavior(enable: true, shared: true), legend: Legend(isVisible: true, position: LegendPosition.bottom), - primaryXAxis: CategoryAxis(labelRotation: 45), - primaryYAxis: NumericAxis(minimum: 0, interval: 1), + primaryXAxis: CategoryAxis( + labelRotation: 45, + majorGridLines: + const MajorGridLines(width: 0), // removes vertical grid lines + ), + primaryYAxis: NumericAxis( + minimum: 0, + interval: 1, + majorGridLines: + const MajorGridLines(width: 0), // removes horizontal grid lines + ), series: rolesWithData.map((role) { final seriesData = filteredDates .map((date) { diff --git a/lib/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart b/lib/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart new file mode 100644 index 0000000..48c1288 --- /dev/null +++ b/lib/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart @@ -0,0 +1,440 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:marco/controller/dashboard/dashboard_controller.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/utils/utils.dart'; +import 'package:intl/intl.dart'; + +// ========================= +// CONSTANTS +// ========================= +class _ChartConstants { + static const List flatColors = [ + Color(0xFFE57373), + Color(0xFF64B5F6), + Color(0xFF81C784), + Color(0xFFFFB74D), + Color(0xFFBA68C8), + Color(0xFFFF8A65), + Color(0xFF4DB6AC), + Color(0xFFA1887F), + Color(0xFFDCE775), + Color(0xFF9575CD), + Color(0xFF7986CB), + Color(0xFFAED581), + Color(0xFFFF7043), + Color(0xFF4FC3F7), + Color(0xFFFFD54F), + Color(0xFF90A4AE), + Color(0xFFE573BB), + Color(0xFF81D4FA), + Color(0xFFBCAAA4), + Color(0xFFA5D6A7), + Color(0xFFCE93D8), + Color(0xFFFF8A65), + Color(0xFF80CBC4), + Color(0xFFFFF176), + Color(0xFF90CAF9), + Color(0xFFE0E0E0), + Color(0xFFF48FB1), + Color(0xFFA1887F), + Color(0xFFB0BEC5), + Color(0xFF81C784), + Color(0xFFFFB74D), + Color(0xFF64B5F6), + ]; + + static const Map durationLabels = { + MonthlyExpenseDuration.oneMonth: "1M", + MonthlyExpenseDuration.threeMonths: "3M", + MonthlyExpenseDuration.sixMonths: "6M", + MonthlyExpenseDuration.twelveMonths: "12M", + MonthlyExpenseDuration.all: "All", + }; + + static const double mobileBreakpoint = 600; + static const double mobileChartHeight = 350; + static const double desktopChartHeight = 400; + static const double mobilePadding = 12; + static const double desktopPadding = 20; + static const double mobileVerticalPadding = 16; + static const double desktopVerticalPadding = 20; + static const double noDataIconSize = 48; + static const double noDataContainerHeight = 220; + static const double labelRotation = 45; + static const int tooltipAnimationDuration = 300; +} + +// ========================= +// MAIN CHART WIDGET +// ========================= +class MonthlyExpenseDashboardChart extends StatelessWidget { + MonthlyExpenseDashboardChart({Key? key}) : super(key: key); + + final DashboardController _controller = Get.find(); + + Color _getColorForIndex(int index) => + _ChartConstants.flatColors[index % _ChartConstants.flatColors.length]; + + BoxDecoration get _containerDecoration => BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.05), + blurRadius: 6, + spreadRadius: 1, + offset: const Offset(0, 2), + ), + ], + ); + + bool _isMobileLayout(double screenWidth) => + screenWidth < _ChartConstants.mobileBreakpoint; + + double _calculateTotalExpense(List data) => + data.fold(0, (sum, item) => sum + item.total); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final isMobile = _isMobileLayout(screenWidth); + + return Obx(() { + final isLoading = _controller.isMonthlyExpenseLoading.value; + final expenseData = _controller.monthlyExpenseList; + final selectedDuration = _controller.selectedMonthlyExpenseDuration.value; + final totalExpense = _calculateTotalExpense(expenseData); + + return Container( + decoration: _containerDecoration, + padding: EdgeInsets.symmetric( + vertical: isMobile + ? _ChartConstants.mobileVerticalPadding + : _ChartConstants.desktopVerticalPadding, + horizontal: isMobile + ? _ChartConstants.mobilePadding + : _ChartConstants.desktopPadding, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ChartHeader( + selectedDuration: selectedDuration, + onDurationChanged: _controller.updateMonthlyExpenseDuration, + totalExpense: totalExpense, + ), + const SizedBox(height: 12), + SizedBox( + height: isMobile + ? _ChartConstants.mobileChartHeight + : _ChartConstants.desktopChartHeight, + child: _buildChartContent( + isLoading: isLoading, + data: expenseData, + isMobile: isMobile, + totalExpense: totalExpense, + ), + ), + ], + ), + ); + }); + } + + Widget _buildChartContent({ + required bool isLoading, + required List data, + required bool isMobile, + required double totalExpense, + }) { + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (data.isEmpty) { + return const _EmptyDataWidget(); + } + + return _MonthlyExpenseChart( + data: data, + getColor: _getColorForIndex, + isMobile: isMobile, + totalExpense: totalExpense, + ); + } +} + +// ========================= +// HEADER WIDGET +// ========================= +class _ChartHeader extends StatelessWidget { + const _ChartHeader({ + Key? key, + required this.selectedDuration, + required this.onDurationChanged, + required this.totalExpense, + }) : super(key: key); + + final MonthlyExpenseDuration selectedDuration; + final ValueChanged onDurationChanged; + final double totalExpense; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(), + const SizedBox(height: 2), + _buildSubtitle(), + const SizedBox(height: 8), + _buildDurationSelector(), + ], + ); + } + + Widget _buildTitle() => + MyText.bodyMedium('Monthly Expense Overview', fontWeight: 700); + + Widget _buildSubtitle() => + MyText.bodySmall('Month-wise total expense', color: Colors.grey); + + Widget _buildDurationSelector() { + return Row( + children: _ChartConstants.durationLabels.entries + .map((entry) => _DurationChip( + label: entry.value, + duration: entry.key, + isSelected: selectedDuration == entry.key, + onSelected: onDurationChanged, + )) + .toList(), + ); + } +} + +// ========================= +// DURATION CHIP WIDGET +// ========================= +class _DurationChip extends StatelessWidget { + const _DurationChip({ + Key? key, + required this.label, + required this.duration, + required this.isSelected, + required this.onSelected, + }) : super(key: key); + + final String label; + final MonthlyExpenseDuration duration; + final bool isSelected; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 4), + child: ChoiceChip( + label: Text(label, style: const TextStyle(fontSize: 12)), + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + selected: isSelected, + onSelected: (_) => onSelected(duration), + selectedColor: Colors.blueAccent.withOpacity(0.15), + backgroundColor: Colors.grey.shade200, + labelStyle: TextStyle( + color: isSelected ? Colors.blueAccent : Colors.black87, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: BorderSide( + color: isSelected ? Colors.blueAccent : Colors.grey.shade300, + ), + ), + ), + ); + } +} + +// ========================= +// EMPTY DATA WIDGET +// ========================= +class _EmptyDataWidget extends StatelessWidget { + const _EmptyDataWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: _ChartConstants.noDataContainerHeight, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.info_outline, + color: Colors.grey.shade400, + size: _ChartConstants.noDataIconSize, + ), + const SizedBox(height: 10), + MyText.bodyMedium( + 'No monthly expense data available.', + textAlign: TextAlign.center, + color: Colors.grey.shade500, + ), + ], + ), + ), + ); + } +} + +// ========================= +// CHART WIDGET +// ========================= +class _MonthlyExpenseChart extends StatelessWidget { + const _MonthlyExpenseChart({ + Key? key, + required this.data, + required this.getColor, + required this.isMobile, + required this.totalExpense, + }) : super(key: key); + + final List data; + final Color Function(int index) getColor; + final bool isMobile; + final double totalExpense; + + @override + Widget build(BuildContext context) { + return SfCartesianChart( + tooltipBehavior: _buildTooltipBehavior(), + primaryXAxis: _buildXAxis(), + primaryYAxis: _buildYAxis(), + series: [_buildColumnSeries()], + ); + } + + TooltipBehavior _buildTooltipBehavior() { + return TooltipBehavior( + enable: true, + builder: _tooltipBuilder, + animationDuration: _ChartConstants.tooltipAnimationDuration, + ); + } + + Widget _tooltipBuilder( + dynamic data, + dynamic point, + dynamic series, + int pointIndex, + int seriesIndex, + ) { + final value = data.total as double; + final percentage = totalExpense > 0 ? (value / totalExpense * 100) : 0; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: Colors.blueAccent, + borderRadius: BorderRadius.circular(4), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${data.monthName} ${data.year}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + Utils.formatCurrency(value), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + Text( + '${percentage.toStringAsFixed(1)}%', + style: const TextStyle( + color: Colors.white70, + fontWeight: FontWeight.w500, + fontSize: 10, + ), + ), + ], + ), + ); + } + + CategoryAxis _buildXAxis() { + return CategoryAxis( + labelRotation: _ChartConstants.labelRotation.toInt(), + majorGridLines: + const MajorGridLines(width: 0), // removes X-axis grid lines + ); + } + + NumericAxis _buildYAxis() { + return NumericAxis( + numberFormat: NumberFormat.simpleCurrency( + locale: 'en_IN', + name: '₹', + decimalDigits: 0, + ), + axisLabelFormatter: (AxisLabelRenderDetails args) { + return ChartAxisLabel(Utils.formatCurrency(args.value), null); + }, + majorGridLines: + const MajorGridLines(width: 0), // removes Y-axis grid lines + ); + } + + ColumnSeries _buildColumnSeries() { + return ColumnSeries( + dataSource: data, + xValueMapper: (d, _) => _ChartFormatter.formatMonthYear(d), + yValueMapper: (d, _) => d.total, + pointColorMapper: (_, index) => getColor(index), + name: 'Monthly Expense', + borderRadius: BorderRadius.circular(4), + dataLabelSettings: _buildDataLabelSettings(), + ); + } + + DataLabelSettings _buildDataLabelSettings() { + return DataLabelSettings( + isVisible: true, + builder: (data, _, __, ___, ____) => Text( + Utils.formatCurrency(data.total), + style: const TextStyle(fontSize: 11), + ), + ); + } +} + +// ========================= +// FORMATTER HELPER +// ========================= +class _ChartFormatter { + static String formatMonthYear(dynamic data) { + try { + final month = data.month ?? 1; + final year = data.year ?? DateTime.now().year; + final date = DateTime(year, month, 1); + final monthName = DateFormat('MMM').format(date); + final shortYear = year % 100; + return '$shortYear $monthName'; + } catch (e) { + return '${data.monthName} ${data.year}'; + } + } +} diff --git a/lib/helpers/widgets/dashbaord/project_progress_chart.dart b/lib/helpers/widgets/dashbaord/project_progress_chart.dart index 0bce9e7..9ada6ef 100644 --- a/lib/helpers/widgets/dashbaord/project_progress_chart.dart +++ b/lib/helpers/widgets/dashbaord/project_progress_chart.dart @@ -5,6 +5,7 @@ import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:marco/model/dashboard/project_progress_model.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/utils/utils.dart'; class ProjectProgressChart extends StatelessWidget { final List data; @@ -47,7 +48,6 @@ class ProjectProgressChart extends StatelessWidget { Color(0xFFFFB74D), Color(0xFF64B5F6), ]; - static final NumberFormat _commaFormatter = NumberFormat.decimalPattern(); Color _getTaskColor(String taskName) { final index = taskName.hashCode % _flatColors.length; @@ -71,7 +71,7 @@ class ProjectProgressChart extends StatelessWidget { color: Colors.grey.withOpacity(0.04), blurRadius: 6, spreadRadius: 1, - offset: Offset(0, 2), + offset: const Offset(0, 2), ), ], ), @@ -102,6 +102,7 @@ class ProjectProgressChart extends StatelessWidget { }); } + // ================= HEADER ================= Widget _buildHeader( String selectedRange, bool isChartView, double screenWidth) { return Column( @@ -129,7 +130,7 @@ class ProjectProgressChart extends StatelessWidget { color: Colors.grey, constraints: BoxConstraints( minHeight: 30, - minWidth: (screenWidth < 400 ? 28 : 36), + minWidth: screenWidth < 400 ? 28 : 36, ), isSelected: [isChartView, !isChartView], onPressed: (index) { @@ -185,50 +186,64 @@ class ProjectProgressChart extends StatelessWidget { ); } + // ================= CHART ================= Widget _buildChart(double height) { final nonZeroData = data.where((d) => d.planned != 0 || d.completed != 0).toList(); - if (nonZeroData.isEmpty) { - return _buildNoDataContainer(height); - } + if (nonZeroData.isEmpty) return _buildNoDataContainer(height); return Container( height: height > 280 ? 280 : height, padding: const EdgeInsets.all(6), decoration: BoxDecoration( - // Remove background color: Colors.transparent, borderRadius: BorderRadius.circular(5), ), child: SfCartesianChart( - tooltipBehavior: TooltipBehavior(enable: true), + tooltipBehavior: TooltipBehavior( + enable: true, + builder: (data, point, series, pointIndex, seriesIndex) { + final task = data as ChartTaskData; + final value = seriesIndex == 0 ? task.planned : task.completed; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: Colors.blueAccent, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + Utils.formatCurrency(value), + style: const TextStyle(color: Colors.white), + ), + ); + }, + ), legend: Legend(isVisible: true, position: LegendPosition.bottom), primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), axisLine: const AxisLine(width: 0), - labelRotation: 0, + labelRotation: 45, ), primaryYAxis: NumericAxis( - labelFormat: '{value}', axisLine: const AxisLine(width: 0), - majorTickLines: const MajorTickLines(size: 0), + majorGridLines: const MajorGridLines(width: 0), + labelFormat: '{value}', + numberFormat: NumberFormat.compact(), ), - series: [ + series: >[ ColumnSeries( name: 'Planned', dataSource: nonZeroData, - xValueMapper: (d, _) => DateFormat('MMM d').format(d.date), + xValueMapper: (d, _) => DateFormat('d MMM').format(d.date), yValueMapper: (d, _) => d.planned, color: _getTaskColor('Planned'), dataLabelSettings: DataLabelSettings( isVisible: true, - builder: (data, point, series, pointIndex, seriesIndex) { - final value = seriesIndex == 0 - ? (data as ChartTaskData).planned - : (data as ChartTaskData).completed; + builder: (data, _, __, ___, ____) { + final value = (data as ChartTaskData).planned; return Text( - _commaFormatter.format(value), + Utils.formatCurrency(value), style: const TextStyle(fontSize: 11), ); }, @@ -237,17 +252,15 @@ class ProjectProgressChart extends StatelessWidget { ColumnSeries( name: 'Completed', dataSource: nonZeroData, - xValueMapper: (d, _) => DateFormat('MMM d').format(d.date), + xValueMapper: (d, _) => DateFormat('d MMM').format(d.date), yValueMapper: (d, _) => d.completed, color: _getTaskColor('Completed'), dataLabelSettings: DataLabelSettings( isVisible: true, - builder: (data, point, series, pointIndex, seriesIndex) { - final value = seriesIndex == 0 - ? (data as ChartTaskData).planned - : (data as ChartTaskData).completed; + builder: (data, _, __, ___, ____) { + final value = (data as ChartTaskData).completed; return Text( - _commaFormatter.format(value), + Utils.formatCurrency(value), style: const TextStyle(fontSize: 11), ); }, @@ -258,14 +271,13 @@ class ProjectProgressChart extends StatelessWidget { ); } + // ================= TABLE ================= Widget _buildTable(double maxHeight, double screenWidth) { final containerHeight = maxHeight > 300 ? 300.0 : maxHeight; final nonZeroData = data.where((d) => d.planned != 0 || d.completed != 0).toList(); - if (nonZeroData.isEmpty) { - return _buildNoDataContainer(containerHeight); - } + if (nonZeroData.isEmpty) return _buildNoDataContainer(containerHeight); return Container( height: containerHeight, @@ -300,10 +312,14 @@ class ProjectProgressChart extends StatelessWidget { return DataRow( cells: [ DataCell(Text(DateFormat('d MMM').format(task.date))), - DataCell(Text('${task.planned}', - style: TextStyle(color: _getTaskColor('Planned')))), - DataCell(Text('${task.completed}', - style: TextStyle(color: _getTaskColor('Completed')))), + DataCell(Text( + Utils.formatCurrency(task.planned), + style: TextStyle(color: _getTaskColor('Planned')), + )), + DataCell(Text( + Utils.formatCurrency(task.completed), + style: TextStyle(color: _getTaskColor('Completed')), + )), ], ); }).toList(), @@ -315,6 +331,7 @@ class ProjectProgressChart extends StatelessWidget { ); } + // ================= NO DATA WIDGETS ================= Widget _buildNoDataContainer(double height) { return Container( height: height > 280 ? 280 : height, diff --git a/lib/model/dashboard/monthly_expence_model.dart b/lib/model/dashboard/monthly_expence_model.dart new file mode 100644 index 0000000..f28082b --- /dev/null +++ b/lib/model/dashboard/monthly_expence_model.dart @@ -0,0 +1,70 @@ +class DashboardMonthlyExpenseResponse { + final bool success; + final String message; + final List data; + final dynamic errors; + final int statusCode; + final String timestamp; + + DashboardMonthlyExpenseResponse({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory DashboardMonthlyExpenseResponse.fromJson(Map json) { + return DashboardMonthlyExpenseResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: (json['data'] as List?) + ?.map((e) => MonthlyExpenseData.fromJson(e)) + .toList() ?? + [], + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: json['timestamp'] ?? '', + ); + } + + Map toJson() => { + 'success': success, + 'message': message, + 'data': data.map((e) => e.toJson()).toList(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp, + }; +} + +class MonthlyExpenseData { + final String monthName; + final int year; + final double total; + final int count; + + MonthlyExpenseData({ + required this.monthName, + required this.year, + required this.total, + required this.count, + }); + + factory MonthlyExpenseData.fromJson(Map json) { + return MonthlyExpenseData( + monthName: json['monthName'] ?? '', + year: json['year'] ?? 0, + total: (json['total'] ?? 0).toDouble(), + count: json['count'] ?? 0, + ); + } + + Map toJson() => { + 'monthName': monthName, + 'year': year, + 'total': total, + 'count': count, + }; +} diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 1289e6c..8f303a8 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -16,8 +16,7 @@ import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart'; import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart'; import 'package:marco/helpers/widgets/dashbaord/expense_breakdown_chart.dart'; import 'package:marco/helpers/widgets/dashbaord/expense_by_status_widget.dart'; - - +import 'package:marco/helpers/widgets/dashbaord/monthly_expense_dashboard_chart.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @@ -79,11 +78,16 @@ class _DashboardScreenState extends State with UIMixin { width: double.infinity, child: DashboardOverviewWidgets.tasksOverview(), ), - ExpenseByStatusWidget(controller: dashboardController), + MySpacing.height(24), + + ExpenseByStatusWidget(controller: dashboardController), MySpacing.height(24), // Expense Type Report Chart ExpenseTypeReportChart(), + + MySpacing.height(24), + MonthlyExpenseDashboardChart(), ], ), ),