8 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
14 changed files with 688 additions and 693 deletions
+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);
}
}
+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:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; // Used indirectly by SpendingPieChart import 'package:forui/forui.dart';
import 'package:intl/intl.dart'; // For date formatting
import 'dart:async';
import 'dart:math'; // For random data generation in sample add
import '../database/database.dart' as db; // Import database with prefix 'db' import '../database/database.dart' as db;
import '../models/category.dart'; // Keep Category model for UI structure (SpendingPieChart) import '../models/category.dart';
// Import TransactionRecord with a different alias to avoid conflict with db.Transaction import '../controllers/expenses_controller.dart';
import '../models/transaction_record.dart' as model; import '../widgets/summary_card.dart';
import '../widgets/summary_item.dart';
import '../widgets/expandable_section.dart'; import '../widgets/expandable_section.dart';
import '../widgets/spending_pie_chart.dart'; import '../widgets/spending_pie_chart.dart';
import '../widgets/transaction_list_item.dart'; import '../widgets/transactions_section.dart';
import '../widgets/filter_chip_widget.dart'; import 'profile_screen.dart';
import '../utils/category_utils.dart'; // Import category utils
import 'profile_screen.dart'; // Import profile screen
class ExpensesScreen extends StatefulWidget { class ExpensesScreen extends StatefulWidget {
final Function toggleTheme; final Function toggleTheme;
final bool isDarkMode; final bool isDarkMode;
final db.AppDatabase database; // Accept database instance final db.AppDatabase database;
const ExpensesScreen({ const ExpensesScreen({
Key? key, Key? key,
required this.toggleTheme, required this.toggleTheme,
required this.isDarkMode, required this.isDarkMode,
required this.database, // Require database instance required this.database,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -34,24 +27,20 @@ class ExpensesScreen extends StatefulWidget {
} }
class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStateMixin { class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStateMixin {
// Animation controllers for UI elements
late AnimationController _pieChartAnimationController; late AnimationController _pieChartAnimationController;
late Animation<double> _pieChartAnimation; late Animation<double> _pieChartAnimation;
late AnimationController _pieChartExpandController; late AnimationController _pieChartExpandController;
late Animation<double> _pieChartHeightFactor; late Animation<double> _pieChartHeightFactor;
late ExpensesController _controller;
// State variables int _selectedNavIndex = 0;
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
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Initialize animation controller for pie chart fade/scale effect _controller = ExpensesController(widget.database);
_pieChartAnimationController = AnimationController( _pieChartAnimationController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 800), duration: const Duration(milliseconds: 800),
@@ -61,485 +50,130 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
// Initialize animation controller for pie chart expand/collapse effect
_pieChartExpandController = AnimationController( _pieChartExpandController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
value: 1.0, // Start expanded value: 1.0,
); );
_pieChartHeightFactor = CurvedAnimation( _pieChartHeightFactor = CurvedAnimation(
parent: _pieChartExpandController, parent: _pieChartExpandController,
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
// Start the pie chart appearance animation
_pieChartAnimationController.forward(); _pieChartAnimationController.forward();
} }
@override @override
void dispose() { void dispose() {
// Dispose controllers to free up resources
_pieChartAnimationController.dispose(); _pieChartAnimationController.dispose();
_pieChartExpandController.dispose(); _pieChartExpandController.dispose();
super.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() { void _togglePieChartVisibility() {
setState(() { setState(() {
_isPieChartExpanded = !_isPieChartExpanded; _controller.togglePieChartVisibility();
if (_isPieChartExpanded) { if (_controller.isPieChartExpanded) {
_pieChartExpandController.forward(); // Expand animation _pieChartExpandController.forward();
} else { } else {
_pieChartExpandController.reverse(); // Collapse animation _pieChartExpandController.reverse();
} }
}); });
} }
// Toggles the visibility of the filter chip row
void _toggleFilterVisibility() { void _toggleFilterVisibility() {
setState(() { setState(() {
_isFilterVisible = !_isFilterVisible; _controller.toggleFilterVisibility();
}); });
} }
// Handles selection of a pie chart slice
void _selectPieCategory(int index) { void _selectPieCategory(int index) {
setState(() { setState(() {
// If the same index is selected, deselect (-1), otherwise select the new index _controller.selectPieCategory(index);
_selectedPieIndex = (_selectedPieIndex == index) ? -1 : 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) { void _applyFilter(String filter) {
setState(() { setState(() {
_selectedFilter = filter; _controller.applyFilter(filter);
// The StreamBuilder listening to _watchTransactions() will automatically });
// rebuild with the new stream based on the updated _selectedFilter.
});
} }
// --- Function to add a sample transaction (for testing/demo) ---
void _addSampleTransaction() async { 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 { try {
// Insert the transaction into the database await _controller.addSampleTransaction();
await widget.database.addTransaction(newTransaction); if (mounted) {
print('Sample transaction added: $randomCategory - \$${randomAmount.toStringAsFixed(2)}'); showFToast(
// Show a confirmation message context: context,
if (mounted) { // Check if the widget is still in the tree builder: (context) => const FToast(
ScaffoldMessenger.of(context).showSnackBar( title: Text('Success'),
SnackBar( description: Text('Transaction added successfully'),
content: Text('Added ${randomCategory} transaction'), ),
duration: const Duration(seconds: 2), );
behavior: SnackBarBehavior.floating,
),
);
} }
} catch (e) { } catch (e) {
print('Error adding transaction: $e'); if (mounted) {
// Show an error message showFToast(
if (mounted) { context: context,
ScaffoldMessenger.of(context).showSnackBar( builder: (context) => FToast(
SnackBar( title: const Text('Error'),
content: Text('Error adding transaction: $e'), description: Text('Error adding transaction: $e'),
backgroundColor: Colors.red, style: FToastStyle.destructive,
behavior: SnackBarBehavior.floating, ),
), );
); }
}
} }
} }
// --- End of add sample transaction function ---
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: AppBar( appBar: _buildAppBar(context),
// 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
body: StreamBuilder<List<Category>>( body: StreamBuilder<List<Category>>(
stream: _watchCategoryTotals(), stream: _controller.watchCategoryTotals(),
builder: (context, categorySnapshot) { builder: (context, categorySnapshot) {
// Handle loading state
if (categorySnapshot.connectionState == ConnectionState.waiting && !categorySnapshot.hasData) { if (categorySnapshot.connectionState == ConnectionState.waiting && !categorySnapshot.hasData) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
// Handle error state
if (categorySnapshot.hasError) { if (categorySnapshot.hasError) {
return Center(child: Text('Error loading categories: ${categorySnapshot.error}')); return Center(child: Text('Error loading categories: ${categorySnapshot.error}'));
} }
// Get categories data (or empty list if null)
final categories = categorySnapshot.data ?? []; final categories = categorySnapshot.data ?? [];
// Calculate total expenses from the categories stream data
final totalExpenses = categories.fold(0.0, (sum, item) => sum + item.amount); final totalExpenses = categories.fold(0.0, (sum, item) => sum + item.amount);
// Main column layout
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, // Stretch children horizontally crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Expanded( Expanded(
// Use SingleChildScrollView for content that might overflow
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const BouncingScrollPhysics(), // iOS-like scroll physics physics: const BouncingScrollPhysics(),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// --- Summary Card --- SummaryCard(totalExpenses: totalExpenses),
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 ---
ExpandableSection( ExpandableSection(
title: 'Spending Breakdown', title: 'Spending Breakdown',
icon: Icons.pie_chart_outline_rounded, // Updated icon icon: Icons.pie_chart_outline_rounded,
isExpanded: _isPieChartExpanded, isExpanded: _controller.isPieChartExpanded,
onTap: _togglePieChartVisibility, onTap: _togglePieChartVisibility,
heightFactor: _pieChartHeightFactor, // Animation controller heightFactor: _pieChartHeightFactor,
child: SpendingPieChart( child: SpendingPieChart(
categories: categories, // Pass categories from stream snapshot categories: categories,
totalExpenses: totalExpenses, // Pass calculated total totalExpenses: totalExpenses,
selectedPieIndex: _selectedPieIndex, selectedPieIndex: _controller.selectedPieIndex,
onSelectPieCategory: _selectPieCategory, // Callback for selection onSelectPieCategory: _selectPieCategory,
animation: _pieChartAnimation, // Appearance animation animation: _pieChartAnimation,
), ),
), ),
TransactionsSection(
// --- Transaction List Section --- transactionsStream: _controller.watchTransactions(),
Column( isFilterVisible: _controller.isFilterVisible,
crossAxisAlignment: CrossAxisAlignment.start, selectedFilter: _controller.selectedFilter,
children: [ onToggleFilter: _toggleFilterVisibility,
// Header with Title and Filter/See All buttons onApplyFilter: _applyFilter,
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
],
), ),
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: _buildBottomNavigationBar(),
bottomNavigationBar: BottomNavigationBar( floatingActionButton: FButton(
currentIndex: _selectedNavIndex, label: const Row(
onTap: (index) { mainAxisSize: MainAxisSize.min,
setState(() { children: [
_selectedNavIndex = index; Icon(Icons.add),
// TODO: Handle navigation based on index (e.g., switch screens) SizedBox(width: 8),
}); Text('Add'),
}, ],
items: const [ // Use const for static items ),
BottomNavigationBarItem( style: FButtonStyle.primary,
icon: Icon(Icons.home_filled), // Use filled icon for selected state onPress: _addSampleTransaction,
label: 'Home', ),
), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
BottomNavigationBarItem( );
icon: Icon(Icons.bar_chart_rounded), }
label: 'Reports',
), AppBar _buildAppBar(BuildContext context) {
// Example with Badge (replace with actual notification count) return AppBar(
// BottomNavigationBarItem( title: Row(
// icon: Badge( mainAxisSize: MainAxisSize.min,
// label: Text('3'), // Example badge count children: [
// child: Icon(Icons.notifications_none_rounded), Icon(
// ), Icons.account_balance_wallet_outlined,
// activeIcon: Badge( // Optional: different badge style when active color: context.theme.colorScheme.primary,
// label: Text('3'), size: 24,
// child: Icon(Icons.notifications_rounded),
// ),
// label: 'Notifications',
// ),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings), // Filled icon when active
label: 'Settings',
), ),
const SizedBox(width: 8),
const Text('My Finances'),
], ],
), ),
// Floating Action Button to add new transaction centerTitle: true,
floatingActionButton: FloatingActionButton.extended( // Use extended FAB leading: IconButton(
onPressed: _addSampleTransaction, // Add sample data on press tooltip: widget.isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode',
tooltip: 'Add Transaction', icon: Icon(
icon: const Icon(Icons.add), widget.isDarkMode ? Icons.wb_sunny_outlined : Icons.nightlight_round,
label: const Text('Add'), 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:flutter/material.dart';
import 'package:forui/forui.dart';
class ProfileScreen extends StatelessWidget { class ProfileScreen extends StatelessWidget {
const ProfileScreen({Key? key}) : super(key: key); const ProfileScreen({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Profile'), title: const Text('Profile'),
@@ -24,13 +23,14 @@ class ProfileScreen extends StatelessWidget {
tag: 'profileAvatar', // Tag must match the one in ExpensesScreen tag: 'profileAvatar', // Tag must match the one in ExpensesScreen
child: Material( // Wrap with Material for Hero animation child: Material( // Wrap with Material for Hero animation
type: MaterialType.transparency, type: MaterialType.transparency,
child: CircleAvatar( child: FAvatar(
radius: 50, size: 100,
backgroundColor: isDark ? Colors.green.shade800 : Colors.green.shade100, backgroundColor: context.theme.colorScheme.primary.withOpacity(0.1),
child: const Icon( image: null,
child: Icon(
Icons.person, Icons.person,
size: 50, size: 50,
color: Colors.green, color: context.theme.colorScheme.primary,
), ),
), ),
), ),
@@ -38,32 +38,23 @@ class ProfileScreen extends StatelessWidget {
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'John Doe', 'John Doe',
style: TextStyle( style: context.theme.typography.xl2.copyWith(
fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black, color: context.theme.colorScheme.foreground,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'john.doe@example.com', 'john.doe@example.com',
style: TextStyle( style: context.theme.typography.base.copyWith(
fontSize: 16, color: context.theme.colorScheme.mutedForeground,
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton( FButton(
onPressed: () {}, label: const Text('Edit Profile'),
style: ElevatedButton.styleFrom( style: FButtonStyle.primary,
backgroundColor: isDark ? Colors.green.shade700 : Colors.green, onPress: () {},
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text('Edit Profile'),
), ),
], ],
), ),
+21 -73
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
class ExpandableSection extends StatelessWidget { class ExpandableSection extends StatelessWidget {
final String title; final String title;
@@ -20,82 +21,29 @@ class ExpandableSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark; return Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
return Column( child: FCollapsible(
children: [ initiallyExpanded: isExpanded,
GestureDetector( title: Row(
onTap: onTap, children: [
child: Container( Icon(
width: double.infinity, icon,
margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), color: context.theme.colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), size: 20,
decoration: BoxDecoration( ),
color: isDark ? Colors.grey.shade800 : Colors.green.shade100, const SizedBox(width: 8),
borderRadius: BorderRadius.vertical( Text(
top: const Radius.circular(16), title,
bottom: Radius.circular(isExpanded ? 0 : 16), 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( child: child,
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,
),
],
); );
} }
} }
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
class FilterChipWidget extends StatelessWidget { class FilterChipWidget extends StatelessWidget {
final String label; final String label;
@@ -14,35 +15,14 @@ class FilterChipWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark; return Padding(
padding: const EdgeInsets.only(right: 8),
return GestureDetector( child: FButton(
onTap: onTap, label: Text(label),
child: Container( style: isSelected
margin: const EdgeInsets.only(right: 8), ? FButtonStyle.primary
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), // Smaller padding : FButtonStyle.secondary,
decoration: BoxDecoration( onPress: onTap,
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),
),
),
), ),
); );
} }
+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:flutter/material.dart';
import 'package:forui/forui.dart';
class SummaryItem extends StatelessWidget { class SummaryItem extends StatelessWidget {
final IconData icon; final IconData icon;
@@ -16,7 +17,6 @@ class SummaryItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Column( return Column(
children: [ children: [
Row( Row(
@@ -24,14 +24,13 @@ class SummaryItem extends StatelessWidget {
Icon( Icon(
icon, icon,
size: 16, size: 16,
color: isDark ? color.withOpacity(0.8) : color, color: color,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
title, title,
style: TextStyle( style: context.theme.typography.sm.copyWith(
fontSize: 13, color: context.theme.colorScheme.mutedForeground,
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
), ),
), ),
], ],
@@ -39,10 +38,9 @@ class SummaryItem extends StatelessWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
amount, amount,
style: TextStyle( style: context.theme.typography.base.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold, 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:flutter/material.dart';
import 'package:forui/forui.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../database/database.dart' as db; // Import database with prefix 'db' import '../database/database.dart' as db; // Import database with prefix 'db'
import '../models/transaction_record.dart'; import '../models/transaction_record.dart';
@@ -16,9 +17,6 @@ class TransactionListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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 // Get category details (icon, color) using the utility
final categoryDetails = CategoryUtils.getCategoryDetails(transaction.categoryName); final categoryDetails = CategoryUtils.getCategoryDetails(transaction.categoryName);
@@ -42,11 +40,6 @@ class TransactionListItem extends StatelessWidget {
displayDate = '${dateFormatter.format(transaction.date)}, ${timeFormatter.format(transaction.date)}'; 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) // Use FadeTransition for item appearance (works with ListView.builder)
return FadeTransition( return FadeTransition(
opacity: animation, // Apply fade animation opacity: animation, // Apply fade animation
@@ -60,16 +53,12 @@ class TransactionListItem extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
// Icon container with category color/icon // Icon container with category color/icon
Container( FAvatar(
padding: const EdgeInsets.all(10), backgroundColor: categoryDetails.colorCode.withOpacity(0.15),
decoration: BoxDecoration(
color: categoryDetails.colorCode.withOpacity(isDark ? 0.25 : 0.15), // Use category color with opacity
borderRadius: BorderRadius.circular(12), // Rounded corners
),
child: Icon( child: Icon(
categoryDetails.iconCode, // Use category icon categoryDetails.iconCode,
color: categoryDetails.colorCode, // Use category color for icon color: categoryDetails.colorCode,
size: 20, // Icon size size: 20,
), ),
), ),
const SizedBox(width: 12), // Spacing const SizedBox(width: 12), // Spacing
@@ -82,9 +71,9 @@ class TransactionListItem extends StatelessWidget {
// Display Merchant if available, otherwise Category Name // Display Merchant if available, otherwise Category Name
Text( Text(
transaction.merchant.isNotEmpty ? transaction.merchant : transaction.categoryName, transaction.merchant.isNotEmpty ? transaction.merchant : transaction.categoryName,
style: theme.textTheme.bodyLarge?.copyWith( style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.w500, // Medium weight for primary text fontWeight: FontWeight.w500,
color: primaryTextColor, color: context.theme.colorScheme.foreground,
), ),
maxLines: 1, // Prevent wrapping maxLines: 1, // Prevent wrapping
overflow: TextOverflow.ellipsis, // Handle long text overflow: TextOverflow.ellipsis, // Handle long text
@@ -93,9 +82,8 @@ class TransactionListItem extends StatelessWidget {
// Display formatted date/time // Display formatted date/time
Text( Text(
displayDate, displayDate,
style: theme.textTheme.bodyMedium?.copyWith( style: context.theme.typography.sm.copyWith(
color: secondaryTextColor, // Lighter color for secondary text color: context.theme.colorScheme.mutedForeground,
fontSize: 12, // Smaller font size
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -109,9 +97,9 @@ class TransactionListItem extends StatelessWidget {
Text( Text(
// Format amount as currency (negative for expense) // Format amount as currency (negative for expense)
NumberFormat.currency(symbol: '-\$', decimalDigits: 2).format(transaction.amount), NumberFormat.currency(symbol: '-\$', decimalDigits: 2).format(transaction.amount),
style: theme.textTheme.bodyLarge?.copyWith( style: context.theme.typography.base.copyWith(
fontWeight: FontWeight.w600, // Bold weight for amount fontWeight: FontWeight.w600,
color: amountColor, // Use expense color 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 -32
View File
@@ -13,10 +13,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "13c1e6c6fd460522ea840abec3f677cc226f5fec7872c04ad7b425517ccf54f7" sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.4.4" version: "7.4.5"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -29,10 +29,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.13.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -101,10 +101,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.9.5" version: "8.10.1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -125,10 +125,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: checked_yaml name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
cli_util: cli_util:
dependency: transitive dependency: transitive
description: description:
@@ -189,26 +189,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.1.0"
drift: drift:
dependency: "direct main" dependency: "direct main"
description: description:
name: drift name: drift
sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5" sha256: b584ddeb2b74436735dd2cf746d2d021e19a9a6770f409212fd5cbc2814ada85
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.26.0" version: "2.26.1"
drift_dev: drift_dev:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: drift_dev name: drift_dev
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588" sha256: "54dc207c6e4662741f60e5752678df183957ab907754ffab0372a7082f6d2816"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.26.0" version: "2.26.1"
equatable: equatable:
dependency: transitive dependency: transitive
description: description:
@@ -221,10 +221,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@@ -270,11 +270,32 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@@ -291,6 +312,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" 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: graphs:
dependency: transitive dependency: transitive
description: description:
@@ -303,10 +332,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@@ -327,10 +356,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -359,10 +388,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.8" version: "10.0.9"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
@@ -427,6 +456,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" 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: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -580,18 +617,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqlite3 name: sqlite3
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.5" version: "2.7.6"
sqlite3_flutter_libs: sqlite3_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14" sha256: "7986c26234c0a5cf4fd83ff4ee39d4195b1f47cdb50a949ec7987ede4dcbdc2a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.32" version: "0.5.33"
sqlparser: sqlparser:
dependency: transitive dependency: transitive
description: description:
@@ -632,6 +669,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
sugar:
dependency: transitive
description:
name: sugar
sha256: a0a97f1d3552a3ef4cbec7c6e62c7b52c151cd5c11e984e8311700a918f73dc5
url: "https://pub.dev"
source: hosted
version: "4.0.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -676,10 +721,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.1" version: "15.0.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@@ -700,10 +745,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web_socket name: web_socket
sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
@@ -729,5 +774,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.7.0 <4.0.0" dart: ">=3.8.0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.32.0"
+2 -31
View File
@@ -31,7 +31,7 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
fl_chart: ^0.71.0 fl_chart: ^0.71.0
intl: ^0.19.0 # For date formatting intl: ^0.20.2 # For date formatting
drift: ^2.18.0 # Updated Drift - основной пакет drift: ^2.18.0 # Updated Drift - основной пакет
sqlite3_flutter_libs: ^0.5.24 # Needed for native platforms sqlite3_flutter_libs: ^0.5.24 # Needed for native platforms
path_provider: ^2.1.3 # To find database file location on native path_provider: ^2.1.3 # To find database file location on native
@@ -40,6 +40,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
forui: ^0.12.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -65,33 +66,3 @@ flutter:
# the material Icons class. # the material Icons class.
uses-material-design: true 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 состояния где нужно. Проведи финальное тестирование на разных устройствах и платформах.