Skip to content

Commit

Permalink
Merge pull request #2 from janczizikow/feat/edit-expenses
Browse files Browse the repository at this point in the history
feature: edit expenses
  • Loading branch information
janczizikow authored May 6, 2020
2 parents 1d23a05 + 43c6a2f commit 73ea31f
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 52 deletions.
8 changes: 7 additions & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ PODS:
- GoogleUtilities/UserDefaults (~> 6.5)
- Protobuf (>= 3.9.2, ~> 3.9)
- Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_plugin_android_lifecycle (0.0.1):
- Flutter
- flutter_secure_storage (3.3.1):
Expand Down Expand Up @@ -130,6 +132,7 @@ DEPENDENCIES:
- app_review (from `.symlinks/plugins/app_review/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
Expand Down Expand Up @@ -173,6 +176,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_plugin_android_lifecycle:
:path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios"
flutter_secure_storage:
Expand Down Expand Up @@ -215,6 +220,7 @@ SPEC CHECKSUMS:
FirebaseInstanceID: cef67c4967c7cecb56ea65d8acbb4834825c587b
FirebaseMessaging: 828e66eb357a893e3cebd9ee0f6bc1941447cc94
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
flutter_local_notifications: 9e4738ce2471c5af910d961a6b7eadcf57c50186
flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
Expand All @@ -241,4 +247,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83

COCOAPODS: 1.8.4
COCOAPODS: 1.9.1
8 changes: 4 additions & 4 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,12 @@ class _MyAppState extends State<MyApp> {
case Root.routeName:
return Theme.of(context).platform == TargetPlatform.iOS
? CupertinoPageRoute(
builder: (context) => new Root(),
settings: settings.copyWith(isInitialRoute: true),
builder: (context) => Root(),
settings: settings,
)
: NoAnimationMaterialPageRoute(
builder: (context) => new Root(),
settings: settings.copyWith(isInitialRoute: true),
builder: (context) => Root(),
settings: settings,
);
case RegisterScreen.routeName:
return platformPageRoute(
Expand Down
19 changes: 19 additions & 0 deletions lib/models/member.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

import 'package:flutter/foundation.dart';

class Member {
Expand Down Expand Up @@ -35,4 +37,21 @@ class Member {

get initials => RegExp(r'\S+').allMatches(fullName).fold('',
(acc, match) => acc + fullName.substring(match.start, match.start + 1));

Map<String, dynamic> toMap() {
return {
'id': id,
'userId': userId,
'groupId': groupId,
'firstName': firstName,
'lastName': lastName,
'avatar': avatar,
'balance': balance,
};
}

@override
String toString() {
return jsonEncode(toMap());
}
}
110 changes: 110 additions & 0 deletions lib/providers/expenses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,61 @@ class ExpensesProvider extends BaseProvider {
}
}

Future<void> updateExpense({
@required String groupId,
@required String expenseId,
@required String name,
@required int amount,
@required String payerId,
@required List<Share> shares,
@required String currency,
@required String date,
}) async {
status = Status.PENDING;
try {
final Expense expense = await _api.updateExpense(
groupId: groupId,
expenseId: expenseId,
name: name,
amount: amount,
shares: shares,
payerId: payerId,
currency: currency,
date: date,
);
if (_expensesByGroupId.containsKey(groupId)) {
final groupExpenses = _expensesByGroupId[groupId];
int expenseIndex =
groupExpenses.indexWhere((expense) => expense.id == expenseId);
if (expenseIndex != -1) {
final previousExpense = groupExpenses[expenseIndex];
groupExpenses[expenseIndex] = expense;
_groupsProvider.selectedGroup.optimisticBalanceUpdate(
previousExpense.amount,
previousExpense.shares,
previousExpense.payerId,
undo: true,
);
} else {
groupExpenses.insert(0, expense);
}
} else {
_expensesByGroupId[groupId] = [expense];
}

_groupsProvider.selectedGroup.optimisticBalanceUpdate(
expense.amount,
expense.shares,
expense.payerId,
);

status = Status.RESOLVED;
} catch (e) {
status = Status.REJECTED;
rethrow;
}
}

Future<void> createPayment({
@required String groupId,
@required int amount,
Expand Down Expand Up @@ -149,6 +204,61 @@ class ExpensesProvider extends BaseProvider {
}
}

Future<void> updatePayment({
@required String groupId,
@required String expenseId,
@required int amount,
@required String from,
@required String to,
@required String currency,
@required String date,
}) async {
assert(_groupsProvider != null);
status = Status.PENDING;
try {
final Expense payment = await _api.updatePayment(
groupId: groupId,
expenseId: expenseId,
amount: amount,
from: from,
to: to,
currency: currency,
date: date,
);

if (_expensesByGroupId.containsKey(groupId)) {
final groupExpenses = _expensesByGroupId[groupId];
int expenseIndex =
groupExpenses.indexWhere((expense) => expense.id == expenseId);
if (expenseIndex != -1) {
final previousExpense = groupExpenses[expenseIndex];
groupExpenses[expenseIndex] = payment;
_groupsProvider.selectedGroup.optimisticBalanceUpdate(
previousExpense.amount,
previousExpense.shares,
previousExpense.payerId,
undo: true,
);
} else {
groupExpenses.insert(0, payment);
}
} else {
_expensesByGroupId[groupId] = [payment];
}

_groupsProvider.selectedGroup.optimisticBalanceUpdate(
payment.amount,
payment.shares,
payment.payerId,
);

status = Status.RESOLVED;
} catch (e) {
status = Status.REJECTED;
rethrow;
}
}

Future<void> deleteExpense(String expenseId) async {
status = Status.PENDING;

Expand Down
105 changes: 86 additions & 19 deletions lib/screens/expense.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,13 @@ class _ExpenseScreenState extends State<ExpenseScreen> {
super.initState();
_nameController = new TextEditingController();
_amountController = new TextEditingController();
_amountController.addListener(_handleAmountChanged);
_amountFocusNode = new FocusNode();
final GroupsProvider groupsProvider =
Provider.of<GroupsProvider>(context, listen: false);
final List<Member> groupMembers = groupsProvider.selectedGroupMembers;
if (widget.expenseId == null) {
final String userId =
Provider.of<AccountProvider>(context, listen: false).account?.id;
final List<Member> groupMembers = groupsProvider.selectedGroupMembers;
_payer = groupMembers.firstWhere((member) => member.userId == userId);
_participants = groupMembers.map((member) {
return Tuple3(member, 0, true);
Expand Down Expand Up @@ -91,14 +90,17 @@ class _ExpenseScreenState extends State<ExpenseScreen> {
_payer = groupsProvider.selectedGroupMembers
.firstWhere((member) => member.userId == expense.payerId);
// TODO: Refactor nested loop
_participants = expense.shares.map((share) {
Member member = groupsProvider.selectedGroupMembers
.firstWhere((member) => member.userId == share.userId);
return Tuple3(member, share.amount, true);
_participants = groupMembers.map((member) {
Share share = expense.shares.firstWhere(
(share) => share.userId == member.userId,
orElse: () => null);
return Tuple3(member, share?.amount ?? 0, share != null);
}).toList();
_date = expense.date;
_isInEditingMode = false;
}
// needs to be added after setting participants
_amountController.addListener(_handleAmountChanged);
}

@override
Expand Down Expand Up @@ -151,6 +153,12 @@ class _ExpenseScreenState extends State<ExpenseScreen> {
}
}

void _toggleEditingMode() {
setState(() {
_isInEditingMode = !_isInEditingMode;
});
}

void _toggleEqualSplit(bool isEqualSplit) {
if (isEqualSplit) {
final List<Tuple3<Member, int, bool>> activeParticipants =
Expand Down Expand Up @@ -365,6 +373,55 @@ class _ExpenseScreenState extends State<ExpenseScreen> {
}
}

Future<void> _handleUpdateExpense() async {
Group group =
Provider.of<GroupsProvider>(context, listen: false).selectedGroup;
assert(group != null);

final bool isValid = _validate();

if (!isValid) {
showErrorDialog(context, _errorMessage);
} else {
try {
List<String> participantsIds = _participants
.where((participation) => participation.item3)
.map((p) => p.item1.userId)
.toList();
await Provider.of<ExpensesProvider>(context, listen: false)
.updateExpense(
groupId: group.id,
expenseId: widget.expenseId,
currency: group.currency,
payerId: _payer.userId,
name: _nameController.text,
shares: _equalSplit
? _participants
.where((participation) => participation.item3)
.map((p) {
return Share(
userId: p.item1.userId,
amount: _payer.userId == p.item1.userId
? (_total / participantsIds.length).ceil()
: (_total / participantsIds.length).floor());
}).toList()
: _participants
.where((participation) => participation.item2 > 0)
.map((p) {
return Share(userId: p.item1.userId, amount: p.item2);
}).toList(),
amount: _total,
date: _date.toIso8601String(),
);
Navigator.of(context).pop();
} on ApiError catch (err) {
showErrorDialog(context, err.message);
} catch (e) {
showErrorDialog(context, 'Failed to add expense!');
}
}
}

Future<void> _handleDeleteExpense() async {
bool result = await showPlatformDialog(
context: context,
Expand Down Expand Up @@ -415,20 +472,22 @@ class _ExpenseScreenState extends State<ExpenseScreen> {
final participantsTotal = _participants.fold(0, (acc, current) {
return acc + (current.item2);
});
final bool isNewExpense = widget.expenseId == null;

return PlatformScaffold(
appBar: PlatformAppBar(
title: Text(widget.expenseId == null ? 'New Expense' : 'Expense'),
trailingActions: widget.expenseId == null
? <Widget>[
PlatformButton(
androidFlat: (_) => MaterialFlatButtonData(
textColor: Colors.white,
),
child: PlatformText('Save'),
onPressed: _handleAddExpense,
),
]
: [],
title: Text(isNewExpense ? 'New Expense' : 'Expense'),
trailingActions: <Widget>[
PlatformButton(
androidFlat: (_) => MaterialFlatButtonData(
textColor: Colors.white,
),
child: PlatformText(_isInEditingMode ? 'Save' : 'Edit'),
onPressed: _isInEditingMode
? isNewExpense ? _handleAddExpense : _handleUpdateExpense
: _toggleEditingMode,
),
],
),
body: SafeArea(
child: Column(
Expand Down Expand Up @@ -486,6 +545,14 @@ class _ExpenseScreenState extends State<ExpenseScreen> {
..._participants
.asMap()
.map((int i, participant) {
// Don't show in active participants in Expense detail
// if not in editing mode
if (!_isInEditingMode &&
!isNewExpense &&
!participant.item3) {
return MapEntry(i, Container());
}

return MapEntry(
i,
ListTile(
Expand Down Expand Up @@ -572,7 +639,7 @@ class _ExpenseScreenState extends State<ExpenseScreen> {
title: Text(
'You must select at least one person to add expense',
textAlign: TextAlign.center,
style: theme.textTheme.subtitle
style: theme.textTheme.subtitle2
.copyWith(color: theme.errorColor),
),
)
Expand Down
Loading

0 comments on commit 73ea31f

Please sign in to comment.