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: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',
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: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
@@ -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"
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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