diff --git a/.gitignore b/.gitignore
index b815a037..faa53133 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
+# ==============================
# Miscellaneous
+# ==============================
*.class
*.log
*.pyc
@@ -15,39 +17,49 @@ migrate_working_dir/
*.secret
*.properties
*.lock
-# IntelliJ related
+
+# ==============================
+# IDE
+# ==============================
*.iml
*.ipr
*.iws
.idea/
-# The .vscode folder contains launch configuration and tasks you configure in
-# VS Code which you may wish to be included in version control, so this line
-# is commented out by default.
-#.vscode/
+# Uncomment if you DON'T want VS Code settings in git
+# .vscode/
-# Flutter/Dart/Pub related
+# ==============================
+# Flutter / Dart / Pub
+# ==============================
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
-/build/
-/coverage/
-pubspec.lock
+build/
+coverage/
-# Symbolication related
-app.*.symbols
+# ⚠️ If this is a PACKAGE keep this ignored
+# ⚠️ If this is an APP you may want to track it
+pubspec.lock
-# Obfuscation related
-app.*.map.json
+# Generated files
+*.g.dart
+*.freezed.dart
+*.gen.dart
-# Android Studio will place build artifacts here
-/android/app/debug
-/android/app/profile
-/android/app/release
+# ==============================
+# iOS
+# ==============================
+ios/Flutter/Debug.xcconfig
+ios/Flutter/Release.xcconfig
+ios/Podfile
-# Flutter generated localization files
-**/l10n/app_localizations.dart
-**/l10n/app_localizations_*.dart
\ No newline at end of file
+# ===========================
+/lib/presentation/navigation/routes.g.dart
+/lib/core/l10n/app_localizations.dart
+/lib/core/l10n/app_localizations_ar.dart
+/lib/core/l10n/app_localizations_en.dart
+/lib/utils/CMakeLists.txt
diff --git a/assets/icons/ic_money_remove.svg b/assets/icons/ic_money_remove.svg
new file mode 100644
index 00000000..c83fd56b
--- /dev/null
+++ b/assets/icons/ic_money_remove.svg
@@ -0,0 +1,17 @@
+
diff --git a/assets/icons/ic_wallet_add.svg b/assets/icons/ic_wallet_add.svg
new file mode 100644
index 00000000..bc236eb4
--- /dev/null
+++ b/assets/icons/ic_wallet_add.svg
@@ -0,0 +1,14 @@
+
diff --git a/ios/.gitignore b/ios/.gitignore
index 7a7f9873..0e7682ca 100644
--- a/ios/.gitignore
+++ b/ios/.gitignore
@@ -32,3 +32,5 @@ Runner/GeneratedPluginRegistrant.*
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+/Flutter/Debug.xcconfig
+/Flutter/Release.xcconfig
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
deleted file mode 100644
index 592ceee8..00000000
--- a/ios/Flutter/Debug.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
deleted file mode 100644
index 592ceee8..00000000
--- a/ios/Flutter/Release.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/lib/di/injection.dart b/lib/core/di/injection.dart
similarity index 52%
rename from lib/di/injection.dart
rename to lib/core/di/injection.dart
index cef2fab7..80de8b42 100644
--- a/lib/di/injection.dart
+++ b/lib/core/di/injection.dart
@@ -1,21 +1,25 @@
import 'package:get_it/get_it.dart';
import 'package:moneyplus/domain/repository/transaction_repository.dart';
import 'package:moneyplus/presentation/account_setup/cubit/account_setup_cubit.dart';
-import 'package:moneyplus/presentation/transactions/cubit/transaction_cubit.dart';
-
-import '../data/repository/account_repository.dart';
-import '../data/repository/authentication_repository.dart';
-import '../data/repository/user_money_repository.dart';
-import '../data/repository/transaction_repository_stub.dart';
-import '../data/service/app_secrets_provider.dart';
-import '../data/service/supabase_service.dart';
-import '../domain/repository/account_repository.dart';
-import '../domain/repository/authentication_repository.dart';
-import '../domain/repository/user_money_repository.dart';
-import '../domain/validator/authentication_validator.dart';
-import '../presentation/home/cubit/home_cubit.dart';
-import '../presentation/income/cubit/add_income_cubit.dart';
-import '../presentation/login/cubit/login_cubit.dart';
+import '../../data/repository/account_repository.dart';
+import '../../data/repository/authentication_repository.dart';
+import '../../data/repository/statistics_repository_impl.dart';
+import '../../data/repository/transaction_repository_stub.dart';
+import '../../data/repository/user_money_repository.dart';
+import '../../data/repository/fake_statistics_repository.dart';
+import '../../data/service/app_secrets_provider.dart';
+import '../../data/service/supabase_service.dart';
+import '../../domain/repository/account_repository.dart';
+import '../../domain/repository/authentication_repository.dart';
+import '../../domain/repository/statistics_repository.dart';
+import '../../domain/repository/user_money_repository.dart';
+import '../../domain/validator/authentication_validator.dart';
+import '../../domain/repository/transaction_repository.dart';
+import '../../presentation/home/cubit/home_cubit.dart';
+import '../../presentation/income/cubit/add_income_cubit.dart';
+import '../../presentation/login/cubit/login_cubit.dart';
+import '../../presentation/statistics/cubit/statistics_cubit.dart';
+import '../../presentation/transactions/cubit/transaction_cubit.dart';
final getIt = GetIt.instance;
@@ -69,4 +73,22 @@ void initDI() {
() =>
TransactionCubit(transactionRepository: getIt()),
);
+
+ // Statistics
+ // TODO: Remove this when done testing UI
+ getIt.registerLazySingleton(
+ () => FakeStatisticsRepository(),
+ );
+
+ // getIt.registerLazySingleton(
+ // () => StatisticsRepositoryImpl(
+ // supabaseService: getIt(),
+ // ),
+ // );
+
+ getIt.registerFactory(
+ () => StatisticsCubit(
+ repository: getIt(),
+ ),
+ );
}
diff --git a/lib/core/l10n/app_ar.arb b/lib/core/l10n/app_ar.arb
index 4e968d56..dfc85f71 100644
--- a/lib/core/l10n/app_ar.arb
+++ b/lib/core/l10n/app_ar.arb
@@ -12,24 +12,23 @@
"moneyAmount": "{amount} {currency} ",
"date": "التاريخ",
"setUpYourAccount": "لنقم بإعداد حسابك",
- "setSalary": "تحديد الراتب",
- "currency": "العملة",
- "salaryDay": "يوم الراتب",
- "fromEachMonth": "من كل شهر",
- "salary": "الراتب",
- "accountSetup": "إعداد الحساب",
- "search": "...بحث",
- "select": "اختيار",
- "next": "التالي",
- "finishSetup": "إنهاء الإعداد",
- "stepOf": "الخطوة {current} من {total}",
- "stepOfTotal": "الخطوة {current} من {total}",
+ "setSalary": "تحديد الراتب",
+ "currency": "العملة",
+ "salaryDay": "يوم الراتب",
+ "fromEachMonth": "من كل شهر",
+ "salary": "الراتب",
+ "accountSetup": "إعداد الحساب",
+ "search": "...بحث",
+ "select": "اختيار",
+ "next": "التالي",
+ "finishSetup": "إنهاء الإعداد",
+ "stepOf": "الخطوة {current} من {total}",
+ "stepOfTotal": "الخطوة {current} من {total}",
"forgetPasswordAppBarTitle": "نسيت كلمة السر",
"forgetPasswordTitle": "نسيت كلمة المرور الخاصة بك",
"forgetPasswordSubtitle": "أدخل بريدك الإلكتروني وسنرسل لك رابطًا لإعادة التعيين",
"forgetPasswordEmailHint": "البريد الإلكتروني",
"forgetPasswordButton": "إرسال رابط إعادة التعيين",
-
"updatePasswordAppBarTitle": "تحديث كلمة المرور",
"updatePasswordTitle": "أدخل كلمة مرور جديدة لـ",
"updatePasswordPasswordHint": "كلمة المرور الجديدة",
@@ -38,9 +37,9 @@
"updatePasswordSuccessMessage": "تم تحديث كلمة المرور بنجاح",
"updatePasswordErrorMessage": "خطأ في تحديث كلمة المرور",
"error": "خطأ",
- "success" : "نجاح",
- "loading" : "تحميل",
- "login_successfully" : "تم تسجيل الدخول بنجاح",
+ "success": "نجاح",
+ "loading": "تحميل",
+ "login_successfully": "تم تسجيل الدخول بنجاح",
"login_welcome_title": "أهلاً بك مجدداً!",
"login_welcome_subtitle": "أدخل بياناتك الخاصة للوصول إلى حسابك",
"login_email_hint": "البريد الإلكتروني",
@@ -74,6 +73,28 @@
"currencyCode": "دينار",
"spendingTrend": "اتجاه الإنفاق",
"noDataAvailable": "لا توجد بيانات متاحة",
+ "statistics": "الإحصائيات",
+ "monthly_overview": "نظرة عامة شهرية",
+ "savings_message": "لقد وفرت {amount} {currency} هذا الشهر",
+ "@savings_message": {
+ "placeholders": {
+ "amount": {},
+ "currency": {}
+ }
+ },
+ "highest_spending_message": "أعلى إنفاق لك كان في {date}",
+ "@highest_spending_message": {
+ "placeholders": {
+ "date": {}
+ }
+ },
+ "no_statistics_title": "لا توجد بيانات كافية بعد",
+ "no_statistics_subtitle": "أضف المزيد من المعاملات لرؤية الإحصائيات",
+ "add_transaction": "إضافة معاملة",
+ "no_monthly_overview": "لم يتم تسجيل أي دخل أو مصروفات لهذه الفترة",
+ "error_loading_statistics": "فشل تحميل الإحصائيات",
+ "retry": "إعادة المحاولة",
+ "noDataAvailable": "لا توجد بيانات متاحة",
"no_spending_categories": "لا يوجد معاملات متاحة",
"month_january": "يناير",
"month_february": "فبراير",
@@ -96,9 +117,9 @@
"transaction": "المعاملات",
"transaction_error_title": "خطأ في المعاملات",
"transaction_error_content": "فشل في تحميل المعاملات",
- "manage_categories" : "ادارةالتصنيفات",
+ "manage_categories": "ادارةالتصنيفات",
"add_new_category": "اضافة تصنيف جديد",
- "add_custom_category" : "اضافة تصنيف مخصص",
- "category_name" : "اسم التصنيف",
- "add" : "اضافة"
+ "add_custom_category": "اضافة تصنيف مخصص",
+ "category_name": "اسم التصنيف",
+ "add": "اضافة"
}
diff --git a/lib/core/l10n/app_en.arb b/lib/core/l10n/app_en.arb
index 01cd477d..0ef30aa9 100644
--- a/lib/core/l10n/app_en.arb
+++ b/lib/core/l10n/app_en.arb
@@ -38,19 +38,18 @@
"next": "Next",
"finishSetup": "Finish setup",
"stepOf": "Step {current} of {total}",
- "stepOfTotal": "Step {current} of {total}",
- "@stepOfTotal": {
- "placeholders": {
- "current": {},
- "total": {}
- }
- },
+ "stepOfTotal": "Step {current} of {total}",
+ "@stepOfTotal": {
+ "placeholders": {
+ "current": {},
+ "total": {}
+ }
+ },
"forgetPasswordAppBarTitle": "Forget Password",
"forgetPasswordTitle": "Forget your password",
"forgetPasswordSubtitle": "Enter your email and we’ll send you a reset link",
"forgetPasswordEmailHint": "Email",
"forgetPasswordButton": "Send Reset Link",
-
"updatePasswordAppBarTitle": "Update Password",
"updatePasswordTitle": "Enter a new password for",
"updatePasswordPasswordHint": "New Password",
@@ -58,11 +57,10 @@
"updatePasswordButton": "Update Password",
"updatePasswordSuccessMessage": "Password updated successfully",
"updatePasswordErrorMessage": "Error updating password",
-
"error": "Error",
- "success" : "Success",
- "loading" : "Loading",
- "login_successfully" : "Login Successfully",
+ "success": "Success",
+ "loading": "Loading",
+ "login_successfully": "Login Successfully",
"login_welcome_title": "Welcome again!",
"login_welcome_subtitle": "Enter your credentials to access your account",
"login_email_hint": "Email",
@@ -93,7 +91,6 @@
"auth_error_not_implemented": "This feature is not available on the server",
"auth_error_default": "An unexpected error occurred. Please try again later",
"no_internet_connection": "No internet connection",
-
"no_transaction_record_title": "No transaction records",
"no_transaction_record_content": "Add your first one to get started",
"add_transaction": "Add transaction",
@@ -103,7 +100,6 @@
"transaction": "Transaction",
"transaction_error_title": "transactions error",
"transaction_error_content": "failed to load transaction",
-
"currencyCode": "IQD",
"@stepOf": {
"placeholders": {
@@ -115,13 +111,7 @@
}
}
},
- "stepOfTotal": "Step {current} of {total}",
- "@stepOfTotal": {
- "placeholders": {
- "current": {},
- "total": {}
- }
- },
+ "monthly_overview": "Monthly Overview",
"no_spending_categories": "No spending categories available",
"month_january": "January",
"month_february": "February",
@@ -135,9 +125,36 @@
"month_october": "October",
"month_november": "November",
"month_december": "December",
- "manage_categories" : "Manage categories",
+ "manage_categories": "Manage categories",
"add_new_category": "Add new category",
- "add_custom_category" : "Add custom category",
- "category_name" : "Category name",
- "add" : "Add"
+ "add_custom_category": "Add custom category",
+ "category_name": "Category name",
+ "stepOfTotal": "Step {current} of {total}",
+ "@stepOfTotal": {
+ "placeholders": {
+ "current": {},
+ "total": {}
+ }
+ },
+ "statistics": "Statistics",
+ "monthly_overview": "Monthly Overview",
+ "savings_message": "You saved {amount} {currency} this month",
+ "@savings_message": {
+ "placeholders": {
+ "amount": {},
+ "currency": {}
+ }
+ },
+ "highest_spending_message": "Your highest spending was on {date}",
+ "@highest_spending_message": {
+ "placeholders": {
+ "date": {}
+ }
+ },
+ "no_statistics_title": "Not enough data yet",
+ "no_statistics_subtitle": "Add more transactions to see insights",
+ "add_transaction": "Add transaction",
+ "no_monthly_overview": "No income or expenses recorded for this period",
+ "error_loading_statistics": "Failed to load statistics",
+ "retry": "Retry"
}
\ No newline at end of file
diff --git a/lib/core/utils/number_formatter.dart b/lib/core/utils/number_formatter.dart
new file mode 100644
index 00000000..f4fc68e1
--- /dev/null
+++ b/lib/core/utils/number_formatter.dart
@@ -0,0 +1,15 @@
+class NumberFormatter {
+ NumberFormatter._();
+
+ static String formatWithCommas(double value) {
+ final intValue = value.toInt();
+ return intValue.toString().replaceAllMapped(
+ RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
+ (Match m) => '${m[1]},',
+ );
+ }
+
+ static String formatWithCurrency(double value, String currency) {
+ return '${formatWithCommas(value)} $currency';
+ }
+}
diff --git a/lib/data/repository/fake_statistics_repository.dart b/lib/data/repository/fake_statistics_repository.dart
new file mode 100644
index 00000000..6724f49d
--- /dev/null
+++ b/lib/data/repository/fake_statistics_repository.dart
@@ -0,0 +1,18 @@
+import '../../domain/entity/monthly_overview.dart';
+import '../../domain/repository/statistics_repository.dart';
+
+class FakeStatisticsRepository implements StatisticsRepository {
+ @override
+ Future getMonthlyOverview({required DateTime month}) async {
+ // Simulate network delay
+ await Future.delayed(const Duration(seconds: 1));
+
+ return const MonthlyOverview(
+ income: 5000.0,
+ expenses: 3500.0,
+ currency: "AED",
+ maxValue: 6000.0,
+ scaleLabels: ["0", "1k", "2k", "3k", "4k", "5k", "6k"],
+ );
+ }
+}
diff --git a/lib/data/repository/statistics_repository_impl.dart b/lib/data/repository/statistics_repository_impl.dart
new file mode 100644
index 00000000..e89bde74
--- /dev/null
+++ b/lib/data/repository/statistics_repository_impl.dart
@@ -0,0 +1,133 @@
+
+import '../../data/service/supabase_service.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;
+
+ @override
+ Future getMonthlyOverview({required DateTime month}) async {
+ final client = await _supabaseService.getClient();
+ // final userId = client.auth.currentUser?.id;
+
+ final userId = "cdc4f388-4c4a-4ab4-84b1-5076fbe759c9";
+ if (userId == null) {
+ return null;
+ }
+
+
+ // Get first and last day of the month
+ final startOfMonth = DateTime(month.year, month.month, 1);
+ final endOfMonth = DateTime(month.year, month.month + 1, 0, 23, 59, 59);
+
+ // Query transactions for the month
+ final response = await client
+ .from('transactions')
+ .select('''
+ amount,
+ transaction_type,
+ currency_id,
+ currencies!inner(abbreviation, abbreviation_ar)
+ ''')
+ .eq('user_id', userId)
+ .limit(100);
+
+ /* .gte('created_at', startOfMonth.toIso8601String())
+ .lte('created_at', endOfMonth.toIso8601String());*/
+
+ final transactions = response as List;
+
+ print('Total transactions found: ${transactions.length}');
+
+ if (transactions.isEmpty) {
+ return null;
+ }
+
+ // Calculate income and expenses
+ // Assuming transaction_type: 1 = income, 2 = expense (adjust based on your schema)
+ double totalIncome = 0;
+ double totalExpenses = 0;
+ String currency = 'IQD';
+
+ for (final transaction in transactions) {
+ final amount = (transaction['amount'] as num).toDouble();
+ final type = transaction['transaction_type'] as int;
+
+ // Get currency from first transaction
+ if (transaction['currencies'] != null) {
+ currency = transaction['currencies']['abbreviation'] ?? 'IQD';
+ }
+
+ if (type == 1) {
+ // Income
+ totalIncome += amount;
+ } else {
+ // Expense
+ totalExpenses += amount;
+ }
+ }
+
+ // Calculate max value and scale labels dynamically
+ final maxAmount = totalIncome > totalExpenses ? totalIncome : totalExpenses;
+ final maxValue = _calculateMaxValue(maxAmount);
+ final scaleLabels = _generateScaleLabels(maxValue);
+
+ return MonthlyOverview(
+ income: totalIncome,
+ expenses: totalExpenses,
+ currency: currency,
+ maxValue: maxValue,
+ scaleLabels: scaleLabels,
+ );
+ }
+
+ double _calculateMaxValue(double maxAmount) {
+ if (maxAmount <= 0) return 100000;
+
+ // Round up to nearest significant value
+ final magnitude = maxAmount.toString().length - 1;
+ final base = _pow(10, magnitude).toDouble();
+ return ((maxAmount / base).ceil() * base).toDouble();
+ }
+
+ int _pow(int base, int exponent) {
+ int result = 1;
+ for (int i = 0; i < exponent; i++) {
+ result *= base;
+ }
+ return result;
+ }
+
+ List _generateScaleLabels(double maxValue) {
+ final step = maxValue / 8;
+ final labels = [];
+
+ for (int i = 0; i <= 8; i++) {
+ final value = step * i;
+ labels.add(_formatScaleValue(value));
+ }
+
+ return labels;
+ }
+
+ String _formatScaleValue(double value) {
+ if (value == 0) return '0';
+ if (value >= 1000000) {
+ final millions = value / 1000000;
+ return millions == millions.truncate()
+ ? '${millions.truncate()}M'
+ : '${millions.toStringAsFixed(1)}M';
+ }
+ if (value >= 1000) {
+ final thousands = value / 1000;
+ return thousands == thousands.truncate()
+ ? '${thousands.truncate()}K'
+ : '${thousands.toStringAsFixed(0)}K';
+ }
+ return value.toStringAsFixed(0);
+ }
+}
\ No newline at end of file
diff --git a/lib/design_system/assets/app_assets.dart b/lib/design_system/assets/app_assets.dart
index 5aefe23a..6b374ff8 100644
--- a/lib/design_system/assets/app_assets.dart
+++ b/lib/design_system/assets/app_assets.dart
@@ -6,7 +6,7 @@ class AppAssets {
static const icAppLogo = '${_icons}ic_app_logo.svg';
static const icArrowDown = '$_icons/ic_arrow_down.svg';
static const icArrowUp = '$_icons/ic_arrow_up.svg';
- static const icArrowRight ='$_icons/ic_arrow_right.svg';
+ static const icArrowRight = '$_icons/ic_arrow_right.svg';
static const String iconCancel = "$_icons/ic_cancel.svg";
static const String iconError = "$_icons/ic_error.svg";
static const String iconSuccess = "$_icons/ic_success.svg";
@@ -19,7 +19,8 @@ class AppAssets {
static const String icAccountGray = '$_icons/ic_account_gray.svg';
static const String icStatisticsPrimary = '$_icons/ic_statistics_primary.svg';
static const String icStatisticsGray = '$_icons/ic_statistics_gray.svg';
- static const String icTransactionPrimary = '$_icons/ic_transaction_primary.svg';
+ static const String icTransactionPrimary =
+ '$_icons/ic_transaction_primary.svg';
static const String icTransactionGray = '$_icons/ic_transaction_gray.svg';
static const String icArrowDownRound = '$_icons/ic_arrow_down_round.svg';
static const String icCalender = '$_icons/ic_calendar.svg';
@@ -56,6 +57,8 @@ class AppAssets {
static const String icLoading = "$_icons/ic_loading.svg";
static const String icAmountPrimary = '$_icons/ic_amount_primary.svg';
static const String icAmountGray = '$_icons/ic_amount_gray.svg';
+ static const String icWalletAdd = '$_icons/ic_wallet_add.svg';
+ static const String icMoneyRemove = '$_icons/ic_money_remove.svg';
static const String lineSeparator = '$_images/line-separator.png';
static const String transactionCoinStack = '$_images/transaction-coin-stack.png';
static const String transactionDetailsBackground = '$_images/transaction-details-background.png';
diff --git a/lib/design_system/constants/design_constants.dart b/lib/design_system/constants/design_constants.dart
new file mode 100644
index 00000000..97806913
--- /dev/null
+++ b/lib/design_system/constants/design_constants.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+
+class DesignConstants {
+ DesignConstants._();
+
+ // Border Radius
+ static const double radiusSmall = 8.0;
+ static const double radiusMedium = 12.0;
+ static const double radiusLarge = 20.0;
+ static const double radiusXLarge = 30.0;
+
+ // Spacing
+ static const double spacingXSmall = 4.0;
+ static const double spacingSmall = 8.0;
+ static const double spacingMedium = 12.0;
+ static const double spacingLarge = 16.0;
+ static const double spacingXLarge = 24.0;
+
+ // Icon Sizes
+ static const double iconSizeSmall = 14.0;
+ static const double iconSizeMedium = 16.0;
+ static const double iconSizeLarge = 28.0;
+
+ // Component Sizes
+ static const double summaryIconContainerSize = 28.0;
+ static const double progressBarHeight = 20.0;
+ static const double savingsBannerHeight = 22.0;
+ static const double indicatorCircleSize = 6.0;
+
+ // Progress Bar
+ static const double progressBarBorderWidth = 0.5;
+ static const double progressBarBackgroundBorderWidth = 1.0;
+ static const double progressBarMinWidth = 20.0;
+
+ // Shadow
+ static const Offset progressBarShadowOffset = Offset(0, 4);
+ static const double progressBarShadowBlur = 8.0;
+ static const double progressBarShadowOpacity = 0.12;
+ static const int strokeAlpha = 10;
+}
+
+class GradientColors {
+ GradientColors._();
+
+ static const Color incomeStart = Color(0xFF0496AD);
+ static const Color incomeEnd = Color(0xFF097C8E);
+
+ static const Color expenseStart = Color(0xFFDC143C);
+ static const Color expenseEnd = Color(0xFFA01A35);
+
+ static List get incomeGradient => [incomeStart, incomeEnd];
+
+ static List get expenseGradient => [expenseStart, expenseEnd];
+}
diff --git a/lib/design_system/widgets/app_bar.dart b/lib/design_system/widgets/app_bar.dart
index aa894fcb..75835e6e 100644
--- a/lib/design_system/widgets/app_bar.dart
+++ b/lib/design_system/widgets/app_bar.dart
@@ -1,29 +1,29 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:moneyplus/design_system/assets/app_assets.dart';
-import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+ import 'package:flutter/material.dart';
+ import 'package:flutter_svg/flutter_svg.dart';
+ import 'package:moneyplus/design_system/assets/app_assets.dart';
+ import 'package:moneyplus/design_system/theme/money_extension_context.dart';
-class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
- final String? title;
- final Widget? leading;
- final Widget? trailing;
- final double? leadingWidth;
- final Color? backgroundColor;
+ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
+ final String? title;
+ final Widget? leading;
+ final Widget? trailing;
+ final double? leadingWidth;
+ final Color? backgroundColor;
- const CustomAppBar({
- super.key,
- this.title,
- this.leading,
- this.trailing,
- this.leadingWidth,
- this.backgroundColor,
- });
+ const CustomAppBar({
+ super.key,
+ this.title,
+ this.leading,
+ this.trailing,
+ this.leadingWidth,
+ this.backgroundColor,
+ });
- @override
- Widget build(BuildContext context) {
- final typo = context.typography;
- final colors = context.colors;
- final contentColor = colors.title;
+ @override
+ Widget build(BuildContext context) {
+ final typo = context.typography;
+ final colors = context.colors;
+ final contentColor = colors.title;
return AppBar(
backgroundColor: backgroundColor,
@@ -32,81 +32,81 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
leadingWidth: leadingWidth,
automaticallyImplyLeading: false,
- title: title != null
- ? Text(title!, style: typo.title.small.copyWith(color: contentColor))
- : null,
+ title: title != null
+ ? Text(title!, style: typo.title.small.copyWith(color: contentColor))
+ : null,
- leading: leading != null
- ? Padding(
- padding: const EdgeInsetsDirectional.only(start: 16.0),
- child: leading!,
- )
- : null,
+ leading: leading != null
+ ? Padding(
+ padding: const EdgeInsetsDirectional.only(start: 16.0),
+ child: leading!,
+ )
+ : null,
- actions: [
- if (trailing != null)
- Padding(
- padding: const EdgeInsetsDirectional.only(end: 16),
- child: trailing!,
- ),
- ],
- );
- }
+ actions: [
+ if (trailing != null)
+ Padding(
+ padding: const EdgeInsetsDirectional.only(end: 16),
+ child: trailing!,
+ ),
+ ],
+ );
+ }
- @override
- Size get preferredSize => const Size.fromHeight(kToolbarHeight);
-}
+ @override
+ Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+ }
-class AppBarCircleButton extends StatelessWidget {
- final String assetPath;
- final VoidCallback? onTap;
+ class AppBarCircleButton extends StatelessWidget {
+ final String assetPath;
+ final VoidCallback? onTap;
- const AppBarCircleButton({super.key, required this.assetPath, this.onTap});
+ const AppBarCircleButton({super.key, required this.assetPath, this.onTap});
- @override
- Widget build(BuildContext context) {
- return GestureDetector(
- onTap: onTap,
- child: Container(
- width: 40,
- height: 40,
- decoration: BoxDecoration(
- color: context.colors.surfaceHigh,
- shape: BoxShape.circle,
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: context.colors.surfaceHigh,
+ shape: BoxShape.circle,
+ ),
+ alignment: Alignment.center,
+ child: SvgPicture.asset(assetPath, width: 20, height: 20),
),
- alignment: Alignment.center,
- child: SvgPicture.asset(assetPath, width: 20, height: 20),
- ),
- );
+ );
+ }
}
-}
-class AppBarCalendar extends StatelessWidget {
- final VoidCallback onTap;
- final String date;
+ class AppBarCalendar extends StatelessWidget {
+ final VoidCallback onTap;
+ final String date;
- const AppBarCalendar({super.key, required this.onTap, required this.date});
+ const AppBarCalendar({super.key, required this.onTap, required this.date});
- @override
- Widget build(BuildContext context) {
- final typo = context.typography;
- final colors = context.colors;
- final contentColor = colors.title;
+ @override
+ Widget build(BuildContext context) {
+ final typo = context.typography;
+ final colors = context.colors;
+ final contentColor = colors.title;
- return GestureDetector(
- onTap: onTap,
- behavior: HitTestBehavior.opaque,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- spacing: 4,
- children: [
- Text(date, style: typo.label.small.copyWith(color: contentColor)),
- SvgPicture.asset(AppAssets.icNormalArrowDown, width: 20, height: 20),
- ],
+ return GestureDetector(
+ onTap: onTap,
+ behavior: HitTestBehavior.opaque,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 4,
+ children: [
+ Text(date, style: typo.label.small.copyWith(color: contentColor)),
+ SvgPicture.asset(AppAssets.icNormalArrowDown, width: 20, height: 20),
+ ],
+ ),
),
- ),
- );
+ );
+ }
}
-}
diff --git a/lib/design_system/widgets/app_empty_view.dart b/lib/design_system/widgets/app_empty_view.dart
new file mode 100644
index 00000000..f6ff2885
--- /dev/null
+++ b/lib/design_system/widgets/app_empty_view.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/design_system/assets/app_assets.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+
+class AppEmptyView extends StatelessWidget {
+ final String title;
+ final String subtitle;
+ final String? buttonText;
+ final VoidCallback? onButtonPressed;
+
+ const AppEmptyView({
+ super.key,
+ required this.title,
+ required this.subtitle,
+ this.buttonText,
+ this.onButtonPressed,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Image.asset(
+ // todo : AppAssets.imgStatisticsEmpty,
+ AppAssets.logo,
+ color: Colors.red,
+ width: 120,
+ height: 120,
+ ),
+ const SizedBox(height: 24),
+ Text(
+ title,
+ style: context.typography.title.medium.copyWith(
+ color: context.colors.title,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ subtitle,
+ style: context.typography.body.small.copyWith(
+ color: context.colors.body,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ if (buttonText != null && onButtonPressed != null) ...[
+ const SizedBox(height: 24),
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ 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!),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/design_system/widgets/app_error_view.dart b/lib/design_system/widgets/app_error_view.dart
new file mode 100644
index 00000000..7112a9ee
--- /dev/null
+++ b/lib/design_system/widgets/app_error_view.dart
@@ -0,0 +1,56 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/core/l10n/app_localizations.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+
+class AppErrorView extends StatelessWidget {
+ final String message;
+ final VoidCallback onRetry;
+
+ const AppErrorView({
+ super.key,
+ required this.message,
+ required this.onRetry,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = AppLocalizations.of(context)!;
+
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.error_outline,
+ size: 48,
+ color: context.colors.primary,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ l10n.error_loading_statistics,
+ style: context.typography.title.small.copyWith(
+ color: context.colors.title,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ message,
+ style: context.typography.body.small.copyWith(
+ color: context.colors.body,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 24),
+ ElevatedButton(
+ onPressed: onRetry,
+ child: Text(l10n.retry),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/design_system/widgets/app_loading_indicator.dart b/lib/design_system/widgets/app_loading_indicator.dart
new file mode 100644
index 00000000..4662ec15
--- /dev/null
+++ b/lib/design_system/widgets/app_loading_indicator.dart
@@ -0,0 +1,15 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+
+class AppLoadingIndicator extends StatelessWidget {
+ const AppLoadingIndicator({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: CircularProgressIndicator(
+ color: context.colors.primary,
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/domain/entity/monthly_overview.dart b/lib/domain/entity/monthly_overview.dart
new file mode 100644
index 00000000..79d827c4
--- /dev/null
+++ b/lib/domain/entity/monthly_overview.dart
@@ -0,0 +1,29 @@
+class MonthlyOverview {
+ final double income;
+ final double expenses;
+ final String currency;
+ final double maxValue;
+ final List scaleLabels;
+
+ const MonthlyOverview({
+ required this.income,
+ required this.expenses,
+ required this.currency,
+ required this.maxValue,
+ required this.scaleLabels,
+ });
+
+ double get savings => income - expenses;
+
+ bool get hasSavings => savings > 0;
+
+ bool get isEmpty => income == 0 && expenses == 0;
+
+ double get incomePercentage => maxValue > 0
+ ? (income / maxValue).clamp(0.0, 1.0)
+ : 0.0;
+
+ double get expensePercentage => maxValue > 0
+ ? (expenses / maxValue).clamp(0.0, 1.0)
+ : 0.0;
+}
\ No newline at end of file
diff --git a/lib/domain/repository/statistics_repository.dart b/lib/domain/repository/statistics_repository.dart
new file mode 100644
index 00000000..b51c3028
--- /dev/null
+++ b/lib/domain/repository/statistics_repository.dart
@@ -0,0 +1,5 @@
+import '../entity/monthly_overview.dart';
+
+abstract class StatisticsRepository {
+ Future getMonthlyOverview({required DateTime month});
+}
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index 613afd71..fc7f6a58 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
-import 'package:moneyplus/di/injection.dart';
import 'package:moneyplus/money_app.dart';
+import 'core/di/injection.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
diff --git a/lib/money_app.dart b/lib/money_app.dart
index dcff974c..3adb9961 100644
--- a/lib/money_app.dart
+++ b/lib/money_app.dart
@@ -7,7 +7,8 @@ import 'package:moneyplus/presentation/navigation/routes.dart';
import 'core/l10n/app_localizations.dart';
final _router = GoRouter(
- routes: $appRoutes,
+ routes: $appRoutes,
+ initialLocation: '/statistics'
);
diff --git a/lib/presentation/account_setup/screen/account_setup_screen.dart b/lib/presentation/account_setup/screen/account_setup_screen.dart
index a461d3d6..3c462347 100644
--- a/lib/presentation/account_setup/screen/account_setup_screen.dart
+++ b/lib/presentation/account_setup/screen/account_setup_screen.dart
@@ -4,11 +4,10 @@ import 'package:flutter_svg/svg.dart';
import 'package:moneyplus/design_system/assets/app_assets.dart';
import 'package:moneyplus/design_system/widgets/app_bar.dart';
import 'package:moneyplus/presentation/account_setup/screen/page1.dart';
-
+import '../../../core/di/injection.dart';
import '../../../core/l10n/app_localizations.dart';
import '../../../design_system/theme/money_extension_context.dart';
import '../../../design_system/widgets/buttons/button/default_button.dart';
-import '../../../di/injection.dart';
import '../cubit/account_setup_cubit.dart';
import '../cubit/account_setup_state.dart';
diff --git a/lib/presentation/forget_password/screen/forget_password_screen.dart b/lib/presentation/forget_password/screen/forget_password_screen.dart
index fcce2451..48f891cb 100644
--- a/lib/presentation/forget_password/screen/forget_password_screen.dart
+++ b/lib/presentation/forget_password/screen/forget_password_screen.dart
@@ -5,12 +5,12 @@ import 'package:moneyplus/design_system/assets/app_assets.dart';
import 'package:moneyplus/design_system/widgets/app_bar.dart';
import 'package:moneyplus/design_system/widgets/app_logo.dart';
import 'package:moneyplus/design_system/widgets/text_field.dart';
-import 'package:moneyplus/di/injection.dart';
import 'package:moneyplus/domain/repository/authentication_repository.dart';
import 'package:moneyplus/presentation/forget_password/cubit/forget_password_cubit.dart';
import 'package:moneyplus/presentation/update_password/screen/update_password_screen.dart';
import 'package:svg_flutter/svg.dart';
+import '../../../core/di/injection.dart';
import '../../../design_system/theme/money_extension_context.dart';
import '../../../design_system/widgets/buttons/button/default_button.dart';
import '../cubit/forget_password_state.dart';
diff --git a/lib/presentation/home/screen/home_screen.dart b/lib/presentation/home/screen/home_screen.dart
index d4acf7ad..fb660f1e 100644
--- a/lib/presentation/home/screen/home_screen.dart
+++ b/lib/presentation/home/screen/home_screen.dart
@@ -6,11 +6,10 @@ import 'package:moneyplus/design_system/theme/money_colors.dart';
import 'package:moneyplus/design_system/theme/money_extension_context.dart';
import 'package:moneyplus/design_system/widgets/income_expense.dart';
import 'package:moneyplus/design_system/widgets/top_spending_card.dart';
-import 'package:moneyplus/di/injection.dart';
import 'package:moneyplus/presentation/home/cubit/home_cubit.dart';
import 'package:moneyplus/presentation/home/cubit/home_state.dart';
import 'package:moneyplus/presentation/home/widget/current_balance.dart';
-
+import '../../../core/di/injection.dart';
import '../../../design_system/widgets/buttons/button/varient_button.dart';
import '../../../design_system/widgets/buttons/secondary/sm_secondary_button.dart';
import '../utils/StringFormattingHelpers.dart';
diff --git a/lib/presentation/income/screen/income_screen.dart b/lib/presentation/income/screen/income_screen.dart
index 0f2e8469..3d0da5c3 100644
--- a/lib/presentation/income/screen/income_screen.dart
+++ b/lib/presentation/income/screen/income_screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:moneyplus/design_system/assets/app_assets.dart';
import 'package:moneyplus/domain/model/form_status.dart';
-
+import '../../../core/di/injection.dart';
import '../../../core/l10n/app_localizations.dart';
import '../../../design_system/theme/money_extension_context.dart';
import '../../../design_system/widgets/app_bar.dart';
@@ -12,7 +12,6 @@ import '../../../design_system/widgets/chip.dart';
import '../../../design_system/widgets/snack_bar.dart';
import '../../../design_system/widgets/text_field.dart';
import '../../../design_system/widgets/text_field_date_Picker.dart';
-import '../../../di/injection.dart';
import '../cubit/add_income_cubit.dart';
import '../cubit/add_income_state.dart';
diff --git a/lib/presentation/navigation/routes.dart b/lib/presentation/navigation/routes.dart
index 5a2cb58a..793eafea 100644
--- a/lib/presentation/navigation/routes.dart
+++ b/lib/presentation/navigation/routes.dart
@@ -1,11 +1,11 @@
-import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:moneyplus/presentation/login/screen/login_screen.dart';
-
-import '../../di/injection.dart';
+import '../../core/di/injection.dart';
import '../login/cubit/login_cubit.dart';
+import '../statistics/cubit/statistics_cubit.dart';
+import '../statistics/statistics_screen.dart';
import '../main_container/screen/main_screen.dart';
part 'routes.g.dart';
@@ -55,3 +55,17 @@ class MainRoute extends GoRouteData with $MainRoute {
return const MainScreen();
}
}
+
+@TypedGoRoute(path: '/statistics')
+@immutable
+class StatisticsRoute extends GoRouteData with $StatisticsRoute {
+ const StatisticsRoute();
+
+ @override
+ Widget build(BuildContext context, GoRouterState state) {
+ return BlocProvider(
+ create: (_) => getIt(),
+ child: const StatisticsScreen(),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/presentation/navigation/routes.g.dart b/lib/presentation/navigation/routes.g.dart
index ebd1694f..1096931f 100644
--- a/lib/presentation/navigation/routes.g.dart
+++ b/lib/presentation/navigation/routes.g.dart
@@ -6,7 +6,12 @@ part of 'routes.dart';
// GoRouterGenerator
// **************************************************************************
-List get $appRoutes => [$onBoardingRoute, $loginRoute, $mainRoute];
+List get $appRoutes => [
+ $onBoardingRoute,
+ $loginRoute,
+ $mainRoute,
+ $statisticsRoute,
+];
RouteBase get $onBoardingRoute =>
GoRouteData.$route(path: '/', factory: $OnBoardingRoute._fromState);
@@ -77,3 +82,29 @@ mixin $MainRoute on GoRouteData {
@override
void replace(BuildContext context) => context.replace(location);
}
+
+RouteBase get $statisticsRoute => GoRouteData.$route(
+ path: '/statistics',
+ factory: $StatisticsRoute._fromState,
+);
+
+mixin $StatisticsRoute on GoRouteData {
+ static StatisticsRoute _fromState(GoRouterState state) =>
+ const StatisticsRoute();
+
+ @override
+ String get location => GoRouteData.$location('/statistics');
+
+ @override
+ void go(BuildContext context) => context.go(location);
+
+ @override
+ Future push(BuildContext context) => context.push(location);
+
+ @override
+ void pushReplacement(BuildContext context) =>
+ context.pushReplacement(location);
+
+ @override
+ void replace(BuildContext context) => context.replace(location);
+}
diff --git a/lib/presentation/statistics/cubit/statistics_cubit.dart b/lib/presentation/statistics/cubit/statistics_cubit.dart
new file mode 100644
index 00000000..80f41634
--- /dev/null
+++ b/lib/presentation/statistics/cubit/statistics_cubit.dart
@@ -0,0 +1,36 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import '../../../domain/repository/statistics_repository.dart';
+import 'statistics_state.dart';
+
+class StatisticsCubit extends Cubit {
+ final StatisticsRepository _repository;
+
+ StatisticsCubit({required StatisticsRepository repository})
+ : _repository = repository,
+ super(const StatisticsIdle());
+
+ Future loadStatistics({DateTime? month}) async {
+ final selectedMonth = month ?? DateTime.now();
+
+ emit(const StatisticsLoading());
+
+ try {
+ final monthlyOverview = await _repository.getMonthlyOverview(
+ month: selectedMonth,
+ );
+
+ emit(
+ StatisticsSuccess(
+ monthlyOverview: monthlyOverview,
+ selectedMonth: selectedMonth,
+ ),
+ );
+ } catch (e) {
+ emit(StatisticsFailure(e.toString()));
+ }
+ }
+
+ void changeMonth(DateTime month) {
+ loadStatistics(month: month);
+ }
+}
diff --git a/lib/presentation/statistics/cubit/statistics_state.dart b/lib/presentation/statistics/cubit/statistics_state.dart
new file mode 100644
index 00000000..5387a977
--- /dev/null
+++ b/lib/presentation/statistics/cubit/statistics_state.dart
@@ -0,0 +1,31 @@
+import '../../../domain/entity/monthly_overview.dart';
+
+sealed class StatisticsState {
+ const StatisticsState();
+}
+
+class StatisticsIdle extends StatisticsState {
+ const StatisticsIdle();
+}
+
+class StatisticsLoading extends StatisticsState {
+ const StatisticsLoading();
+}
+
+class StatisticsSuccess extends StatisticsState {
+ final MonthlyOverview? monthlyOverview;
+ final DateTime selectedMonth;
+
+ const StatisticsSuccess({
+ required this.monthlyOverview,
+ required this.selectedMonth,
+ });
+
+ bool get hasNoData => monthlyOverview == null;
+}
+
+class StatisticsFailure extends StatisticsState {
+ final String message;
+
+ const StatisticsFailure(this.message);
+}
\ No newline at end of file
diff --git a/lib/presentation/statistics/statistics_screen.dart b/lib/presentation/statistics/statistics_screen.dart
new file mode 100644
index 00000000..ebdecc6b
--- /dev/null
+++ b/lib/presentation/statistics/statistics_screen.dart
@@ -0,0 +1,79 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:moneyplus/core/l10n/app_localizations.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.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 'cubit/statistics_cubit.dart';
+import 'cubit/statistics_state.dart';
+import 'widgets/monthly_overview/monthly_overview_section.dart';
+
+class StatisticsScreen extends StatefulWidget {
+ const StatisticsScreen({super.key});
+
+ @override
+ State createState() => _StatisticsScreenState();
+}
+
+class _StatisticsScreenState extends State {
+ @override
+ void initState() {
+ super.initState();
+ context.read().loadStatistics();
+ }
+
+ void _onAddTransaction() {
+ // Navigate to add transaction
+ }
+
+ void _onRetry() {
+ context.read().loadStatistics();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: context.colors.surface,
+ body: SafeArea(
+ child: BlocBuilder(
+ builder: (context, state) {
+ return switch (state) {
+ StatisticsIdle() => const SizedBox.shrink(),
+ StatisticsLoading() => const AppLoadingIndicator(),
+ StatisticsSuccess() => _buildSuccess(context, state),
+ StatisticsFailure(:final message) => AppErrorView(
+ message: message,
+ onRetry: _onRetry,
+ ),
+ };
+ },
+ ),
+ ),
+ );
+ }
+
+ Widget _buildSuccess(BuildContext context, StatisticsSuccess state) {
+ final l10n = AppLocalizations.of(context)!;
+
+ if (state.hasNoData) {
+ return AppEmptyView(
+ title: l10n.no_statistics_title,
+ subtitle: l10n.no_statistics_subtitle,
+ buttonText: l10n.add_transaction,
+ onButtonPressed: _onAddTransaction,
+ );
+ }
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ children: [
+ if (state.monthlyOverview != null)
+ MonthlyOverviewSection(overview: state.monthlyOverview!),
+ // TODO: Add other sections here
+ ],
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/presentation/statistics/utils.dart b/lib/presentation/statistics/utils.dart
new file mode 100644
index 00000000..4725c0e1
--- /dev/null
+++ b/lib/presentation/statistics/utils.dart
@@ -0,0 +1,8 @@
+String formatNumber(double value) {
+ if (value >= 1000000) {
+ return '${(value / 1000000).toStringAsFixed(1)}M';
+ } else if (value >= 1000) {
+ return '${(value / 1000).toStringAsFixed(0)},${(value % 1000).toStringAsFixed(0).padLeft(3, '0')}';
+ }
+ return value.toStringAsFixed(0);
+}
diff --git a/lib/presentation/statistics/widgets/monthly_overview/monthly_overview_section.dart b/lib/presentation/statistics/widgets/monthly_overview/monthly_overview_section.dart
new file mode 100644
index 00000000..9e8c1229
--- /dev/null
+++ b/lib/presentation/statistics/widgets/monthly_overview/monthly_overview_section.dart
@@ -0,0 +1,111 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:moneyplus/core/l10n/app_localizations.dart';
+import 'package:moneyplus/design_system/assets/app_assets.dart';
+import 'package:moneyplus/design_system/constants/design_constants.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+import '../../../../domain/entity/monthly_overview.dart';
+import '../section_empty_view.dart';
+import 'overview_progress_bar.dart';
+import 'overview_savings_banner.dart';
+import 'overview_scale_labels.dart';
+import 'summary_item.dart';
+
+class MonthlyOverviewSection extends StatelessWidget {
+ final MonthlyOverview overview;
+
+ const MonthlyOverviewSection({super.key, required this.overview});
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = AppLocalizations.of(context)!;
+
+ if (overview.isEmpty) {
+ return SectionEmptyView(
+ title: l10n.monthly_overview,
+ message: l10n.no_monthly_overview,
+ );
+ }
+
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _Content(overview: overview),
+ if (overview.hasSavings)
+ OverviewSavingsBanner(
+ savings: overview.savings,
+ currency: overview.currency,
+ ),
+ ],
+ );
+ }
+}
+
+class _Content extends StatelessWidget {
+ final MonthlyOverview overview;
+
+ const _Content({required this.overview});
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = AppLocalizations.of(context)!;
+
+ return Container(
+ padding: const EdgeInsets.all(DesignConstants.spacingMedium),
+ decoration: BoxDecoration(
+ color: context.colors.surfaceLow,
+ borderRadius: const BorderRadius.all(
+ Radius.circular(DesignConstants.radiusMedium),
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ l10n.monthly_overview,
+ style: context.typography.label.medium.copyWith(
+ color: context.colors.title,
+ ),
+ ),
+ const SizedBox(height: DesignConstants.spacingMedium),
+ Row(
+ children: [
+ Expanded(
+ child: SummaryItem(
+ icon: SvgPicture.asset(
+ AppAssets.icWalletAdd,
+ width: DesignConstants.iconSizeMedium,
+ height: DesignConstants.iconSizeMedium,
+ ),
+ label: l10n.income,
+ value: overview.income,
+ currency: overview.currency,
+ isIncome: true,
+ ),
+ ),
+ const SizedBox(width: DesignConstants.spacingXLarge),
+ Expanded(
+ child: SummaryItem(
+ icon: SvgPicture.asset(
+ AppAssets.icMoneyRemove,
+ width: DesignConstants.iconSizeMedium,
+ height: DesignConstants.iconSizeMedium,
+ ),
+ label: l10n.expense,
+ value: overview.expenses,
+ currency: overview.currency,
+ isIncome: false,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: DesignConstants.spacingXLarge),
+ OverviewProgressBar(overview: overview),
+ const SizedBox(height: DesignConstants.spacingMedium),
+ OverviewScaleLabels(labels: overview.scaleLabels),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/statistics/widgets/monthly_overview/overview_progress_bar.dart b/lib/presentation/statistics/widgets/monthly_overview/overview_progress_bar.dart
new file mode 100644
index 00000000..2cd74547
--- /dev/null
+++ b/lib/presentation/statistics/widgets/monthly_overview/overview_progress_bar.dart
@@ -0,0 +1,107 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/design_system/constants/design_constants.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+import '../../../../domain/entity/monthly_overview.dart';
+
+class OverviewProgressBar extends StatelessWidget {
+ final MonthlyOverview overview;
+
+ const OverviewProgressBar({super.key, required this.overview});
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ _ProgressBar(
+ percentage: overview.incomePercentage,
+ gradientColors: GradientColors.incomeGradient,
+ shadowColor: GradientColors.incomeStart,
+ ),
+ const SizedBox(height: DesignConstants.spacingSmall),
+ _ProgressBar(
+ percentage: overview.expensePercentage,
+ gradientColors: GradientColors.expenseGradient,
+ shadowColor: GradientColors.expenseStart,
+ ),
+ ],
+ );
+ }
+}
+
+class _ProgressBar extends StatelessWidget {
+ final double percentage;
+ final List gradientColors;
+ final Color shadowColor;
+
+ const _ProgressBar({
+ required this.percentage,
+ required this.gradientColors,
+ required this.shadowColor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final strokeColor = context.colors.stroke;
+
+ return SizedBox(
+ height: DesignConstants.progressBarHeight,
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ final totalWidth = constraints.maxWidth;
+ final progressWidth = totalWidth * percentage;
+
+ return Stack(
+ children: [
+
+ Container(
+ height: DesignConstants.progressBarHeight,
+ decoration: BoxDecoration(
+ color: strokeColor.withAlpha(DesignConstants.strokeAlpha),
+ borderRadius: BorderRadius.circular(
+ DesignConstants.radiusXLarge,
+ ),
+ border: Border.all(
+ color: strokeColor.withAlpha(DesignConstants.strokeAlpha),
+ width: DesignConstants.progressBarBackgroundBorderWidth,
+ ),
+ ),
+ ),
+
+ if (progressWidth > 0)
+ Container(
+ width: progressWidth.clamp(
+ DesignConstants.progressBarMinWidth,
+ totalWidth,
+ ),
+ height: DesignConstants.progressBarHeight,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(
+ DesignConstants.radiusXLarge,
+ ),
+ gradient: LinearGradient(
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ colors: gradientColors,
+ ),
+ border: Border.all(
+ color: strokeColor.withAlpha(DesignConstants.strokeAlpha),
+ width: DesignConstants.progressBarBorderWidth,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: shadowColor.withOpacity(
+ DesignConstants.progressBarShadowOpacity,
+ ),
+ offset: DesignConstants.progressBarShadowOffset,
+ blurRadius: DesignConstants.progressBarShadowBlur,
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/statistics/widgets/monthly_overview/overview_savings_banner.dart b/lib/presentation/statistics/widgets/monthly_overview/overview_savings_banner.dart
new file mode 100644
index 00000000..80adaa8a
--- /dev/null
+++ b/lib/presentation/statistics/widgets/monthly_overview/overview_savings_banner.dart
@@ -0,0 +1,62 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:moneyplus/core/l10n/app_localizations.dart';
+import 'package:moneyplus/core/utils/number_formatter.dart';
+import 'package:moneyplus/design_system/assets/app_assets.dart';
+import 'package:moneyplus/design_system/constants/design_constants.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+
+class OverviewSavingsBanner extends StatelessWidget {
+ final double savings;
+ final String currency;
+
+ const OverviewSavingsBanner({
+ super.key,
+ required this.savings,
+ required this.currency,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = AppLocalizations.of(context)!;
+
+ return Container(
+ height: DesignConstants.savingsBannerHeight,
+ padding: const EdgeInsets.symmetric(
+ horizontal: DesignConstants.spacingSmall,
+ ),
+ decoration: BoxDecoration(
+ color: context.colors.secondaryVariant,
+ borderRadius: const BorderRadius.only(
+ bottomLeft: Radius.circular(DesignConstants.radiusMedium),
+ bottomRight: Radius.circular(DesignConstants.radiusMedium),
+ ),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SvgPicture.asset(
+ AppAssets.icWalletAdd,
+ width: DesignConstants.iconSizeSmall,
+ height: DesignConstants.iconSizeSmall,
+ colorFilter: ColorFilter.mode(
+ context.colors.secondary,
+ BlendMode.srcIn,
+ ),
+ ),
+ const SizedBox(width: DesignConstants.spacingXSmall),
+ Text(
+ l10n.savings_message(
+ NumberFormatter.formatWithCommas(savings),
+ currency,
+ ),
+ style: context.typography.label.xSmall?.copyWith(
+ color: context.colors.secondary,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/presentation/statistics/widgets/monthly_overview/overview_scale_labels.dart b/lib/presentation/statistics/widgets/monthly_overview/overview_scale_labels.dart
new file mode 100644
index 00000000..a3935925
--- /dev/null
+++ b/lib/presentation/statistics/widgets/monthly_overview/overview_scale_labels.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+
+class OverviewScaleLabels extends StatelessWidget {
+ final List labels;
+
+ const OverviewScaleLabels({super.key, required this.labels});
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: labels
+ .map(
+ (label) => Text(
+ label,
+ style: context.typography.label.xSmall?.copyWith(
+ color: context.colors.body,
+ ),
+ ),
+ )
+ .toList(),
+ );
+ }
+}
diff --git a/lib/presentation/statistics/widgets/monthly_overview/summary_item.dart b/lib/presentation/statistics/widgets/monthly_overview/summary_item.dart
new file mode 100644
index 00000000..cbbaf6db
--- /dev/null
+++ b/lib/presentation/statistics/widgets/monthly_overview/summary_item.dart
@@ -0,0 +1,99 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/core/utils/number_formatter.dart';
+import 'package:moneyplus/design_system/constants/design_constants.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+
+class SummaryItem extends StatelessWidget {
+ final Widget icon;
+ final String label;
+ final double value;
+ final String currency;
+ final bool isIncome;
+
+ const SummaryItem({
+ super.key,
+ required this.icon,
+ required this.label,
+ required this.value,
+ required this.currency,
+ required this.isIncome,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final gradientColors = isIncome
+ ? GradientColors.incomeGradient
+ : GradientColors.expenseGradient;
+
+ return Row(
+ children: [
+ Container(
+ width: DesignConstants.summaryIconContainerSize,
+ height: DesignConstants.summaryIconContainerSize,
+ decoration: BoxDecoration(
+ color: isIncome
+ ? context.colors.secondaryVariant
+ : context.colors.primaryVariant,
+ borderRadius: BorderRadius.circular(DesignConstants.radiusSmall),
+ ),
+ alignment: Alignment.center,
+ child: icon,
+ ),
+ const SizedBox(width: DesignConstants.spacingSmall),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ children: [
+ Container(
+ width: DesignConstants.indicatorCircleSize,
+ height: DesignConstants.indicatorCircleSize,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: gradientColors,
+ ),
+ ),
+ ),
+ const SizedBox(width: DesignConstants.spacingXSmall),
+ Text(
+ label,
+ style: context.typography.label.xSmall?.copyWith(
+ color: isIncome
+ ? context.colors.secondary
+ : context.colors.primary,
+ ),
+ ),
+ ],
+ ),
+ RichText(
+ text: TextSpan(
+ children: [
+ TextSpan(
+ text: isIncome ? '+' : '-',
+ style: context.typography.label.medium.copyWith(
+ color: isIncome
+ ? context.colors.green
+ : context.colors.primary,
+ ),
+ ),
+ TextSpan(
+ text: NumberFormatter.formatWithCurrency(value, currency),
+ style: context.typography.label.medium.copyWith(
+ color: context.colors.title,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/presentation/statistics/widgets/section_empty_view.dart b/lib/presentation/statistics/widgets/section_empty_view.dart
new file mode 100644
index 00000000..a799f273
--- /dev/null
+++ b/lib/presentation/statistics/widgets/section_empty_view.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:moneyplus/design_system/assets/app_assets.dart';
+import 'package:moneyplus/design_system/constants/design_constants.dart';
+import 'package:moneyplus/design_system/theme/money_extension_context.dart';
+
+class SectionEmptyView extends StatelessWidget {
+ final String title;
+ final String message;
+
+ const SectionEmptyView({
+ super.key,
+ required this.title,
+ required this.message,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.all(DesignConstants.spacingMedium),
+ decoration: BoxDecoration(
+ color: context.colors.surfaceLow,
+ borderRadius: BorderRadius.circular(DesignConstants.radiusMedium),
+ ),
+ child: Column(
+ children: [
+ Align(
+ alignment: AlignmentDirectional.centerStart,
+ child: Text(
+ title,
+ style: context.typography.label.medium.copyWith(
+ color: context.colors.title,
+ ),
+ ),
+ ),
+ const SizedBox(height: 24),
+ Image.asset(
+ // todo : AppAssets.imgStatisticsEmpty,
+ AppAssets.logo,
+ width: 80,
+ height: 80,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ message,
+ style: context.typography.body.small.copyWith(
+ color: context.colors.body,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/presentation/transactions/screen/transactions_screen.dart b/lib/presentation/transactions/screen/transactions_screen.dart
index d2d943e4..b4e97335 100644
--- a/lib/presentation/transactions/screen/transactions_screen.dart
+++ b/lib/presentation/transactions/screen/transactions_screen.dart
@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:moneyplus/core/l10n/app_localizations.dart';
import 'package:moneyplus/design_system/theme/money_extension_context.dart';
import 'package:moneyplus/design_system/widgets/snack_bar.dart';
-import 'package:moneyplus/di/injection.dart';
import 'package:moneyplus/presentation/transactions/cubit/transaction_cubit.dart';
import 'package:moneyplus/presentation/transactions/cubit/transaction_state.dart';
import 'package:moneyplus/presentation/transactions/widget/empty_transactions.dart';
@@ -11,6 +10,7 @@ import 'package:moneyplus/presentation/transactions/widget/loading_view.dart';
import 'package:moneyplus/presentation/transactions/widget/tabs_row.dart';
import 'package:moneyplus/presentation/transactions/widget/transaction_app_bar.dart';
import 'package:moneyplus/presentation/transactions/widget/transactions_list.dart';
+import '../../../core/di/injection.dart';
class TransactionsScreen extends StatelessWidget {
const TransactionsScreen({super.key});
diff --git a/lib/presentation/update_password/screen/update_password_screen.dart b/lib/presentation/update_password/screen/update_password_screen.dart
index 8c512bcb..a6b7386c 100644
--- a/lib/presentation/update_password/screen/update_password_screen.dart
+++ b/lib/presentation/update_password/screen/update_password_screen.dart
@@ -7,10 +7,9 @@ import 'package:moneyplus/design_system/widgets/app_bar.dart';
import 'package:moneyplus/design_system/widgets/app_logo.dart';
import 'package:moneyplus/design_system/widgets/snack_bar.dart';
import 'package:moneyplus/design_system/widgets/text_field.dart';
-import 'package:moneyplus/di/injection.dart';
import 'package:moneyplus/domain/repository/authentication_repository.dart';
import 'package:moneyplus/money_app.dart';
-
+import '../../../core/di/injection.dart';
import '../../../design_system/widgets/buttons/button/default_button.dart';
import '../cubit/update_password_cubit.dart';
import '../cubit/update_password_state.dart';