Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
985a075
feat: add categories breakdown entity and fromJson converter
mohamedshemees Feb 4, 2026
8bb42cb
feat: add statistics repository to get categories breakdown
mohamedshemees Feb 4, 2026
82be7e0
add:CategoryBreakdown widget skeleton for statistics screen
mohamedshemees Feb 4, 2026
b1cceff
feat: add statistics screen and related logic
mohamedshemees Feb 4, 2026
02f0f0e
feat: enhance CategoryBreakdown widget UI
mohamedshemees Feb 4, 2026
28a0759
Merge remote-tracking branch 'origin/dev' into feature/transactions-c…
mohamedshemees Feb 8, 2026
1fa0833
Merge remote-tracking branch 'origin/dev' into feature/transactions-c…
mohamedshemees Feb 11, 2026
ef9c0d4
Merge remote-tracking branch 'origin/dev' into feature/transactions-c…
mohamedshemees Feb 17, 2026
dc4ecdb
feature: Integrate categories breakdown into statistics screen
mohamedshemees Feb 18, 2026
a4cd65d
Merge remote-tracking branch 'origin/dev' into feature/transactions-c…
mohamedshemees Feb 19, 2026
055f525
fix: remove hard coded data from breakdown function
mohamedshemees Feb 19, 2026
fc936fa
feat: add top bar and enhance state management
mohamedshemees Feb 20, 2026
60735ed
feat: add localization to statistics screen
mohamedshemees Feb 20, 2026
a4b16de
feat: add empty state icon empty statitics card
mohamedshemees Feb 20, 2026
29fc8b5
refactor: use DefaultButton and update image in AppEmptyView
mohamedshemees Feb 25, 2026
8217bad
Merge remote-tracking branch 'origin/dev' into feature/transactions-c…
mohamedshemees Feb 25, 2026
262241e
refactor: delete routs.g.dart
mohamedshemees Feb 25, 2026
debcb57
fix: add missing comma in app_en.arb localization file
mohamedshemees Feb 25, 2026
575a1b9
fix: add missing string
mohamedshemees Feb 25, 2026
2a6c7aa
Merge remote-tracking branch 'origin/dev' into feature/transactions-c…
mohamedshemees Feb 26, 2026
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
Binary file added assets/images/img_no_analysis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions lib/core/l10n/app_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,7 @@
"categoriesFilterTitle": "تصفية الفئات",
"categoriesFilterClear": "مسح",
"categoriesFilterApply": "تطبيق"
"categoriesBreakdown": "تفاصيل الفئات",
"no_monthly_breakdown": "لا يوجد نفقات لتحليلها بعد",
"totalSpend": "إجمالي الإنفاق"
}
3 changes: 3 additions & 0 deletions lib/core/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,7 @@
"categoriesFilterTitle": "Categories filter",
"categoriesFilterClear": "Clear",
"categoriesFilterApply": "Apply"
"categoriesBreakdown": "Categories Breakdown",
"no_monthly_breakdown": "No expenses to analyze yet",
"totalSpend": "Total Spend"
}
41 changes: 0 additions & 41 deletions lib/data/repository/fake_statistics_repository.dart

This file was deleted.

50 changes: 37 additions & 13 deletions lib/data/repository/statistics_repository_impl.dart
Original file line number Diff line number Diff line change
@@ -1,40 +1,43 @@
import '../../core/service/supabase_service.dart';
import '../../core/errors/error_model.dart';
import '../../core/errors/result.dart';
import '../../data/service/supabase_service.dart';
import '../../domain/entity/categories_breakdown.dart';
import '../../domain/entity/monthly_overview.dart';
import '../../domain/repository/statistics_repository.dart';

class StatisticsRepositoryImpl implements StatisticsRepository {
final SupabaseService _supabaseService;

StatisticsRepositoryImpl({required SupabaseService supabaseService})
: _supabaseService = supabaseService;
: _supabaseService = supabaseService;

@override
Future<MonthlyOverview?> getMonthlyOverview({required DateTime month}) async {
Future<Result<MonthlyOverview>> getMonthlyOverview({
required DateTime month,
}) async {
try {
final client = await _supabaseService.getClient();
final userId = client.auth.currentUser?.id;

if (userId == null) {
return null;
return Result.error(ErrorModel('User not logged in'));
}

// Call RPC function
final response = await client.rpc(
'get_monthly_overview',
params: {
'in_year': month.year,
'in_month': month.month,
},
params: {'in_year': month.year, 'in_month': month.month},
);

if (response == null) {
return _createEmptyOverview();
return Result.success(_createEmptyOverview());
}

return _mapResponseToOverview(response as Map<String, dynamic>);
return Result.success(
_mapResponseToOverview(response as Map<String, dynamic>),
);
} catch (e) {
print('Error fetching monthly overview: $e');
rethrow;
return Result.error(ErrorModel(e.toString()));
}
}

Expand Down Expand Up @@ -112,4 +115,25 @@ class StatisticsRepositoryImpl implements StatisticsRepository {
}
return value.toStringAsFixed(0);
}
}

@override
Future<Result<CategoriesBreakdown>> getCategoriesBreakDown({
required DateTime date,
}) async {
try {
final client = await _supabaseService.getClient();
final data = await client.rpc(
'get_expenses_categories_breakdown',
params: {'in_year': date.year, 'in_month': date.month},
);
if (data == null) {
return Result.success(
CategoriesBreakdown(categories: [], totalSpend: 0.0),
);
}
return Result.success(CategoriesBreakdown.fromJson(data));
} catch (e) {
return Result.error(ErrorModel(e.toString()));
}
}
}
1 change: 1 addition & 0 deletions lib/design_system/assets/app_assets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class AppAssets {
static const String icEmptyTransactionImage = '$_images/empty_transaction_image.png';
static const String icEmptyTransactionPattern = '$_icons/empty_transaction_pattern.svg';
static const String icFilter = "$_icons/ic_filter.svg";
static const String imgNoAnalysis = "$_images/img_no_analysis.png";
static const String icAddAmount = "$_icons/ic_add_transaction_income.svg";
static const String icAddExpense = "$_icons/ic_add_transaction_expense.svg";
}
25 changes: 8 additions & 17 deletions lib/design_system/widgets/app_empty_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:moneyplus/design_system/assets/app_assets.dart';
import 'package:moneyplus/design_system/theme/money_extension_context.dart';

import 'buttons/button/default_button.dart';

class AppEmptyView extends StatelessWidget {
final String title;
final String subtitle;
Expand All @@ -25,11 +27,9 @@ class AppEmptyView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
// todo : AppAssets.imgStatisticsEmpty,
AppAssets.logo,
color: Colors.red,
width: 120,
height: 120,
AppAssets.imgNoAnalysis,
width: 105,
height: 96,
),
const SizedBox(height: 24),
Text(
Expand All @@ -50,18 +50,9 @@ class AppEmptyView extends StatelessWidget {
if (buttonText != null && onButtonPressed != null) ...[
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
child: DefaultButton(
onPressed: onButtonPressed,
style: ElevatedButton.styleFrom(
backgroundColor: context.colors.primary,
foregroundColor: context.colors.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: Text(buttonText!),
text: buttonText!,
),
),
],
Expand All @@ -70,4 +61,4 @@ class AppEmptyView extends StatelessWidget {
),
);
}
}
}
36 changes: 36 additions & 0 deletions lib/domain/entity/categories_breakdown.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

class CategoriesBreakdown {
final List<BreakDownCategory> categories;
final double totalSpend;

CategoriesBreakdown({
required this.categories,
required this.totalSpend,
});

factory CategoriesBreakdown.fromJson(Map<String, dynamic> json) => CategoriesBreakdown(
categories: List<BreakDownCategory>.from(json["categories"].map((category) => BreakDownCategory.fromJson(category))),
totalSpend: json["total_spend"]?.toDouble(),
);
}

class BreakDownCategory {
final int? id;
final String name;
final double spend;
final double percentage;

BreakDownCategory({
this.id,
required this.name,
required this.spend,
required this.percentage,
});

factory BreakDownCategory.fromJson(Map<String, dynamic> json) => BreakDownCategory(
id: json["id"],
name: json["name"],
spend: json["spend"]?.toDouble(),
percentage: json["percentage"]?.toDouble(),
);
}
9 changes: 7 additions & 2 deletions lib/domain/repository/statistics_repository.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import '../entity/monthly_overview.dart';

import 'package:moneyplus/domain/entity/categories_breakdown.dart';

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

abstract class StatisticsRepository {
Future<MonthlyOverview?> getMonthlyOverview({required DateTime month});
}
Future<Result<MonthlyOverview>> getMonthlyOverview({required DateTime month});
Future<Result<CategoriesBreakdown>> getCategoriesBreakDown({required DateTime date});
}
1 change: 1 addition & 0 deletions lib/presentation/main_container/screen/main_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:moneyplus/design_system/theme/money_extension_context.dart';
import 'package:moneyplus/presentation/main_container/cubit/main_state.dart';
import 'package:moneyplus/presentation/statistics/statistics_screen.dart';
import 'package:moneyplus/presentation/transactions/screen/transactions_screen.dart';

import '../../../design_system/widgets/nav_bar.dart';
Expand Down
39 changes: 26 additions & 13 deletions lib/presentation/statistics/cubit/statistics_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,33 @@ class StatisticsCubit extends Cubit<StatisticsState> {

emit(const StatisticsLoading());

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

emit(
StatisticsSuccess(
monthlyOverview: monthlyOverview,
selectedMonth: selectedMonth,
),
);
} catch (e) {
emit(StatisticsFailure(e.toString()));
}
categoriesBreakdownResult.when(
onSuccess: (categoriesBreakdown) {
emit(
StatisticsSuccess(
monthlyOverview: monthlyOverview,
selectedMonth: selectedMonth,
categoriesBreakdown: categoriesBreakdown,
),
);
},
onError: (error) {
emit(StatisticsFailure(error.message));
},
);
},
onError: (error) {
emit(StatisticsFailure(error.message));
},
);
}

void changeMonth(DateTime month) {
Expand Down
10 changes: 7 additions & 3 deletions lib/presentation/statistics/cubit/statistics_state.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:moneyplus/domain/entity/categories_breakdown.dart';

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

sealed class StatisticsState {
Expand All @@ -13,19 +15,21 @@ class StatisticsLoading extends StatisticsState {
}

class StatisticsSuccess extends StatisticsState {
final MonthlyOverview? monthlyOverview;
final MonthlyOverview monthlyOverview;
final DateTime selectedMonth;
final CategoriesBreakdown categoriesBreakdown;

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

bool get hasNoData => monthlyOverview == null;
bool get hasNoData => monthlyOverview.isEmpty && categoriesBreakdown.categories.isEmpty;
}

class StatisticsFailure extends StatisticsState {
final String message;

const StatisticsFailure(this.message);
}
}
Loading
Loading