Abschnitt 5
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:expense_tracker/widgets/chart/chart_bar.dart';
|
||||
import 'package:expense_tracker/models/expense.dart';
|
||||
|
||||
class Chart extends StatelessWidget {
|
||||
const Chart({super.key, required this.expenses});
|
||||
|
||||
final List<Expense> expenses;
|
||||
|
||||
List<ExpenseBucket> get buckets {
|
||||
return [
|
||||
ExpenseBucket.forCategory(expenses, Category.food),
|
||||
ExpenseBucket.forCategory(expenses, Category.leisure),
|
||||
ExpenseBucket.forCategory(expenses, Category.travel),
|
||||
ExpenseBucket.forCategory(expenses, Category.work),
|
||||
];
|
||||
}
|
||||
|
||||
double get maxTotalExpense {
|
||||
double maxTotalExpense = 0;
|
||||
|
||||
for (final bucket in buckets) {
|
||||
if (bucket.totalExpenses > maxTotalExpense) {
|
||||
maxTotalExpense = bucket.totalExpenses;
|
||||
}
|
||||
}
|
||||
|
||||
return maxTotalExpense;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDarkMode =
|
||||
MediaQuery.of(context).platformBrightness == Brightness.dark;
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 8,
|
||||
),
|
||||
width: double.infinity,
|
||||
height: 180,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.0)
|
||||
],
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
for (final bucket in buckets) // alternative to map()
|
||||
ChartBar(
|
||||
fill: bucket.totalExpenses == 0
|
||||
? 0
|
||||
: bucket.totalExpenses / maxTotalExpense,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: buckets
|
||||
.map(
|
||||
(bucket) => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Icon(
|
||||
categoryIcons[bucket.category],
|
||||
color: isDarkMode
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChartBar extends StatelessWidget {
|
||||
const ChartBar({
|
||||
super.key,
|
||||
required this.fill,
|
||||
});
|
||||
|
||||
final double fill;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDarkMode =
|
||||
MediaQuery.of(context).platformBrightness == Brightness.dark;
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: FractionallySizedBox(
|
||||
heightFactor: fill,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(8)),
|
||||
color: isDarkMode
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Theme.of(context).colorScheme.primary.withOpacity(0.65),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:expense_tracker/widgets/expenses_list/expenses_list.dart';
|
||||
import 'package:expense_tracker/widgets/new_expense.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:expense_tracker/models/expense.dart';
|
||||
import 'package:expense_tracker/widgets/chart/chart.dart';
|
||||
|
||||
class Expenses extends StatefulWidget {
|
||||
const Expenses({super.key});
|
||||
@@ -30,11 +31,50 @@ class _ExpensesState extends State<Expenses> {
|
||||
|
||||
void _openAddExpenseOverlay() {
|
||||
showModalBottomSheet(
|
||||
context: context, builder: (ctx) => const NewExpense());
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (ctx) => NewExpense(onAddExpense: _addExpense),
|
||||
);
|
||||
}
|
||||
|
||||
void _addExpense(Expense expense) {
|
||||
setState(() {
|
||||
_registeredExpenses.add(expense);
|
||||
});
|
||||
}
|
||||
|
||||
void _removeExpense(Expense expense) {
|
||||
final expenseIndex = _registeredExpenses.indexOf(expense);
|
||||
|
||||
setState(() {
|
||||
_registeredExpenses.remove(expense);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 3),
|
||||
content: const Text('Expense deleted.'),
|
||||
action: SnackBarAction(
|
||||
label: 'Undo',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_registeredExpenses.insert(expenseIndex, expense);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget mainContent = const Center(
|
||||
child: Text('No expenses found. Start adding some!'),
|
||||
);
|
||||
|
||||
if (_registeredExpenses.isNotEmpty) {
|
||||
mainContent = ExpensesList(
|
||||
expenses: _registeredExpenses, onRemoveExpense: _removeExpense);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Flutter ExpenseTracker'),
|
||||
@@ -47,9 +87,9 @@ class _ExpensesState extends State<Expenses> {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
const Text('The chart'),
|
||||
Chart(expenses: _registeredExpenses),
|
||||
Expanded(
|
||||
child: ExpensesList(expenses: _registeredExpenses),
|
||||
child: mainContent,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -15,8 +15,12 @@ class ExpenseItem extends StatelessWidget {
|
||||
vertical: 16,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(expense.title),
|
||||
Text(
|
||||
expense.title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
|
||||
@@ -6,15 +6,28 @@ class ExpensesList extends StatelessWidget {
|
||||
const ExpensesList({
|
||||
super.key,
|
||||
required this.expenses,
|
||||
required this.onRemoveExpense,
|
||||
});
|
||||
|
||||
final List<Expense> expenses;
|
||||
final void Function(Expense expense) onRemoveExpense;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: expenses.length,
|
||||
itemBuilder: (ctx, index) => ExpenseItem(expenses[index]),
|
||||
itemBuilder: (ctx, index) => Dismissible(
|
||||
key: ValueKey(expenses[index]),
|
||||
background: Container(
|
||||
color: Theme.of(context).colorScheme.error.withOpacity(0.75),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: Theme.of(context).cardTheme.margin!.horizontal),
|
||||
),
|
||||
onDismissed: (direction) {
|
||||
onRemoveExpense(expenses[index]);
|
||||
},
|
||||
child: ExpenseItem(expenses[index]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:expense_tracker/models/expense.dart';
|
||||
import 'package:expense_tracker/widgets/expenses.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -6,7 +7,9 @@ import 'package:intl/intl.dart';
|
||||
final formatter = DateFormat('dd.MM.yyyy');
|
||||
|
||||
class NewExpense extends StatefulWidget {
|
||||
const NewExpense({super.key});
|
||||
const NewExpense({super.key, required this.onAddExpense});
|
||||
|
||||
final void Function(Expense expense) onAddExpense;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -60,7 +63,13 @@ class _NewExpenseState extends State<NewExpense> {
|
||||
return;
|
||||
}
|
||||
|
||||
// ...
|
||||
widget.onAddExpense(Expense(
|
||||
title: _titleController.text,
|
||||
amount: enteredAmount,
|
||||
date: _selectedDate!,
|
||||
category: _selectedCategory,
|
||||
));
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -72,7 +81,7 @@ class _NewExpenseState extends State<NewExpense> {
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
@@ -124,12 +133,12 @@ class _NewExpenseState extends State<NewExpense> {
|
||||
items: Category.values
|
||||
.map(
|
||||
(category) => DropdownMenuItem(
|
||||
value: category,
|
||||
child: Text(
|
||||
category.name.toUpperCase(),
|
||||
),
|
||||
),
|
||||
)
|
||||
value: category,
|
||||
child: Text(
|
||||
category.name.toUpperCase(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
|
||||
Reference in New Issue
Block a user