This commit is contained in:
Hillel Coren 2018-08-26 14:32:57 -07:00
parent 150f1f68b0
commit 52bb232b85
10 changed files with 207 additions and 732 deletions

View File

@ -40,6 +40,20 @@ class QuoteFields {
static const String quoteDate = 'quoteDate'; static const String quoteDate = 'quoteDate';
static const String validUntil = 'validUntil'; static const String validUntil = 'validUntil';
static const String quoteStatusId = 'quoteStatusId'; static const String quoteStatusId = 'quoteStatusId';
static String convertField(String field) {
if (field == InvoiceFields.invoiceStatusId) {
return QuoteFields.quoteStatusId;
} else if (field == InvoiceFields.invoiceNumber) {
return QuoteFields.quoteNumber;
} else if (field == InvoiceFields.invoiceDate) {
return QuoteFields.quoteDate;
} else if (field == InvoiceFields.dueDate) {
return QuoteFields.validUntil;
} else {
return field;
}
}
} }
class InvoiceFields { class InvoiceFields {

View File

@ -9,7 +9,7 @@ import 'package:invoiceninja_flutter/ui/invoice/invoice_list_vm.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
class InvoiceList extends StatelessWidget { class InvoiceList extends StatelessWidget {
final InvoiceListVM viewModel; final EntityListVM viewModel;
const InvoiceList({ const InvoiceList({
Key key, Key key,

View File

@ -24,7 +24,6 @@ class InvoiceListBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StoreConnector<AppState, InvoiceListVM>( return StoreConnector<AppState, InvoiceListVM>(
//rebuildOnChange: true,
converter: InvoiceListVM.fromStore, converter: InvoiceListVM.fromStore,
builder: (context, vm) { builder: (context, vm) {
return InvoiceList( return InvoiceList(
@ -35,7 +34,7 @@ class InvoiceListBuilder extends StatelessWidget {
} }
} }
class InvoiceListVM { class EntityListVM {
final UserEntity user; final UserEntity user;
final ListUIState listState; final ListUIState listState;
final List<int> invoiceList; final List<int> invoiceList;
@ -51,7 +50,7 @@ class InvoiceListVM {
final Function(BuildContext) onViewClientFilterPressed; final Function(BuildContext) onViewClientFilterPressed;
final Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction; final Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction;
InvoiceListVM({ EntityListVM({
@required this.user, @required this.user,
@required this.listState, @required this.listState,
@required this.invoiceList, @required this.invoiceList,
@ -67,6 +66,40 @@ class InvoiceListVM {
@required this.onViewClientFilterPressed, @required this.onViewClientFilterPressed,
@required this.onEntityAction, @required this.onEntityAction,
}); });
}
class InvoiceListVM extends EntityListVM {
InvoiceListVM({
UserEntity user,
ListUIState listState,
List<int> invoiceList,
BuiltMap<int, InvoiceEntity> invoiceMap,
BuiltMap<int, ClientEntity> clientMap,
String filter,
bool isLoading,
bool isLoaded,
Function(BuildContext, InvoiceEntity) onInvoiceTap,
Function(BuildContext, InvoiceEntity, DismissDirection) onDismissed,
Function(BuildContext) onRefreshed,
Function onClearClientFilterPressed,
Function(BuildContext) onViewClientFilterPressed,
Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction,
}) : super(
user: user,
listState: listState,
invoiceList: invoiceList,
invoiceMap: invoiceMap,
clientMap: clientMap,
filter: filter,
isLoading: isLoading,
isLoaded: isLoaded,
onInvoiceTap: onInvoiceTap,
onDismissed: onDismissed,
onRefreshed: onRefreshed,
onClearClientFilterPressed: onClearClientFilterPressed,
onViewClientFilterPressed: onViewClientFilterPressed,
onEntityAction: onEntityAction,
);
static InvoiceListVM fromStore(Store<AppState> store) { static InvoiceListVM fromStore(Store<AppState> store) {
Future<Null> _handleRefresh(BuildContext context) { Future<Null> _handleRefresh(BuildContext context) {
@ -113,14 +146,12 @@ class InvoiceListVM {
break; break;
case EntityAction.markSent: case EntityAction.markSent:
store.dispatch(MarkSentInvoiceRequest( store.dispatch(MarkSentInvoiceRequest(
popCompleter( popCompleter(context, localization.markedInvoiceAsSent),
context, localization.markedInvoiceAsSent),
invoice.id)); invoice.id));
break; break;
case EntityAction.email: case EntityAction.email:
store.dispatch(ShowEmailInvoice( store.dispatch(ShowEmailInvoice(
completer: popCompleter( completer: popCompleter(context, localization.emailedInvoice),
context, localization.emailedInvoice),
invoice: invoice, invoice: invoice,
context: context)); context: context));
break; break;
@ -131,20 +162,17 @@ class InvoiceListVM {
break; break;
case EntityAction.restore: case EntityAction.restore:
store.dispatch(RestoreInvoiceRequest( store.dispatch(RestoreInvoiceRequest(
popCompleter( popCompleter(context, localization.restoredInvoice),
context, localization.restoredInvoice),
invoice.id)); invoice.id));
break; break;
case EntityAction.archive: case EntityAction.archive:
store.dispatch(ArchiveInvoiceRequest( store.dispatch(ArchiveInvoiceRequest(
popCompleter( popCompleter(context, localization.archivedInvoice),
context, localization.archivedInvoice),
invoice.id)); invoice.id));
break; break;
case EntityAction.delete: case EntityAction.delete:
store.dispatch(DeleteInvoiceRequest( store.dispatch(DeleteInvoiceRequest(
popCompleter( popCompleter(context, localization.deletedInvoice),
context, localization.deletedInvoice),
invoice.id)); invoice.id));
break; break;
} }
@ -155,25 +183,21 @@ class InvoiceListVM {
if (direction == DismissDirection.endToStart) { if (direction == DismissDirection.endToStart) {
if (invoice.isDeleted || invoice.isArchived) { if (invoice.isDeleted || invoice.isArchived) {
store.dispatch(RestoreInvoiceRequest( store.dispatch(RestoreInvoiceRequest(
snackBarCompleter( snackBarCompleter(context, localization.restoredInvoice),
context, localization.restoredInvoice),
invoice.id)); invoice.id));
} else { } else {
store.dispatch(ArchiveInvoiceRequest( store.dispatch(ArchiveInvoiceRequest(
snackBarCompleter( snackBarCompleter(context, localization.archivedInvoice),
context, localization.archivedInvoice),
invoice.id)); invoice.id));
} }
} else if (direction == DismissDirection.startToEnd) { } else if (direction == DismissDirection.startToEnd) {
if (invoice.isDeleted) { if (invoice.isDeleted) {
store.dispatch(RestoreInvoiceRequest( store.dispatch(RestoreInvoiceRequest(
snackBarCompleter( snackBarCompleter(context, localization.restoredInvoice),
context, localization.restoredInvoice),
invoice.id)); invoice.id));
} else { } else {
store.dispatch(DeleteInvoiceRequest( store.dispatch(DeleteInvoiceRequest(
snackBarCompleter( snackBarCompleter(context, localization.deletedInvoice),
context, localization.deletedInvoice),
invoice.id)); invoice.id));
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/edit_icon_button.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/edit_icon_button.dart';
import 'package:invoiceninja_flutter/ui/app/one_value_header.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -13,7 +14,7 @@ import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view_vm.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
class InvoiceView extends StatefulWidget { class InvoiceView extends StatefulWidget {
final InvoiceViewVM viewModel; final EntityViewVM viewModel;
const InvoiceView({ const InvoiceView({
Key key, Key key,
@ -35,18 +36,26 @@ class _InvoiceViewState extends State<InvoiceView> {
List<Widget> _buildView() { List<Widget> _buildView() {
final invoice = widget.viewModel.invoice; final invoice = widget.viewModel.invoice;
final user = widget.viewModel.company.user; final user = widget.viewModel.company.user;
final color = invoice.isPastDue
? Colors.red
: InvoiceStatusColors.colors[invoice.invoiceStatusId];
final widgets = <Widget>[ final widgets = <Widget>[
TwoValueHeader( invoice.isQuote
backgroundColor: invoice.isPastDue ? OneValueHeader(
? Colors.red backgroundColor: color,
: InvoiceStatusColors.colors[invoice.invoiceStatusId], label: localization.totalAmount,
label1: localization.totalAmount, value: formatNumber(invoice.amount, context,
value1: clientId: invoice.clientId),
formatNumber(invoice.amount, context, clientId: invoice.clientId), )
label2: localization.balanceDue, : TwoValueHeader(
value2: formatNumber(invoice.balance, context, backgroundColor: color,
clientId: invoice.clientId), label1: localization.totalAmount,
), value1: formatNumber(invoice.amount, context,
clientId: invoice.clientId),
label2: localization.balanceDue,
value2: formatNumber(invoice.balance, context,
clientId: invoice.clientId),
),
]; ];
final Map<String, String> fields = { final Map<String, String> fields = {
@ -79,6 +88,9 @@ class _InvoiceViewState extends State<InvoiceView> {
final List<Widget> fieldWidgets = []; final List<Widget> fieldWidgets = [];
fields.forEach((field, value) { fields.forEach((field, value) {
if (invoice.isQuote) {
field = QuoteFields.convertField(field);
}
if (value != null && value.isNotEmpty) { if (value != null && value.isNotEmpty) {
fieldWidgets.add(Column( fieldWidgets.add(Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -254,7 +266,7 @@ class _CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
@required this.viewModel, @required this.viewModel,
}); });
final InvoiceViewVM viewModel; final EntityViewVM viewModel;
@override @override
final Size preferredSize = const Size(double.infinity, 54.0); final Size preferredSize = const Size(double.infinity, 54.0);
@ -271,10 +283,12 @@ class _CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
actions: invoice.isNew actions: invoice.isNew
? [] ? []
: [ : [
user.canEditEntity(invoice) ? EditIconButton( user.canEditEntity(invoice)
isVisible: !invoice.isDeleted, ? EditIconButton(
onPressed: () => viewModel.onEditPressed(context), isVisible: !invoice.isDeleted,
) : Container(), onPressed: () => viewModel.onEditPressed(context),
)
: Container(),
ActionMenuButton( ActionMenuButton(
user: user, user: user,
customActions: [ customActions: [

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:redux/redux.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
@ -8,7 +9,6 @@ import 'package:invoiceninja_flutter/ui/invoice/invoice_screen.dart';
import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/utils/pdf.dart'; import 'package:invoiceninja_flutter/utils/pdf.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart';
import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view.dart'; import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view.dart';
@ -36,7 +36,7 @@ class InvoiceViewScreen extends StatelessWidget {
} }
} }
class InvoiceViewVM { class EntityViewVM {
final CompanyEntity company; final CompanyEntity company;
final InvoiceEntity invoice; final InvoiceEntity invoice;
final ClientEntity client; final ClientEntity client;
@ -48,7 +48,7 @@ class InvoiceViewVM {
final Function(BuildContext) onRefreshed; final Function(BuildContext) onRefreshed;
final Function onBackPressed; final Function onBackPressed;
InvoiceViewVM({ EntityViewVM({
@required this.company, @required this.company,
@required this.invoice, @required this.invoice,
@required this.client, @required this.client,
@ -61,6 +61,48 @@ class InvoiceViewVM {
@required this.onRefreshed, @required this.onRefreshed,
}); });
@override
bool operator ==(dynamic other) =>
client == other.client &&
company == other.company &&
invoice == other.quote &&
isSaving == other.isSaving &&
isDirty == other.isDirty;
@override
int get hashCode =>
client.hashCode ^
company.hashCode ^
invoice.hashCode ^
isSaving.hashCode ^
isDirty.hashCode;
}
class InvoiceViewVM extends EntityViewVM {
InvoiceViewVM({
CompanyEntity company,
InvoiceEntity invoice,
ClientEntity client,
bool isSaving,
bool isDirty,
Function(BuildContext, EntityAction) onActionSelected,
Function(BuildContext, [InvoiceItemEntity]) onEditPressed,
Function(BuildContext) onClientPressed,
Function(BuildContext) onRefreshed,
Function onBackPressed,
}) : super(
company: company,
invoice: invoice,
client: client,
isSaving: isSaving,
isDirty: isDirty,
onActionSelected: onActionSelected,
onEditPressed: onEditPressed,
onClientPressed: onClientPressed,
onRefreshed: onRefreshed,
onBackPressed: onBackPressed,
);
factory InvoiceViewVM.fromStore(Store<AppState> store) { factory InvoiceViewVM.fromStore(Store<AppState> store) {
final state = store.state; final state = store.state;
final invoice = state.invoiceState.map[state.invoiceUIState.selectedId]; final invoice = state.invoiceState.map[state.invoiceUIState.selectedId];
@ -141,20 +183,4 @@ class InvoiceViewVM {
} }
}); });
} }
@override
bool operator ==(dynamic other) =>
client == other.client &&
company == other.company &&
invoice == other.quote &&
isSaving == other.isSaving &&
isDirty == other.isDirty;
@override
int get hashCode =>
client.hashCode ^
company.hashCode ^
invoice.hashCode ^
isSaving.hashCode ^
isDirty.hashCode;
} }

View File

@ -1,182 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/data/models/invoice_model.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart';
import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart';
import 'package:invoiceninja_flutter/ui/invoice/invoice_list_item.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_list_vm.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
class QuoteList extends StatelessWidget {
final QuoteListVM viewModel;
const QuoteList({
Key key,
@required this.viewModel,
}) : super(key: key);
void _showMenu(
BuildContext context, InvoiceEntity quote, ClientEntity client) async {
final user = viewModel.user;
final message = await showDialog<String>(
context: context,
builder: (BuildContext context) => SimpleDialog(children: <Widget>[
user.canCreate(EntityType.quote)
? ListTile(
leading: Icon(Icons.control_point_duplicate),
title: Text(AppLocalization.of(context).clone),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.clone),
)
: Container(),
user.canEditEntity(quote) && !quote.isPublic
? ListTile(
leading: Icon(Icons.publish),
title: Text(AppLocalization.of(context).markSent),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.markSent),
)
: Container(),
user.canEditEntity(quote) && client.hasEmailAddress
? ListTile(
leading: Icon(Icons.send),
title: Text(AppLocalization.of(context).email),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.email),
)
: Container(),
ListTile(
leading: Icon(Icons.picture_as_pdf),
title: Text(AppLocalization.of(context).pdf),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.pdf),
),
Divider(),
user.canEditEntity(quote) && !quote.isActive
? ListTile(
leading: Icon(Icons.restore),
title: Text(AppLocalization.of(context).restore),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.restore),
)
: Container(),
user.canEditEntity(quote) && quote.isActive
? ListTile(
leading: Icon(Icons.archive),
title: Text(AppLocalization.of(context).archive),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.archive),
)
: Container(),
user.canEditEntity(quote) && !quote.isDeleted
? ListTile(
leading: Icon(Icons.delete),
title: Text(AppLocalization.of(context).delete),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.delete),
)
: Container(),
]));
if (message != null) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: message,
)));
}
}
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final listState = viewModel.listState;
final filteredClientId = listState.filterClientId;
final filteredClient =
filteredClientId != null ? viewModel.clientMap[filteredClientId] : null;
return Column(
children: <Widget>[
filteredClient != null
? Material(
color: Colors.orangeAccent,
elevation: 6.0,
child: InkWell(
onTap: () => viewModel.onViewClientFilterPressed(context),
child: Row(
children: <Widget>[
SizedBox(width: 18.0),
Expanded(
child: Text(
localization.clientsInvoices.replaceFirst(
':client', filteredClient.displayName),
style: TextStyle(
color: Colors.white,
fontSize: 16.0,
),
),
),
IconButton(
icon: Icon(
Icons.close,
color: Colors.white,
),
onPressed: () => viewModel.onClearClientFilterPressed(),
)
],
),
),
)
: Container(),
Expanded(
child: !viewModel.isLoaded
? LoadingIndicator()
: RefreshIndicator(
onRefresh: () => viewModel.onRefreshed(context),
child: viewModel.quoteList.isEmpty
? Opacity(
opacity: 0.5,
child: Center(
child: Text(
AppLocalization.of(context).noRecordsFound,
style: TextStyle(
fontSize: 18.0,
),
),
),
)
: ListView.builder(
shrinkWrap: true,
itemCount: viewModel.quoteList.length,
itemBuilder: (BuildContext context, index) {
final quoteId = viewModel.quoteList[index];
final quote = viewModel.quoteMap[quoteId];
final client =
viewModel.clientMap[quote.clientId];
return Column(
children: <Widget>[
InvoiceListItem(
user: viewModel.user,
filter: viewModel.filter,
invoice: quote,
client: viewModel.clientMap[quote.clientId],
onDismissed: (DismissDirection direction) =>
viewModel.onDismissed(
context, quote, direction),
onTap: () =>
viewModel.onQuoteTap(context, quote),
onLongPress: () =>
_showMenu(context, quote, client),
),
Divider(
height: 1.0,
),
],
);
},
),
),
),
],
);
}
}

View File

@ -1,93 +0,0 @@
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/ui/app/entity_state_label.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/app/dismissible_entity.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
class QuoteListItem extends StatelessWidget {
final UserEntity user;
final DismissDirectionCallback onDismissed;
final GestureTapCallback onTap;
final GestureTapCallback onLongPress;
final InvoiceEntity invoice;
final ClientEntity client;
final String filter;
const QuoteListItem({
@required this.user,
@required this.onDismissed,
@required this.onTap,
@required this.onLongPress,
@required this.invoice,
@required this.client,
@required this.filter,
});
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final filterMatch = filter != null && filter.isNotEmpty
? (invoice.matchesFilterValue(filter) ??
client.matchesFilterValue(filter))
: null;
return DismissibleEntity(
user: user,
entity: invoice,
onDismissed: onDismissed,
child: ListTile(
onTap: onTap,
onLongPress: onLongPress,
title: Container(
width: MediaQuery.of(context).size.width,
child: Row(
children: <Widget>[
Expanded(
child: Text(
client.displayName,
style: Theme.of(context).textTheme.title,
),
),
Text(
formatNumber(invoice.amount, context,
clientId: invoice.clientId),
style: Theme.of(context).textTheme.title),
],
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: filterMatch == null
? Text(invoice.invoiceNumber)
: Text(
filterMatch,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
Text(
invoice.isPastDue
? localization.pastDue
: localization.lookup(
'invoice_status_${invoice.invoiceStatusId}'),
style: TextStyle(
color: invoice.isPastDue
? Colors.red
: InvoiceStatusColors.colors[invoice.invoiceStatusId],
)),
],
),
EntityStateLabel(invoice),
],
),
),
);
}
}

View File

@ -8,7 +8,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_list.dart'; import 'package:invoiceninja_flutter/ui/invoice/invoice_list.dart';
import 'package:invoiceninja_flutter/ui/invoice/invoice_list_vm.dart';
import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/utils/pdf.dart'; import 'package:invoiceninja_flutter/utils/pdf.dart';
@ -24,10 +25,9 @@ class QuoteListBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StoreConnector<AppState, QuoteListVM>( return StoreConnector<AppState, QuoteListVM>(
//rebuildOnChange: true,
converter: QuoteListVM.fromStore, converter: QuoteListVM.fromStore,
builder: (context, vm) { builder: (context, vm) {
return QuoteList( return InvoiceList(
viewModel: vm, viewModel: vm,
); );
}, },
@ -35,38 +35,39 @@ class QuoteListBuilder extends StatelessWidget {
} }
} }
class QuoteListVM { class QuoteListVM extends EntityListVM {
final UserEntity user;
final ListUIState listState;
final List<int> quoteList;
final BuiltMap<int, InvoiceEntity> quoteMap;
final BuiltMap<int, ClientEntity> clientMap;
final String filter;
final bool isLoading;
final bool isLoaded;
final Function(BuildContext, InvoiceEntity) onQuoteTap;
final Function(BuildContext, InvoiceEntity, DismissDirection) onDismissed;
final Function(BuildContext) onRefreshed;
final Function onClearClientFilterPressed;
final Function(BuildContext) onViewClientFilterPressed;
final Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction;
QuoteListVM({ QuoteListVM({
@required this.user, UserEntity user,
@required this.listState, ListUIState listState,
@required this.quoteList, List<int> invoiceList,
@required this.quoteMap, BuiltMap<int, InvoiceEntity> invoiceMap,
@required this.clientMap, BuiltMap<int, ClientEntity> clientMap,
@required this.isLoading, String filter,
@required this.isLoaded, bool isLoading,
@required this.filter, bool isLoaded,
@required this.onQuoteTap, Function(BuildContext, InvoiceEntity) onInvoiceTap,
@required this.onDismissed, Function(BuildContext, InvoiceEntity, DismissDirection) onDismissed,
@required this.onRefreshed, Function(BuildContext) onRefreshed,
@required this.onClearClientFilterPressed, Function onClearClientFilterPressed,
@required this.onViewClientFilterPressed, Function(BuildContext) onViewClientFilterPressed,
@required this.onEntityAction, Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction,
}); }) : super(
user: user,
listState: listState,
invoiceList: invoiceList,
invoiceMap: invoiceMap,
clientMap: clientMap,
filter: filter,
isLoading: isLoading,
isLoaded: isLoaded,
onInvoiceTap: onInvoiceTap,
onDismissed: onDismissed,
onRefreshed: onRefreshed,
onClearClientFilterPressed: onClearClientFilterPressed,
onViewClientFilterPressed: onViewClientFilterPressed,
onEntityAction: onEntityAction,
);
static QuoteListVM fromStore(Store<AppState> store) { static QuoteListVM fromStore(Store<AppState> store) {
Future<Null> _handleRefresh(BuildContext context) { Future<Null> _handleRefresh(BuildContext context) {
@ -84,17 +85,17 @@ class QuoteListVM {
return QuoteListVM( return QuoteListVM(
user: state.user, user: state.user,
listState: state.quoteListState, listState: state.quoteListState,
quoteList: memoizedFilteredQuoteList( invoiceList: memoizedFilteredQuoteList(
state.quoteState.map, state.quoteState.map,
state.quoteState.list, state.quoteState.list,
state.clientState.map, state.clientState.map,
state.quoteListState), state.quoteListState),
quoteMap: state.quoteState.map, invoiceMap: state.quoteState.map,
clientMap: state.clientState.map, clientMap: state.clientState.map,
isLoading: state.isLoading, isLoading: state.isLoading,
isLoaded: state.quoteState.isLoaded && state.clientState.isLoaded, isLoaded: state.quoteState.isLoaded && state.clientState.isLoaded,
filter: state.quoteListState.filter, filter: state.quoteListState.filter,
onQuoteTap: (context, quote) { onInvoiceTap: (context, quote) {
store.dispatch(ViewQuote(quoteId: quote.id, context: context)); store.dispatch(ViewQuote(quoteId: quote.id, context: context));
}, },
onRefreshed: (context) => _handleRefresh(context), onRefreshed: (context) => _handleRefresh(context),

View File

@ -1,312 +0,0 @@
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/edit_icon_button.dart';
import 'package:invoiceninja_flutter/ui/app/one_value_header.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/app/actions_menu_button.dart';
import 'package:invoiceninja_flutter/ui/app/icon_message.dart';
import 'package:invoiceninja_flutter/ui/app/invoice/invoice_item_view.dart';
import 'package:invoiceninja_flutter/ui/quote/view/quote_view_vm.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
class QuoteView extends StatefulWidget {
final QuoteViewVM viewModel;
const QuoteView({
Key key,
@required this.viewModel,
}) : super(key: key);
@override
_QuoteViewState createState() => new _QuoteViewState();
}
class _QuoteViewState extends State<QuoteView> {
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final client = viewModel.client;
final company = viewModel.company;
List<Widget> _buildView() {
final quote = widget.viewModel.quote;
final user = widget.viewModel.company.user;
final widgets = <Widget>[
OneValueHeader(
backgroundColor: quote.isPastDue
? Colors.red
: InvoiceStatusColors.colors[quote.invoiceStatusId],
label: localization.totalAmount,
value: formatNumber(quote.amount, context, clientId: quote.clientId),
),
];
final Map<String, String> fields = {
QuoteFields.quoteStatusId: quote.isPastDue
? localization.pastDue
: localization.lookup('invoice_status_${quote.invoiceStatusId}'),
QuoteFields.quoteDate: formatDate(quote.invoiceDate, context),
QuoteFields.validUntil: formatDate(quote.dueDate, context),
InvoiceFields.partial: formatNumber(quote.partial, context,
clientId: quote.clientId, zeroIsNull: true),
InvoiceFields.partialDueDate: formatDate(quote.partialDueDate, context),
InvoiceFields.poNumber: quote.poNumber,
InvoiceFields.discount: formatNumber(quote.discount, context,
clientId: quote.clientId,
zeroIsNull: true,
formatNumberType: quote.isAmountDiscount
? FormatNumberType.money
: FormatNumberType.percent),
};
if (quote.customTextValue1.isNotEmpty) {
final label1 = company.getCustomFieldLabel(CustomFieldType.invoice1);
fields[label1] = quote.customTextValue1;
}
if (quote.customTextValue2.isNotEmpty) {
final label2 = company.getCustomFieldLabel(CustomFieldType.invoice2);
fields[label2] = quote.customTextValue2;
}
final List<Widget> fieldWidgets = [];
fields.forEach((field, value) {
if (value != null && value.isNotEmpty) {
fieldWidgets.add(Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Flexible(
child: Text(
localization.lookup(field),
style: TextStyle(
fontWeight: FontWeight.w300,
),
),
),
Flexible(
child: Text(
value,
style: TextStyle(
fontWeight: FontWeight.w600,
),
)),
],
));
}
});
widgets.addAll([
Material(
color: Theme.of(context).canvasColor,
child: ListTile(
title: Text(client?.displayName ?? ''),
leading: Icon(FontAwesomeIcons.users, size: 18.0),
trailing: Icon(Icons.navigate_next),
onTap: () => viewModel.onClientPressed(context),
),
),
Container(
color: Theme.of(context).backgroundColor,
height: 12.0,
),
Container(
color: Theme.of(context).canvasColor,
child: Padding(
padding: EdgeInsets.only(left: 16.0, top: 10.0, right: 16.0),
child: GridView.count(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
primary: true,
crossAxisCount: 2,
children: fieldWidgets,
childAspectRatio: 3.5,
),
),
),
Container(
color: Theme.of(context).backgroundColor,
height: 12.0,
),
]);
if (quote.privateNotes != null && quote.privateNotes.isNotEmpty) {
widgets.addAll([
IconMessage(quote.privateNotes),
Container(
color: Theme.of(context).backgroundColor,
height: 12.0,
),
]);
}
quote.invoiceItems.forEach((quoteItem) {
widgets.addAll([
InvoiceItemListTile(
invoice: quote,
invoiceItem: quoteItem,
onTap: () => user.canEditEntity(quote)
? viewModel.onEditPressed(context, quoteItem)
: null,
),
]);
});
widgets.addAll([
Container(
color: Theme.of(context).backgroundColor,
height: 12.0,
),
]);
Widget surchargeRow(String label, double amount) {
return Container(
color: Theme.of(context).canvasColor,
child: Padding(
padding: const EdgeInsets.only(
left: 16.0, top: 12.0, right: 16.0, bottom: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text(label),
SizedBox(
width: 80.0,
child: Align(
alignment: Alignment.centerRight,
child: Text(formatNumber(amount, context,
clientId: quote.clientId))),
),
],
),
),
);
}
if (quote.customValue1 != 0 && company.enableCustomInvoiceTaxes1) {
widgets.add(surchargeRow(
company.getCustomFieldLabel(CustomFieldType.surcharge1),
quote.customValue1));
}
if (quote.customValue2 != 0 && company.enableCustomInvoiceTaxes2) {
widgets.add(surchargeRow(
company.getCustomFieldLabel(CustomFieldType.surcharge2),
quote.customValue2));
}
quote
.calculateTaxes(company.enableInclusiveTaxes)
.forEach((taxName, taxAmount) {
widgets.add(surchargeRow(taxName, taxAmount));
});
if (quote.customValue1 != 0 && !company.enableCustomInvoiceTaxes1) {
widgets.add(surchargeRow(
company.getCustomFieldLabel(CustomFieldType.surcharge1),
quote.customValue1));
}
if (quote.customValue2 != 0 && !company.enableCustomInvoiceTaxes2) {
widgets.add(surchargeRow(
company.getCustomFieldLabel(CustomFieldType.surcharge2),
quote.customValue2));
}
return widgets;
}
return WillPopScope(
onWillPop: () async {
viewModel.onBackPressed();
return true;
},
child: Scaffold(
appBar: _CustomAppBar(
viewModel: viewModel,
),
body: Builder(
builder: (BuildContext context) {
return RefreshIndicator(
onRefresh: () => viewModel.onRefreshed(context),
child: Container(
color: Theme.of(context).backgroundColor,
child: ListView(
children: _buildView(),
),
),
);
},
),
),
);
}
}
class _CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
const _CustomAppBar({
@required this.viewModel,
});
final QuoteViewVM viewModel;
@override
final Size preferredSize = const Size(double.infinity, 54.0);
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final quote = viewModel.quote;
final client = viewModel.client;
final user = viewModel.company.user;
return AppBar(
title: Text((localization.quote + ' ' + quote.invoiceNumber) ?? ''),
actions: quote.isNew
? []
: [
user.canEditEntity(quote)
? EditIconButton(
isVisible: !quote.isDeleted,
onPressed: () => viewModel.onEditPressed(context),
)
: Container(),
ActionMenuButton(
user: user,
customActions: [
user.canCreate(EntityType.quote)
? ActionMenuChoice(
action: EntityAction.clone,
icon: Icons.control_point_duplicate,
label: AppLocalization.of(context).clone,
)
: null,
user.canEditEntity(quote) && !quote.isPublic
? ActionMenuChoice(
action: EntityAction.markSent,
icon: Icons.publish,
label: AppLocalization.of(context).markSent,
)
: null,
user.canEditEntity(quote) && client.hasEmailAddress
? ActionMenuChoice(
action: EntityAction.email,
icon: Icons.send,
label: AppLocalization.of(context).email,
)
: null,
ActionMenuChoice(
action: EntityAction.pdf,
icon: Icons.picture_as_pdf,
label: AppLocalization.of(context).pdf,
),
],
isSaving: viewModel.isSaving,
entity: quote,
onSelected: viewModel.onActionSelected,
)
],
);
}
}

View File

@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/client/client_actions.dart'; import 'package:invoiceninja_flutter/redux/client/client_actions.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view.dart';
import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart'; import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart';
import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
@ -11,7 +13,6 @@ import 'package:invoiceninja_flutter/utils/pdf.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/quote/view/quote_view.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart'; import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart';
@ -28,7 +29,7 @@ class QuoteViewScreen extends StatelessWidget {
return QuoteViewVM.fromStore(store); return QuoteViewVM.fromStore(store);
}, },
builder: (context, viewModel) { builder: (context, viewModel) {
return QuoteView( return InvoiceView(
viewModel: viewModel, viewModel: viewModel,
); );
}, },
@ -36,30 +37,31 @@ class QuoteViewScreen extends StatelessWidget {
} }
} }
class QuoteViewVM { class QuoteViewVM extends EntityViewVM {
final CompanyEntity company;
final InvoiceEntity quote;
final ClientEntity client;
final bool isSaving;
final bool isDirty;
final Function(BuildContext, EntityAction) onActionSelected;
final Function(BuildContext, [InvoiceItemEntity]) onEditPressed;
final Function(BuildContext) onClientPressed;
final Function(BuildContext) onRefreshed;
final Function onBackPressed;
QuoteViewVM({ QuoteViewVM({
@required this.company, CompanyEntity company,
@required this.quote, InvoiceEntity invoice,
@required this.client, ClientEntity client,
@required this.isSaving, bool isSaving,
@required this.isDirty, bool isDirty,
@required this.onActionSelected, Function(BuildContext, EntityAction) onActionSelected,
@required this.onEditPressed, Function(BuildContext, [InvoiceItemEntity]) onEditPressed,
@required this.onBackPressed, Function(BuildContext) onClientPressed,
@required this.onClientPressed, Function(BuildContext) onRefreshed,
@required this.onRefreshed, Function onBackPressed,
}); }) : super(
company: company,
invoice: invoice,
client: client,
isSaving: isSaving,
isDirty: isDirty,
onActionSelected: onActionSelected,
onEditPressed: onEditPressed,
onClientPressed: onClientPressed,
onRefreshed: onRefreshed,
onBackPressed: onBackPressed,
);
factory QuoteViewVM.fromStore(Store<AppState> store) { factory QuoteViewVM.fromStore(Store<AppState> store) {
final state = store.state; final state = store.state;
@ -77,11 +79,11 @@ class QuoteViewVM {
company: state.selectedCompany, company: state.selectedCompany,
isSaving: state.isSaving, isSaving: state.isSaving,
isDirty: quote.isNew, isDirty: quote.isNew,
quote: quote, invoice: quote,
client: client, client: client,
onEditPressed: (BuildContext context, [InvoiceItemEntity invoiceItem]) { onEditPressed: (BuildContext context, [InvoiceItemEntity invoiceItem]) {
final Completer<InvoiceEntity> completer = final Completer<InvoiceEntity> completer =
new Completer<InvoiceEntity>(); new Completer<InvoiceEntity>();
store.dispatch(EditQuote( store.dispatch(EditQuote(
quote: quote, quote: quote,
context: context, context: context,
@ -90,8 +92,8 @@ class QuoteViewVM {
completer.future.then((invoice) { completer.future.then((invoice) {
Scaffold.of(context).showSnackBar(SnackBar( Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow( content: SnackBarRow(
message: AppLocalization.of(context).updatedQuote, message: AppLocalization.of(context).updatedQuote,
))); )));
}); });
}, },
onRefreshed: (context) => _handleRefresh(context), onRefreshed: (context) => _handleRefresh(context),
@ -114,19 +116,17 @@ class QuoteViewVM {
case EntityAction.email: case EntityAction.email:
store.dispatch(ShowEmailQuote( store.dispatch(ShowEmailQuote(
completer: completer:
snackBarCompleter(context, localization.emailedQuote), snackBarCompleter(context, localization.emailedQuote),
quote: quote, quote: quote,
context: context)); context: context));
break; break;
case EntityAction.archive: case EntityAction.archive:
store.dispatch(ArchiveQuoteRequest( store.dispatch(ArchiveQuoteRequest(
popCompleter(context, localization.archivedQuote), popCompleter(context, localization.archivedQuote), quote.id));
quote.id));
break; break;
case EntityAction.delete: case EntityAction.delete:
store.dispatch(DeleteQuoteRequest( store.dispatch(DeleteQuoteRequest(
popCompleter(context, localization.deletedQuote), popCompleter(context, localization.deletedQuote), quote.id));
quote.id));
break; break;
case EntityAction.restore: case EntityAction.restore:
store.dispatch(RestoreQuoteRequest( store.dispatch(RestoreQuoteRequest(
@ -135,26 +135,9 @@ class QuoteViewVM {
break; break;
case EntityAction.clone: case EntityAction.clone:
Navigator.of(context).pop(); Navigator.of(context).pop();
store.dispatch( store.dispatch(EditQuote(context: context, quote: quote.clone));
EditQuote(context: context, quote: quote.clone));
break; break;
} }
}); });
} }
@override
bool operator ==(dynamic other) =>
client == other.client &&
company == other.company &&
quote == other.quote &&
isSaving == other.isSaving &&
isDirty == other.isDirty;
@override
int get hashCode =>
client.hashCode ^
company.hashCode ^
quote.hashCode ^
isSaving.hashCode ^
isDirty.hashCode;
} }