diff --git a/lib/components/pages/new_project/new_project_page.dart b/lib/components/pages/new_project/new_project_page.dart index 7e43844..c2dd166 100644 --- a/lib/components/pages/new_project/new_project_page.dart +++ b/lib/components/pages/new_project/new_project_page.dart @@ -9,7 +9,7 @@ import '../../../model/project_data.dart'; import '../../../utils/navigator/navigator.dart'; import '../../../utils/switches/text_switch.dart'; import '../../../utils/tiles/header_tile.dart'; -import '../../../utils/tiles/participant_tile.dart'; +import 'participant_tile.dart'; import '../instances/instances_list_page.dart'; class NewProjectPage extends StatefulWidget { @@ -44,6 +44,7 @@ class _NewProjectPageState extends State { children: [ Expanded( child: SelectFormField( + enabled: widget.project == null, validator: (value) => value == null || value.isEmpty ? 'You must select a project instance!' : null, @@ -62,17 +63,20 @@ class _NewProjectPageState extends State { widget.projectData.instance = Instance.fromName(v); }); }, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: "Project instance", - border: OutlineInputBorder(), - suffixIcon: Icon(Icons.arrow_drop_down), + border: const OutlineInputBorder(), + suffixIcon: const Icon(Icons.arrow_drop_down), + labelStyle: + TextStyle(color: Theme.of(context).colorScheme.onPrimary), ), ), ), const SizedBox(width: 5), IconButton( - onPressed: () { - navigatorPush(context, () => const InstancesListPage()); + onPressed: () async { + await navigatorPush(context, () => const InstancesListPage()); + setState(() {}); }, icon: const Icon(Icons.settings), ), @@ -99,19 +103,58 @@ class _NewProjectPageState extends State { border: const OutlineInputBorder(), ), ), + if (widget.project != null) + const SizedBox( + height: 12, + ), + if (widget.project != null) + SelectFormField( + type: SelectFormFieldType.dropdown, + initialValue: + widget.project!.currentParticipant?.pseudo ?? "anonymous", + items: [ + ...widget.project!.participants.map>((e) => { + 'value': e.pseudo, + }), + const {'value': 'anonymous', 'label': 'Anonymous'}, + ], + onChanged: (v) { + setState(() { + if (v == 'anonymous') { + widget.project!.currentParticipant = null; + widget.project!.currentParticipantId = null; + } else { + widget.project!.currentParticipant = + widget.project!.participantByPseudo(v); + widget.project!.currentParticipantId = + widget.project!.currentParticipant?.localId; + } + }); + }, + decoration: const InputDecoration( + labelText: "Who are you ?", + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.arrow_drop_down), + ), + ), const SizedBox( height: 12, ), - if (widget.project != null) ParticipantListWidget(widget.project!), + if (widget.project != null) + ParticipantListWidget( + widget.project!, + reloadParent: () => setState(() {}), + ), ]), ); } } class ParticipantListWidget extends StatefulWidget { - const ParticipantListWidget(this.project, {super.key}); + const ParticipantListWidget(this.project, {super.key, this.reloadParent}); final Project project; + final Function()? reloadParent; @override State createState() => _ParticipantListWidgetState(); @@ -140,7 +183,7 @@ class _ParticipantListWidgetState extends State { ? null : widget.project.participants.elementAt(index), setHasNew: setHasNew, - onChange: () => setState(() {}), + onChange: widget.reloadParent ?? () => setState(() {}), ), itemCount: widget.project.participants.length + (hasNew ? 1 : 0), ), diff --git a/lib/utils/tiles/participant_tile.dart b/lib/components/pages/new_project/participant_tile.dart similarity index 85% rename from lib/utils/tiles/participant_tile.dart rename to lib/components/pages/new_project/participant_tile.dart index 3c696bf..b3067bf 100644 --- a/lib/utils/tiles/participant_tile.dart +++ b/lib/components/pages/new_project/participant_tile.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../../model/participant.dart'; -import '../../model/project.dart'; -import '../dialogs/confirm_box.dart'; +import '../../../model/participant.dart'; +import '../../../model/project.dart'; +import '../../../utils/dialogs/confirm_box.dart'; class ParticipantTile extends StatefulWidget { ParticipantTile({ @@ -33,12 +33,8 @@ class _ParticipantTileState extends State { bool hasParticipant = widget.participant != null; if (!hasParticipant) edit = true; - bool isMe = widget.participant == widget.project.currentParticipant; - controller = TextEditingController( - text: hasParticipant - ? widget.participant!.pseudo + (isMe && !edit ? ' (me)' : '') - : '', + text: hasParticipant ? widget.participant!.pseudo : '', ); return ListTile( @@ -57,9 +53,6 @@ class _ParticipantTileState extends State { border: edit ? null : InputBorder.none, ), controller: controller, - style: TextStyle( - fontWeight: isMe ? FontWeight.bold : FontWeight.normal, - ), ), trailing: Row( mainAxisSize: MainAxisSize.min, @@ -82,7 +75,7 @@ class _ParticipantTileState extends State { return; } } - setState(() {}); + widget.onChange(); }, icon: Icon(edit ? Icons.done : Icons.edit), ), @@ -98,6 +91,11 @@ class _ParticipantTileState extends State { onValidate: () async { await widget.project .deleteParticipant(widget.participant!); + if (widget.project.currentParticipant == + widget.participant) { + widget.project.currentParticipant = null; + widget.project.currentParticipantId = null; + } widget.onChange(); if (context.mounted) Navigator.of(context).pop(); }, diff --git a/lib/components/pages/project/balances/balancing_page_part.dart b/lib/components/pages/project/balances/balancing_page_part.dart index 5d2a725..b03a623 100644 --- a/lib/components/pages/project/balances/balancing_page_part.dart +++ b/lib/components/pages/project/balances/balancing_page_part.dart @@ -39,27 +39,19 @@ class _BalancingPagePartState extends State { double w = MediaQuery.of(context).size.width / 2; bool isMe = widget.project.currentParticipant == p; items.add( - GestureDetector( - onTap: () async { - widget.project.currentParticipant = p; - widget.project.currentParticipantId = p.localId; - widget.project.conn.save(); - setState(() {}); - }, - child: Padding( - padding: isMe - ? const EdgeInsets.only(top: 5, bottom: 10) - : const EdgeInsets.symmetric(vertical: 5), - child: CustomPaint( - painter: SharePainter( - participant: p, - share: parts[p]!, - isMe: isMe, - maxShare: maxShare, - screenW: w, - ), - child: Container(height: 30), + Padding( + padding: isMe + ? const EdgeInsets.only(top: 5, bottom: 10) + : const EdgeInsets.symmetric(vertical: 5), + child: CustomPaint( + painter: SharePainter( + participant: p, + share: parts[p]!, + isMe: isMe, + maxShare: maxShare, + screenW: w, ), + child: Container(height: 30), ), ), ); diff --git a/lib/components/pages/project/expenses/entry_table.dart b/lib/components/pages/project/expenses/entry_table.dart new file mode 100644 index 0000000..2e3ff00 --- /dev/null +++ b/lib/components/pages/project/expenses/entry_table.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class TableHeaderCell extends StatelessWidget { + const TableHeaderCell({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Text( + text, + textAlign: TextAlign.left, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ); + } +} diff --git a/lib/components/pages/project/expenses/new_entry.dart b/lib/components/pages/project/expenses/new_entry.dart index 228eace..438f722 100644 --- a/lib/components/pages/project/expenses/new_entry.dart +++ b/lib/components/pages/project/expenses/new_entry.dart @@ -11,6 +11,7 @@ import '../../../../model/project.dart'; import '../../../../screens/new_screen.dart'; import '../../../../utils/formatter/decimal.dart'; import '../../../../utils/time.dart'; +import 'entry_table.dart'; class NewEntryPage extends StatelessWidget { const NewEntryPage(this.project, {super.key, this.item}); @@ -118,8 +119,14 @@ class _NewEntrySubPageState extends State { autocorrect: false, validator: (value) { try { - if (double.parse(value!) > 0) return null; - return 'Amount can\'t be null'; + double v = double.parse(value!); + if (v <= 0) { + return 'Amount can\'t be null'; + } + if (v + 0.001 < widget.bill.getTotalFixed()) { + return 'Amount is smaller than fixed values'; + } + return null; } catch (e) { return 'Amount must be a valid value'; } @@ -217,106 +224,39 @@ class _NewEntrySubPageState extends State { 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: 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, - ), - ), - ), - 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, - ), - ), - ), - ], + child: Table(columnWidths: const { + 0: FixedColumnWidth(50), + 1: FlexColumnWidth(5), + 2: FlexColumnWidth(2), + 3: FlexColumnWidth(2), + }, children: [ + TableRow( + children: [ + TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Checkbox( + value: widget.bill.allParticipants(), + tristate: true, + onChanged: (value) { + value ??= false; + setState(() { + widget.bill.shares.updateAll( + (k, v) => BillPart(share: value! ? 1 : null), + ); + }); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3), + ), ), - ] + - getRows(widget.bill, sharesController, fixedsController), - ), + ), + const TableHeaderCell(text: "For whom ?"), + const TableHeaderCell(text: "Rate"), + const TableHeaderCell(text: "Total"), + ], + ), + ...getRows(widget.bill, sharesController, fixedsController), + ]), ), ], ), @@ -330,10 +270,11 @@ class _NewEntrySubPageState extends State { ) { final List rows = []; - double total = widget.bill.totalShares; + double shareSize = widget.bill.getTotalShares(); + double fixedValue = widget.bill.getTotalFixed(); - double fixedBonus = widget.bill.totalFixed / - widget.bill.shares.values.where((e) => e.share != null).length; + // double fixedBonus = widget.bill.getTotalFixed() / + // widget.bill.shares.values.where((e) => e.share != null).length; widget.bill.shares.forEach((participant, amount) { final newShareValue = amount.share == null @@ -348,7 +289,9 @@ class _NewEntrySubPageState extends State { double price = max( amount.fixed ?? - widget.bill.amount * (amount.share ?? 0) / total - fixedBonus, + (widget.bill.amount - fixedValue) * + (amount.share ?? 0) / + shareSize, 0); if (price.isNaN) price = 0; diff --git a/lib/components/pages/project/project_page.dart b/lib/components/pages/project/project_page.dart index f2c84a1..5cdf8e2 100644 --- a/lib/components/pages/project/project_page.dart +++ b/lib/components/pages/project/project_page.dart @@ -20,16 +20,6 @@ class ProjectPage extends StatefulWidget { class _ProjectPageState extends State { int pageIndex = 0; - List pages = []; - - @override - void initState() { - super.initState(); - pages = [ - ItemList(widget.project), - BalancingPagePart(widget.project), - ]; - } @override Widget build(BuildContext context) { @@ -63,7 +53,9 @@ class _ProjectPageState extends State { actionMenu(), ], ), - body: pages[pageIndex], + body: pageIndex == 0 + ? ItemList(widget.project) + : BalancingPagePart(widget.project), floatingActionButton: widget.project.participants.isNotEmpty && pageIndex == 0 ? MainFloatingActionButton( @@ -228,7 +220,10 @@ class _MainFloatingActionButtonState extends State { builder: (context) => NewEntryPage(widget.project), ), ); - if (widget.onDone != null) widget.onDone!(); + if (widget.onDone != null) { + widget.onDone!(); + print('New item!'); + } }, ), ], diff --git a/lib/components/pages/projects_list/projects_list.dart b/lib/components/pages/projects_list/projects_list.dart index 5928016..54fb8e2 100644 --- a/lib/components/pages/projects_list/projects_list.dart +++ b/lib/components/pages/projects_list/projects_list.dart @@ -60,8 +60,15 @@ class _ProjectsListState extends State { onTap: () async { AppData.current = project; await project.conn.loadParticipants(); - await project.conn.loadEntries(); + int err = await project.conn.loadEntries(); if (context.mounted) { + if (err > 0) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$err errors when loading items.'), + ), + ); + } navigatorPush(context, () => ProjectPage(project)); } }, diff --git a/lib/model/app_data.dart b/lib/model/app_data.dart index c996df1..676c2f3 100644 --- a/lib/model/app_data.dart +++ b/lib/model/app_data.dart @@ -49,16 +49,16 @@ class AppData { sharedPreferences = await SharedPreferences.getInstance(); db = await SplitrDatabase.instance.database; + AppData.instances = await Instance.getAllInstances(); + + AppData.projects = await Project.getAllProjects(); + if (!sharedPreferences.containsKey("firstRun")) { - firstRun = true; + firstRun = AppData.projects.isEmpty; } else { firstRun = sharedPreferences.getBool("firstRun")!; } - AppData.instances = await Instance.getAllInstances(); - - AppData.projects = await Project.getAllProjects(); - if (sharedPreferences.containsKey("lastProject")) { try { _current = diff --git a/lib/model/bill_data.dart b/lib/model/bill_data.dart index b223bf1..7d181c8 100644 --- a/lib/model/bill_data.dart +++ b/lib/model/bill_data.dart @@ -40,22 +40,25 @@ amount: $amount shares: ${shares.entries.map((e) => "${e.key.pseudo}:${e.value}").join(",")}"""; } - double get totalShares { - return (shares.values - .where((element) => element.share != null) - .map((e) => e.share) - .toList() + - [0]) - .reduce((a, b) => a! + b!)!; + bool? allParticipants() { + int n = + shares.values.where((e) => e.fixed != null || e.share != null).length; + + if (n == 0) { + return false; + } else if (n == shares.length) { + return true; + } else { + return null; + } + } + + double getTotalShares() { + return shares.values.map((e) => e.share ?? 0).fold(0, (a, b) => a + b); } - double get totalFixed { - return (shares.values - .where((element) => element.fixed != null) - .map((e) => e.fixed) - .toList() + - [0]) - .reduce((a, b) => a! + b!)!; + double getTotalFixed() { + return shares.values.map((e) => e.fixed ?? 0).fold(0, (a, b) => a + b); } Future toItemOf(Project project) async { diff --git a/lib/model/connectors/local/project.dart b/lib/model/connectors/local/project.dart index 5c1f97a..0fdcdb4 100644 --- a/lib/model/connectors/local/project.dart +++ b/lib/model/connectors/local/project.dart @@ -13,7 +13,7 @@ class LocalProject { final Project project; - Future loadEntries() async { + Future loadEntries() async { final rawItems = await AppData.db.query( tableItems, where: '${ItemFields.project} = ?', @@ -22,12 +22,18 @@ class LocalProject { ); project.items.clear(); + int err = 0; for (Map e in rawItems) { - Item item = Item.fromJson(e, project: project); - project.items.add(item); - await item.conn.loadParts(); + try { + Item item = Item.fromJson(e, project: project); + project.items.add(item); + await item.conn.loadParts(); + } on StateError { + err++; + } } + return err; } Future loadParticipants() async { diff --git a/lib/model/item.dart b/lib/model/item.dart index b527a12..9f8e71e 100644 --- a/lib/model/item.dart +++ b/lib/model/item.dart @@ -66,19 +66,20 @@ class Item { double shareOf(Participant participant) { double totalRate = 0; ItemPart? pip; + double fixedTotal = 0; if (itemParts.isNotEmpty) { for (ItemPart ip in itemParts) { if (ip.participant == participant) pip = ip; totalRate += ip.rate ?? 0; + fixedTotal += ip.amount ?? 0; } } if (pip == null || pip.amount == null && pip.rate == null) { return emitter == participant ? amount : 0; } - // return (emitter == participant ? amount : 0) - rate * amount / totalRate; return (emitter == participant ? amount : 0) - - (pip.amount ?? pip.rate! * amount / totalRate); + (pip.amount ?? pip.rate! * (amount - fixedTotal) / totalRate); } String toParticipantsString() { diff --git a/lib/model/project.dart b/lib/model/project.dart index 938d403..5a7c22b 100644 --- a/lib/model/project.dart +++ b/lib/model/project.dart @@ -160,6 +160,14 @@ class Project { } } + Participant? participantByPseudo(String pseudo) { + try { + return participants.firstWhere((element) => element.pseudo == pseudo); + } catch (e) { + return null; + } + } + Item? itemByRemoteId(String id) { try { return items.firstWhere((element) => element.remoteId == id); diff --git a/lib/utils/formatter/decimal.dart b/lib/utils/formatter/decimal.dart index e63ce52..1669f6b 100644 --- a/lib/utils/formatter/decimal.dart +++ b/lib/utils/formatter/decimal.dart @@ -38,7 +38,7 @@ class DecimalTextInputFormatter extends TextInputFormatter { } if (truncated.isEmpty || - RegExp(r'^[0-9]+(.[0-9]{0,2})?$').hasMatch(truncated)) { + RegExp(r'^[0-9]*(\.[0-9]{0,2})?$').hasMatch(truncated)) { return TextEditingValue( text: truncated, selection: newSelection,