Quotes
This commit is contained in:
parent
150f1f68b0
commit
52bb232b85
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue