21 Commits
Author SHA1 Message Date
Sanders 1f0a9496ce fix 2025-06-09 10:26:55 +03:00
Sanders 38d12105fa feat: добавить запросы для ИИ к каждому этапу разработки финансового приложения 2025-06-08 20:23:36 +03:00
Sanders b38ad51a9e feat: добавить план разработки финансового приложения в CSV 2025-06-08 20:18:24 +03:00
Sanders e3ba9dd5dd fix: обновить использование FToast на showFToast в expenses_screen 2025-06-08 01:10:54 +03:00
Sanders 7f3dfb6459 fix: обновить вызов FToast с правильным API в Forui 2025-06-08 01:10:26 +03:00
Sanders 6eef7e95be Вот рекомендуемое сообщение коммита:
```
feat: обновить интерфейс с использованием библиотеки Forui
```

Это сообщение коммита:
- Начинается с `feat:`, что указывает на новую функциональность
- Кратко описывает основное изменение - обновление интерфейса с использованием библиотеки Forui
- Написано на русском языке
- Соответствует стандартам conventional commits
- Уложено в рекомендованные 72 символа
2025-06-08 00:27:42 +03:00
Sanders 90e5ac0c46 feat: добавить контроллер расходов с базовой функциональностью 2025-06-08 00:19:29 +03:00
Sanders b2e9eb86c2 refactor: Разделить ExpensesScreen на модульные компоненты 2025-06-08 00:18:07 +03:00
Sanders 65299a0b00 Merge pull request 'dev' (#1) from dev into master
Reviewed-on: #1
2025-05-04 14:49:30 +03:00
Sanders 187fcf4abf fix: persist pie slice selection on tap 2025-05-04 14:39:18 +03:00
Sanders 034c405a29 feat: display category/total amount in pie chart center 2025-05-04 14:37:36 +03:00
Sanders dca892c8a0 fix: use default WebDatabase constructor for web connection 2025-05-04 13:19:27 +03:00
Sanders e7cc15872a fix: specify sqlite3.wasm URI for web database connection 2025-05-04 13:18:17 +03:00
Sanders 4f2e8ee1f2 fix 2025-05-04 13:16:44 +03:00
Sanders 411574fc21 fix: explicitly configure WASM path for web database connection 2025-05-04 13:16:22 +03:00
Sanders ea0393b609 fix: remove obsolete Drift WASM initialization for web 2025-05-04 13:14:21 +03:00
Sanders 3156802df6 fix: initialize Drift WASM module for web 2025-05-04 13:12:53 +03:00
Sanders c088933cef fix 2025-05-04 13:12:24 +03:00
Sanders 1153ed6f23 refactor: implement conditional imports for database connection 2025-05-04 13:09:06 +03:00
Sanders 1b97a921b2 fix: remove unnecessary drift_web dependency 2025-05-04 13:05:34 +03:00
Sanders 224e300419 feat: add web support for drift database 2025-05-04 12:54:16 +03:00
21 changed files with 907 additions and 770 deletions
-8
View File
@@ -478,13 +478,6 @@
</list>
</value>
</entry>
<entry key="rxdart">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/rxdart-0.28.0/lib" />
</list>
</value>
</entry>
<entry key="shelf">
<value>
<list>
@@ -725,7 +718,6 @@
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/pub_semver-2.2.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/pubspec_parse-1.5.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/recase-4.1.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/rxdart-0.28.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/shelf-1.4.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/shelf_web_socket-3.0.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/source_gen-2.0.0/lib" />
+4
View File
@@ -0,0 +1,4 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- drift: true
@@ -0,0 +1,76 @@
import 'dart:async';
import 'dart:math';
import 'package:drift/drift.dart' show Value;
import '../database/database.dart' as db;
import '../models/category.dart';
import '../utils/category_utils.dart';
class ExpensesController {
final db.AppDatabase database;
// State variables
int _selectedPieIndex = -1;
bool _isPieChartExpanded = true;
bool _isFilterVisible = false;
String _selectedFilter = 'All';
ExpensesController(this.database);
// Getters
int get selectedPieIndex => _selectedPieIndex;
bool get isPieChartExpanded => _isPieChartExpanded;
bool get isFilterVisible => _isFilterVisible;
String get selectedFilter => _selectedFilter;
// Stream that provides transactions based on the selected filter
Stream<List<db.Transaction>> watchTransactions() {
return database.watchFilteredTransactions(_selectedFilter);
}
// Stream that provides category totals calculated from transactions
Stream<List<Category>> watchCategoryTotals() {
return database.calculateCategoryTotals();
}
// Handles selection of a pie chart slice
void selectPieCategory(int index) {
_selectedPieIndex = (_selectedPieIndex == index) ? -1 : index;
}
// Toggles the visibility of the pie chart section
void togglePieChartVisibility() {
_isPieChartExpanded = !_isPieChartExpanded;
}
// Toggles the visibility of the filter chip row
void toggleFilterVisibility() {
_isFilterVisible = !_isFilterVisible;
}
// Applies the selected filter to the transaction list
void applyFilter(String filter) {
_selectedFilter = filter;
}
// Adds a sample transaction for testing/demo
Future<void> addSampleTransaction() async {
final random = Random();
final categories = CategoryUtils.getAllCategoryNames();
final randomCategory = categories[random.nextInt(categories.length)];
final randomAmount = (random.nextDouble() * 100) + 5;
final randomDay = random.nextInt(7);
final randomHour = random.nextInt(24);
final randomMerchant = [
'Amazon', 'Local Cafe', 'Gas Station', 'Online Store', 'Supermarket'
][random.nextInt(5)];
final newTransaction = db.TransactionsCompanion.insert(
categoryName: randomCategory,
amount: randomAmount,
date: DateTime.now().subtract(Duration(days: randomDay, hours: randomHour)),
merchant: randomMerchant,
);
await database.addTransaction(newTransaction);
}
}
+10 -23
View File
@@ -1,15 +1,14 @@
import 'dart:io';
// ignore_for_file: unused_import // Часто генерируется при условном импорте
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
// import 'package:flutter/material.dart'; // Не требуется напрямую, т.к. Category импортируется
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
// import 'package:rxdart/rxdart.dart'; // Не используется
// Условный импорт бэкенда базы данных
// Выбирает реализацию connect() в зависимости от платформы
import 'database_connection/connection.dart' // Базовый импорт
if (dart.library.html) 'database_connection/connection_web.dart' // Для Веб
if (dart.library.io) 'database_connection/connection_native.dart'; // Для Нативных платформ (Android, iOS, Desktop)
// Импортируем модель категории для возвращаемого типа и утилиты
import '../models/category.dart';
// Удален ненужный импорт: import '../models/transaction_record.dart';
import '../utils/category_utils.dart'; // Helper for category details
// Эта строка указывает Drift сгенерировать файл database.g.dart
@@ -30,7 +29,10 @@ class Transactions extends Table {
// Аннотация @DriftDatabase указывает Drift сгенерировать код для этой базы данных
@DriftDatabase(tables: [Transactions])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
// Используем функцию connect() из условного импорта для создания соединения
// Во время компиляции будет выбрана правильная реализация connect()
// из connection_web.dart или connection_native.dart.
AppDatabase() : super(connect());
// Версия схемы. Увеличивайте при изменении структуры таблиц.
@override
@@ -128,18 +130,3 @@ class AppDatabase extends _$AppDatabase {
}
}
}
// Функция для открытия соединения с базой данных
LazyDatabase _openConnection() {
// Вычисление пути к файлу базы данных в папке документов приложения
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
// Создаем файл 'db.sqlite' в этой папке
final file = File(p.join(dbFolder.path, 'db.sqlite'));
print("Database file path: ${file.path}"); // Логируем путь для отладки
// Используем NativeDatabase для открытия соединения
// logStatements: true полезен для отладки SQL-запросов
return NativeDatabase(file, logStatements: false);
});
}
@@ -0,0 +1,12 @@
// lib/database/database_connection/connection.dart
import 'package:drift/drift.dart';
// Этот файл служит базой для условного импорта.
// Реализации находятся в connection_web.dart и connection_native.dart.
// Определим функцию-заглушку, чтобы основной файл database.dart
// мог ее импортировать без ошибок анализатора.
// Во время выполнения будет вызвана реализация из соответствующего
// файла (web или native) благодаря условному импорту.
QueryExecutor connect() => throw UnsupportedError(
'Stub connect function should not be called. Ensure conditional imports are set up correctly.');
@@ -0,0 +1,33 @@
// lib/database/database_connection/connection_native.dart
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
// sqlite3_flutter_libs и sqlite3 импортируются для возможной тонкой настройки,
// но часто NativeDatabase справляется автоматически.
// import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
// import 'package:sqlite3/sqlite3.dart';
QueryExecutor connect() {
print("Connecting to database using NativeDatabase (Lazy)");
// Используем LazyDatabase для отложенной инициализации на нативных платформах
return LazyDatabase(() async {
// Получаем папку для хранения документов приложения
final dbFolder = await getApplicationDocumentsDirectory();
// Создаем путь к файлу 'db.sqlite' в этой папке
final file = File(p.join(dbFolder.path, 'db.sqlite'));
print("Database file path (Native): ${file.path}"); // Логируем путь для отладки
// Настройка библиотеки sqlite3 (обычно не требуется для Android/iOS/macOS с sqlite3_flutter_libs)
// if (Platform.isWindows || Platform.isLinux) {
// // Может потребоваться дополнительная настройка для Desktop
// // await applyWorkaroundToOpenSqlite3Library();
// }
// Используем NativeDatabase для открытия соединения
// logStatements: true полезен для отладки SQL-запросов
return NativeDatabase(file, logStatements: false);
});
}
@@ -0,0 +1,15 @@
// lib/database/database_connection/connection_web.dart
import 'package:drift/drift.dart';
import 'package:drift/web.dart';
// import 'package:drift/wasm.dart'; // <-- Удалить этот импорт, он больше не нужен здесь
QueryExecutor connect() {
print("Connecting to database using WebDatabase (default configuration)");
// Используем стандартный конструктор WebDatabase.
// Drift попытается автоматически найти и загрузить sqlite3.wasm по пути /sqlite3.wasm
return WebDatabase(
'db', // Имя базы данных в IndexedDB
logStatements: false,
);
}
+28
View File
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; // Для kIsWeb
// import 'package:drift/wasm.dart'; // <-- Удалить этот импорт
import 'app.dart'; // Import the new app root widget
import 'database/database.dart'; // Import the database
@@ -6,11 +8,37 @@ Future<void> main() async {
// Необходимо для асинхронных операций перед runApp, например, инициализации БД
WidgetsFlutterBinding.ensureInitialized();
// --- Удалить весь этот блок ---
// // Инициализация WASM-модуля SQLite для веб-платформы
// if (kIsWeb) {
// print("Running on Web, attempting to initialize sqlite3.wasm...");
// // Указываем Drift, где найти файл sqlite3.wasm.
// // WasmDatabase.resolveFile попытается найти его автоматически (обычно в корне /sqlite3.wasm).
// final result = await WasmDatabase.resolveFile('sqlite3.wasm'); // <-- Ошибка здесь
//
// if (result.isSuccessful) {
// print("sqlite3.wasm loaded successfully.");
// } else {
// // Если файл не найден или произошла ошибка загрузки
// print("Error loading sqlite3.wasm: ${result.errorMessage}");
// // Здесь можно предпринять действия, если загрузка не удалась,
// // например, показать сообщение об ошибке пользователю или использовать
// // альтернативное хранилище. Пока просто выводим ошибку в консоль.
// // Приложение может не работать корректно без базы данных.
// }
// } else {
// print("Running on Native platform, skipping WASM initialization.");
// }
// --- Конец удаляемого блока ---
// Создаем единственный экземпляр базы данных для всего приложения
// WebDatabase (вызываемый через connect() на вебе) должен сам справиться с WASM
final database = AppDatabase();
// Опционально: Вставляем начальные данные, если база данных пуста
// Это полезно для первого запуска или демонстрации
// Делаем это после создания экземпляра БД
await database.insertInitialDataIfNeeded();
// Запускаем приложение, передавая экземпляр базы данных
+160 -470
View File
@@ -1,32 +1,25 @@
import 'package:drift/drift.dart' show Value; // Only import Value for optional fields
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; // Used indirectly by SpendingPieChart
import 'package:intl/intl.dart'; // For date formatting
import 'dart:async';
import 'dart:math'; // For random data generation in sample add
import 'package:forui/forui.dart';
import '../database/database.dart' as db; // Import database with prefix 'db'
import '../models/category.dart'; // Keep Category model for UI structure (SpendingPieChart)
// Import TransactionRecord with a different alias to avoid conflict with db.Transaction
import '../models/transaction_record.dart' as model;
import '../widgets/summary_item.dart';
import '../database/database.dart' as db;
import '../models/category.dart';
import '../controllers/expenses_controller.dart';
import '../widgets/summary_card.dart';
import '../widgets/expandable_section.dart';
import '../widgets/spending_pie_chart.dart';
import '../widgets/transaction_list_item.dart';
import '../widgets/filter_chip_widget.dart';
import '../utils/category_utils.dart'; // Import category utils
import 'profile_screen.dart'; // Import profile screen
import '../widgets/transactions_section.dart';
import 'profile_screen.dart';
class ExpensesScreen extends StatefulWidget {
final Function toggleTheme;
final bool isDarkMode;
final db.AppDatabase database; // Accept database instance
final db.AppDatabase database;
const ExpensesScreen({
Key? key,
required this.toggleTheme,
required this.isDarkMode,
required this.database, // Require database instance
required this.database,
}) : super(key: key);
@override
@@ -34,24 +27,20 @@ class ExpensesScreen extends StatefulWidget {
}
class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStateMixin {
// Animation controllers for UI elements
late AnimationController _pieChartAnimationController;
late Animation<double> _pieChartAnimation;
late AnimationController _pieChartExpandController;
late Animation<double> _pieChartHeightFactor;
late ExpensesController _controller;
// State variables
int _selectedPieIndex = -1; // Index of the selected pie chart slice
int _selectedNavIndex = 0; // Index for bottom navigation bar
bool _isPieChartExpanded = true; // Controls visibility of the pie chart section
bool _isFilterVisible = false; // Controls visibility of the filter chips
String _selectedFilter = 'All'; // Currently selected transaction filter
int _selectedNavIndex = 0;
@override
void initState() {
super.initState();
// Initialize animation controller for pie chart fade/scale effect
_controller = ExpensesController(widget.database);
_pieChartAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
@@ -61,485 +50,130 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
curve: Curves.easeInOut,
);
// Initialize animation controller for pie chart expand/collapse effect
_pieChartExpandController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
value: 1.0, // Start expanded
value: 1.0,
);
_pieChartHeightFactor = CurvedAnimation(
parent: _pieChartExpandController,
curve: Curves.easeInOut,
);
// Start the pie chart appearance animation
_pieChartAnimationController.forward();
}
@override
void dispose() {
// Dispose controllers to free up resources
_pieChartAnimationController.dispose();
_pieChartExpandController.dispose();
super.dispose();
}
// Stream that provides transactions based on the selected filter
Stream<List<db.Transaction>> _watchTransactions() {
return widget.database.watchFilteredTransactions(_selectedFilter);
}
// Stream that provides category totals calculated from transactions
Stream<List<Category>> _watchCategoryTotals() {
return widget.database.calculateCategoryTotals();
}
// Toggles the visibility of the pie chart section with animation
void _togglePieChartVisibility() {
setState(() {
_isPieChartExpanded = !_isPieChartExpanded;
if (_isPieChartExpanded) {
_pieChartExpandController.forward(); // Expand animation
_controller.togglePieChartVisibility();
if (_controller.isPieChartExpanded) {
_pieChartExpandController.forward();
} else {
_pieChartExpandController.reverse(); // Collapse animation
_pieChartExpandController.reverse();
}
});
}
// Toggles the visibility of the filter chip row
void _toggleFilterVisibility() {
setState(() {
_isFilterVisible = !_isFilterVisible;
_controller.toggleFilterVisibility();
});
}
// Handles selection of a pie chart slice
void _selectPieCategory(int index) {
setState(() {
// If the same index is selected, deselect (-1), otherwise select the new index
_selectedPieIndex = (_selectedPieIndex == index) ? -1 : index;
_controller.selectPieCategory(index);
});
// Optionally apply filter when pie slice is selected/deselected
// This requires getting the category name from the index, potentially async
// _applyFilterBasedOnPieSelection(index);
}
// Applies the selected filter to the transaction list
void _applyFilter(String filter) {
setState(() {
_selectedFilter = filter;
// The StreamBuilder listening to _watchTransactions() will automatically
// rebuild with the new stream based on the updated _selectedFilter.
});
setState(() {
_controller.applyFilter(filter);
});
}
// --- Function to add a sample transaction (for testing/demo) ---
void _addSampleTransaction() async {
final random = Random();
// Get available category names from our utility class
final categories = CategoryUtils.getAllCategoryNames();
// Pick a random category, amount, date, and merchant
final randomCategory = categories[random.nextInt(categories.length)];
final randomAmount = (random.nextDouble() * 100) + 5; // Amount between 5 and 105
final randomDay = random.nextInt(7); // Within the last week
final randomHour = random.nextInt(24);
final randomMerchant = ['Amazon', 'Local Cafe', 'Gas Station', 'Online Store', 'Supermarket'][random.nextInt(5)];
// Create a companion object for inserting into the Drift table
final newTransaction = db.TransactionsCompanion.insert(
categoryName: randomCategory,
amount: randomAmount,
date: DateTime.now().subtract(Duration(days: randomDay, hours: randomHour)),
merchant: randomMerchant,
);
try {
// Insert the transaction into the database
await widget.database.addTransaction(newTransaction);
print('Sample transaction added: $randomCategory - \$${randomAmount.toStringAsFixed(2)}');
// Show a confirmation message
if (mounted) { // Check if the widget is still in the tree
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Added ${randomCategory} transaction'),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
await _controller.addSampleTransaction();
if (mounted) {
showFToast(
context: context,
builder: (context) => const FToast(
title: Text('Success'),
description: Text('Transaction added successfully'),
),
);
}
} catch (e) {
print('Error adding transaction: $e');
// Show an error message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error adding transaction: $e'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
if (mounted) {
showFToast(
context: context,
builder: (context) => FToast(
title: const Text('Error'),
description: Text('Error adding transaction: $e'),
style: FToastStyle.destructive,
),
);
}
}
}
// --- End of add sample transaction function ---
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final theme = Theme.of(context); // Get theme for easier access to styles
return Scaffold(
appBar: AppBar(
// Title with icon
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.account_balance_wallet_outlined, // Updated icon
color: isDark ? Colors.greenAccent.shade100 : Colors.green.shade800,
size: 24,
),
const SizedBox(width: 8),
const Text('My Finances'), // Updated title
],
),
centerTitle: true,
// Theme toggle button
leading: IconButton(
tooltip: isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode',
icon: Icon(
widget.isDarkMode ? Icons.wb_sunny_outlined : Icons.nightlight_round,
color: widget.isDarkMode ? Colors.yellow.shade300 : Colors.blue.shade700,
),
onPressed: () => widget.toggleTheme(),
),
// Profile avatar button
actions: [
Padding(
padding: const EdgeInsets.only(right: 12.0), // Adjusted padding
child: Hero(
tag: 'profileAvatar', // Tag for Hero animation
child: Material(
type: MaterialType.transparency, // Needed for Hero animation across routes
child: IconButton(
tooltip: 'View Profile',
icon: CircleAvatar(
radius: 18, // Slightly smaller avatar
backgroundColor: isDark ? Colors.green.shade800 : Colors.green.shade100,
child: const Icon(Icons.person_outline, color: Colors.green, size: 20),
),
onPressed: () {
// Navigate to ProfileScreen with a fade transition
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => const ProfileScreen(),
transitionsBuilder: (_, animation, __, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 350), // Adjusted duration
),
);
},
),
),
),
),
],
),
// Use StreamBuilder to get category totals for the summary and pie chart
appBar: _buildAppBar(context),
body: StreamBuilder<List<Category>>(
stream: _watchCategoryTotals(),
stream: _controller.watchCategoryTotals(),
builder: (context, categorySnapshot) {
// Handle loading state
if (categorySnapshot.connectionState == ConnectionState.waiting && !categorySnapshot.hasData) {
return const Center(child: CircularProgressIndicator());
return const Center(child: CircularProgressIndicator());
}
// Handle error state
if (categorySnapshot.hasError) {
return Center(child: Text('Error loading categories: ${categorySnapshot.error}'));
}
// Get categories data (or empty list if null)
final categories = categorySnapshot.data ?? [];
// Calculate total expenses from the categories stream data
final totalExpenses = categories.fold(0.0, (sum, item) => sum + item.amount);
// Main column layout
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, // Stretch children horizontally
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
// Use SingleChildScrollView for content that might overflow
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(), // iOS-like scroll physics
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// --- Summary Card ---
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Card(
elevation: 2, // Subtle shadow
child: Padding(
padding: const EdgeInsets.all(16.0), // Adjusted padding
child: Column(
children: [
Text(
'Total Expenses This Period', // More descriptive title
style: theme.textTheme.titleMedium, // Use theme style
),
const SizedBox(height: 8),
// Display total expenses amount
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, // Align baseline
children: [
Text(
'\$',
style: TextStyle(
fontSize: 20, // Smaller dollar sign
fontWeight: FontWeight.w500, // Medium weight
color: theme.colorScheme.primary,
),
),
Text(
NumberFormat.currency(symbol: '', decimalDigits: 2).format(totalExpenses), // Format number
style: TextStyle(
fontSize: 36, // Slightly smaller amount
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
letterSpacing: -1, // Tighten spacing
),
),
],
),
const SizedBox(height: 16),
// Divider line
Divider(height: 1, thickness: 1, indent: 20, endIndent: 20, color: theme.dividerColor.withOpacity(0.5)),
const SizedBox(height: 16),
// Income / Expenses summary items
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, // Distribute space
children: [
const SummaryItem( // Example Income - Replace with real data
icon: Icons.arrow_downward_rounded,
title: 'Income',
amount: '\$2,450.00',
color: Colors.green,
),
// Vertical divider
Container(
height: 35,
width: 1,
color: theme.dividerColor.withOpacity(0.5),
),
SummaryItem( // Expenses using calculated total
icon: Icons.arrow_upward_rounded,
title: 'Expenses',
amount: '\$${NumberFormat.currency(symbol: '', decimalDigits: 2).format(totalExpenses)}',
color: Colors.red,
),
],
),
],
),
),
),
),
// --- Pie Chart Section ---
SummaryCard(totalExpenses: totalExpenses),
ExpandableSection(
title: 'Spending Breakdown',
icon: Icons.pie_chart_outline_rounded, // Updated icon
isExpanded: _isPieChartExpanded,
icon: Icons.pie_chart_outline_rounded,
isExpanded: _controller.isPieChartExpanded,
onTap: _togglePieChartVisibility,
heightFactor: _pieChartHeightFactor, // Animation controller
heightFactor: _pieChartHeightFactor,
child: SpendingPieChart(
categories: categories, // Pass categories from stream snapshot
totalExpenses: totalExpenses, // Pass calculated total
selectedPieIndex: _selectedPieIndex,
onSelectPieCategory: _selectPieCategory, // Callback for selection
animation: _pieChartAnimation, // Appearance animation
categories: categories,
totalExpenses: totalExpenses,
selectedPieIndex: _controller.selectedPieIndex,
onSelectPieCategory: _selectPieCategory,
animation: _pieChartAnimation,
),
),
// --- Transaction List Section ---
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with Title and Filter/See All buttons
Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 4), // Adjusted padding
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Recent Transactions',
style: theme.textTheme.titleLarge?.copyWith( // Use theme style
fontWeight: FontWeight.w600, // Bold weight
),
),
// Filter button
Row(
children: [
InkWell( // Use InkWell for ripple effect
onTap: _toggleFilterVisibility,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: isDark ? Colors.grey.shade800 : Colors.grey.shade200,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Text(
_selectedFilter, // Display current filter
style: TextStyle(
fontSize: 13, // Smaller font
color: isDark ? Colors.white70 : Colors.black87,
),
),
const SizedBox(width: 4),
Icon(
Icons.filter_list_alt, // Updated icon
size: 18, // Slightly larger icon
color: isDark ? Colors.white70 : Colors.black87,
),
],
),
),
),
// "See All" button (optional)
// TextButton(
// onPressed: () { /* Navigate to full list */ },
// child: Text('See All'),
// ),
],
),
],
),
),
// --- Filter Chips Row (Animated Visibility) ---
AnimatedContainer(
duration: const Duration(milliseconds: 300), // Animation duration
curve: Curves.easeInOut, // Animation curve
height: _isFilterVisible ? 50 : 0, // Animate height
clipBehavior: Clip.hardEdge, // Prevent overflow during animation
decoration: const BoxDecoration(), // Needed for clipBehavior
padding: EdgeInsets.symmetric(
vertical: _isFilterVisible ? 8 : 0, // Animate padding
),
child: ListView( // Use ListView for horizontal scrolling
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
// "All" filter chip
FilterChipWidget(
label: 'All',
isSelected: _selectedFilter == 'All',
onTap: () => _applyFilter('All')
),
// Dynamically generate filter chips from categories
...CategoryUtils.getAllCategoryNames().map((name) =>
FilterChipWidget(
label: name,
isSelected: _selectedFilter == name,
onTap: () => _applyFilter(name)
)
).toList(),
],
),
),
// --- Transactions List (using StreamBuilder) ---
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Card( // Wrap list in a Card for background/border
margin: EdgeInsets.zero,
elevation: 0, // No shadow for inner card
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: theme.dividerColor.withOpacity(0.5), width: 1) // Subtle border
),
clipBehavior: Clip.antiAlias, // Clip list items to card shape
child: StreamBuilder<List<db.Transaction>>(
stream: _watchTransactions(), // Stream based on _selectedFilter
builder: (context, transactionSnapshot) {
// Handle loading state for transactions
if (transactionSnapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(
height: 150, // Placeholder height
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
// Handle error state for transactions
if (transactionSnapshot.hasError) {
return SizedBox(
height: 150,
child: Center(child: Text('Error: ${transactionSnapshot.error}')),
);
}
// Get transaction data (or empty list)
final transactions = transactionSnapshot.data ?? [];
// Display message if no transactions match the filter
if (transactions.isEmpty) {
return SizedBox(
height: 150,
child: Center(
child: Text(
_selectedFilter == 'All'
? 'No transactions yet.'
: 'No transactions found for $_selectedFilter.',
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey),
),
),
);
}
// Use ListView.builder to display transactions efficiently
return ListView.separated(
itemCount: transactions.length,
physics: const NeverScrollableScrollPhysics(), // Disable inner scrolling
shrinkWrap: true, // Fit content height
padding: const EdgeInsets.symmetric(vertical: 8.0), // Padding inside the card
separatorBuilder: (context, index) => Divider(
height: 1, thickness: 1, indent: 16, endIndent: 16,
color: theme.dividerColor.withOpacity(0.3),
),
itemBuilder: (context, index) {
// Get the db.Transaction object from the stream
final dbTransaction = transactions[index];
// Get the corresponding category details (icon, color)
final categoryDetails = CategoryUtils.getCategoryDetails(dbTransaction.categoryName);
// Create the model.TransactionRecord needed by TransactionListItem
final transactionRecord = model.TransactionRecord(
dbTransaction.categoryName,
dbTransaction.amount,
categoryDetails.iconCode, // Get icon from utils
categoryDetails.colorCode, // Get color from utils
dbTransaction.date,
dbTransaction.merchant,
);
// Use the TransactionListItem widget with the correct model type
return TransactionListItem(
transaction: transactionRecord, // Pass the model.TransactionRecord
animation: kAlwaysCompleteAnimation, // Required by FadeTransition
);
},
);
},
),
),
),
const SizedBox(height: 16), // Bottom padding inside scroll view
],
TransactionsSection(
transactionsStream: _controller.watchTransactions(),
isFilterVisible: _controller.isFilterVisible,
selectedFilter: _controller.selectedFilter,
onToggleFilter: _toggleFilterVisibility,
onApplyFilter: _applyFilter,
),
const SizedBox(height: 80), // Extra bottom padding below list to avoid FAB overlap
const SizedBox(height: 80),
],
),
),
@@ -548,51 +182,107 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
);
},
),
// Bottom Navigation Bar
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedNavIndex,
onTap: (index) {
setState(() {
_selectedNavIndex = index;
// TODO: Handle navigation based on index (e.g., switch screens)
});
},
items: const [ // Use const for static items
BottomNavigationBarItem(
icon: Icon(Icons.home_filled), // Use filled icon for selected state
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.bar_chart_rounded),
label: 'Reports',
),
// Example with Badge (replace with actual notification count)
// BottomNavigationBarItem(
// icon: Badge(
// label: Text('3'), // Example badge count
// child: Icon(Icons.notifications_none_rounded),
// ),
// activeIcon: Badge( // Optional: different badge style when active
// label: Text('3'),
// child: Icon(Icons.notifications_rounded),
// ),
// label: 'Notifications',
// ),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings), // Filled icon when active
label: 'Settings',
bottomNavigationBar: _buildBottomNavigationBar(),
floatingActionButton: FButton(
label: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add),
SizedBox(width: 8),
Text('Add'),
],
),
style: FButtonStyle.primary,
onPress: _addSampleTransaction,
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
);
}
AppBar _buildAppBar(BuildContext context) {
return AppBar(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.account_balance_wallet_outlined,
color: context.theme.colorScheme.primary,
size: 24,
),
const SizedBox(width: 8),
const Text('My Finances'),
],
),
// Floating Action Button to add new transaction
floatingActionButton: FloatingActionButton.extended( // Use extended FAB
onPressed: _addSampleTransaction, // Add sample data on press
tooltip: 'Add Transaction',
icon: const Icon(Icons.add),
label: const Text('Add'),
centerTitle: true,
leading: IconButton(
tooltip: widget.isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode',
icon: Icon(
widget.isDarkMode ? Icons.wb_sunny_outlined : Icons.nightlight_round,
color: widget.isDarkMode ? Colors.yellow.shade300 : Colors.blue.shade700,
),
onPressed: () => widget.toggleTheme(),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, // Standard location
actions: [
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Hero(
tag: 'profileAvatar',
child: Material(
type: MaterialType.transparency,
child: IconButton(
tooltip: 'View Profile',
icon: FAvatar(
size: 36,
backgroundColor: context.theme.colorScheme.primary.withOpacity(0.1),
child: Icon(
Icons.person_outline,
color: context.theme.colorScheme.primary,
size: 20,
),
),
onPressed: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => const ProfileScreen(),
transitionsBuilder: (_, animation, __, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 350),
),
);
},
),
),
),
),
],
);
}
BottomNavigationBar _buildBottomNavigationBar() {
return BottomNavigationBar(
currentIndex: _selectedNavIndex,
onTap: (index) {
setState(() {
_selectedNavIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_filled),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.bar_chart_rounded),
label: 'Reports',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
label: 'Settings',
),
],
);
}
}
+15 -24
View File
@@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
class ProfileScreen extends StatelessWidget {
const ProfileScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: const Text('Profile'),
@@ -24,13 +23,14 @@ class ProfileScreen extends StatelessWidget {
tag: 'profileAvatar', // Tag must match the one in ExpensesScreen
child: Material( // Wrap with Material for Hero animation
type: MaterialType.transparency,
child: CircleAvatar(
radius: 50,
backgroundColor: isDark ? Colors.green.shade800 : Colors.green.shade100,
child: const Icon(
child: FAvatar(
size: 100,
backgroundColor: context.theme.colorScheme.primary.withOpacity(0.1),
image: null,
child: Icon(
Icons.person,
size: 50,
color: Colors.green,
color: context.theme.colorScheme.primary,
),
),
),
@@ -38,32 +38,23 @@ class ProfileScreen extends StatelessWidget {
const SizedBox(height: 24),
Text(
'John Doe',
style: TextStyle(
fontSize: 24,
style: context.theme.typography.xl2.copyWith(
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black,
color: context.theme.colorScheme.foreground,
),
),
const SizedBox(height: 8),
Text(
'john.doe@example.com',
style: TextStyle(
fontSize: 16,
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
style: context.theme.typography.base.copyWith(
color: context.theme.colorScheme.mutedForeground,
),
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: isDark ? Colors.green.shade700 : Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text('Edit Profile'),
FButton(
label: const Text('Edit Profile'),
style: FButtonStyle.primary,
onPress: () {},
),
],
),
+21 -73
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
class ExpandableSection extends StatelessWidget {
final String title;
@@ -20,82 +21,29 @@ class ExpandableSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Column(
children: [
GestureDetector(
onTap: onTap,
child: Container(
width: double.infinity,
margin: const EdgeInsets.fromLTRB(16, 8, 16, 0),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
color: isDark ? Colors.grey.shade800 : Colors.green.shade100,
borderRadius: BorderRadius.vertical(
top: const Radius.circular(16),
bottom: Radius.circular(isExpanded ? 0 : 16),
return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: FCollapsible(
initiallyExpanded: isExpanded,
title: Row(
children: [
Icon(
icon,
color: context.theme.colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
Text(
title,
style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.bold,
color: context.theme.colorScheme.primary,
),
),
child: Row(
children: [
Icon(
icon,
color: isDark ? Colors.green.shade300 : Colors.green.shade800,
size: 20,
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isDark ? Colors.green.shade300 : Colors.green.shade800,
),
),
const Spacer(),
AnimatedRotation(
turns: isExpanded ? 0.5 : 0,
duration: const Duration(milliseconds: 300),
child: Icon(
Icons.keyboard_arrow_down,
color: isDark ? Colors.green.shade300 : Colors.green.shade800,
),
),
],
),
),
],
),
AnimatedBuilder(
animation: heightFactor,
builder: (context, innerChild) {
return ClipRect(
child: Align(
heightFactor: heightFactor.value,
child: Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 0),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(16),
),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withOpacity(0.3)
: Colors.green.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: innerChild,
),
),
);
},
child: child,
),
],
child: child,
),
);
}
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
class FilterChipWidget extends StatelessWidget {
final String label;
@@ -14,35 +15,14 @@ class FilterChipWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), // Smaller padding
decoration: BoxDecoration(
color: isSelected
? (isDark ? Colors.green.shade700 : Colors.green.shade100)
: (isDark ? Colors.grey.shade800 : Colors.grey.shade200),
borderRadius: BorderRadius.circular(14), // Smaller radius
border: isSelected
? Border.all(
color: isDark ? Colors.green.shade300 : Colors.green,
width: 1,
)
: null,
),
child: Text(
label,
style: TextStyle(
fontSize: 12, // Smaller text
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected
? (isDark ? Colors.white : Colors.green.shade700)
: (isDark ? Colors.white70 : Colors.grey.shade700),
),
),
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FButton(
label: Text(label),
style: isSelected
? FButtonStyle.primary
: FButtonStyle.secondary,
onPress: onTap,
),
);
}
+113 -30
View File
@@ -24,6 +24,7 @@ class SpendingPieChart extends StatelessWidget {
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final theme = Theme.of(context);
final currencyFormatter = NumberFormat.currency(locale: 'en_US', symbol: '\$'); // Or your preferred locale/symbol
// Handle the case where there are no categories to display
if (categories.isEmpty) {
@@ -61,38 +62,48 @@ class SpendingPieChart extends StatelessWidget {
// --- Pie Chart ---
Expanded(
flex: 5, // Give more space to the chart itself
child: PieChart(
PieChartData(
// Handle touch events on the pie chart
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {
// Ignore events not related to interaction
if (!event.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse.touchedSection == null) {
// If touch ends outside a section, deselect
if (event is FlPanEndEvent || event is FlTapUpEvent) {
if (selectedPieIndex != -1) {
onSelectPieCategory(-1); // Deselect
child: Stack( // Use Stack to overlay text on the chart center
alignment: Alignment.center,
children: [
PieChart(
PieChartData(
// Handle touch events on the pie chart
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, PieTouchResponse? pieTouchResponse) {
// We are only interested in TapUp events to trigger selection changes
if (event is FlTapUpEvent) {
final section = pieTouchResponse?.touchedSection;
if (section != null) {
// Tap occurred ON a section
final touchedIndex = section.touchedSectionIndex;
// Toggle selection: if tapped section is already selected, deselect (-1), otherwise select it.
onSelectPieCategory(touchedIndex == selectedPieIndex ? -1 : touchedIndex);
} else {
// Tap occurred OUTSIDE any section
// Deselect if something was selected
if (selectedPieIndex != -1) {
onSelectPieCategory(-1);
}
}
}
}
return;
}
// Get the index of the touched section
final touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex;
// Call the callback, toggling selection if the same slice is touched again
onSelectPieCategory(touchedIndex == selectedPieIndex ? -1 : touchedIndex);
},
},
),
borderData: FlBorderData(show: false), // No border around the chart
sectionsSpace: 2, // Space between slices
centerSpaceRadius: 50, // Radius of the center hole
sections: _generatePieSections(context), // Generate slices data
startDegreeOffset: -90, // Start chart from the top (12 o'clock)
),
// Optional animation when data changes
swapAnimationDuration: const Duration(milliseconds: 250),
swapAnimationCurve: Curves.easeInOut,
),
borderData: FlBorderData(show: false), // No border around the chart
sectionsSpace: 2, // Space between slices
centerSpaceRadius: 50, // Radius of the center hole
sections: _generatePieSections(context), // Generate slices data
startDegreeOffset: -90, // Start chart from the top (12 o'clock)
),
// Optional animation when data changes
swapAnimationDuration: const Duration(milliseconds: 250),
swapAnimationCurve: Curves.easeInOut,
// --- Center Text (Displayed when a slice is selected) ---
if (selectedPieIndex != -1)
_buildCenterText(context, currencyFormatter)
else // Optional: Display total or default text when nothing is selected
_buildDefaultCenterText(context, currencyFormatter),
],
),
),
const SizedBox(width: 8), // Spacing between chart and legend
@@ -116,11 +127,77 @@ class SpendingPieChart extends StatelessWidget {
);
}
// Builds the text displayed in the center when a slice is selected
Widget _buildCenterText(BuildContext context, NumberFormat formatter) {
final theme = Theme.of(context);
// Check if selectedPieIndex is valid before accessing categories
if (selectedPieIndex < 0 || selectedPieIndex >= categories.length) {
// Return an empty container or default text if index is invalid
return _buildDefaultCenterText(context, formatter);
}
final selectedCategory = categories[selectedPieIndex];
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
selectedCategory.name,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.textTheme.bodyLarge?.color?.withOpacity(0.8),
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
formatter.format(selectedCategory.amount), // Format the amount as currency
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: selectedCategory.colorCode, // Use category color for amount
),
textAlign: TextAlign.center,
),
],
);
}
// Builds the default text displayed in the center when no slice is selected
Widget _buildDefaultCenterText(BuildContext context, NumberFormat formatter) {
final theme = Theme.of(context);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Total', // Label for the total amount
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.textTheme.bodyLarge?.color?.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
formatter.format(totalExpenses), // Display total expenses
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.textTheme.bodyLarge?.color // Use default text color
),
textAlign: TextAlign.center,
),
],
);
}
// Builds a single item for the legend
Widget _buildPieLegendItem(BuildContext context, int index) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final theme = Theme.of(context);
final isSelected = index == selectedPieIndex; // Check if this item is selected
// Check if index is valid before accessing categories
if (index < 0 || index >= categories.length) {
return const SizedBox.shrink(); // Return empty if index is invalid
}
final category = categories[index];
// Calculate percentage, handle totalExpenses being zero
final percentage = totalExpenses > 0 ? (category.amount / totalExpenses * 100) : 0.0;
@@ -188,6 +265,12 @@ class SpendingPieChart extends StatelessWidget {
final isDark = Theme.of(context).brightness == Brightness.dark;
return List.generate(categories.length, (i) {
// Check if index is valid before accessing categories
if (i < 0 || i >= categories.length) {
// This should ideally not happen if List.generate is used correctly,
// but adding a safeguard.
return PieChartSectionData(); // Return an empty section
}
final isTouched = i == selectedPieIndex; // Check if this slice is selected
// Make selected slice slightly larger
final double radius = isTouched ? 65 : 55;
+86
View File
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import 'package:intl/intl.dart';
import 'summary_item.dart';
class SummaryCard extends StatelessWidget {
final double totalExpenses;
const SummaryCard({
Key? key,
required this.totalExpenses,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: FCard(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'Total Expenses This Period',
style: context.theme.typography.lg,
),
const SizedBox(height: 8),
_buildTotalAmount(context),
const SizedBox(height: 16),
FSeparator(),
const SizedBox(height: 16),
_buildSummaryItems(),
],
),
),
),
);
}
Widget _buildTotalAmount(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'\$',
style: context.theme.typography.xl.copyWith(
color: context.theme.colorScheme.primary,
),
),
Text(
NumberFormat.currency(symbol: '', decimalDigits: 2).format(totalExpenses),
style: context.theme.typography.xl4.copyWith(
color: context.theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildSummaryItems() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SummaryItem(
icon: Icons.arrow_downward_rounded,
title: 'Income',
amount: '\$2,450.00',
color: Colors.green,
),
Container(
height: 35,
width: 1,
color: Colors.grey.withOpacity(0.3),
),
SummaryItem(
icon: Icons.arrow_upward_rounded,
title: 'Expenses',
amount: '\$${NumberFormat.currency(symbol: '', decimalDigits: 2).format(totalExpenses)}',
color: Colors.red,
),
],
);
}
}
+6 -8
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
class SummaryItem extends StatelessWidget {
final IconData icon;
@@ -16,7 +17,6 @@ class SummaryItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Column(
children: [
Row(
@@ -24,14 +24,13 @@ class SummaryItem extends StatelessWidget {
Icon(
icon,
size: 16,
color: isDark ? color.withOpacity(0.8) : color,
color: color,
),
const SizedBox(width: 4),
Text(
title,
style: TextStyle(
fontSize: 13,
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
),
),
],
@@ -39,10 +38,9 @@ class SummaryItem extends StatelessWidget {
const SizedBox(height: 4),
Text(
amount,
style: TextStyle(
fontSize: 16,
style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
color: context.theme.colorScheme.foreground,
),
),
],
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import 'package:intl/intl.dart';
import '../database/database.dart' as db; // Import database with prefix 'db'
import '../models/transaction_record.dart';
@@ -16,9 +17,6 @@ class TransactionListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final theme = Theme.of(context);
// Get category details (icon, color) using the utility
final categoryDetails = CategoryUtils.getCategoryDetails(transaction.categoryName);
@@ -42,11 +40,6 @@ class TransactionListItem extends StatelessWidget {
displayDate = '${dateFormatter.format(transaction.date)}, ${timeFormatter.format(transaction.date)}';
}
// Determine text colors based on theme
Color primaryTextColor = theme.textTheme.bodyLarge?.color ?? (isDark ? Colors.white : Colors.black87);
Color secondaryTextColor = theme.textTheme.bodyMedium?.color ?? (isDark ? Colors.white70 : Colors.grey.shade600);
Color amountColor = isDark ? Colors.red.shade200 : Colors.red.shade700; // Expense color
// Use FadeTransition for item appearance (works with ListView.builder)
return FadeTransition(
opacity: animation, // Apply fade animation
@@ -60,16 +53,12 @@ class TransactionListItem extends StatelessWidget {
child: Row(
children: [
// Icon container with category color/icon
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: categoryDetails.colorCode.withOpacity(isDark ? 0.25 : 0.15), // Use category color with opacity
borderRadius: BorderRadius.circular(12), // Rounded corners
),
FAvatar(
backgroundColor: categoryDetails.colorCode.withOpacity(0.15),
child: Icon(
categoryDetails.iconCode, // Use category icon
color: categoryDetails.colorCode, // Use category color for icon
size: 20, // Icon size
categoryDetails.iconCode,
color: categoryDetails.colorCode,
size: 20,
),
),
const SizedBox(width: 12), // Spacing
@@ -82,9 +71,9 @@ class TransactionListItem extends StatelessWidget {
// Display Merchant if available, otherwise Category Name
Text(
transaction.merchant.isNotEmpty ? transaction.merchant : transaction.categoryName,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500, // Medium weight for primary text
color: primaryTextColor,
style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.w500,
color: context.theme.colorScheme.foreground,
),
maxLines: 1, // Prevent wrapping
overflow: TextOverflow.ellipsis, // Handle long text
@@ -93,9 +82,8 @@ class TransactionListItem extends StatelessWidget {
// Display formatted date/time
Text(
displayDate,
style: theme.textTheme.bodyMedium?.copyWith(
color: secondaryTextColor, // Lighter color for secondary text
fontSize: 12, // Smaller font size
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -109,9 +97,9 @@ class TransactionListItem extends StatelessWidget {
Text(
// Format amount as currency (negative for expense)
NumberFormat.currency(symbol: '-\$', decimalDigits: 2).format(transaction.amount),
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600, // Bold weight for amount
color: amountColor, // Use expense color
style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.w600,
color: context.theme.colorScheme.destructive,
),
),
],
@@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import '../database/database.dart' as db;
import '../models/transaction_record.dart' as model;
import '../utils/category_utils.dart';
import 'filter_chip_widget.dart';
import 'transaction_list_item.dart';
class TransactionsSection extends StatelessWidget {
final Stream<List<db.Transaction>> transactionsStream;
final bool isFilterVisible;
final String selectedFilter;
final VoidCallback onToggleFilter;
final Function(String) onApplyFilter;
const TransactionsSection({
Key? key,
required this.transactionsStream,
required this.isFilterVisible,
required this.selectedFilter,
required this.onToggleFilter,
required this.onApplyFilter,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
_buildFilterChips(),
_buildTransactionsList(context),
const SizedBox(height: 16),
],
);
}
Widget _buildHeader(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Recent Transactions',
style: context.theme.typography.xl.copyWith(
fontWeight: FontWeight.w600,
),
),
FButton(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(selectedFilter),
const SizedBox(width: 4),
const Icon(Icons.filter_list_alt, size: 18),
],
),
style: FButtonStyle.secondary,
onPress: onToggleFilter,
),
],
),
);
}
Widget _buildFilterChips() {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: isFilterVisible ? 50 : 0,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
padding: EdgeInsets.symmetric(
vertical: isFilterVisible ? 8 : 0,
),
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
FilterChipWidget(
label: 'All',
isSelected: selectedFilter == 'All',
onTap: () => onApplyFilter('All'),
),
...CategoryUtils.getAllCategoryNames().map((name) =>
FilterChipWidget(
label: name,
isSelected: selectedFilter == name,
onTap: () => onApplyFilter(name),
),
).toList(),
],
),
);
}
Widget _buildTransactionsList(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: FCard(
child: StreamBuilder<List<db.Transaction>>(
stream: transactionsStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(
height: 150,
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
if (snapshot.hasError) {
return SizedBox(
height: 150,
child: Center(child: Text('Error: ${snapshot.error}')),
);
}
final transactions = snapshot.data ?? [];
if (transactions.isEmpty) {
return SizedBox(
height: 150,
child: Center(
child: Text(
selectedFilter == 'All'
? 'No transactions yet.'
: 'No transactions found for $selectedFilter.',
style: context.theme.typography.base.copyWith(
color: context.theme.colorScheme.mutedForeground,
),
),
),
);
}
return ListView.separated(
itemCount: transactions.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8.0),
separatorBuilder: (context, index) => FSeparator(),
itemBuilder: (context, index) {
final dbTransaction = transactions[index];
final categoryDetails = CategoryUtils.getCategoryDetails(
dbTransaction.categoryName,
);
final transactionRecord = model.TransactionRecord(
dbTransaction.categoryName,
dbTransaction.amount,
categoryDetails.iconCode,
categoryDetails.colorCode,
dbTransaction.date,
dbTransaction.merchant,
);
return TransactionListItem(
transaction: transactionRecord,
animation: kAlwaysCompleteAnimation,
);
},
);
},
),
),
);
}
}
+77 -40
View File
@@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: "13c1e6c6fd460522ea840abec3f677cc226f5fec7872c04ad7b425517ccf54f7"
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
url: "https://pub.dev"
source: hosted
version: "7.4.4"
version: "7.4.5"
args:
dependency: transitive
description:
@@ -29,10 +29,10 @@ packages:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.12.0"
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
@@ -101,10 +101,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
url: "https://pub.dev"
source: hosted
version: "8.9.5"
version: "8.10.1"
characters:
dependency: transitive
description:
@@ -125,10 +125,10 @@ packages:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "2.0.4"
cli_util:
dependency: transitive
description:
@@ -189,26 +189,26 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.1.0"
drift:
dependency: "direct main"
description:
name: drift
sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
sha256: b584ddeb2b74436735dd2cf746d2d021e19a9a6770f409212fd5cbc2814ada85
url: "https://pub.dev"
source: hosted
version: "2.26.0"
version: "2.26.1"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
sha256: "54dc207c6e4662741f60e5752678df183957ab907754ffab0372a7082f6d2816"
url: "https://pub.dev"
source: hosted
version: "2.26.0"
version: "2.26.1"
equatable:
dependency: transitive
description:
@@ -221,10 +221,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
ffi:
dependency: transitive
description:
@@ -270,11 +270,32 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
forui:
dependency: "direct main"
description:
name: forui
sha256: e77b5774aef52883d148dba1d7b6ce7a723c5e1901b13142aea1bb61b9e68d9e
url: "https://pub.dev"
source: hosted
version: "0.12.0"
forui_assets:
dependency: transitive
description:
name: forui_assets
sha256: "795b47478ddf7cafb72e943f97ae004a4aa01f1ae40f0b2d68768c35ee680da4"
url: "https://pub.dev"
source: hosted
version: "0.12.0"
frontend_server_client:
dependency: transitive
description:
@@ -291,6 +312,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: transitive
description:
name: google_fonts
sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82
url: "https://pub.dev"
source: hosted
version: "6.2.1"
graphs:
dependency: transitive
description:
@@ -303,10 +332,10 @@ packages:
dependency: transitive
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
http_multi_server:
dependency: transitive
description:
@@ -327,10 +356,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
version: "0.20.2"
io:
dependency: transitive
description:
@@ -359,10 +388,10 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "10.0.9"
leak_tracker_flutter_testing:
dependency: transitive
description:
@@ -427,6 +456,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nitrogen_types:
dependency: transitive
description:
name: nitrogen_types
sha256: "5d4ec453aea14e34607bc6021ff1381192e8fcd8541bf103638baa4d013e668f"
url: "https://pub.dev"
source: hosted
version: "0.4.0+1"
package_config:
dependency: transitive
description:
@@ -539,14 +576,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart:
dependency: "direct main"
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
shelf:
dependency: transitive
description:
@@ -588,18 +617,18 @@ packages:
dependency: transitive
description:
name: sqlite3
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38
url: "https://pub.dev"
source: hosted
version: "2.7.5"
version: "2.7.6"
sqlite3_flutter_libs:
dependency: "direct main"
description:
name: sqlite3_flutter_libs
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
sha256: "7986c26234c0a5cf4fd83ff4ee39d4195b1f47cdb50a949ec7987ede4dcbdc2a"
url: "https://pub.dev"
source: hosted
version: "0.5.32"
version: "0.5.33"
sqlparser:
dependency: transitive
description:
@@ -640,6 +669,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
sugar:
dependency: transitive
description:
name: sugar
sha256: a0a97f1d3552a3ef4cbec7c6e62c7b52c151cd5c11e984e8311700a918f73dc5
url: "https://pub.dev"
source: hosted
version: "4.0.0"
term_glyph:
dependency: transitive
description:
@@ -684,10 +721,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "14.3.1"
version: "15.0.0"
watcher:
dependency: transitive
description:
@@ -708,10 +745,10 @@ packages:
dependency: transitive
description:
name: web_socket
sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
@@ -737,5 +774,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.32.0"
+10 -39
View File
@@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ^3.7.0
sdk: ^3.7.0 # Drift 2.18 requires Dart 3.4+, Drift 2.16 requires Dart 3.3+
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -31,21 +31,22 @@ dependencies:
flutter:
sdk: flutter
fl_chart: ^0.71.0
intl: ^0.19.0 # For date formatting
drift: ^2.26.0 # Added Drift
sqlite3_flutter_libs: ^0.5.22 # Recommended for Drift on Flutter
path_provider: ^2.1.3 # To find database file location
path: ^1.9.0 # To construct database file path
rxdart: ^0.28.0 # Added rxdart for combining streams in database
intl: ^0.20.2 # For date formatting
drift: ^2.18.0 # Updated Drift - основной пакет
sqlite3_flutter_libs: ^0.5.24 # Needed for native platforms
path_provider: ^2.1.3 # To find database file location on native
path: ^1.9.0 # To construct database file path on native
# rxdart: ^0.28.0 # Removed as it wasn't used in database.dart
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
forui: ^0.12.0
dev_dependencies:
flutter_test:
sdk: flutter
drift_dev: ^2.18.0 # Added Drift code generator
build_runner: ^2.4.11 # Added build_runner
drift_dev: ^2.18.0 # Updated Drift code generator
build_runner: ^2.4.11 # Updated build_runner
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
@@ -65,33 +66,3 @@ flutter:
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
+25
View File
@@ -0,0 +1,25 @@
Этап,Название,Описание,Файлы для создания,Зависимости,Статус,Запрос для ИИ
1,Базовая настройка проекта,Создание Flutter проекта и настройка зависимостей,pubspec.yaml,Нет,Не начато,"Создай новый Flutter проект для финансового приложения. В pubspec.yaml добавь следующие зависимости: flutter, cupertino_icons, drift (для базы данных), sqlite3_flutter_libs, path_provider, path, forui (UI библиотека), fl_chart (для графиков), intl (для форматирования). Также добавь dev_dependencies: drift_dev, build_runner для генерации кода базы данных. Настрой минимальную версию SDK: '>=3.0.0 <4.0.0'."
2,Основная структура приложения,Создание main.dart и базового MyApp виджета,lib/main.dart,Этап 1,Не начато,"Создай lib/main.dart с базовой структурой Flutter приложения. Включи: main() функцию с runApp(), StatefulWidget MyApp с поддержкой переключения темы (светлая/темная), базовый Scaffold с AppBar и заглушкой для будущего контента. MyApp должен принимать database как параметр конструктора. Добавь состояние для управления темой (_isDarkMode) и метод toggleTheme()."
3,Базовая тема приложения,Создание светлой и темной темы,lib/theme/app_theme.dart,Этап 2,Не начато,"Создай lib/theme/app_theme.dart с классом AppTheme, содержащим статические методы lightTheme и darkTheme. Используй зеленый цвет как основной (Colors.green). Для светлой темы: scaffoldBackgroundColor - Colors.grey.shade50, для темной - Color(0xFF121212). Настрой ColorScheme.fromSeed с соответствующими brightness и цветами. Добавь кастомизацию для AppBar, Card, Button стилей."
4,Модели данных,Создание базовых моделей для категорий и транзакций,lib/models/category.dart lib/models/transaction_record.dart,Этап 2,Не начато,"Создай две модели данных: 1) lib/models/category.dart с классом Category содержащим поля: String name, double amount, Color colorCode, IconData iconCode. 2) lib/models/transaction_record.dart с классом TransactionRecord содержащим: String categoryName, double amount, IconData iconCode, Color color, DateTime date, String merchant, и геттер id возвращающий 1. Импортируй flutter/material.dart в оба файла."
5,Утилиты категорий,Создание утилит для работы с категориями,lib/utils/category_utils.dart,Этап 4,Не начато,"Создай lib/utils/category_utils.dart с классом CategoryUtils. Добавь приватную Map _categoryDetails с предопределенными категориями: 'Groceries' (Icons.shopping_cart_outlined, Colors.green.shade400), 'Subscriptions' (Icons.subscriptions_outlined, Colors.orange.shade400), 'Restaurant' (Icons.restaurant_menu_outlined, Colors.red.shade400), 'Shopping' (Icons.shopping_bag_outlined, Colors.blue.shade400), 'Transport' (Icons.directions_bus_filled_outlined, Colors.purple.shade400), 'Travel' (Icons.flight_takeoff_outlined, Colors.cyan.shade400). Добавь методы: getCategoryDetails(), getCategoryByName(), getAllCategoryNames()."
6,Настройка базы данных - схема,Создание схемы таблиц Drift,lib/database/database.dart,Этап 4,Не начато,"Создай lib/database/database.dart с Drift базой данных. Определи таблицу Transactions с полями: id (autoIncrement), categoryName (text), amount (real), date (dateTime), merchant (text). Создай класс AppDatabase extends _$AppDatabase с schemaVersion = 1. Добавь методы: watchAllTransactions(), watchFilteredTransactions(String categoryName), addTransaction(), calculateCategoryTotals() возвращающий Stream<List<Category>>, insertInitialDataIfNeeded() с тестовыми данными. Используй аннотации @DriftDatabase и @DataClassName."
7,Настройка базы данных - подключение,Создание подключений для разных платформ,lib/database/database_connection/connection.dart lib/database/database_connection/connection_native.dart lib/database/database_connection/connection_web.dart,Этап 6,Не начато,"Создай систему подключения к базе данных для разных платформ: 1) lib/database/database_connection/connection.dart - базовый файл с функцией connect() throwing UnsupportedError. 2) connection_native.dart - для мобильных платформ, используй LazyDatabase с path_provider для получения пути к файлу db.sqlite. 3) connection_web.dart - для веб платформы, используй WebDatabase с именем 'db'. Добавь логирование путей и состояний подключения."
8,Базовый экран расходов,Создание простого экрана без функциональности,lib/screens/expenses_screen.dart,Этап 3,Не начато,"Создай lib/screens/expenses_screen.dart с StatefulWidget ExpensesScreen. Добавь параметры конструктора: Function toggleTheme, bool isDarkMode, AppDatabase database. Создай базовый UI с AppBar (иконка кошелька, заголовок 'My Finances', кнопка переключения темы, аватар профиля), Scaffold body с заглушками для будущих виджетов, BottomNavigationBar с тремя вкладками (Home, Reports, Settings), FloatingActionButton для добавления транзакций. Используй forui компоненты."
9,Контроллер расходов,Создание контроллера для управления состоянием,lib/controllers/expenses_controller.dart,Этап 6,Не начато,"Создай lib/controllers/expenses_controller.dart с классом ExpensesController. Добавь приватные поля состояния: _selectedPieIndex, _isPieChartExpanded, _isFilterVisible, _selectedFilter. Создай геттеры для всех состояний. Добавь методы: selectPieCategory(), togglePieChartVisibility(), toggleFilterVisibility(), applyFilter(), watchTransactions(), watchCategoryTotals(), addSampleTransaction() с рандомными данными. Контроллер должен принимать AppDatabase в конструкторе."
10,Карточка сводки,Виджет для отображения общих расходов,lib/widgets/summary_card.dart,Этап 8,Не начато,"Создай lib/widgets/summary_card.dart с StatelessWidget SummaryCard. Принимай double totalExpenses в конструкторе. Используй FCard из forui для обертки. Отображай: заголовок 'Total Expenses This Period', большую сумму с символом доллара (используй NumberFormat), FSeparator, и SummaryItem виджеты для Income (\$2,450.00, зеленый, стрелка вниз) и Expenses (переданная сумма, красный, стрелка вверх). Добавь отступы и стилизацию с помощью context.theme.typography."
11,Элемент сводки,Виджет для отдельных элементов сводки,lib/widgets/summary_item.dart,Этап 10,Не начато,"Создай lib/widgets/summary_item.dart с StatelessWidget SummaryItem. Параметры конструктора: IconData icon, String title, String amount, Color color. Создай Column с Row содержащим иконку и заголовок, затем текст с суммой. Используй context.theme.typography для стилизации: sm для заголовка с mutedForeground цветом, base для суммы с bold весом и foreground цветом. Добавь SizedBox для отступов между элементами."
12,Раздел транзакций - базовый,Простой список транзакций без фильтров,lib/widgets/transactions_section.dart,Этап 9,Не начато,"Создай lib/widgets/transactions_section.dart с StatelessWidget TransactionsSection. Параметры: Stream<List<db.Transaction>> transactionsStream, bool isFilterVisible, String selectedFilter, VoidCallback onToggleFilter, Function(String) onApplyFilter. Создай Column с заголовком 'Recent Transactions', кнопкой фильтра (FButton.secondary), анимированными фильтр чипами, и StreamBuilder для списка транзакций в FCard. Обрабатывай состояния loading, error, empty. Используй TransactionListItem для отображения каждой транзакции."
13,Элемент списка транзакций,Виджет для отображения одной транзакции,lib/widgets/transaction_list_item.dart,Этап 12,Не начато,"Создай lib/widgets/transaction_list_item.dart с StatelessWidget TransactionListItem. Параметры: TransactionRecord transaction, Animation<double> animation. Используй FadeTransition для анимации. Создай InkWell с Row содержащим: FAvatar с иконкой категории, Expanded Column с названием магазина/категории и датой (форматируй как 'Today', 'Yesterday' или дату), Text с суммой в формате -\$XX.XX красным цветом. Используй CategoryUtils для получения деталей категории. Добавь onTap с print для отладки."
14,Фильтр чипы,Виджеты для фильтрации транзакций,lib/widgets/filter_chip_widget.dart,Этап 12,Не начато,"Создай lib/widgets/filter_chip_widget.dart с StatelessWidget FilterChipWidget. Параметры: String label, bool isSelected, VoidCallback onTap. Используй FButton с динамическим стилем: FButtonStyle.primary для выбранного состояния, FButtonStyle.secondary для обычного. Оберни в Padding с отступом справа. Кнопка должна отображать переданный label и вызывать onTap при нажатии."
15,Раздел транзакций - с фильтрами,Добавление функциональности фильтрации,lib/widgets/transactions_section.dart (обновление),Этап 14,Не начато,"Обнови lib/widgets/transactions_section.dart добавив полную функциональность фильтрации. В _buildFilterChips() создай AnimatedContainer с ListView.builder для FilterChipWidget. Добавь чип 'All' и чипы для всех категорий из CategoryUtils.getAllCategoryNames(). В _buildTransactionsList() обнови логику отображения пустого состояния с учетом выбранного фильтра. Убедись что анимация показа/скрытия фильтров работает плавно с duration 300ms и Curves.easeInOut."
16,Расширяемая секция,Виджет для сворачивания/разворачивания секций,lib/widgets/expandable_section.dart,Этап 8,Не начато,"Создай lib/widgets/expandable_section.dart с StatelessWidget ExpandableSection. Параметры: String title, IconData icon, bool isExpanded, VoidCallback onTap, Widget child, Animation<double> heightFactor. Используй FCollapsible из forui с initiallyExpanded = isExpanded. В title создай Row с иконкой (primary цвет, размер 20) и текстом (bold, primary цвет). Оберни в Padding с отступами fromLTRB(16, 8, 16, 0). Передавай child в FCollapsible."
17,Круговая диаграмма,Виджет для отображения диаграммы расходов,lib/widgets/spending_pie_chart.dart,Этап 16,Не начато,"Создай lib/widgets/spending_pie_chart.dart с StatelessWidget SpendingPieChart. Параметры: List<Category> categories, double totalExpenses, int selectedPieIndex, Function(int) onSelectPieCategory, Animation<double> animation. Используй fl_chart PieChart с настройкой pieTouchData для обработки тапов. Создай Row с Expanded для диаграммы (flex: 5) и легенды (flex: 4). В центре диаграммы показывай выбранную категорию или общую сумму. Добавь анимацию scale и opacity. Легенда должна быть интерактивной с выделением выбранного элемента."
18,Экран профиля,Простой экран профиля пользователя,lib/screens/profile_screen.dart,Этап 8,Не начато,"Создай lib/screens/profile_screen.dart с StatelessWidget ProfileScreen. Создай Scaffold с AppBar (заголовок 'Profile', кнопка назад), body с Center содержащим Column: Hero анимированный FAvatar (tag: 'profileAvatar', размер 100, иконка person), имя пользователя 'John Doe' (xl2 typography, bold), email 'john.doe@example.com' (base typography, mutedForeground), FButton 'Edit Profile' (primary style, пустой onPress). Используй SizedBox для отступов между элементами."
19,Интеграция базы данных,Подключение всех компонентов к базе данных,Обновление существующих файлов,Этап 17,Не начато,"Интегрируй базу данных во все компоненты приложения. Обнови main.dart для инициализации AppDatabase и передачи в MyApp. В ExpensesScreen подключи StreamBuilder для categoryTotals, передавай streams в TransactionsSection. Убедись что все методы контроллера корректно работают с базой данных. Добавь обработку ошибок и состояний загрузки. Протестируй добавление транзакций и фильтрацию. Добавь вызов insertInitialDataIfNeeded() при первом запуске."
20,Добавление транзакций,Функциональность добавления новых транзакций,Обновление контроллера и экранов,Этап 19,Не начато,"Расширь функциональность добавления транзакций. Обнови ExpensesController.addSampleTransaction() для генерации более реалистичных данных с случайными категориями, суммами (5-100), датами (последние 7 дней), и магазинами. В ExpensesScreen добавь обработку успеха/ошибки с showFToast. Рассмотри создание отдельного экрана или диалога для ручного добавления транзакций с полями для категории, суммы, даты и магазина."
21,Анимации и переходы,Добавление анимаций для улучшения UX,Обновление виджетов,Этап 20,Не начато,"Улучши анимации во всем приложении. В ExpensesScreen добавь AnimationController для pie chart с forward() в initState. Обнови TransactionListItem для поддержки FadeTransition. В SpendingPieChart добавь AnimatedBuilder с scale и opacity анимациями. Добавь Hero анимацию для перехода к ProfileScreen. В TransactionsSection улучши AnimatedContainer для фильтров. Добавь swapAnimationDuration для PieChart при изменении данных."
22,Тестирование и отладка,Тестирование всей функциональности,Создание тестов,Этап 21,Не начато,"Создай комплексные тесты для приложения. Добавь unit тесты для: CategoryUtils методов, ExpensesController логики, database операций. Создай widget тесты для: SummaryCard, TransactionListItem, FilterChipWidget. Добавь integration тесты для: добавления транзакций, фильтрации, переключения темы. Создай test/widget_test.dart, test/unit_test.dart, integration_test/app_test.dart. Используй flutter_test, mockito для моков базы данных."
23,Оптимизация производительности,Оптимизация кода и производительности,Рефакторинг существующих файлов,Этап 22,Не начато,"Оптимизируй производительность приложения. Добавь const конструкторы где возможно. Используй ListView.builder вместо Column для больших списков. Оптимизируй StreamBuilder подписки. Добавь memo для дорогих вычислений в CategoryUtils. Используй RepaintBoundary для изоляции перерисовок. Оптимизируй анимации с AnimatedBuilder. Добавь lazy loading для больших списков транзакций. Проведи профилирование с Flutter Inspector."
24,Финальная полировка,Исправление багов и улучшение UI/UX,Обновление всех компонентов,Этап 23,Не начато,"Выполни финальную полировку приложения. Исправь все найденные баги. Улучши accessibility с Semantics виджетами. Добавь поддержку разных размеров экранов и ориентаций. Оптимизируй цветовые схемы для лучшей читаемости. Добавь haptic feedback для интерактивных элементов. Улучши error handling и user feedback. Добавь loading состояния где нужно. Проведи финальное тестирование на разных устройствах и платформах."
1 Этап Название Описание Файлы для создания Зависимости Статус Запрос для ИИ
2 1 Базовая настройка проекта Создание Flutter проекта и настройка зависимостей pubspec.yaml Нет Не начато Создай новый Flutter проект для финансового приложения. В pubspec.yaml добавь следующие зависимости: flutter, cupertino_icons, drift (для базы данных), sqlite3_flutter_libs, path_provider, path, forui (UI библиотека), fl_chart (для графиков), intl (для форматирования). Также добавь dev_dependencies: drift_dev, build_runner для генерации кода базы данных. Настрой минимальную версию SDK: '>=3.0.0 <4.0.0'.
3 2 Основная структура приложения Создание main.dart и базового MyApp виджета lib/main.dart Этап 1 Не начато Создай lib/main.dart с базовой структурой Flutter приложения. Включи: main() функцию с runApp(), StatefulWidget MyApp с поддержкой переключения темы (светлая/темная), базовый Scaffold с AppBar и заглушкой для будущего контента. MyApp должен принимать database как параметр конструктора. Добавь состояние для управления темой (_isDarkMode) и метод toggleTheme().
4 3 Базовая тема приложения Создание светлой и темной темы lib/theme/app_theme.dart Этап 2 Не начато Создай lib/theme/app_theme.dart с классом AppTheme, содержащим статические методы lightTheme и darkTheme. Используй зеленый цвет как основной (Colors.green). Для светлой темы: scaffoldBackgroundColor - Colors.grey.shade50, для темной - Color(0xFF121212). Настрой ColorScheme.fromSeed с соответствующими brightness и цветами. Добавь кастомизацию для AppBar, Card, Button стилей.
5 4 Модели данных Создание базовых моделей для категорий и транзакций lib/models/category.dart lib/models/transaction_record.dart Этап 2 Не начато Создай две модели данных: 1) lib/models/category.dart с классом Category содержащим поля: String name, double amount, Color colorCode, IconData iconCode. 2) lib/models/transaction_record.dart с классом TransactionRecord содержащим: String categoryName, double amount, IconData iconCode, Color color, DateTime date, String merchant, и геттер id возвращающий 1. Импортируй flutter/material.dart в оба файла.
6 5 Утилиты категорий Создание утилит для работы с категориями lib/utils/category_utils.dart Этап 4 Не начато Создай lib/utils/category_utils.dart с классом CategoryUtils. Добавь приватную Map _categoryDetails с предопределенными категориями: 'Groceries' (Icons.shopping_cart_outlined, Colors.green.shade400), 'Subscriptions' (Icons.subscriptions_outlined, Colors.orange.shade400), 'Restaurant' (Icons.restaurant_menu_outlined, Colors.red.shade400), 'Shopping' (Icons.shopping_bag_outlined, Colors.blue.shade400), 'Transport' (Icons.directions_bus_filled_outlined, Colors.purple.shade400), 'Travel' (Icons.flight_takeoff_outlined, Colors.cyan.shade400). Добавь методы: getCategoryDetails(), getCategoryByName(), getAllCategoryNames().
7 6 Настройка базы данных - схема Создание схемы таблиц Drift lib/database/database.dart Этап 4 Не начато Создай lib/database/database.dart с Drift базой данных. Определи таблицу Transactions с полями: id (autoIncrement), categoryName (text), amount (real), date (dateTime), merchant (text). Создай класс AppDatabase extends _$AppDatabase с schemaVersion = 1. Добавь методы: watchAllTransactions(), watchFilteredTransactions(String categoryName), addTransaction(), calculateCategoryTotals() возвращающий Stream<List<Category>>, insertInitialDataIfNeeded() с тестовыми данными. Используй аннотации @DriftDatabase и @DataClassName.
8 7 Настройка базы данных - подключение Создание подключений для разных платформ lib/database/database_connection/connection.dart lib/database/database_connection/connection_native.dart lib/database/database_connection/connection_web.dart Этап 6 Не начато Создай систему подключения к базе данных для разных платформ: 1) lib/database/database_connection/connection.dart - базовый файл с функцией connect() throwing UnsupportedError. 2) connection_native.dart - для мобильных платформ, используй LazyDatabase с path_provider для получения пути к файлу db.sqlite. 3) connection_web.dart - для веб платформы, используй WebDatabase с именем 'db'. Добавь логирование путей и состояний подключения.
9 8 Базовый экран расходов Создание простого экрана без функциональности lib/screens/expenses_screen.dart Этап 3 Не начато Создай lib/screens/expenses_screen.dart с StatefulWidget ExpensesScreen. Добавь параметры конструктора: Function toggleTheme, bool isDarkMode, AppDatabase database. Создай базовый UI с AppBar (иконка кошелька, заголовок 'My Finances', кнопка переключения темы, аватар профиля), Scaffold body с заглушками для будущих виджетов, BottomNavigationBar с тремя вкладками (Home, Reports, Settings), FloatingActionButton для добавления транзакций. Используй forui компоненты.
10 9 Контроллер расходов Создание контроллера для управления состоянием lib/controllers/expenses_controller.dart Этап 6 Не начато Создай lib/controllers/expenses_controller.dart с классом ExpensesController. Добавь приватные поля состояния: _selectedPieIndex, _isPieChartExpanded, _isFilterVisible, _selectedFilter. Создай геттеры для всех состояний. Добавь методы: selectPieCategory(), togglePieChartVisibility(), toggleFilterVisibility(), applyFilter(), watchTransactions(), watchCategoryTotals(), addSampleTransaction() с рандомными данными. Контроллер должен принимать AppDatabase в конструкторе.
11 10 Карточка сводки Виджет для отображения общих расходов lib/widgets/summary_card.dart Этап 8 Не начато Создай lib/widgets/summary_card.dart с StatelessWidget SummaryCard. Принимай double totalExpenses в конструкторе. Используй FCard из forui для обертки. Отображай: заголовок 'Total Expenses This Period', большую сумму с символом доллара (используй NumberFormat), FSeparator, и SummaryItem виджеты для Income (\$2,450.00, зеленый, стрелка вниз) и Expenses (переданная сумма, красный, стрелка вверх). Добавь отступы и стилизацию с помощью context.theme.typography.
12 11 Элемент сводки Виджет для отдельных элементов сводки lib/widgets/summary_item.dart Этап 10 Не начато Создай lib/widgets/summary_item.dart с StatelessWidget SummaryItem. Параметры конструктора: IconData icon, String title, String amount, Color color. Создай Column с Row содержащим иконку и заголовок, затем текст с суммой. Используй context.theme.typography для стилизации: sm для заголовка с mutedForeground цветом, base для суммы с bold весом и foreground цветом. Добавь SizedBox для отступов между элементами.
13 12 Раздел транзакций - базовый Простой список транзакций без фильтров lib/widgets/transactions_section.dart Этап 9 Не начато Создай lib/widgets/transactions_section.dart с StatelessWidget TransactionsSection. Параметры: Stream<List<db.Transaction>> transactionsStream, bool isFilterVisible, String selectedFilter, VoidCallback onToggleFilter, Function(String) onApplyFilter. Создай Column с заголовком 'Recent Transactions', кнопкой фильтра (FButton.secondary), анимированными фильтр чипами, и StreamBuilder для списка транзакций в FCard. Обрабатывай состояния loading, error, empty. Используй TransactionListItem для отображения каждой транзакции.
14 13 Элемент списка транзакций Виджет для отображения одной транзакции lib/widgets/transaction_list_item.dart Этап 12 Не начато Создай lib/widgets/transaction_list_item.dart с StatelessWidget TransactionListItem. Параметры: TransactionRecord transaction, Animation<double> animation. Используй FadeTransition для анимации. Создай InkWell с Row содержащим: FAvatar с иконкой категории, Expanded Column с названием магазина/категории и датой (форматируй как 'Today', 'Yesterday' или дату), Text с суммой в формате -\$XX.XX красным цветом. Используй CategoryUtils для получения деталей категории. Добавь onTap с print для отладки.
15 14 Фильтр чипы Виджеты для фильтрации транзакций lib/widgets/filter_chip_widget.dart Этап 12 Не начато Создай lib/widgets/filter_chip_widget.dart с StatelessWidget FilterChipWidget. Параметры: String label, bool isSelected, VoidCallback onTap. Используй FButton с динамическим стилем: FButtonStyle.primary для выбранного состояния, FButtonStyle.secondary для обычного. Оберни в Padding с отступом справа. Кнопка должна отображать переданный label и вызывать onTap при нажатии.
16 15 Раздел транзакций - с фильтрами Добавление функциональности фильтрации lib/widgets/transactions_section.dart (обновление) Этап 14 Не начато Обнови lib/widgets/transactions_section.dart добавив полную функциональность фильтрации. В _buildFilterChips() создай AnimatedContainer с ListView.builder для FilterChipWidget. Добавь чип 'All' и чипы для всех категорий из CategoryUtils.getAllCategoryNames(). В _buildTransactionsList() обнови логику отображения пустого состояния с учетом выбранного фильтра. Убедись что анимация показа/скрытия фильтров работает плавно с duration 300ms и Curves.easeInOut.
17 16 Расширяемая секция Виджет для сворачивания/разворачивания секций lib/widgets/expandable_section.dart Этап 8 Не начато Создай lib/widgets/expandable_section.dart с StatelessWidget ExpandableSection. Параметры: String title, IconData icon, bool isExpanded, VoidCallback onTap, Widget child, Animation<double> heightFactor. Используй FCollapsible из forui с initiallyExpanded = isExpanded. В title создай Row с иконкой (primary цвет, размер 20) и текстом (bold, primary цвет). Оберни в Padding с отступами fromLTRB(16, 8, 16, 0). Передавай child в FCollapsible.
18 17 Круговая диаграмма Виджет для отображения диаграммы расходов lib/widgets/spending_pie_chart.dart Этап 16 Не начато Создай lib/widgets/spending_pie_chart.dart с StatelessWidget SpendingPieChart. Параметры: List<Category> categories, double totalExpenses, int selectedPieIndex, Function(int) onSelectPieCategory, Animation<double> animation. Используй fl_chart PieChart с настройкой pieTouchData для обработки тапов. Создай Row с Expanded для диаграммы (flex: 5) и легенды (flex: 4). В центре диаграммы показывай выбранную категорию или общую сумму. Добавь анимацию scale и opacity. Легенда должна быть интерактивной с выделением выбранного элемента.
19 18 Экран профиля Простой экран профиля пользователя lib/screens/profile_screen.dart Этап 8 Не начато Создай lib/screens/profile_screen.dart с StatelessWidget ProfileScreen. Создай Scaffold с AppBar (заголовок 'Profile', кнопка назад), body с Center содержащим Column: Hero анимированный FAvatar (tag: 'profileAvatar', размер 100, иконка person), имя пользователя 'John Doe' (xl2 typography, bold), email 'john.doe@example.com' (base typography, mutedForeground), FButton 'Edit Profile' (primary style, пустой onPress). Используй SizedBox для отступов между элементами.
20 19 Интеграция базы данных Подключение всех компонентов к базе данных Обновление существующих файлов Этап 17 Не начато Интегрируй базу данных во все компоненты приложения. Обнови main.dart для инициализации AppDatabase и передачи в MyApp. В ExpensesScreen подключи StreamBuilder для categoryTotals, передавай streams в TransactionsSection. Убедись что все методы контроллера корректно работают с базой данных. Добавь обработку ошибок и состояний загрузки. Протестируй добавление транзакций и фильтрацию. Добавь вызов insertInitialDataIfNeeded() при первом запуске.
21 20 Добавление транзакций Функциональность добавления новых транзакций Обновление контроллера и экранов Этап 19 Не начато Расширь функциональность добавления транзакций. Обнови ExpensesController.addSampleTransaction() для генерации более реалистичных данных с случайными категориями, суммами (5-100), датами (последние 7 дней), и магазинами. В ExpensesScreen добавь обработку успеха/ошибки с showFToast. Рассмотри создание отдельного экрана или диалога для ручного добавления транзакций с полями для категории, суммы, даты и магазина.
22 21 Анимации и переходы Добавление анимаций для улучшения UX Обновление виджетов Этап 20 Не начато Улучши анимации во всем приложении. В ExpensesScreen добавь AnimationController для pie chart с forward() в initState. Обнови TransactionListItem для поддержки FadeTransition. В SpendingPieChart добавь AnimatedBuilder с scale и opacity анимациями. Добавь Hero анимацию для перехода к ProfileScreen. В TransactionsSection улучши AnimatedContainer для фильтров. Добавь swapAnimationDuration для PieChart при изменении данных.
23 22 Тестирование и отладка Тестирование всей функциональности Создание тестов Этап 21 Не начато Создай комплексные тесты для приложения. Добавь unit тесты для: CategoryUtils методов, ExpensesController логики, database операций. Создай widget тесты для: SummaryCard, TransactionListItem, FilterChipWidget. Добавь integration тесты для: добавления транзакций, фильтрации, переключения темы. Создай test/widget_test.dart, test/unit_test.dart, integration_test/app_test.dart. Используй flutter_test, mockito для моков базы данных.
24 23 Оптимизация производительности Оптимизация кода и производительности Рефакторинг существующих файлов Этап 22 Не начато Оптимизируй производительность приложения. Добавь const конструкторы где возможно. Используй ListView.builder вместо Column для больших списков. Оптимизируй StreamBuilder подписки. Добавь memo для дорогих вычислений в CategoryUtils. Используй RepaintBoundary для изоляции перерисовок. Оптимизируй анимации с AnimatedBuilder. Добавь lazy loading для больших списков транзакций. Проведи профилирование с Flutter Inspector.
25 24 Финальная полировка Исправление багов и улучшение UI/UX Обновление всех компонентов Этап 23 Не начато Выполни финальную полировку приложения. Исправь все найденные баги. Улучши accessibility с Semantics виджетами. Добавь поддержку разных размеров экранов и ориентаций. Оптимизируй цветовые схемы для лучшей читаемости. Добавь haptic feedback для интерактивных элементов. Улучши error handling и user feedback. Добавь loading состояния где нужно. Проведи финальное тестирование на разных устройствах и платформах.
+25
View File
@@ -0,0 +1,25 @@
Этап,Название,Описание,Файлы для создания,Зависимости,Статус,Запрос для ИИ
1,Базовая настройка проекта,Создание Flutter проекта и настройка зависимостей,pubspec.yaml,Нет,Не начато,"Создай новый Flutter проект для финансового приложения. В pubspec.yaml добавь следующие зависимости: flutter, cupertino_icons, drift (для базы данных), sqlite3_flutter_libs, path_provider, path, forui (UI библиотека), fl_chart (для графиков), intl (для форматирования). Также добавь dev_dependencies: drift_dev, build_runner для генерации кода базы данных. Настрой минимальную версию SDK: '>=3.0.0 <4.0.0'."
2,Основная структура приложения,Создание main.dart и базового MyApp виджета,lib/main.dart,Этап 1,Не начато,"Создай lib/main.dart с базовой структурой Flutter приложения. Включи: main() функцию с runApp(), StatefulWidget MyApp с поддержкой переключения темы (светлая/темная), базовый Scaffold с AppBar и заглушкой для будущего контента. MyApp должен принимать database как параметр конструктора. Добавь состояние для управления темой (_isDarkMode) и метод toggleTheme()."
3,Базовая тема приложения,Создание светлой и темной темы,lib/theme/app_theme.dart,Этап 2,Не начато,"Создай lib/theme/app_theme.dart с классом AppTheme, содержащим статические методы lightTheme и darkTheme. Используй зеленый цвет как основной (Colors.green). Для светлой темы: scaffoldBackgroundColor - Colors.grey.shade50, для темной - Color(0xFF121212). Настрой ColorScheme.fromSeed с соответствующими brightness и цветами. Добавь кастомизацию для AppBar, Card, Button стилей."
4,Модели данных,Создание базовых моделей для категорий и транзакций,lib/models/category.dart lib/models/transaction_record.dart,Этап 2,Не начато,"Создай две модели данных: 1) lib/models/category.dart с классом Category содержащим поля: String name, double amount, Color colorCode, IconData iconCode. 2) lib/models/transaction_record.dart с классом TransactionRecord содержащим: String categoryName, double amount, IconData iconCode, Color color, DateTime date, String merchant, и геттер id возвращающий 1. Импортируй flutter/material.dart в оба файла."
5,Утилиты категорий,Создание утилит для работы с категориями,lib/utils/category_utils.dart,Этап 4,Не начато,"Создай lib/utils/category_utils.dart с классом CategoryUtils. Добавь приватную Map _categoryDetails с предопределенными категориями: 'Groceries' (Icons.shopping_cart_outlined, Colors.green.shade400), 'Subscriptions' (Icons.subscriptions_outlined, Colors.orange.shade400), 'Restaurant' (Icons.restaurant_menu_outlined, Colors.red.shade400), 'Shopping' (Icons.shopping_bag_outlined, Colors.blue.shade400), 'Transport' (Icons.directions_bus_filled_outlined, Colors.purple.shade400), 'Travel' (Icons.flight_takeoff_outlined, Colors.cyan.shade400). Добавь методы: getCategoryDetails(), getCategoryByName(), getAllCategoryNames()."
6,Настройка базы данных - схема,Создание схемы таблиц Drift,lib/database/database.dart,Этап 4,Не начато,"Создай lib/database/database.dart с Drift базой данных. Определи таблицу Transactions с полями: id (autoIncrement), categoryName (text), amount (real), date (dateTime), merchant (text). Создай класс AppDatabase extends _$AppDatabase с schemaVersion = 1. Добавь методы: watchAllTransactions(), watchFilteredTransactions(String categoryName), addTransaction(), calculateCategoryTotals() возвращающий Stream<List<Category>>, insertInitialDataIfNeeded() с тестовыми данными. Используй аннотации @DriftDatabase и @DataClassName."
7,Настройка базы данных - подключение,Создание подключений для разных платформ,lib/database/database_connection/connection.dart lib/database/database_connection/connection_native.dart lib/database/database_connection/connection_web.dart,Этап 6,Не начато,"Создай систему подключения к базе данных для разных платформ: 1) lib/database/database_connection/connection.dart - базовый файл с функцией connect() throwing UnsupportedError. 2) connection_native.dart - для мобильных платформ, используй LazyDatabase с path_provider для получения пути к файлу db.sqlite. 3) connection_web.dart - для веб платформы, используй WebDatabase с именем 'db'. Добавь логирование путей и состояний подключения."
8,Базовый экран расходов,Создание простого экрана без функциональности,lib/screens/expenses_screen.dart,Этап 3,Не начато,"Создай lib/screens/expenses_screen.dart с StatefulWidget ExpensesScreen. Добавь параметры конструктора: Function toggleTheme, bool isDarkMode, AppDatabase database. Создай базовый UI с AppBar (иконка кошелька, заголовок 'My Finances', кнопка переключения темы, аватар профиля), Scaffold body с заглушками для будущих виджетов, BottomNavigationBar с тремя вкладками (Home, Reports, Settings), FloatingActionButton для добавления транзакций. Используй forui компоненты."
9,Контроллер расходов,Создание контроллера для управления состоянием,lib/controllers/expenses_controller.dart,Этап 6,Не начато,"Создай lib/controllers/expenses_controller.dart с классом ExpensesController. Добавь приватные поля состояния: _selectedPieIndex, _isPieChartExpanded, _isFilterVisible, _selectedFilter. Создай геттеры для всех состояний. Добавь методы: selectPieCategory(), togglePieChartVisibility(), toggleFilterVisibility(), applyFilter(), watchTransactions(), watchCategoryTotals(), addSampleTransaction() с рандомными данными. Контроллер должен принимать AppDatabase в конструкторе."
10,Карточка сводки,Виджет для отображения общих расходов,lib/widgets/summary_card.dart,Этап 8,Не начато,"Создай lib/widgets/summary_card.dart с StatelessWidget SummaryCard. Принимай double totalExpenses в конструкторе. Используй FCard из forui для обертки. Отображай: заголовок 'Total Expenses This Period', большую сумму с символом доллара (используй NumberFormat), FSeparator, и SummaryItem виджеты для Income (\$2,450.00, зеленый, стрелка вниз) и Expenses (переданная сумма, красный, стрелка вверх). Добавь отступы и стилизацию с помощью context.theme.typography."
11,Элемент сводки,Виджет для отдельных элементов сводки,lib/widgets/summary_item.dart,Этап 10,Не начато,"Создай lib/widgets/summary_item.dart с StatelessWidget SummaryItem. Параметры конструктора: IconData icon, String title, String amount, Color color. Создай Column с Row содержащим иконку и заголовок, затем текст с суммой. Используй context.theme.typography для стилизации: sm для заголовка с mutedForeground цветом, base для суммы с bold весом и foreground цветом. Добавь SizedBox для отступов между элементами."
12,Раздел транзакций - базовый,Простой список транзакций без фильтров,lib/widgets/transactions_section.dart,Этап 9,Не начато,"Создай lib/widgets/transactions_section.dart с StatelessWidget TransactionsSection. Параметры: Stream<List<db.Transaction>> transactionsStream, bool isFilterVisible, String selectedFilter, VoidCallback onToggleFilter, Function(String) onApplyFilter. Создай Column с заголовком 'Recent Transactions', кнопкой фильтра (FButton.secondary), анимированными фильтр чипами, и StreamBuilder для списка транзакций в FCard. Обрабатывай состояния loading, error, empty. Используй TransactionListItem для отображения каждой транзакции."
13,Элемент списка транзакций,Виджет для отображения одной транзакции,lib/widgets/transaction_list_item.dart,Этап 12,Не начато,"Создай lib/widgets/transaction_list_item.dart с StatelessWidget TransactionListItem. Параметры: TransactionRecord transaction, Animation<double> animation. Используй FadeTransition для анимации. Создай InkWell с Row содержащим: FAvatar с иконкой категории, Expanded Column с названием магазина/категории и датой (форматируй как 'Today', 'Yesterday' или дату), Text с суммой в формате -\$XX.XX красным цветом. Используй CategoryUtils для получения деталей категории. Добавь onTap с print для отладки."
14,Фильтр чипы,Виджеты для фильтрации транзакций,lib/widgets/filter_chip_widget.dart,Этап 12,Не начато,"Создай lib/widgets/filter_chip_widget.dart с StatelessWidget FilterChipWidget. Параметры: String label, bool isSelected, VoidCallback onTap. Используй FButton с динамическим стилем: FButtonStyle.primary для выбранного состояния, FButtonStyle.secondary для обычного. Оберни в Padding с отступом справа. Кнопка должна отображать переданный label и вызывать onTap при нажатии."
15,Раздел транзакций - с фильтрами,Добавление функциональности фильтрации,lib/widgets/transactions_section.dart (обновление),Этап 14,Не начато,"Обнови lib/widgets/transactions_section.dart добавив полную функциональность фильтрации. В _buildFilterChips() создай AnimatedContainer с ListView.builder для FilterChipWidget. Добавь чип 'All' и чипы для всех категорий из CategoryUtils.getAllCategoryNames(). В _buildTransactionsList() обнови логику отображения пустого состояния с учетом выбранного фильтра. Убедись что анимация показа/скрытия фильтров работает плавно с duration 300ms и Curves.easeInOut."
16,Расширяемая секция,Виджет для сворачивания/разворачивания секций,lib/widgets/expandable_section.dart,Этап 8,Не начато,"Создай lib/widgets/expandable_section.dart с StatelessWidget ExpandableSection. Параметры: String title, IconData icon, bool isExpanded, VoidCallback onTap, Widget child, Animation<double> heightFactor. Используй FCollapsible из forui с initiallyExpanded = isExpanded. В title создай Row с иконкой (primary цвет, размер 20) и текстом (bold, primary цвет). Оберни в Padding с отступами fromLTRB(16, 8, 16, 0). Передавай child в FCollapsible."
17,Круговая диаграмма,Виджет для отображения диаграммы расходов,lib/widgets/spending_pie_chart.dart,Этап 16,Не начато,"Создай lib/widgets/spending_pie_chart.dart с StatelessWidget SpendingPieChart. Параметры: List<Category> categories, double totalExpenses, int selectedPieIndex, Function(int) onSelectPieCategory, Animation<double> animation. Используй fl_chart PieChart с настройкой pieTouchData для обработки тапов. Создай Row с Expanded для диаграммы (flex: 5) и легенды (flex: 4). В центре диаграммы показывай выбранную категорию или общую сумму. Добавь анимацию scale и opacity. Легенда должна быть интерактивной с выделением выбранного элемента."
18,Экран профиля,Простой экран профиля пользователя,lib/screens/profile_screen.dart,Этап 8,Не начато,"Создай lib/screens/profile_screen.dart с StatelessWidget ProfileScreen. Создай Scaffold с AppBar (заголовок 'Profile', кнопка назад), body с Center содержащим Column: Hero анимированный FAvatar (tag: 'profileAvatar', размер 100, иконка person), имя пользователя 'John Doe' (xl2 typography, bold), email 'john.doe@example.com' (base typography, mutedForeground), FButton 'Edit Profile' (primary style, пустой onPress). Используй SizedBox для отступов между элементами."
19,Интеграция базы данных,Подключение всех компонентов к базе данных,Обновление существующих файлов,Этап 17,Не начато,"Интегрируй базу данных во все компоненты приложения. Обнови main.dart для инициализации AppDatabase и передачи в MyApp. В ExpensesScreen подключи StreamBuilder для categoryTotals, передавай streams в TransactionsSection. Убедись что все методы контроллера корректно работают с базой данных. Добавь обработку ошибок и состояний загрузки. Протестируй добавление транзакций и фильтрацию. Добавь вызов insertInitialDataIfNeeded() при первом запуске."
20,Добавление транзакций,Функциональность добавления новых транзакций,Обновление контроллера и экранов,Этап 19,Не начато,"Расширь функциональность добавления транзакций. Обнови ExpensesController.addSampleTransaction() для генерации более реалистичных данных с случайными категориями, суммами (5-100), датами (последние 7 дней), и магазинами. В ExpensesScreen добавь обработку успеха/ошибки с showFToast. Рассмотри создание отдельного экрана или диалога для ручного добавления транзакций с полями для категории, суммы, даты и магазина."
21,Анимации и переходы,Добавление анимаций для улучшения UX,Обновление виджетов,Этап 20,Не начато,"Улучши анимации во всем приложении. В ExpensesScreen добавь AnimationController для pie chart с forward() в initState. Обнови TransactionListItem для поддержки FadeTransition. В SpendingPieChart добавь AnimatedBuilder с scale и opacity анимациями. Добавь Hero анимацию для перехода к ProfileScreen. В TransactionsSection улучши AnimatedContainer для фильтров. Добавь swapAnimationDuration для PieChart при изменении данных."
22,Тестирование и отладка,Тестирование всей функциональности,Создание тестов,Этап 21,Не начато,"Создай комплексные тесты для приложения. Добавь unit тесты для: CategoryUtils методов, ExpensesController логики, database операций. Создай widget тесты для: SummaryCard, TransactionListItem, FilterChipWidget. Добавь integration тесты для: добавления транзакций, фильтрации, переключения темы. Создай test/widget_test.dart, test/unit_test.dart, integration_test/app_test.dart. Используй flutter_test, mockito для моков базы данных."
23,Оптимизация производительности,Оптимизация кода и производительности,Рефакторинг существующих файлов,Этап 22,Не начато,"Оптимизируй производительность приложения. Добавь const конструкторы где возможно. Используй ListView.builder вместо Column для больших списков. Оптимизируй StreamBuilder подписки. Добавь memo для дорогих вычислений в CategoryUtils. Используй RepaintBoundary для изоляции перерисовок. Оптимизируй анимации с AnimatedBuilder. Добавь lazy loading для больших списков транзакций. Проведи профилирование с Flutter Inspector."
24,Финальная полировка,Исправление багов и улучшение UI/UX,Обновление всех компонентов,Этап 23,Не начато,"Выполни финальную полировку приложения. Исправь все найденные баги. Улучши accessibility с Semantics виджетами. Добавь поддержку разных размеров экранов и ориентаций. Оптимизируй цветовые схемы для лучшей читаемости. Добавь haptic feedback для интерактивных элементов. Улучши error handling и user feedback. Добавь loading состояния где нужно. Проведи финальное тестирование на разных устройствах и платформах."
1 Этап Название Описание Файлы для создания Зависимости Статус Запрос для ИИ
2 1 Базовая настройка проекта Создание Flutter проекта и настройка зависимостей pubspec.yaml Нет Не начато Создай новый Flutter проект для финансового приложения. В pubspec.yaml добавь следующие зависимости: flutter, cupertino_icons, drift (для базы данных), sqlite3_flutter_libs, path_provider, path, forui (UI библиотека), fl_chart (для графиков), intl (для форматирования). Также добавь dev_dependencies: drift_dev, build_runner для генерации кода базы данных. Настрой минимальную версию SDK: '>=3.0.0 <4.0.0'.
3 2 Основная структура приложения Создание main.dart и базового MyApp виджета lib/main.dart Этап 1 Не начато Создай lib/main.dart с базовой структурой Flutter приложения. Включи: main() функцию с runApp(), StatefulWidget MyApp с поддержкой переключения темы (светлая/темная), базовый Scaffold с AppBar и заглушкой для будущего контента. MyApp должен принимать database как параметр конструктора. Добавь состояние для управления темой (_isDarkMode) и метод toggleTheme().
4 3 Базовая тема приложения Создание светлой и темной темы lib/theme/app_theme.dart Этап 2 Не начато Создай lib/theme/app_theme.dart с классом AppTheme, содержащим статические методы lightTheme и darkTheme. Используй зеленый цвет как основной (Colors.green). Для светлой темы: scaffoldBackgroundColor - Colors.grey.shade50, для темной - Color(0xFF121212). Настрой ColorScheme.fromSeed с соответствующими brightness и цветами. Добавь кастомизацию для AppBar, Card, Button стилей.
5 4 Модели данных Создание базовых моделей для категорий и транзакций lib/models/category.dart lib/models/transaction_record.dart Этап 2 Не начато Создай две модели данных: 1) lib/models/category.dart с классом Category содержащим поля: String name, double amount, Color colorCode, IconData iconCode. 2) lib/models/transaction_record.dart с классом TransactionRecord содержащим: String categoryName, double amount, IconData iconCode, Color color, DateTime date, String merchant, и геттер id возвращающий 1. Импортируй flutter/material.dart в оба файла.
6 5 Утилиты категорий Создание утилит для работы с категориями lib/utils/category_utils.dart Этап 4 Не начато Создай lib/utils/category_utils.dart с классом CategoryUtils. Добавь приватную Map _categoryDetails с предопределенными категориями: 'Groceries' (Icons.shopping_cart_outlined, Colors.green.shade400), 'Subscriptions' (Icons.subscriptions_outlined, Colors.orange.shade400), 'Restaurant' (Icons.restaurant_menu_outlined, Colors.red.shade400), 'Shopping' (Icons.shopping_bag_outlined, Colors.blue.shade400), 'Transport' (Icons.directions_bus_filled_outlined, Colors.purple.shade400), 'Travel' (Icons.flight_takeoff_outlined, Colors.cyan.shade400). Добавь методы: getCategoryDetails(), getCategoryByName(), getAllCategoryNames().
7 6 Настройка базы данных - схема Создание схемы таблиц Drift lib/database/database.dart Этап 4 Не начато Создай lib/database/database.dart с Drift базой данных. Определи таблицу Transactions с полями: id (autoIncrement), categoryName (text), amount (real), date (dateTime), merchant (text). Создай класс AppDatabase extends _$AppDatabase с schemaVersion = 1. Добавь методы: watchAllTransactions(), watchFilteredTransactions(String categoryName), addTransaction(), calculateCategoryTotals() возвращающий Stream<List<Category>>, insertInitialDataIfNeeded() с тестовыми данными. Используй аннотации @DriftDatabase и @DataClassName.
8 7 Настройка базы данных - подключение Создание подключений для разных платформ lib/database/database_connection/connection.dart lib/database/database_connection/connection_native.dart lib/database/database_connection/connection_web.dart Этап 6 Не начато Создай систему подключения к базе данных для разных платформ: 1) lib/database/database_connection/connection.dart - базовый файл с функцией connect() throwing UnsupportedError. 2) connection_native.dart - для мобильных платформ, используй LazyDatabase с path_provider для получения пути к файлу db.sqlite. 3) connection_web.dart - для веб платформы, используй WebDatabase с именем 'db'. Добавь логирование путей и состояний подключения.
9 8 Базовый экран расходов Создание простого экрана без функциональности lib/screens/expenses_screen.dart Этап 3 Не начато Создай lib/screens/expenses_screen.dart с StatefulWidget ExpensesScreen. Добавь параметры конструктора: Function toggleTheme, bool isDarkMode, AppDatabase database. Создай базовый UI с AppBar (иконка кошелька, заголовок 'My Finances', кнопка переключения темы, аватар профиля), Scaffold body с заглушками для будущих виджетов, BottomNavigationBar с тремя вкладками (Home, Reports, Settings), FloatingActionButton для добавления транзакций. Используй forui компоненты.
10 9 Контроллер расходов Создание контроллера для управления состоянием lib/controllers/expenses_controller.dart Этап 6 Не начато Создай lib/controllers/expenses_controller.dart с классом ExpensesController. Добавь приватные поля состояния: _selectedPieIndex, _isPieChartExpanded, _isFilterVisible, _selectedFilter. Создай геттеры для всех состояний. Добавь методы: selectPieCategory(), togglePieChartVisibility(), toggleFilterVisibility(), applyFilter(), watchTransactions(), watchCategoryTotals(), addSampleTransaction() с рандомными данными. Контроллер должен принимать AppDatabase в конструкторе.
11 10 Карточка сводки Виджет для отображения общих расходов lib/widgets/summary_card.dart Этап 8 Не начато Создай lib/widgets/summary_card.dart с StatelessWidget SummaryCard. Принимай double totalExpenses в конструкторе. Используй FCard из forui для обертки. Отображай: заголовок 'Total Expenses This Period', большую сумму с символом доллара (используй NumberFormat), FSeparator, и SummaryItem виджеты для Income (\$2,450.00, зеленый, стрелка вниз) и Expenses (переданная сумма, красный, стрелка вверх). Добавь отступы и стилизацию с помощью context.theme.typography.
12 11 Элемент сводки Виджет для отдельных элементов сводки lib/widgets/summary_item.dart Этап 10 Не начато Создай lib/widgets/summary_item.dart с StatelessWidget SummaryItem. Параметры конструктора: IconData icon, String title, String amount, Color color. Создай Column с Row содержащим иконку и заголовок, затем текст с суммой. Используй context.theme.typography для стилизации: sm для заголовка с mutedForeground цветом, base для суммы с bold весом и foreground цветом. Добавь SizedBox для отступов между элементами.
13 12 Раздел транзакций - базовый Простой список транзакций без фильтров lib/widgets/transactions_section.dart Этап 9 Не начато Создай lib/widgets/transactions_section.dart с StatelessWidget TransactionsSection. Параметры: Stream<List<db.Transaction>> transactionsStream, bool isFilterVisible, String selectedFilter, VoidCallback onToggleFilter, Function(String) onApplyFilter. Создай Column с заголовком 'Recent Transactions', кнопкой фильтра (FButton.secondary), анимированными фильтр чипами, и StreamBuilder для списка транзакций в FCard. Обрабатывай состояния loading, error, empty. Используй TransactionListItem для отображения каждой транзакции.
14 13 Элемент списка транзакций Виджет для отображения одной транзакции lib/widgets/transaction_list_item.dart Этап 12 Не начато Создай lib/widgets/transaction_list_item.dart с StatelessWidget TransactionListItem. Параметры: TransactionRecord transaction, Animation<double> animation. Используй FadeTransition для анимации. Создай InkWell с Row содержащим: FAvatar с иконкой категории, Expanded Column с названием магазина/категории и датой (форматируй как 'Today', 'Yesterday' или дату), Text с суммой в формате -\$XX.XX красным цветом. Используй CategoryUtils для получения деталей категории. Добавь onTap с print для отладки.
15 14 Фильтр чипы Виджеты для фильтрации транзакций lib/widgets/filter_chip_widget.dart Этап 12 Не начато Создай lib/widgets/filter_chip_widget.dart с StatelessWidget FilterChipWidget. Параметры: String label, bool isSelected, VoidCallback onTap. Используй FButton с динамическим стилем: FButtonStyle.primary для выбранного состояния, FButtonStyle.secondary для обычного. Оберни в Padding с отступом справа. Кнопка должна отображать переданный label и вызывать onTap при нажатии.
16 15 Раздел транзакций - с фильтрами Добавление функциональности фильтрации lib/widgets/transactions_section.dart (обновление) Этап 14 Не начато Обнови lib/widgets/transactions_section.dart добавив полную функциональность фильтрации. В _buildFilterChips() создай AnimatedContainer с ListView.builder для FilterChipWidget. Добавь чип 'All' и чипы для всех категорий из CategoryUtils.getAllCategoryNames(). В _buildTransactionsList() обнови логику отображения пустого состояния с учетом выбранного фильтра. Убедись что анимация показа/скрытия фильтров работает плавно с duration 300ms и Curves.easeInOut.
17 16 Расширяемая секция Виджет для сворачивания/разворачивания секций lib/widgets/expandable_section.dart Этап 8 Не начато Создай lib/widgets/expandable_section.dart с StatelessWidget ExpandableSection. Параметры: String title, IconData icon, bool isExpanded, VoidCallback onTap, Widget child, Animation<double> heightFactor. Используй FCollapsible из forui с initiallyExpanded = isExpanded. В title создай Row с иконкой (primary цвет, размер 20) и текстом (bold, primary цвет). Оберни в Padding с отступами fromLTRB(16, 8, 16, 0). Передавай child в FCollapsible.
18 17 Круговая диаграмма Виджет для отображения диаграммы расходов lib/widgets/spending_pie_chart.dart Этап 16 Не начато Создай lib/widgets/spending_pie_chart.dart с StatelessWidget SpendingPieChart. Параметры: List<Category> categories, double totalExpenses, int selectedPieIndex, Function(int) onSelectPieCategory, Animation<double> animation. Используй fl_chart PieChart с настройкой pieTouchData для обработки тапов. Создай Row с Expanded для диаграммы (flex: 5) и легенды (flex: 4). В центре диаграммы показывай выбранную категорию или общую сумму. Добавь анимацию scale и opacity. Легенда должна быть интерактивной с выделением выбранного элемента.
19 18 Экран профиля Простой экран профиля пользователя lib/screens/profile_screen.dart Этап 8 Не начато Создай lib/screens/profile_screen.dart с StatelessWidget ProfileScreen. Создай Scaffold с AppBar (заголовок 'Profile', кнопка назад), body с Center содержащим Column: Hero анимированный FAvatar (tag: 'profileAvatar', размер 100, иконка person), имя пользователя 'John Doe' (xl2 typography, bold), email 'john.doe@example.com' (base typography, mutedForeground), FButton 'Edit Profile' (primary style, пустой onPress). Используй SizedBox для отступов между элементами.
20 19 Интеграция базы данных Подключение всех компонентов к базе данных Обновление существующих файлов Этап 17 Не начато Интегрируй базу данных во все компоненты приложения. Обнови main.dart для инициализации AppDatabase и передачи в MyApp. В ExpensesScreen подключи StreamBuilder для categoryTotals, передавай streams в TransactionsSection. Убедись что все методы контроллера корректно работают с базой данных. Добавь обработку ошибок и состояний загрузки. Протестируй добавление транзакций и фильтрацию. Добавь вызов insertInitialDataIfNeeded() при первом запуске.
21 20 Добавление транзакций Функциональность добавления новых транзакций Обновление контроллера и экранов Этап 19 Не начато Расширь функциональность добавления транзакций. Обнови ExpensesController.addSampleTransaction() для генерации более реалистичных данных с случайными категориями, суммами (5-100), датами (последние 7 дней), и магазинами. В ExpensesScreen добавь обработку успеха/ошибки с showFToast. Рассмотри создание отдельного экрана или диалога для ручного добавления транзакций с полями для категории, суммы, даты и магазина.
22 21 Анимации и переходы Добавление анимаций для улучшения UX Обновление виджетов Этап 20 Не начато Улучши анимации во всем приложении. В ExpensesScreen добавь AnimationController для pie chart с forward() в initState. Обнови TransactionListItem для поддержки FadeTransition. В SpendingPieChart добавь AnimatedBuilder с scale и opacity анимациями. Добавь Hero анимацию для перехода к ProfileScreen. В TransactionsSection улучши AnimatedContainer для фильтров. Добавь swapAnimationDuration для PieChart при изменении данных.
23 22 Тестирование и отладка Тестирование всей функциональности Создание тестов Этап 21 Не начато Создай комплексные тесты для приложения. Добавь unit тесты для: CategoryUtils методов, ExpensesController логики, database операций. Создай widget тесты для: SummaryCard, TransactionListItem, FilterChipWidget. Добавь integration тесты для: добавления транзакций, фильтрации, переключения темы. Создай test/widget_test.dart, test/unit_test.dart, integration_test/app_test.dart. Используй flutter_test, mockito для моков базы данных.
24 23 Оптимизация производительности Оптимизация кода и производительности Рефакторинг существующих файлов Этап 22 Не начато Оптимизируй производительность приложения. Добавь const конструкторы где возможно. Используй ListView.builder вместо Column для больших списков. Оптимизируй StreamBuilder подписки. Добавь memo для дорогих вычислений в CategoryUtils. Используй RepaintBoundary для изоляции перерисовок. Оптимизируй анимации с AnimatedBuilder. Добавь lazy loading для больших списков транзакций. Проведи профилирование с Flutter Inspector.
25 24 Финальная полировка Исправление багов и улучшение UI/UX Обновление всех компонентов Этап 23 Не начато Выполни финальную полировку приложения. Исправь все найденные баги. Улучши accessibility с Semantics виджетами. Добавь поддержку разных размеров экранов и ориентаций. Оптимизируй цветовые схемы для лучшей читаемости. Добавь haptic feedback для интерактивных элементов. Улучши error handling и user feedback. Добавь loading состояния где нужно. Проведи финальное тестирование на разных устройствах и платформах.