basic csv loader, transaction list & half done stats page

This commit is contained in:
2024-02-04 22:34:28 +01:00
commit 3abee9ff6f
179 changed files with 6999 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
class AutocompleteInput extends StatelessWidget {
final List<String> options;
final String hintText;
final String? errorText;
final ValueChanged<String>? onChanged;
const AutocompleteInput({
super.key,
required this.options,
required this.hintText,
required this.errorText,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) => options.where((String option) =>option.contains(textEditingValue.text.toLowerCase())),
fieldViewBuilder: (
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) {
return TextFormField(
controller: textEditingController,
focusNode: focusNode,
decoration: InputDecoration(
hintText: hintText,
errorText: errorText
),
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
onChanged: onChanged,
);
},
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
child: SizedBox(
height: 200.0,
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final String option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option),
),
);
},
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/account/account_bloc.dart';
import 'package:tunas/pages/transactions/widgets/transaction_add_form.dart';
class TransactionAddDialog extends StatelessWidget {
const TransactionAddDialog({super.key});
static void show(BuildContext context) => showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (_) => BlocProvider.value(
value: BlocProvider.of<AccountBloc>(context),
child: const TransactionAddDialog()
)
);
static void hide(BuildContext context) => Navigator.pop(context);
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Add transaction'),
actions: [
TextButton(
onPressed: () => TransactionAddDialog.hide(context),
child: Text('Close')
),
TextButton(
onPressed: () => context.read<AccountBloc>().add(TransactionAdd()),
child: Text('Add')
),
],
content: SizedBox(
height: 400,
child: TransactionAddForm(),
)
);
}
}

View File

@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:tunas/domains/account/account_bloc.dart';
import 'autocomplete_input.dart';
class TransactionAddForm extends StatelessWidget {
const TransactionAddForm({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
_TransactionDateInput(),
_TransactionCategoryInput(),
_TransactionDescriptionInput(),
_TransactionAccountInput(),
_TransactionValueInput()
],
);
}
}
class _TransactionDateInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<AccountBloc, AccountState>(
buildWhen: (previous, current) => previous.transactionDate != current.transactionDate,
builder: (context, state) => SizedBox(
width: 500,
child: TextFormField(
initialValue: DateFormat('dd-MM-yyyy', 'fr_FR').format(state.transactionDate.value ?? DateTime.now()),
keyboardType: TextInputType.datetime,
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
showDatePicker(
context: context,
firstDate: DateTime.fromMicrosecondsSinceEpoch(0),
lastDate: DateTime.now()
).then((value) => context.read<AccountBloc>().add(TransactionDateChange(value)));
},
decoration: InputDecoration(
hintText: 'Date',
errorText: state.transactionDate.isNotValid ? state.transactionDate.error?.message : null
),
)
)
);
}
}
class _TransactionCategoryInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<AccountBloc, AccountState>(
buildWhen: (previous, current) => previous.transactionCategory != current.transactionCategory,
builder: (context, state) => SizedBox(
width: 500,
child: AutocompleteInput(
options: state.categories,
hintText: 'Category',
errorText: state.transactionCategory.isNotValid ? state.transactionCategory.error?.message : null,
onChanged: (value) => context.read<AccountBloc>().add(TransactionCategoryChange(value)),
),
),
);
}
}
class _TransactionDescriptionInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<AccountBloc, AccountState>(
buildWhen: (previous, current) => previous.transactionDescription != current.transactionDescription,
builder: (context, state) => SizedBox(
width: 500,
child: TextField(
decoration: InputDecoration(
hintText: 'Description',
errorText: state.transactionDescription.isNotValid ? state.transactionDescription.error?.message : null
),
onChanged: (value) => context.read<AccountBloc>().add(TransactionDescriptionChange(value))
)
),
);
}
}
class _TransactionAccountInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<AccountBloc, AccountState>(
buildWhen: (previous, current) => previous.transactionAccount != current.transactionAccount,
builder: (context, state) => SizedBox(
width: 500,
child: AutocompleteInput(
options: state.accountsTotals.keys.toList(),
hintText: 'Account',
errorText: state.transactionAccount.isNotValid ? state.transactionAccount.error?.message : null,
onChanged: (value) => context.read<AccountBloc>().add(TransactionAccountChange(value)),
),
),
);
}
}
class _TransactionValueInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<AccountBloc, AccountState>(
buildWhen: (previous, current) => previous.transactionValue != current.transactionValue,
builder: (context, state) => SizedBox(
width: 500,
child: TextField(
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '\$\$\$',
errorText: state.transactionValue.isNotValid ? state.transactionValue.error?.message : null
),
onChanged: (value) => context.read<AccountBloc>().add(TransactionValueChange(value))
)
),
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tunas/repositories/account/models/transaction.dart';
class TransactionLine extends StatelessWidget {
final Transaction transaction;
final double subTotal;
const TransactionLine({super.key, required this.transaction, required this.subTotal});
@override
Widget build(BuildContext context) {
return MergeSemantics(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10),
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 100,
child: Text(DateFormat('dd-MM-yyyy', 'fr_FR').format(transaction.date))
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
transaction.category,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)
),
Text(transaction.description),
],
),
),
SizedBox(
width: 100,
child: Text(transaction.account),
),
SizedBox(
width: 120,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
NumberFormat(transaction.value > 0 ? '+#######.00 €' : '#######.00 €', 'fr_FR').format(transaction.value),
style: TextStyle(
color: transaction.value > 0 ? Colors.green : Colors.red
)
),
Text(
NumberFormat('#######.00 €', 'fr_FR').format(subTotal),
style: TextStyle(
color: subTotal > 0 ? Colors.green : Colors.red
)
),
],
),
)
],
)
)
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/account/account_bloc.dart';
import 'package:tunas/pages/transactions/widgets/transaction_add_dialog.dart';
class TransactionsActions extends StatelessWidget {
const TransactionsActions({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) => Container(
padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10),
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Transactions',
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 35,
),
),
ElevatedButton.icon(
onPressed: () => TransactionAddDialog.show(context),
icon: const Icon(
Icons.add
),
label: const Text('Add'),
),
],
)
)
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
class TransactionsHeader extends StatelessWidget {
const TransactionsHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10),
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.black))
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 100,
child: Text('Date')
),
Expanded(
child: Text('Description')
),
SizedBox(
width: 100,
child: Text('Account'),
),
SizedBox(
width: 120,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('Amount'),
Text('SubTotal'),
],
),
)
],
),
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/account/account_bloc.dart';
import 'package:tunas/pages/transactions/widgets/transaction_line.dart';
class TransactionsList extends StatelessWidget {
const TransactionsList({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AccountBloc, AccountState>(
buildWhen: (previous, current) => previous.transactionsLines != current.transactionsLines,
builder: (context, state) => Expanded(
child: ListView.builder(
itemCount: state.transactionsLines.length,
itemBuilder: (context, index) => TransactionLine(
transaction: state.transactionsLines[index].transaction,
subTotal: state.transactionsLines[index].subTotal
)
)
)
);
}
}