Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions lib/data/repository/statistics_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import '../../core/errors/result.dart';
import '../../core/service/supabase_service.dart';
import '../../domain/entity/categories_breakdown.dart';
import '../../domain/entity/monthly_overview.dart';
import '../../domain/entity/spending_trend_point.dart';
import '../../domain/repository/statistics_repository.dart';

class StatisticsRepositoryImpl implements StatisticsRepository {
Expand Down Expand Up @@ -135,4 +136,35 @@ class StatisticsRepositoryImpl implements StatisticsRepository {
return Result.error(ErrorModel(e.toString()));
}
}

@override
Future<Result<SpendingTrend>> getSpendingTrend({required DateTime month}) async {
try {
final client = await _supabaseService.getClient();

final data = await client.rpc(
'get_spending_trend',
params: {'in_year': month.year, 'in_month': month.month},
);

if (data == null || (data as List).isEmpty) {
return Result.success(SpendingTrend(points: [], currency: 'IQD'));
}

final points = (data as List<dynamic>).map((item) {
final map = item as Map<String, dynamic>;
return SpendingTrendPoint(
date: DateTime.parse(map['spend_date'] as String),
amount: (map['total_amount'] as num).toDouble(),
);
}).toList();

final currency = (data.first as Map<String, dynamic>)['currency'] as String? ?? 'IQD';

return Result.success(SpendingTrend(points: points, currency: currency));
} catch (e) {
return Result.error(ErrorModel(e.toString()));
}
}

}
15 changes: 15 additions & 0 deletions lib/domain/entity/spending_trend_point.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class SpendingTrendPoint {
final DateTime date;
final double amount;

const SpendingTrendPoint({required this.date, required this.amount});
}

class SpendingTrend {
final List<SpendingTrendPoint> points;
final String currency;

const SpendingTrend({required this.points, required this.currency});

bool get isEmpty => points.isEmpty;
}
2 changes: 2 additions & 0 deletions lib/domain/repository/statistics_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import '../entity/monthly_overview.dart';
import 'package:moneyplus/domain/entity/categories_breakdown.dart';

import '../../core/errors/result.dart';
import '../entity/spending_trend_point.dart';

abstract class StatisticsRepository {
Future<Result<MonthlyOverview>> getMonthlyOverview({required DateTime month});
Future<Result<CategoriesBreakdown>> getCategoriesBreakDown({required DateTime date});
Future<Result<SpendingTrend>> getSpendingTrend({required DateTime month});
}
28 changes: 18 additions & 10 deletions lib/presentation/statistics/cubit/statistics_cubit.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';

import '../../../domain/entity/spending_trend_point.dart';
import '../../../domain/repository/statistics_repository.dart';
import 'statistics_state.dart';

Expand All @@ -11,35 +13,41 @@ class StatisticsCubit extends Cubit<StatisticsState> {

Future<void> loadStatistics({DateTime? month}) async {
final selectedMonth = month ?? DateTime(2026, 2, 1);

emit(const StatisticsLoading());

final monthlyOverviewResult = await _repository.getMonthlyOverview(
month: selectedMonth,
);

monthlyOverviewResult.when(
onSuccess: (monthlyOverview) async {
final categoriesBreakdownResult =
await _repository.getCategoriesBreakDown(date:selectedMonth);
final categoriesResult = await _repository.getCategoriesBreakDown(
date: selectedMonth,
);
final trendResult = await _repository.getSpendingTrend(
month: selectedMonth,
);

categoriesBreakdownResult.when(
categoriesResult.when(
onSuccess: (categoriesBreakdown) {
final spendingTrend = trendResult.when(
onSuccess: (trend) => trend,
onError: (_) => SpendingTrend(points: [], currency: 'IQD'),
);

emit(
StatisticsSuccess(
monthlyOverview: monthlyOverview,
selectedMonth: selectedMonth,
categoriesBreakdown: categoriesBreakdown,
spendingTrend: spendingTrend,
),
);
},
onError: (error) {
emit(StatisticsFailure(error.message));
},
onError: (error) => emit(StatisticsFailure(error.message)),
);
},
onError: (error) {
emit(StatisticsFailure(error.message));
},
onError: (error) => emit(StatisticsFailure(error.message)),
);
}

Expand Down
3 changes: 3 additions & 0 deletions lib/presentation/statistics/cubit/statistics_state.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:moneyplus/domain/entity/categories_breakdown.dart';

import '../../../domain/entity/monthly_overview.dart';
import '../../../domain/entity/spending_trend_point.dart';

sealed class StatisticsState {
const StatisticsState();
Expand All @@ -18,11 +19,13 @@ class StatisticsSuccess extends StatisticsState {
final MonthlyOverview monthlyOverview;
final DateTime selectedMonth;
final CategoriesBreakdown categoriesBreakdown;
final SpendingTrend spendingTrend;

const StatisticsSuccess({
required this.monthlyOverview,
required this.selectedMonth,
required this.categoriesBreakdown,
required this.spendingTrend,
});

bool get hasNoData => monthlyOverview.isEmpty && categoriesBreakdown.categories.isEmpty;
Expand Down
14 changes: 14 additions & 0 deletions lib/presentation/statistics/statistics_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import 'package:moneyplus/design_system/widgets/app_bar.dart';
import 'package:moneyplus/design_system/widgets/app_empty_view.dart';
import 'package:moneyplus/design_system/widgets/app_error_view.dart';
import 'package:moneyplus/design_system/widgets/app_loading_indicator.dart';
import 'package:moneyplus/presentation/statistics/utils.dart';
import 'package:moneyplus/presentation/statistics/widgets/CategoryBreakdown.dart';
import 'package:moneyplus/presentation/statistics/widgets/highest_spending_banner.dart';
import 'package:moneyplus/presentation/transactions/screen/transactions_screen.dart';

import '../../design_system/chart/spending_trend_graph.dart';
import '../transactions/widget/add_transaction_bottom_sheet.dart';
import '../widgets/drop_down_date_dialog.dart';
import 'cubit/statistics_cubit.dart';
Expand Down Expand Up @@ -88,6 +91,8 @@ class _StatisticsViewState extends State<StatisticsView> {
);
}

final trendDataPoints = state.spendingTrend.toDataPoints();

return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
Expand All @@ -98,6 +103,15 @@ class _StatisticsViewState extends State<StatisticsView> {
CategoryBreakdownWidget(
categoriesBreakdown: state.categoriesBreakdown,
),
const SizedBox(height: 16),

SpendingTrendGraph(
data: trendDataPoints,
currency: state.spendingTrend.currency,
),
const SizedBox(height: 8),
HighestSpendingBanner(trend: state.spendingTrend),
const SizedBox(height: 16),
],
),
),
Expand Down
13 changes: 13 additions & 0 deletions lib/presentation/statistics/utils.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@

import 'package:moneyplus/design_system/chart/models/data_point.dart';

import '../../domain/entity/spending_trend_point.dart';

String formatNumber(double value) {
if (value >= 1000000) {
return '${(value / 1000000).toStringAsFixed(1)}M';
Expand All @@ -6,3 +11,11 @@ String formatNumber(double value) {
}
return value.toStringAsFixed(0);
}

extension SpendingTrendMapper on SpendingTrend {
List<DataPoint> toDataPoints() {
return points
.map((p) => DataPoint(date: p.date, amount: p.amount))
.toList();
}
}
72 changes: 72 additions & 0 deletions lib/presentation/statistics/widgets/highest_spending_banner.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:moneyplus/core/l10n/app_localizations.dart';
import 'package:moneyplus/design_system/constants/design_constants.dart';
import 'package:moneyplus/design_system/theme/money_extension_context.dart';

import '../../../domain/entity/spending_trend_point.dart';

class HighestSpendingBanner extends StatelessWidget {
final SpendingTrend trend;

const HighestSpendingBanner({super.key, required this.trend});

@override
Widget build(BuildContext context) {
if (trend.isEmpty) return const SizedBox.shrink();

final l10n = AppLocalizations.of(context)!;
final peak = trend.points.reduce((a, b) => a.amount >= b.amount ? a : b);
final day = peak.date.day;
final monthAbbr = _monthAbbr(peak.date.month);

return Container(
height: DesignConstants.savingsBannerHeight,
padding: const EdgeInsets.symmetric(
horizontal: DesignConstants.spacingSmall,
),
decoration: BoxDecoration(
color: context.colors.redVariant,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(DesignConstants.radiusMedium),
bottomRight: Radius.circular(DesignConstants.radiusMedium),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.warning_amber_rounded,
size: 12,
color: context.colors.primary,
),
const SizedBox(width: DesignConstants.spacingXSmall),
Text(
l10n.highest_spending_message('$day $monthAbbr'),
style: context.typography.label.xSmall?.copyWith(
color: context.colors.red,
),
),
],
),
);
}

String _monthAbbr(int month) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
return months[month - 1];
}
}
Loading