From 29621b915fd10201331a97e8e4698df34da3fdbe Mon Sep 17 00:00:00 2001 From: Bhasher Date: Sun, 26 Mar 2023 20:12:39 +0200 Subject: [PATCH] New entry using new screen template --- lib/components/new_entry.dart | 628 ++++++++++++---------------- lib/components/project/drawer.dart | 10 +- lib/screens/new_project_screen.dart | 2 +- lib/screens/new_screen.dart | 89 ++-- pubspec.yaml | 2 +- 5 files changed, 326 insertions(+), 405 deletions(-) diff --git a/lib/components/new_entry.dart b/lib/components/new_entry.dart index c3268f0..e80ee06 100644 --- a/lib/components/new_entry.dart +++ b/lib/components/new_entry.dart @@ -3,34 +3,60 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:select_form_field/select_form_field.dart'; -import 'package:shared/utils/colors.dart'; import '../model/bill_data.dart'; import '../model/item.dart'; import '../model/participant.dart'; import '../model/project.dart'; +import '../screens/new_screen.dart'; import '../utils/formatter/decimal.dart'; import '../utils/time.dart'; -class NewEntryPage extends StatefulWidget { +class NewEntryPage extends StatelessWidget { const NewEntryPage(this.project, {super.key, this.item}); final Project project; final Item? item; @override - State createState() => _NewEntryPageState(); + Widget build(BuildContext context) { + BillData bill = BillData(item: item, project: project); + return NewScreen( + title: item == null ? 'Add new expense' : 'Update expense', + onValidate: (context, key) async { + if (key.currentState!.validate()) { + Item item = await bill.toItemOf(project); + await item.conn.saveRecursively(); + Navigator.pop(context, true); + } + }, + child: NewEntrySubPage( + project, + bill, + item: item, + ), + ); + } +} + +class NewEntrySubPage extends StatefulWidget { + const NewEntrySubPage(this.project, this.bill, {super.key, this.item}); + + final Project project; + final Item? item; + final BillData bill; + + @override + State createState() => _NewEntrySubPageState(); } -class _NewEntryPageState extends State { - late BillData bill; +class _NewEntrySubPageState extends State { final dateController = TextEditingController(); final titleController = TextEditingController(); final emitterController = TextEditingController(); final amountController = TextEditingController(); final Map sharesController = {}; final Map fixedsController = {}; - final _formKey = GlobalKey(); @override void dispose() { @@ -42,19 +68,18 @@ class _NewEntryPageState extends State { @override void initState() { super.initState(); - bill = BillData(item: widget.item, project: widget.project); - titleController.text = bill.title; - dateController.text = daysElapsed(bill.date); - emitterController.text = bill.emitter.pseudo; - amountController.text = bill.amount.toStringAsFixed(2); + titleController.text = widget.bill.title; + dateController.text = daysElapsed(widget.bill.date); + emitterController.text = widget.bill.emitter.pseudo; + amountController.text = widget.bill.amount.toStringAsFixed(2); for (Participant participant in widget.project.participants) { if (widget.item == null) { - bill.shares[participant] = BillPart(share: 1); + widget.bill.shares[participant] = BillPart(share: 1); sharesController[participant] = TextEditingController(text: "1"); fixedsController[participant] = TextEditingController(); - } else if (bill.shares[participant] == null) { - bill.shares[participant] = BillPart(); + } else if (widget.bill.shares[participant] == null) { + widget.bill.shares[participant] = BillPart(); sharesController[participant] = TextEditingController(text: "0"); fixedsController[participant] = TextEditingController(text: "0.00"); } else { @@ -66,289 +91,233 @@ class _NewEntryPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: true, - appBar: AppBar( - elevation: 4, - title: Column( - children: [ - Text( - widget.item == null ? 'Add new expense' : 'Update expense', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox( + height: 5, + ), + TextFormField( + controller: titleController, + validator: (value) => + value == null || value.isEmpty ? 'Title can\'t be empty' : null, + autocorrect: true, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 10), + labelText: "What", + border: OutlineInputBorder(), ), - ], - ), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Form( - key: _formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Column( - children: [ - const SizedBox( - height: 12, // <-- SEE HERE - ), - TextFormField( - controller: titleController, - validator: (value) => value == null || value.isEmpty - ? 'Title can\'t be empty' - : null, - autocorrect: true, - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(horizontal: 10), - labelText: "What", - border: OutlineInputBorder(), + onChanged: (value) => widget.bill.title = value, + ), + const SizedBox( + height: 12, // <-- SEE HERE + ), + TextFormField( + autocorrect: false, + validator: (value) { + try { + if (double.parse(value!) > 0) return null; + return 'Amount can\'t be null'; + } catch (e) { + return 'Amount must be a valid value'; + } + }, + inputFormatters: [ + DecimalTextInputFormatter(2), + ], + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + suffixText: ' €', + contentPadding: EdgeInsets.symmetric(horizontal: 10), + labelText: "How much", + border: OutlineInputBorder(), + ), + controller: amountController, + onChanged: (value) { + try { + double parsed = double.parse(value); + setState(() { + widget.bill.amount = parsed; + }); + } catch (e) {} + }, + ), + const SizedBox( + height: 12, // <-- SEE HERE + ), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Column( + children: [ + SelectFormField( + type: SelectFormFieldType.dropdown, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 10), + labelText: "Who paid", + border: OutlineInputBorder(), + ), + items: widget.project.participants + .map((p) => { + 'value': p.pseudo, + 'label': p.pseudo, + }) + .toList(), + controller: emitterController, + onChanged: (value) { + widget.bill.emitter = widget.project.participants + .firstWhere((element) => element.pseudo == value); + }, ), - onChanged: (value) => bill.title = value, - ), - const SizedBox( - height: 12, // <-- SEE HERE - ), - TextFormField( - autocorrect: false, - validator: (value) { - try { - if (double.parse(value!) > 0) return null; - return 'Amount can\'t be null'; - } catch (e) { - return 'Amount must be a valid value'; - } - }, - inputFormatters: [ - DecimalTextInputFormatter(2), - ], - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - decoration: const InputDecoration( - suffixText: ' €', - contentPadding: EdgeInsets.symmetric(horizontal: 10), - labelText: "How much", - border: OutlineInputBorder(), + ], + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + children: [ + TextField( + readOnly: true, + controller: dateController, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 10), + labelText: "When", + border: OutlineInputBorder(), + hintText: 'Pick your Date', + ), + onTap: () async { + DateTime? date = await showDatePicker( + context: context, + initialDate: widget.bill.date, + firstDate: DateTime(1900), + lastDate: DateTime(2100), + ); + if (date != null) { + dateController.text = daysElapsed(date); + widget.bill.date = date; + } + }, ), - controller: amountController, - onChanged: (value) { - try { - double parsed = double.parse(value); - setState(() { - bill.amount = parsed; - }); - } catch (e) {} - }, - ), - const SizedBox( - height: 12, // <-- SEE HERE - ), - Row( + ], + ), + ), + ), + ], + ), + const SizedBox( + height: 35, // <-- SEE HERE + ), + const Divider(), + Padding( + padding: const EdgeInsets.only(top: 25), + child: Table( + // border: const TableBorder( + // horizontalInside: BorderSide( + // width: 1, + // color: Colors.blue, + // ), + // verticalInside: BorderSide( + // width: 1, + // color: Colors.blue, + // ), + // bottom: BorderSide( + // width: 1, + // color: Colors.blue, + // ), + // left: BorderSide( + // width: 1, + // color: Colors.blue, + // ), + // right: BorderSide( + // width: 1, + // color: Colors.blue, + // ), + // top: BorderSide( + // width: 1, + // color: Colors.blue, + // ), + // ), + columnWidths: const { + 0: FixedColumnWidth(50), + 1: FlexColumnWidth(5), + 2: FlexColumnWidth(2), + 3: FlexColumnWidth(2), + }, + children: [ + TableRow( children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Column( - children: [ - SelectFormField( - type: SelectFormFieldType.dropdown, - decoration: const InputDecoration( - contentPadding: - EdgeInsets.symmetric(horizontal: 10), - labelText: "Who paid", - border: OutlineInputBorder(), - ), - items: widget.project.participants - .map((p) => { - 'value': p.pseudo, - 'label': p.pseudo, - }) - .toList(), - controller: emitterController, - onChanged: (value) { - bill.emitter = widget.project.participants - .firstWhere((element) => - element.pseudo == value); - }, - ), - ], + TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Checkbox( + value: widget.bill.shares.values + .where((e) => + e.fixed != null || e.share != null) + .length != + widget.bill.shares.length + ? widget.bill.shares.values + .where((e) => + e.fixed != null || e.share != null) + .isNotEmpty + ? null + : false + : true, + tristate: true, + onChanged: (value) { + value ??= false; + setState(() { + widget.bill.shares.updateAll( + (k, v) => BillPart(share: value! ? 1 : null), + ); + }); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3), + ), + ), + ), + const TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Text( + "For whom ?", + textAlign: TextAlign.left, + style: TextStyle( + fontWeight: FontWeight.bold, ), ), ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Column( - children: [ - TextField( - readOnly: true, - controller: dateController, - decoration: const InputDecoration( - contentPadding: - EdgeInsets.symmetric(horizontal: 10), - labelText: "When", - border: OutlineInputBorder(), - hintText: 'Pick your Date', - ), - onTap: () async { - DateTime? date = await showDatePicker( - context: context, - initialDate: bill.date, - firstDate: DateTime(1900), - lastDate: DateTime(2100), - ); - if (date != null) { - dateController.text = daysElapsed(date); - bill.date = date; - } - }, - ), - ], + const TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Text( + "Rate", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + const TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Text( + "Total", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, ), ), ), ], ), - const SizedBox( - height: 35, // <-- SEE HERE - ), - const Divider(), - Padding( - padding: const EdgeInsets.only(top: 25), - child: Table( - // border: const TableBorder( - // horizontalInside: BorderSide( - // width: 1, - // color: Colors.blue, - // ), - // verticalInside: BorderSide( - // width: 1, - // color: Colors.blue, - // ), - // bottom: BorderSide( - // width: 1, - // color: Colors.blue, - // ), - // left: BorderSide( - // width: 1, - // color: Colors.blue, - // ), - // right: BorderSide( - // width: 1, - // color: Colors.blue, - // ), - // top: BorderSide( - // width: 1, - // color: Colors.blue, - // ), - // ), - columnWidths: const { - 0: FixedColumnWidth(50), - 1: FlexColumnWidth(5), - 2: FlexColumnWidth(2), - 3: FlexColumnWidth(2), - }, - children: [ - TableRow( - children: [ - TableCell( - verticalAlignment: - TableCellVerticalAlignment.middle, - child: Checkbox( - value: bill.shares.values - .where((e) => - e.fixed != null || - e.share != null) - .length != - bill.shares.length - ? bill.shares.values - .where((e) => - e.fixed != null || - e.share != null) - .isNotEmpty - ? null - : false - : true, - tristate: true, - onChanged: (value) { - value ??= false; - setState(() { - bill.shares.updateAll( - (k, v) => BillPart( - share: value! ? 1 : null), - ); - }); - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(3), - ), - ), - ), - const TableCell( - verticalAlignment: - TableCellVerticalAlignment.middle, - child: Text( - "For whom ?", - textAlign: TextAlign.left, - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - const TableCell( - verticalAlignment: - TableCellVerticalAlignment.middle, - child: Text( - "Rate", - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - const TableCell( - verticalAlignment: - TableCellVerticalAlignment.middle, - child: Text( - "Total", - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ] + - getRows(bill, sharesController, fixedsController), - ), - ), - ], - ), - Padding( - padding: const EdgeInsets.only(top: 10), - child: Align( - alignment: Alignment.bottomCenter, - child: ElevatedButton( - onPressed: () async { - if (_formKey.currentState!.validate()) { - Item item = await bill.toItemOf(widget.project); - await item.conn.saveRecursively(); - Navigator.pop(context, true); - } - }, - child: Text(widget.item == null ? 'Create' : 'Update'), - ), - ), - ), - ], + ] + + getRows(widget.bill, sharesController, fixedsController), ), ), - ), + ], ), ); } @@ -360,12 +329,12 @@ class _NewEntryPageState extends State { ) { final List rows = []; - double total = max(bill.totalShares, 1); + double total = widget.bill.totalShares; - double fixedBonus = bill.totalFixed / - bill.shares.values.where((e) => e.share != null).length; + double fixedBonus = widget.bill.totalFixed / + widget.bill.shares.values.where((e) => e.share != null).length; - bill.shares.forEach((participant, amount) { + widget.bill.shares.forEach((participant, amount) { final newShareValue = amount.share == null ? amount.fixed == null ? "0" @@ -378,10 +347,13 @@ class _NewEntryPageState extends State { double price = max( amount.fixed ?? - bill.amount * (amount.share ?? 0) / (total != 0 ? total : 1) - - fixedBonus, + widget.bill.amount * (amount.share ?? 0) / total - fixedBonus, 0); + if (price.isNaN) price = 0; + + print(price); + try { if (fixedsController[participant]!.text == "" || double.parse(fixedsController[participant]!.text) != price) { @@ -394,14 +366,14 @@ class _NewEntryPageState extends State { TableCell( verticalAlignment: TableCellVerticalAlignment.middle, child: Checkbox( - value: bill.shares[participant]!.fixed != null || - bill.shares[participant]!.share != null, + value: widget.bill.shares[participant]!.fixed != null || + widget.bill.shares[participant]!.share != null, onChanged: (value) { setState(() { if (value != null && value) { - bill.shares[participant] = BillPart(share: 1); + widget.bill.shares[participant] = BillPart(share: 1); } else { - bill.shares[participant] = BillPart(); + widget.bill.shares[participant] = BillPart(); } }); }, @@ -424,9 +396,9 @@ class _NewEntryPageState extends State { child: Focus( onFocusChange: (value) { setState(() { - if (bill.shares[participant]!.share != null) { - bill.shares[participant]!.share = - bill.shares[participant]!.share! + 0.00001; + if (widget.bill.shares[participant]!.share != null) { + widget.bill.shares[participant]!.share = + widget.bill.shares[participant]!.share! + 0.00001; } }); }, @@ -437,7 +409,7 @@ class _NewEntryPageState extends State { onChanged: (value) { try { setState(() { - bill.shares[participant] = BillPart( + widget.bill.shares[participant] = BillPart( share: double.parse(value) > 0 ? double.parse(value) : null); @@ -459,12 +431,12 @@ class _NewEntryPageState extends State { if (value) return; setState(() { if (fixedsController[participant]!.text.isEmpty && - bill.shares[participant]!.share == null) { - bill.shares[participant] = BillPart(); + widget.bill.shares[participant]!.share == null) { + widget.bill.shares[participant] = BillPart(); } - if (bill.shares[participant]!.fixed != null) { - bill.shares[participant]!.fixed = - bill.shares[participant]!.fixed! + 0.00001; + if (widget.bill.shares[participant]!.fixed != null) { + widget.bill.shares[participant]!.fixed = + widget.bill.shares[participant]!.fixed! + 0.00001; } }); }, @@ -480,7 +452,7 @@ class _NewEntryPageState extends State { onChanged: (value) { try { setState(() { - bill.shares[participant] = + widget.bill.shares[participant] = BillPart(fixed: double.parse(value)); }); } catch (e) {} @@ -489,9 +461,6 @@ class _NewEntryPageState extends State { ), ), ), - // child: Center( - // child: Text("${price.toStringAsFixed(2)} €"), - // ), ), ], )); @@ -500,50 +469,3 @@ class _NewEntryPageState extends State { return rows; } } - -class TitleField extends StatelessWidget { - const TitleField( - this.text, { - super.key, - }); - - final String text; - - @override - Widget build(BuildContext context) { - // return Padding( - // padding: const EdgeInsets.only(left: 8.0, top: 20), - // child: Align( - // // alignment: Alignment.centerLeft, - // alignment: Alignment.center, - // child: Text( - // text, - // maxLines: 1, - // textAlign: TextAlign.left, - // style: const TextStyle( - // fontWeight: FontWeight.bold, - // fontSize: 18, - // fontFamily: 'Lato', - // ), - // ), - // ), - // ); - return Padding( - padding: const EdgeInsets.only(left: 8.0, top: 20), - child: Align( - alignment: Alignment.bottomLeft, - child: Text( - text, - maxLines: 1, - textAlign: TextAlign.left, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 11, - fontFamily: 'Lato', - color: ColorModel.text.withOpacity(0.6), - ), - ), - ), - ); - } -} diff --git a/lib/components/project/drawer.dart b/lib/components/project/drawer.dart index 609cd4d..3edf9e1 100644 --- a/lib/components/project/drawer.dart +++ b/lib/components/project/drawer.dart @@ -98,6 +98,7 @@ class _ProjectsDrawerState extends State { Text( '$share €', style: TextStyle( + fontWeight: current ? FontWeight.bold : null, color: share > 0 ? const Color.fromARGB(255, 76, 175, 80) : share < 0 @@ -107,11 +108,16 @@ class _ProjectsDrawerState extends State { ), ], ), - subtitle: (participant.firstname != null || - participant.lastname != null) + subtitle: (participant.firstname != null && + participant.firstname!.isNotEmpty || + participant.lastname != null && + participant.lastname!.isNotEmpty) ? Text( "${participant.firstname} ${participant.lastname}") : null, + visualDensity: const VisualDensity( + vertical: VisualDensity.minimumDensity, + ), onTap: () async { widget.project.currentParticipant = participant; await widget.project.conn.save(); diff --git a/lib/screens/new_project_screen.dart b/lib/screens/new_project_screen.dart index da43b50..101888a 100644 --- a/lib/screens/new_project_screen.dart +++ b/lib/screens/new_project_screen.dart @@ -33,7 +33,7 @@ class NewProjectScreen extends StatelessWidget { : project == null ? 'Create' : 'Update', - page: NewProjectPage( + child: NewProjectPage( projectData: setupData, project: project, ), diff --git a/lib/screens/new_screen.dart b/lib/screens/new_screen.dart index 92bd660..c7aefed 100644 --- a/lib/screens/new_screen.dart +++ b/lib/screens/new_screen.dart @@ -1,75 +1,68 @@ -import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; -import 'main_screen.dart'; - class NewScreen extends StatelessWidget { NewScreen({ super.key, this.title, - required this.page, + required this.child, this.onValidate, this.buttonTitle, }); String? title; - Widget page; + Widget child; void Function(BuildContext context, GlobalKey formKey)? onValidate; final GlobalKey formKey = GlobalKey(); String? buttonTitle; @override Widget build(BuildContext context) { - return DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) { - return MaterialApp( - title: title ?? 'Shared', - theme: defaultTheme, - darkTheme: defaultDarkTheme, - home: Scaffold( - appBar: title == null - ? null - : AppBar( - title: Text(title!), - elevation: 4, + return Scaffold( + appBar: title == null + ? null + : AppBar( + title: Text( + title!, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, ), - body: Builder( - builder: (context) => Form( - key: formKey, - child: SafeArea( - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 20), - child: Column( - children: [ - Expanded( - child: page, - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 20), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: onValidate == null - ? null - : () => onValidate!(context, formKey), - child: Padding( - padding: const EdgeInsets.all(15.0), - child: Text(buttonTitle == null - ? "Finish" - : buttonTitle!), - ), - ), + ), + ), + body: Builder( + builder: (context) => Form( + key: formKey, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20), + child: Column( + children: [ + Expanded( + child: child, + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 20), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: onValidate == null + ? null + : () => onValidate!(context, formKey), + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Text( + buttonTitle == null ? "Finish" : buttonTitle!), ), ), - ], + ), ), - ), + ], ), ), ), ), - ); - }); + ), + ); } } diff --git a/pubspec.yaml b/pubspec.yaml index ce9e78c..2ad34e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.0.3 +version: 0.0.4 environment: sdk: '>=2.18.6 <3.0.0'