Function to reorder Invoice Items and Quote Items #208

This commit is contained in:
Hillel Coren 2021-04-12 17:51:40 +03:00
parent e2e75a5fba
commit 9c13075f82
15 changed files with 238 additions and 79 deletions

View File

@ -217,6 +217,22 @@ abstract class InvoiceEntity extends Object
@memoized
int get hashCode;
InvoiceEntity moveLineItem(int oldIndex, int newIndex) {
final lineItem = lineItems[oldIndex];
InvoiceEntity invoice = rebuild((b) => b..lineItems.removeAt(oldIndex));
invoice = invoice.rebuild((b) => b
..lineItems.replace(<InvoiceItemEntity>[
...invoice.lineItems.sublist(0, newIndex),
lineItem,
...invoice.lineItems.sublist(
newIndex,
invoice.lineItems.length,
)
])
..isChanged = true);
return invoice;
}
InvoiceEntity get clone => rebuild((b) => b
..id = BaseEntity.nextId
..isChanged = false

View File

@ -165,6 +165,16 @@ class AddCreditItem implements PersistUI {
final InvoiceItemEntity creditItem;
}
class MoveCreditItem implements PersistUI {
MoveCreditItem({
this.oldIndex,
this.newIndex,
});
final int oldIndex;
final int newIndex;
}
class AddCreditItems implements PersistUI {
AddCreditItems(this.creditItems);

View File

@ -79,6 +79,9 @@ final editingReducer = combineReducers<InvoiceEntity>([
TypedReducer<InvoiceEntity, AddCreditItem>((invoice, action) {
return invoice.rebuild((b) => b..isChanged = true);
}),
TypedReducer<InvoiceEntity, MoveCreditItem>((invoice, action) {
return invoice.moveLineItem(action.oldIndex, action.newIndex);
}),
TypedReducer<InvoiceEntity, DeleteCreditItem>((invoice, action) {
return invoice.rebuild((b) => b..isChanged = true);
}),

View File

@ -162,6 +162,16 @@ class AddInvoiceItem implements PersistUI {
final InvoiceItemEntity invoiceItem;
}
class MoveInvoiceItem implements PersistUI {
MoveInvoiceItem({
this.oldIndex,
this.newIndex,
});
final int oldIndex;
final int newIndex;
}
class AddInvoiceItems implements PersistUI {
AddInvoiceItems(this.lineItems);

View File

@ -82,6 +82,9 @@ final editingReducer = combineReducers<InvoiceEntity>([
TypedReducer<InvoiceEntity, AddInvoiceItem>((invoice, action) {
return invoice.rebuild((b) => b..isChanged = true);
}),
TypedReducer<InvoiceEntity, MoveInvoiceItem>((invoice, action) {
return invoice.moveLineItem(action.oldIndex, action.newIndex);
}),
TypedReducer<InvoiceEntity, DeleteInvoiceItem>((invoice, action) {
return invoice.rebuild((b) => b..isChanged = true);
}),

View File

@ -165,6 +165,16 @@ class AddQuoteItem implements PersistUI {
final InvoiceItemEntity quoteItem;
}
class MoveQuoteItem implements PersistUI {
MoveQuoteItem({
this.oldIndex,
this.newIndex,
});
final int oldIndex;
final int newIndex;
}
class AddQuoteItems implements PersistUI {
AddQuoteItems(this.quoteItems);

View File

@ -79,6 +79,9 @@ final editingReducer = combineReducers<InvoiceEntity>([
TypedReducer<InvoiceEntity, AddQuoteItem>((invoice, action) {
return invoice.rebuild((b) => b..isChanged = true);
}),
TypedReducer<InvoiceEntity, MoveQuoteItem>((invoice, action) {
return invoice.moveLineItem(action.oldIndex, action.newIndex);
}),
TypedReducer<InvoiceEntity, DeleteQuoteItem>((invoice, action) {
return invoice.rebuild((b) => b..isChanged = true);
}),

View File

@ -194,6 +194,16 @@ class AddRecurringInvoiceItem implements PersistUI {
final InvoiceItemEntity invoiceItem;
}
class MoveRecurringInvoiceItem implements PersistUI {
MoveRecurringInvoiceItem({
this.oldIndex,
this.newIndex,
});
final int oldIndex;
final int newIndex;
}
class AddRecurringInvoiceItems implements PersistUI {
AddRecurringInvoiceItems(this.items);

View File

@ -90,6 +90,9 @@ final editingReducer = combineReducers<InvoiceEntity>([
(recurringInvoice, action) {
return recurringInvoice.rebuild((b) => b..isChanged = true);
}),
TypedReducer<InvoiceEntity, MoveRecurringInvoiceItem>((invoice, action) {
return invoice.moveLineItem(action.oldIndex, action.newIndex);
}),
TypedReducer<InvoiceEntity, DeleteRecurringInvoiceItem>(
(recurringInvoice, action) {
return recurringInvoice.rebuild((b) => b..isChanged = true);

View File

@ -55,6 +55,7 @@ class CreditEditItemsVM extends EntityEditItemsVM {
Function(int) onRemoveInvoiceItemPressed,
Function onDoneInvoiceItemPressed,
Function(InvoiceItemEntity, int) onChangedInvoiceItem,
Function(int, int) onMovedInvoiceItem,
}) : super(
state: state,
company: company,
@ -65,29 +66,34 @@ class CreditEditItemsVM extends EntityEditItemsVM {
onRemoveInvoiceItemPressed: onRemoveInvoiceItemPressed,
clearSelectedInvoiceItem: onDoneInvoiceItemPressed,
onChangedInvoiceItem: onChangedInvoiceItem,
onMovedInvoiceItem: onMovedInvoiceItem,
);
factory CreditEditItemsVM.fromStore(Store<AppState> store, bool isTasks) {
return CreditEditItemsVM(
state: store.state,
company: store.state.company,
invoice: store.state.creditUIState.editing,
invoiceItemIndex: store.state.creditUIState.editingItemIndex,
onRemoveInvoiceItemPressed: (index) =>
store.dispatch(DeleteCreditItem(index)),
onDoneInvoiceItemPressed: () => store.dispatch(EditCreditItem()),
onChangedInvoiceItem: (creditItem, index) {
final credit = store.state.creditUIState.editing;
if (index == credit.lineItems.length) {
store.dispatch(AddCreditItem(
creditItem: creditItem.rebuild((b) => b
..typeId = isTasks
? InvoiceItemEntity.TYPE_TASK
: InvoiceItemEntity.TYPE_STANDARD)));
} else {
store.dispatch(
UpdateCreditItem(creditItem: creditItem, index: index));
}
});
state: store.state,
company: store.state.company,
invoice: store.state.creditUIState.editing,
invoiceItemIndex: store.state.creditUIState.editingItemIndex,
onRemoveInvoiceItemPressed: (index) =>
store.dispatch(DeleteCreditItem(index)),
onDoneInvoiceItemPressed: () => store.dispatch(EditCreditItem()),
onChangedInvoiceItem: (creditItem, index) {
final credit = store.state.creditUIState.editing;
if (index == credit.lineItems.length) {
store.dispatch(AddCreditItem(
creditItem: creditItem.rebuild((b) => b
..typeId = isTasks
? InvoiceItemEntity.TYPE_TASK
: InvoiceItemEntity.TYPE_STANDARD)));
} else {
store
.dispatch(UpdateCreditItem(creditItem: creditItem, index: index));
}
},
onMovedInvoiceItem: (oldIndex, newIndex) => store.dispatch(
MoveCreditItem(oldIndex: oldIndex, newIndex: newIndex),
),
);
}
}

View File

@ -9,11 +9,13 @@ import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart';
import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart';
import 'package:invoiceninja_flutter/ui/app/forms/growable_form_field.dart';
import 'package:invoiceninja_flutter/ui/app/icon_text.dart';
import 'package:invoiceninja_flutter/ui/app/invoice/tax_rate_dropdown.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_items_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
class InvoiceEditItemsDesktop extends StatefulWidget {
const InvoiceEditItemsDesktop({
@ -542,6 +544,7 @@ class _InvoiceEditItemsDesktopState extends State<InvoiceEditItemsDesktop> {
textAlign: TextAlign.right,
),
),
/*
IconButton(
icon: Icon(Icons.clear),
tooltip: localization.remove,
@ -552,6 +555,49 @@ class _InvoiceEditItemsDesktopState extends State<InvoiceEditItemsDesktop> {
_updateTable();
},
),
*/
PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
enabled: !lineItems[index].isEmpty,
itemBuilder: (BuildContext context) {
final options = {
if (index > 0)
localization.moveTop: MdiIcons.chevronDoubleUp,
if (index > 1)
localization.moveUp: MdiIcons.chevronUp,
if (index < lineItems.length - 3)
localization.moveDown: MdiIcons.chevronDown,
if (index < lineItems.length - 2)
localization.moveBottom: MdiIcons.chevronDoubleDown,
localization.remove: Icons.clear,
};
return options.keys
.map((option) => PopupMenuItem<String>(
child: IconText(
icon: options[option],
text: option,
),
value: option,
))
.toList();
},
onSelected: (String action) {
if (action == localization.moveTop) {
viewModel.onMovedInvoiceItem(index, 0);
} else if (action == localization.moveUp) {
viewModel.onMovedInvoiceItem(index, index - 1);
} else if (action == localization.moveDown) {
viewModel.onMovedInvoiceItem(index, index + 1);
} else if (action == localization.moveBottom) {
viewModel.onMovedInvoiceItem(
index, lineItems.length - 2);
} else if (action == localization.remove) {
viewModel.onRemoveInvoiceItemPressed(index);
}
_updateTable();
},
)
])
],
),

View File

@ -54,6 +54,7 @@ class EntityEditItemsVM {
@required this.onRemoveInvoiceItemPressed,
@required this.clearSelectedInvoiceItem,
@required this.onChangedInvoiceItem,
@required this.onMovedInvoiceItem,
});
final AppState state;
@ -65,6 +66,7 @@ class EntityEditItemsVM {
final Function(int) onRemoveInvoiceItemPressed;
final Function clearSelectedInvoiceItem;
final Function(InvoiceItemEntity, int) onChangedInvoiceItem;
final Function(int, int) onMovedInvoiceItem;
}
class InvoiceEditItemsVM extends EntityEditItemsVM {
@ -78,6 +80,7 @@ class InvoiceEditItemsVM extends EntityEditItemsVM {
Function(int) onRemoveInvoiceItemPressed,
Function clearSelectedInvoiceItem,
Function(InvoiceItemEntity, int) onChangedInvoiceItem,
Function(int, int) onMovedInvoiceItem,
}) : super(
state: state,
company: company,
@ -88,32 +91,37 @@ class InvoiceEditItemsVM extends EntityEditItemsVM {
onRemoveInvoiceItemPressed: onRemoveInvoiceItemPressed,
clearSelectedInvoiceItem: clearSelectedInvoiceItem,
onChangedInvoiceItem: onChangedInvoiceItem,
onMovedInvoiceItem: onMovedInvoiceItem,
);
factory InvoiceEditItemsVM.fromStore(Store<AppState> store, bool isTasks) {
return InvoiceEditItemsVM(
state: store.state,
company: store.state.company,
invoice: store.state.invoiceUIState.editing,
invoiceItemIndex: store.state.invoiceUIState.editingItemIndex,
addLineItem: () =>
store.dispatch(AddInvoiceItem(invoiceItem: InvoiceItemEntity())),
deleteLineItem: null,
onRemoveInvoiceItemPressed: (index) =>
store.dispatch(DeleteInvoiceItem(index)),
clearSelectedInvoiceItem: () => store.dispatch(EditInvoiceItem()),
onChangedInvoiceItem: (invoiceItem, index) {
final invoice = store.state.invoiceUIState.editing;
if (index == invoice.lineItems.length) {
store.dispatch(AddInvoiceItem(
invoiceItem: invoiceItem.rebuild((b) => b
..typeId = isTasks
? InvoiceItemEntity.TYPE_TASK
: InvoiceItemEntity.TYPE_STANDARD)));
} else {
store.dispatch(
UpdateInvoiceItem(invoiceItem: invoiceItem, index: index));
}
});
state: store.state,
company: store.state.company,
invoice: store.state.invoiceUIState.editing,
invoiceItemIndex: store.state.invoiceUIState.editingItemIndex,
addLineItem: () =>
store.dispatch(AddInvoiceItem(invoiceItem: InvoiceItemEntity())),
deleteLineItem: null,
onRemoveInvoiceItemPressed: (index) =>
store.dispatch(DeleteInvoiceItem(index)),
clearSelectedInvoiceItem: () => store.dispatch(EditInvoiceItem()),
onChangedInvoiceItem: (invoiceItem, index) {
final invoice = store.state.invoiceUIState.editing;
if (index == invoice.lineItems.length) {
store.dispatch(AddInvoiceItem(
invoiceItem: invoiceItem.rebuild((b) => b
..typeId = isTasks
? InvoiceItemEntity.TYPE_TASK
: InvoiceItemEntity.TYPE_STANDARD)));
} else {
store.dispatch(
UpdateInvoiceItem(invoiceItem: invoiceItem, index: index));
}
},
onMovedInvoiceItem: (oldIndex, newIndex) => store.dispatch(
MoveInvoiceItem(oldIndex: oldIndex, newIndex: newIndex),
),
);
}
}

View File

@ -53,6 +53,7 @@ class QuoteEditItemsVM extends EntityEditItemsVM {
Function(int) onRemoveInvoiceItemPressed,
Function onDoneInvoiceItemPressed,
Function(InvoiceItemEntity, int) onChangedInvoiceItem,
Function(int, int) onMovedInvoiceItem,
}) : super(
state: state,
company: company,
@ -63,24 +64,29 @@ class QuoteEditItemsVM extends EntityEditItemsVM {
onRemoveInvoiceItemPressed: onRemoveInvoiceItemPressed,
clearSelectedInvoiceItem: onDoneInvoiceItemPressed,
onChangedInvoiceItem: onChangedInvoiceItem,
onMovedInvoiceItem: onMovedInvoiceItem,
);
factory QuoteEditItemsVM.fromStore(Store<AppState> store) {
return QuoteEditItemsVM(
state: store.state,
company: store.state.company,
invoice: store.state.quoteUIState.editing,
invoiceItemIndex: store.state.quoteUIState.editingItemIndex,
onRemoveInvoiceItemPressed: (index) =>
store.dispatch(DeleteQuoteItem(index)),
onDoneInvoiceItemPressed: () => store.dispatch(EditQuoteItem()),
onChangedInvoiceItem: (quoteItem, index) {
final quote = store.state.quoteUIState.editing;
if (index == quote.lineItems.length) {
store.dispatch(AddQuoteItem(quoteItem: quoteItem));
} else {
store.dispatch(UpdateQuoteItem(quoteItem: quoteItem, index: index));
}
});
state: store.state,
company: store.state.company,
invoice: store.state.quoteUIState.editing,
invoiceItemIndex: store.state.quoteUIState.editingItemIndex,
onRemoveInvoiceItemPressed: (index) =>
store.dispatch(DeleteQuoteItem(index)),
onDoneInvoiceItemPressed: () => store.dispatch(EditQuoteItem()),
onChangedInvoiceItem: (quoteItem, index) {
final quote = store.state.quoteUIState.editing;
if (index == quote.lineItems.length) {
store.dispatch(AddQuoteItem(quoteItem: quoteItem));
} else {
store.dispatch(UpdateQuoteItem(quoteItem: quoteItem, index: index));
}
},
onMovedInvoiceItem: (oldIndex, newIndex) => store.dispatch(
MoveQuoteItem(oldIndex: oldIndex, newIndex: newIndex),
),
);
}
}

View File

@ -55,6 +55,7 @@ class RecurringInvoiceEditItemsVM extends EntityEditItemsVM {
Function(int) onRemoveInvoiceItemPressed,
Function onDoneInvoiceItemPressed,
Function(InvoiceItemEntity, int) onChangedInvoiceItem,
Function(int, int) onMovedInvoiceItem,
}) : super(
state: state,
company: company,
@ -65,31 +66,35 @@ class RecurringInvoiceEditItemsVM extends EntityEditItemsVM {
onRemoveInvoiceItemPressed: onRemoveInvoiceItemPressed,
clearSelectedInvoiceItem: onDoneInvoiceItemPressed,
onChangedInvoiceItem: onChangedInvoiceItem,
onMovedInvoiceItem: onMovedInvoiceItem,
);
factory RecurringInvoiceEditItemsVM.fromStore(
Store<AppState> store, bool isTasks) {
return RecurringInvoiceEditItemsVM(
state: store.state,
company: store.state.company,
invoice: store.state.recurringInvoiceUIState.editing,
invoiceItemIndex: store.state.quoteUIState.editingItemIndex,
onRemoveInvoiceItemPressed: (index) =>
store.dispatch(DeleteRecurringInvoiceItem(index)),
onDoneInvoiceItemPressed: () =>
store.dispatch(EditRecurringInvoiceItem()),
onChangedInvoiceItem: (item, index) {
final invoice = store.state.recurringInvoiceUIState.editing;
if (index == invoice.lineItems.length) {
store.dispatch(AddRecurringInvoiceItem(
invoiceItem: item.rebuild((b) => b
..typeId = isTasks
? InvoiceItemEntity.TYPE_TASK
: InvoiceItemEntity.TYPE_STANDARD)));
} else {
store
.dispatch(UpdateRecurringInvoiceItem(item: item, index: index));
}
});
state: store.state,
company: store.state.company,
invoice: store.state.recurringInvoiceUIState.editing,
invoiceItemIndex: store.state.quoteUIState.editingItemIndex,
onRemoveInvoiceItemPressed: (index) =>
store.dispatch(DeleteRecurringInvoiceItem(index)),
onDoneInvoiceItemPressed: () =>
store.dispatch(EditRecurringInvoiceItem()),
onChangedInvoiceItem: (item, index) {
final invoice = store.state.recurringInvoiceUIState.editing;
if (index == invoice.lineItems.length) {
store.dispatch(AddRecurringInvoiceItem(
invoiceItem: item.rebuild((b) => b
..typeId = isTasks
? InvoiceItemEntity.TYPE_TASK
: InvoiceItemEntity.TYPE_STANDARD)));
} else {
store.dispatch(UpdateRecurringInvoiceItem(item: item, index: index));
}
},
onMovedInvoiceItem: (oldIndex, newIndex) => store.dispatch(
MoveRecurringInvoiceItem(oldIndex: oldIndex, newIndex: newIndex),
),
);
}
}

View File

@ -15,6 +15,10 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map<String, Map<String, String>> _localizedValues = {
'en': {
// STARTER: lang key - do not remove comment
'move_top': 'Move Top',
'move_up': 'Move Up',
'move_down': 'Move Down',
'move_bottom': 'Move Bottom',
'subdomain_help': 'Lowercase letters, numbers and hyphens are supported',
'body_variable_missing':
'Error: the custom email must include a \$body variable',
@ -56409,6 +56413,22 @@ mixin LocalizationsProvider on LocaleCodeAware {
_localizedValues[localeCode]['subdomain_help'] ??
_localizedValues['en']['subdomain_help'];
String get moveTop =>
_localizedValues[localeCode]['move_top'] ??
_localizedValues['en']['move_top'];
String get moveUp =>
_localizedValues[localeCode]['move_up'] ??
_localizedValues['en']['move_up'];
String get moveDown =>
_localizedValues[localeCode]['move_down'] ??
_localizedValues['en']['move_down'];
String get moveBottom =>
_localizedValues[localeCode]['move_bottom'] ??
_localizedValues['en']['move_bottom'];
String lookup(String key) {
final lookupKey = toSnakeCase(key);