Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f0a9496ce | ||
|
|
38d12105fa | ||
|
|
b38ad51a9e | ||
|
|
e3ba9dd5dd | ||
|
|
7f3dfb6459 | ||
|
|
6eef7e95be | ||
|
|
90e5ac0c46 | ||
|
|
b2e9eb86c2 |
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,25 @@
|
||||
import 'package:drift/drift.dart' show Value; // Only import Value for optional fields
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart'; // Used indirectly by SpendingPieChart
|
||||
import 'package:intl/intl.dart'; // For date formatting
|
||||
import 'dart:async';
|
||||
import 'dart:math'; // For random data generation in sample add
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
import '../database/database.dart' as db; // Import database with prefix 'db'
|
||||
import '../models/category.dart'; // Keep Category model for UI structure (SpendingPieChart)
|
||||
// Import TransactionRecord with a different alias to avoid conflict with db.Transaction
|
||||
import '../models/transaction_record.dart' as model;
|
||||
import '../widgets/summary_item.dart';
|
||||
import '../database/database.dart' as db;
|
||||
import '../models/category.dart';
|
||||
import '../controllers/expenses_controller.dart';
|
||||
import '../widgets/summary_card.dart';
|
||||
import '../widgets/expandable_section.dart';
|
||||
import '../widgets/spending_pie_chart.dart';
|
||||
import '../widgets/transaction_list_item.dart';
|
||||
import '../widgets/filter_chip_widget.dart';
|
||||
import '../utils/category_utils.dart'; // Import category utils
|
||||
import 'profile_screen.dart'; // Import profile screen
|
||||
import '../widgets/transactions_section.dart';
|
||||
import 'profile_screen.dart';
|
||||
|
||||
class ExpensesScreen extends StatefulWidget {
|
||||
final Function toggleTheme;
|
||||
final bool isDarkMode;
|
||||
final db.AppDatabase database; // Accept database instance
|
||||
final db.AppDatabase database;
|
||||
|
||||
const ExpensesScreen({
|
||||
Key? key,
|
||||
required this.toggleTheme,
|
||||
required this.isDarkMode,
|
||||
required this.database, // Require database instance
|
||||
required this.database,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -34,24 +27,20 @@ class ExpensesScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStateMixin {
|
||||
// Animation controllers for UI elements
|
||||
late AnimationController _pieChartAnimationController;
|
||||
late Animation<double> _pieChartAnimation;
|
||||
late AnimationController _pieChartExpandController;
|
||||
late Animation<double> _pieChartHeightFactor;
|
||||
late ExpensesController _controller;
|
||||
|
||||
// State variables
|
||||
int _selectedPieIndex = -1; // Index of the selected pie chart slice
|
||||
int _selectedNavIndex = 0; // Index for bottom navigation bar
|
||||
bool _isPieChartExpanded = true; // Controls visibility of the pie chart section
|
||||
bool _isFilterVisible = false; // Controls visibility of the filter chips
|
||||
String _selectedFilter = 'All'; // Currently selected transaction filter
|
||||
int _selectedNavIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize animation controller for pie chart fade/scale effect
|
||||
_controller = ExpensesController(widget.database);
|
||||
|
||||
_pieChartAnimationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
@@ -61,485 +50,130 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
// Initialize animation controller for pie chart expand/collapse effect
|
||||
_pieChartExpandController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
value: 1.0, // Start expanded
|
||||
value: 1.0,
|
||||
);
|
||||
_pieChartHeightFactor = CurvedAnimation(
|
||||
parent: _pieChartExpandController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
// Start the pie chart appearance animation
|
||||
_pieChartAnimationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Dispose controllers to free up resources
|
||||
_pieChartAnimationController.dispose();
|
||||
_pieChartExpandController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Stream that provides transactions based on the selected filter
|
||||
Stream<List<db.Transaction>> _watchTransactions() {
|
||||
return widget.database.watchFilteredTransactions(_selectedFilter);
|
||||
}
|
||||
|
||||
// Stream that provides category totals calculated from transactions
|
||||
Stream<List<Category>> _watchCategoryTotals() {
|
||||
return widget.database.calculateCategoryTotals();
|
||||
}
|
||||
|
||||
// Toggles the visibility of the pie chart section with animation
|
||||
void _togglePieChartVisibility() {
|
||||
setState(() {
|
||||
_isPieChartExpanded = !_isPieChartExpanded;
|
||||
if (_isPieChartExpanded) {
|
||||
_pieChartExpandController.forward(); // Expand animation
|
||||
_controller.togglePieChartVisibility();
|
||||
if (_controller.isPieChartExpanded) {
|
||||
_pieChartExpandController.forward();
|
||||
} else {
|
||||
_pieChartExpandController.reverse(); // Collapse animation
|
||||
_pieChartExpandController.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggles the visibility of the filter chip row
|
||||
void _toggleFilterVisibility() {
|
||||
setState(() {
|
||||
_isFilterVisible = !_isFilterVisible;
|
||||
_controller.toggleFilterVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
// Handles selection of a pie chart slice
|
||||
void _selectPieCategory(int index) {
|
||||
setState(() {
|
||||
// If the same index is selected, deselect (-1), otherwise select the new index
|
||||
_selectedPieIndex = (_selectedPieIndex == index) ? -1 : index;
|
||||
_controller.selectPieCategory(index);
|
||||
});
|
||||
// Optionally apply filter when pie slice is selected/deselected
|
||||
// This requires getting the category name from the index, potentially async
|
||||
// _applyFilterBasedOnPieSelection(index);
|
||||
}
|
||||
|
||||
// Applies the selected filter to the transaction list
|
||||
void _applyFilter(String filter) {
|
||||
setState(() {
|
||||
_selectedFilter = filter;
|
||||
// The StreamBuilder listening to _watchTransactions() will automatically
|
||||
// rebuild with the new stream based on the updated _selectedFilter.
|
||||
});
|
||||
setState(() {
|
||||
_controller.applyFilter(filter);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Function to add a sample transaction (for testing/demo) ---
|
||||
void _addSampleTransaction() async {
|
||||
final random = Random();
|
||||
// Get available category names from our utility class
|
||||
final categories = CategoryUtils.getAllCategoryNames();
|
||||
// Pick a random category, amount, date, and merchant
|
||||
final randomCategory = categories[random.nextInt(categories.length)];
|
||||
final randomAmount = (random.nextDouble() * 100) + 5; // Amount between 5 and 105
|
||||
final randomDay = random.nextInt(7); // Within the last week
|
||||
final randomHour = random.nextInt(24);
|
||||
final randomMerchant = ['Amazon', 'Local Cafe', 'Gas Station', 'Online Store', 'Supermarket'][random.nextInt(5)];
|
||||
|
||||
// Create a companion object for inserting into the Drift table
|
||||
final newTransaction = db.TransactionsCompanion.insert(
|
||||
categoryName: randomCategory,
|
||||
amount: randomAmount,
|
||||
date: DateTime.now().subtract(Duration(days: randomDay, hours: randomHour)),
|
||||
merchant: randomMerchant,
|
||||
);
|
||||
|
||||
try {
|
||||
// Insert the transaction into the database
|
||||
await widget.database.addTransaction(newTransaction);
|
||||
print('Sample transaction added: $randomCategory - \$${randomAmount.toStringAsFixed(2)}');
|
||||
// Show a confirmation message
|
||||
if (mounted) { // Check if the widget is still in the tree
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Added ${randomCategory} transaction'),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
await _controller.addSampleTransaction();
|
||||
if (mounted) {
|
||||
showFToast(
|
||||
context: context,
|
||||
builder: (context) => const FToast(
|
||||
title: Text('Success'),
|
||||
description: Text('Transaction added successfully'),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error adding transaction: $e');
|
||||
// Show an error message
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error adding transaction: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
showFToast(
|
||||
context: context,
|
||||
builder: (context) => FToast(
|
||||
title: const Text('Error'),
|
||||
description: Text('Error adding transaction: $e'),
|
||||
style: FToastStyle.destructive,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- End of add sample transaction function ---
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final theme = Theme.of(context); // Get theme for easier access to styles
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Title with icon
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_balance_wallet_outlined, // Updated icon
|
||||
color: isDark ? Colors.greenAccent.shade100 : Colors.green.shade800,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('My Finances'), // Updated title
|
||||
],
|
||||
),
|
||||
centerTitle: true,
|
||||
// Theme toggle button
|
||||
leading: IconButton(
|
||||
tooltip: isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode',
|
||||
icon: Icon(
|
||||
widget.isDarkMode ? Icons.wb_sunny_outlined : Icons.nightlight_round,
|
||||
color: widget.isDarkMode ? Colors.yellow.shade300 : Colors.blue.shade700,
|
||||
),
|
||||
onPressed: () => widget.toggleTheme(),
|
||||
),
|
||||
// Profile avatar button
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0), // Adjusted padding
|
||||
child: Hero(
|
||||
tag: 'profileAvatar', // Tag for Hero animation
|
||||
child: Material(
|
||||
type: MaterialType.transparency, // Needed for Hero animation across routes
|
||||
child: IconButton(
|
||||
tooltip: 'View Profile',
|
||||
icon: CircleAvatar(
|
||||
radius: 18, // Slightly smaller avatar
|
||||
backgroundColor: isDark ? Colors.green.shade800 : Colors.green.shade100,
|
||||
child: const Icon(Icons.person_outline, color: Colors.green, size: 20),
|
||||
),
|
||||
onPressed: () {
|
||||
// Navigate to ProfileScreen with a fade transition
|
||||
Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (_, __, ___) => const ProfileScreen(),
|
||||
transitionsBuilder: (_, animation, __, child) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
transitionDuration: const Duration(milliseconds: 350), // Adjusted duration
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Use StreamBuilder to get category totals for the summary and pie chart
|
||||
appBar: _buildAppBar(context),
|
||||
body: StreamBuilder<List<Category>>(
|
||||
stream: _watchCategoryTotals(),
|
||||
stream: _controller.watchCategoryTotals(),
|
||||
builder: (context, categorySnapshot) {
|
||||
// Handle loading state
|
||||
if (categorySnapshot.connectionState == ConnectionState.waiting && !categorySnapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
// Handle error state
|
||||
if (categorySnapshot.hasError) {
|
||||
return Center(child: Text('Error loading categories: ${categorySnapshot.error}'));
|
||||
}
|
||||
|
||||
// Get categories data (or empty list if null)
|
||||
final categories = categorySnapshot.data ?? [];
|
||||
// Calculate total expenses from the categories stream data
|
||||
final totalExpenses = categories.fold(0.0, (sum, item) => sum + item.amount);
|
||||
|
||||
// Main column layout
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // Stretch children horizontally
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
// Use SingleChildScrollView for content that might overflow
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(), // iOS-like scroll physics
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// --- Summary Card ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Card(
|
||||
elevation: 2, // Subtle shadow
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0), // Adjusted padding
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Total Expenses This Period', // More descriptive title
|
||||
style: theme.textTheme.titleMedium, // Use theme style
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Display total expenses amount
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center, // Align baseline
|
||||
children: [
|
||||
Text(
|
||||
'\$',
|
||||
style: TextStyle(
|
||||
fontSize: 20, // Smaller dollar sign
|
||||
fontWeight: FontWeight.w500, // Medium weight
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
NumberFormat.currency(symbol: '', decimalDigits: 2).format(totalExpenses), // Format number
|
||||
style: TextStyle(
|
||||
fontSize: 36, // Slightly smaller amount
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
letterSpacing: -1, // Tighten spacing
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Divider line
|
||||
Divider(height: 1, thickness: 1, indent: 20, endIndent: 20, color: theme.dividerColor.withOpacity(0.5)),
|
||||
const SizedBox(height: 16),
|
||||
// Income / Expenses summary items
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround, // Distribute space
|
||||
children: [
|
||||
const SummaryItem( // Example Income - Replace with real data
|
||||
icon: Icons.arrow_downward_rounded,
|
||||
title: 'Income',
|
||||
amount: '\$2,450.00',
|
||||
color: Colors.green,
|
||||
),
|
||||
// Vertical divider
|
||||
Container(
|
||||
height: 35,
|
||||
width: 1,
|
||||
color: theme.dividerColor.withOpacity(0.5),
|
||||
),
|
||||
SummaryItem( // Expenses using calculated total
|
||||
icon: Icons.arrow_upward_rounded,
|
||||
title: 'Expenses',
|
||||
amount: '\$${NumberFormat.currency(symbol: '', decimalDigits: 2).format(totalExpenses)}',
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// --- Pie Chart Section ---
|
||||
SummaryCard(totalExpenses: totalExpenses),
|
||||
ExpandableSection(
|
||||
title: 'Spending Breakdown',
|
||||
icon: Icons.pie_chart_outline_rounded, // Updated icon
|
||||
isExpanded: _isPieChartExpanded,
|
||||
icon: Icons.pie_chart_outline_rounded,
|
||||
isExpanded: _controller.isPieChartExpanded,
|
||||
onTap: _togglePieChartVisibility,
|
||||
heightFactor: _pieChartHeightFactor, // Animation controller
|
||||
heightFactor: _pieChartHeightFactor,
|
||||
child: SpendingPieChart(
|
||||
categories: categories, // Pass categories from stream snapshot
|
||||
totalExpenses: totalExpenses, // Pass calculated total
|
||||
selectedPieIndex: _selectedPieIndex,
|
||||
onSelectPieCategory: _selectPieCategory, // Callback for selection
|
||||
animation: _pieChartAnimation, // Appearance animation
|
||||
categories: categories,
|
||||
totalExpenses: totalExpenses,
|
||||
selectedPieIndex: _controller.selectedPieIndex,
|
||||
onSelectPieCategory: _selectPieCategory,
|
||||
animation: _pieChartAnimation,
|
||||
),
|
||||
),
|
||||
|
||||
// --- Transaction List Section ---
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with Title and Filter/See All buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 4), // Adjusted padding
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Recent Transactions',
|
||||
style: theme.textTheme.titleLarge?.copyWith( // Use theme style
|
||||
fontWeight: FontWeight.w600, // Bold weight
|
||||
),
|
||||
),
|
||||
// Filter button
|
||||
Row(
|
||||
children: [
|
||||
InkWell( // Use InkWell for ripple effect
|
||||
onTap: _toggleFilterVisibility,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.grey.shade800 : Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
_selectedFilter, // Display current filter
|
||||
style: TextStyle(
|
||||
fontSize: 13, // Smaller font
|
||||
color: isDark ? Colors.white70 : Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.filter_list_alt, // Updated icon
|
||||
size: 18, // Slightly larger icon
|
||||
color: isDark ? Colors.white70 : Colors.black87,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// "See All" button (optional)
|
||||
// TextButton(
|
||||
// onPressed: () { /* Navigate to full list */ },
|
||||
// child: Text('See All'),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Filter Chips Row (Animated Visibility) ---
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300), // Animation duration
|
||||
curve: Curves.easeInOut, // Animation curve
|
||||
height: _isFilterVisible ? 50 : 0, // Animate height
|
||||
clipBehavior: Clip.hardEdge, // Prevent overflow during animation
|
||||
decoration: const BoxDecoration(), // Needed for clipBehavior
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: _isFilterVisible ? 8 : 0, // Animate padding
|
||||
),
|
||||
child: ListView( // Use ListView for horizontal scrolling
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: [
|
||||
// "All" filter chip
|
||||
FilterChipWidget(
|
||||
label: 'All',
|
||||
isSelected: _selectedFilter == 'All',
|
||||
onTap: () => _applyFilter('All')
|
||||
),
|
||||
// Dynamically generate filter chips from categories
|
||||
...CategoryUtils.getAllCategoryNames().map((name) =>
|
||||
FilterChipWidget(
|
||||
label: name,
|
||||
isSelected: _selectedFilter == name,
|
||||
onTap: () => _applyFilter(name)
|
||||
)
|
||||
).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Transactions List (using StreamBuilder) ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Card( // Wrap list in a Card for background/border
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 0, // No shadow for inner card
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: theme.dividerColor.withOpacity(0.5), width: 1) // Subtle border
|
||||
),
|
||||
clipBehavior: Clip.antiAlias, // Clip list items to card shape
|
||||
child: StreamBuilder<List<db.Transaction>>(
|
||||
stream: _watchTransactions(), // Stream based on _selectedFilter
|
||||
builder: (context, transactionSnapshot) {
|
||||
// Handle loading state for transactions
|
||||
if (transactionSnapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
height: 150, // Placeholder height
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
);
|
||||
}
|
||||
// Handle error state for transactions
|
||||
if (transactionSnapshot.hasError) {
|
||||
return SizedBox(
|
||||
height: 150,
|
||||
child: Center(child: Text('Error: ${transactionSnapshot.error}')),
|
||||
);
|
||||
}
|
||||
// Get transaction data (or empty list)
|
||||
final transactions = transactionSnapshot.data ?? [];
|
||||
|
||||
// Display message if no transactions match the filter
|
||||
if (transactions.isEmpty) {
|
||||
return SizedBox(
|
||||
height: 150,
|
||||
child: Center(
|
||||
child: Text(
|
||||
_selectedFilter == 'All'
|
||||
? 'No transactions yet.'
|
||||
: 'No transactions found for $_selectedFilter.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Use ListView.builder to display transactions efficiently
|
||||
return ListView.separated(
|
||||
itemCount: transactions.length,
|
||||
physics: const NeverScrollableScrollPhysics(), // Disable inner scrolling
|
||||
shrinkWrap: true, // Fit content height
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0), // Padding inside the card
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1, thickness: 1, indent: 16, endIndent: 16,
|
||||
color: theme.dividerColor.withOpacity(0.3),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
// Get the db.Transaction object from the stream
|
||||
final dbTransaction = transactions[index];
|
||||
// Get the corresponding category details (icon, color)
|
||||
final categoryDetails = CategoryUtils.getCategoryDetails(dbTransaction.categoryName);
|
||||
// Create the model.TransactionRecord needed by TransactionListItem
|
||||
final transactionRecord = model.TransactionRecord(
|
||||
dbTransaction.categoryName,
|
||||
dbTransaction.amount,
|
||||
categoryDetails.iconCode, // Get icon from utils
|
||||
categoryDetails.colorCode, // Get color from utils
|
||||
dbTransaction.date,
|
||||
dbTransaction.merchant,
|
||||
);
|
||||
|
||||
// Use the TransactionListItem widget with the correct model type
|
||||
return TransactionListItem(
|
||||
transaction: transactionRecord, // Pass the model.TransactionRecord
|
||||
animation: kAlwaysCompleteAnimation, // Required by FadeTransition
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16), // Bottom padding inside scroll view
|
||||
],
|
||||
TransactionsSection(
|
||||
transactionsStream: _controller.watchTransactions(),
|
||||
isFilterVisible: _controller.isFilterVisible,
|
||||
selectedFilter: _controller.selectedFilter,
|
||||
onToggleFilter: _toggleFilterVisibility,
|
||||
onApplyFilter: _applyFilter,
|
||||
),
|
||||
const SizedBox(height: 80), // Extra bottom padding below list to avoid FAB overlap
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -548,51 +182,107 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
);
|
||||
},
|
||||
),
|
||||
// Bottom Navigation Bar
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _selectedNavIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_selectedNavIndex = index;
|
||||
// TODO: Handle navigation based on index (e.g., switch screens)
|
||||
});
|
||||
},
|
||||
items: const [ // Use const for static items
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home_filled), // Use filled icon for selected state
|
||||
label: 'Home',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.bar_chart_rounded),
|
||||
label: 'Reports',
|
||||
),
|
||||
// Example with Badge (replace with actual notification count)
|
||||
// BottomNavigationBarItem(
|
||||
// icon: Badge(
|
||||
// label: Text('3'), // Example badge count
|
||||
// child: Icon(Icons.notifications_none_rounded),
|
||||
// ),
|
||||
// activeIcon: Badge( // Optional: different badge style when active
|
||||
// label: Text('3'),
|
||||
// child: Icon(Icons.notifications_rounded),
|
||||
// ),
|
||||
// label: 'Notifications',
|
||||
// ),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
activeIcon: Icon(Icons.settings), // Filled icon when active
|
||||
label: 'Settings',
|
||||
bottomNavigationBar: _buildBottomNavigationBar(),
|
||||
floatingActionButton: FButton(
|
||||
label: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add),
|
||||
SizedBox(width: 8),
|
||||
Text('Add'),
|
||||
],
|
||||
),
|
||||
style: FButtonStyle.primary,
|
||||
onPress: _addSampleTransaction,
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||
);
|
||||
}
|
||||
|
||||
AppBar _buildAppBar(BuildContext context) {
|
||||
return AppBar(
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_balance_wallet_outlined,
|
||||
color: context.theme.colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('My Finances'),
|
||||
],
|
||||
),
|
||||
// Floating Action Button to add new transaction
|
||||
floatingActionButton: FloatingActionButton.extended( // Use extended FAB
|
||||
onPressed: _addSampleTransaction, // Add sample data on press
|
||||
tooltip: 'Add Transaction',
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add'),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
tooltip: widget.isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode',
|
||||
icon: Icon(
|
||||
widget.isDarkMode ? Icons.wb_sunny_outlined : Icons.nightlight_round,
|
||||
color: widget.isDarkMode ? Colors.yellow.shade300 : Colors.blue.shade700,
|
||||
),
|
||||
onPressed: () => widget.toggleTheme(),
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, // Standard location
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: Hero(
|
||||
tag: 'profileAvatar',
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: IconButton(
|
||||
tooltip: 'View Profile',
|
||||
icon: FAvatar(
|
||||
size: 36,
|
||||
backgroundColor: context.theme.colorScheme.primary.withOpacity(0.1),
|
||||
child: Icon(
|
||||
Icons.person_outline,
|
||||
color: context.theme.colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (_, __, ___) => const ProfileScreen(),
|
||||
transitionsBuilder: (_, animation, __, child) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
transitionDuration: const Duration(milliseconds: 350),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
BottomNavigationBar _buildBottomNavigationBar() {
|
||||
return BottomNavigationBar(
|
||||
currentIndex: _selectedNavIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_selectedNavIndex = index;
|
||||
});
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home_filled),
|
||||
label: 'Home',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.bar_chart_rounded),
|
||||
label: 'Reports',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
activeIcon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Profile'),
|
||||
@@ -24,13 +23,14 @@ class ProfileScreen extends StatelessWidget {
|
||||
tag: 'profileAvatar', // Tag must match the one in ExpensesScreen
|
||||
child: Material( // Wrap with Material for Hero animation
|
||||
type: MaterialType.transparency,
|
||||
child: CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: isDark ? Colors.green.shade800 : Colors.green.shade100,
|
||||
child: const Icon(
|
||||
child: FAvatar(
|
||||
size: 100,
|
||||
backgroundColor: context.theme.colorScheme.primary.withOpacity(0.1),
|
||||
image: null,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 50,
|
||||
color: Colors.green,
|
||||
color: context.theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -38,32 +38,23 @@ class ProfileScreen extends StatelessWidget {
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'John Doe',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
style: context.theme.typography.xl2.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
color: context.theme.colorScheme.foreground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'john.doe@example.com',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
|
||||
style: context.theme.typography.base.copyWith(
|
||||
color: context.theme.colorScheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isDark ? Colors.green.shade700 : Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: const Text('Edit Profile'),
|
||||
FButton(
|
||||
label: const Text('Edit Profile'),
|
||||
style: FButtonStyle.primary,
|
||||
onPress: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
class ExpandableSection extends StatelessWidget {
|
||||
final String title;
|
||||
@@ -20,82 +21,29 @@ class ExpandableSection extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.grey.shade800 : Colors.green.shade100,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: const Radius.circular(16),
|
||||
bottom: Radius.circular(isExpanded ? 0 : 16),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: FCollapsible(
|
||||
initiallyExpanded: isExpanded,
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: context.theme.colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: context.theme.typography.base.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isDark ? Colors.green.shade300 : Colors.green.shade800,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.green.shade300 : Colors.green.shade800,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
AnimatedRotation(
|
||||
turns: isExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: isDark ? Colors.green.shade300 : Colors.green.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
AnimatedBuilder(
|
||||
animation: heightFactor,
|
||||
builder: (context, innerChild) {
|
||||
return ClipRect(
|
||||
child: Align(
|
||||
heightFactor: heightFactor.value,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
bottom: Radius.circular(16),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark
|
||||
? Colors.black.withOpacity(0.3)
|
||||
: Colors.green.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: innerChild,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
class FilterChipWidget extends StatelessWidget {
|
||||
final String label;
|
||||
@@ -14,35 +15,14 @@ class FilterChipWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), // Smaller padding
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? (isDark ? Colors.green.shade700 : Colors.green.shade100)
|
||||
: (isDark ? Colors.grey.shade800 : Colors.grey.shade200),
|
||||
borderRadius: BorderRadius.circular(14), // Smaller radius
|
||||
border: isSelected
|
||||
? Border.all(
|
||||
color: isDark ? Colors.green.shade300 : Colors.green,
|
||||
width: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12, // Smaller text
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected
|
||||
? (isDark ? Colors.white : Colors.green.shade700)
|
||||
: (isDark ? Colors.white70 : Colors.grey.shade700),
|
||||
),
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FButton(
|
||||
label: Text(label),
|
||||
style: isSelected
|
||||
? FButtonStyle.primary
|
||||
: FButtonStyle.secondary,
|
||||
onPress: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
class SummaryItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
@@ -16,7 +17,6 @@ class SummaryItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -24,14 +24,13 @@ class SummaryItem extends StatelessWidget {
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: isDark ? color.withOpacity(0.8) : color,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isDark ? Colors.grey.shade400 : Colors.grey.shade700,
|
||||
style: context.theme.typography.sm.copyWith(
|
||||
color: context.theme.colorScheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -39,10 +38,9 @@ class SummaryItem extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
amount,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
style: context.theme.typography.base.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
color: context.theme.colorScheme.foreground,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../database/database.dart' as db; // Import database with prefix 'db'
|
||||
import '../models/transaction_record.dart';
|
||||
@@ -16,9 +17,6 @@ class TransactionListItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// Get category details (icon, color) using the utility
|
||||
final categoryDetails = CategoryUtils.getCategoryDetails(transaction.categoryName);
|
||||
|
||||
@@ -42,11 +40,6 @@ class TransactionListItem extends StatelessWidget {
|
||||
displayDate = '${dateFormatter.format(transaction.date)}, ${timeFormatter.format(transaction.date)}';
|
||||
}
|
||||
|
||||
// Determine text colors based on theme
|
||||
Color primaryTextColor = theme.textTheme.bodyLarge?.color ?? (isDark ? Colors.white : Colors.black87);
|
||||
Color secondaryTextColor = theme.textTheme.bodyMedium?.color ?? (isDark ? Colors.white70 : Colors.grey.shade600);
|
||||
Color amountColor = isDark ? Colors.red.shade200 : Colors.red.shade700; // Expense color
|
||||
|
||||
// Use FadeTransition for item appearance (works with ListView.builder)
|
||||
return FadeTransition(
|
||||
opacity: animation, // Apply fade animation
|
||||
@@ -60,16 +53,12 @@ class TransactionListItem extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon container with category color/icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: categoryDetails.colorCode.withOpacity(isDark ? 0.25 : 0.15), // Use category color with opacity
|
||||
borderRadius: BorderRadius.circular(12), // Rounded corners
|
||||
),
|
||||
FAvatar(
|
||||
backgroundColor: categoryDetails.colorCode.withOpacity(0.15),
|
||||
child: Icon(
|
||||
categoryDetails.iconCode, // Use category icon
|
||||
color: categoryDetails.colorCode, // Use category color for icon
|
||||
size: 20, // Icon size
|
||||
categoryDetails.iconCode,
|
||||
color: categoryDetails.colorCode,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12), // Spacing
|
||||
@@ -82,9 +71,9 @@ class TransactionListItem extends StatelessWidget {
|
||||
// Display Merchant if available, otherwise Category Name
|
||||
Text(
|
||||
transaction.merchant.isNotEmpty ? transaction.merchant : transaction.categoryName,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500, // Medium weight for primary text
|
||||
color: primaryTextColor,
|
||||
style: context.theme.typography.base.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.theme.colorScheme.foreground,
|
||||
),
|
||||
maxLines: 1, // Prevent wrapping
|
||||
overflow: TextOverflow.ellipsis, // Handle long text
|
||||
@@ -93,9 +82,8 @@ class TransactionListItem extends StatelessWidget {
|
||||
// Display formatted date/time
|
||||
Text(
|
||||
displayDate,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: secondaryTextColor, // Lighter color for secondary text
|
||||
fontSize: 12, // Smaller font size
|
||||
style: context.theme.typography.sm.copyWith(
|
||||
color: context.theme.colorScheme.mutedForeground,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -109,9 +97,9 @@ class TransactionListItem extends StatelessWidget {
|
||||
Text(
|
||||
// Format amount as currency (negative for expense)
|
||||
NumberFormat.currency(symbol: '-\$', decimalDigits: 2).format(transaction.amount),
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600, // Bold weight for amount
|
||||
color: amountColor, // Use expense color
|
||||
style: context.theme.typography.base.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.theme.colorScheme.destructive,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
import '../database/database.dart' as db;
|
||||
import '../models/transaction_record.dart' as model;
|
||||
import '../utils/category_utils.dart';
|
||||
import 'filter_chip_widget.dart';
|
||||
import 'transaction_list_item.dart';
|
||||
|
||||
class TransactionsSection extends StatelessWidget {
|
||||
final Stream<List<db.Transaction>> transactionsStream;
|
||||
final bool isFilterVisible;
|
||||
final String selectedFilter;
|
||||
final VoidCallback onToggleFilter;
|
||||
final Function(String) onApplyFilter;
|
||||
|
||||
const TransactionsSection({
|
||||
Key? key,
|
||||
required this.transactionsStream,
|
||||
required this.isFilterVisible,
|
||||
required this.selectedFilter,
|
||||
required this.onToggleFilter,
|
||||
required this.onApplyFilter,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
_buildFilterChips(),
|
||||
_buildTransactionsList(context),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Recent Transactions',
|
||||
style: context.theme.typography.xl.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
FButton(
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(selectedFilter),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.filter_list_alt, size: 18),
|
||||
],
|
||||
),
|
||||
style: FButtonStyle.secondary,
|
||||
onPress: onToggleFilter,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChips() {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
height: isFilterVisible ? 50 : 0,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: isFilterVisible ? 8 : 0,
|
||||
),
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: [
|
||||
FilterChipWidget(
|
||||
label: 'All',
|
||||
isSelected: selectedFilter == 'All',
|
||||
onTap: () => onApplyFilter('All'),
|
||||
),
|
||||
...CategoryUtils.getAllCategoryNames().map((name) =>
|
||||
FilterChipWidget(
|
||||
label: name,
|
||||
isSelected: selectedFilter == name,
|
||||
onTap: () => onApplyFilter(name),
|
||||
),
|
||||
).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransactionsList(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: FCard(
|
||||
child: StreamBuilder<List<db.Transaction>>(
|
||||
stream: transactionsStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
height: 150,
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return SizedBox(
|
||||
height: 150,
|
||||
child: Center(child: Text('Error: ${snapshot.error}')),
|
||||
);
|
||||
}
|
||||
|
||||
final transactions = snapshot.data ?? [];
|
||||
|
||||
if (transactions.isEmpty) {
|
||||
return SizedBox(
|
||||
height: 150,
|
||||
child: Center(
|
||||
child: Text(
|
||||
selectedFilter == 'All'
|
||||
? 'No transactions yet.'
|
||||
: 'No transactions found for $selectedFilter.',
|
||||
style: context.theme.typography.base.copyWith(
|
||||
color: context.theme.colorScheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: transactions.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
separatorBuilder: (context, index) => FSeparator(),
|
||||
itemBuilder: (context, index) {
|
||||
final dbTransaction = transactions[index];
|
||||
final categoryDetails = CategoryUtils.getCategoryDetails(
|
||||
dbTransaction.categoryName,
|
||||
);
|
||||
final transactionRecord = model.TransactionRecord(
|
||||
dbTransaction.categoryName,
|
||||
dbTransaction.amount,
|
||||
categoryDetails.iconCode,
|
||||
categoryDetails.colorCode,
|
||||
dbTransaction.date,
|
||||
dbTransaction.merchant,
|
||||
);
|
||||
|
||||
return TransactionListItem(
|
||||
transaction: transactionRecord,
|
||||
animation: kAlwaysCompleteAnimation,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+77
-32
@@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "13c1e6c6fd460522ea840abec3f677cc226f5fec7872c04ad7b425517ccf54f7"
|
||||
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.4.4"
|
||||
version: "7.4.5"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -29,10 +29,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
version: "2.13.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -101,10 +101,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
|
||||
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.5"
|
||||
version: "8.10.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -125,10 +125,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -189,26 +189,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
|
||||
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.1.0"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
|
||||
sha256: b584ddeb2b74436735dd2cf746d2d021e19a9a6770f409212fd5cbc2814ada85
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.26.0"
|
||||
version: "2.26.1"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
|
||||
sha256: "54dc207c6e4662741f60e5752678df183957ab907754ffab0372a7082f6d2816"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.26.0"
|
||||
version: "2.26.1"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -221,10 +221,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -270,11 +270,32 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
forui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: forui
|
||||
sha256: e77b5774aef52883d148dba1d7b6ce7a723c5e1901b13142aea1bb61b9e68d9e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.0"
|
||||
forui_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: forui_assets
|
||||
sha256: "795b47478ddf7cafb72e943f97ae004a4aa01f1ae40f0b2d68768c35ee680da4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -291,6 +312,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
google_fonts:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.1"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -303,10 +332,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -327,10 +356,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -359,10 +388,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "10.0.9"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -427,6 +456,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
nitrogen_types:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nitrogen_types
|
||||
sha256: "5d4ec453aea14e34607bc6021ff1381192e8fcd8541bf103638baa4d013e668f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0+1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -580,18 +617,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||
sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.5"
|
||||
version: "2.7.6"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
|
||||
sha256: "7986c26234c0a5cf4fd83ff4ee39d4195b1f47cdb50a949ec7987ede4dcbdc2a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.32"
|
||||
version: "0.5.33"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -632,6 +669,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
sugar:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sugar
|
||||
sha256: a0a97f1d3552a3ef4cbec7c6e62c7b52c151cd5c11e984e8311700a918f73dc5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -676,10 +721,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.1"
|
||||
version: "15.0.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -700,10 +745,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -729,5 +774,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
flutter: ">=3.32.0"
|
||||
|
||||
@@ -31,7 +31,7 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
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 - основной пакет
|
||||
sqlite3_flutter_libs: ^0.5.24 # Needed for native platforms
|
||||
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.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
forui: ^0.12.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -65,33 +66,3 @@ flutter:
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/to/asset-from-package
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
|
||||
@@ -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 состояния где нужно. Проведи финальное тестирование на разных устройствах и платформах."
|
||||
|
@@ -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 состояния где нужно. Проведи финальное тестирование на разных устройствах и платформах."
|
||||
|
Reference in New Issue
Block a user