This commit is contained in:
Hillel Coren 2018-08-23 22:47:49 -07:00
parent cdb130418e
commit b4b2afaa4f
9 changed files with 850 additions and 54 deletions

View File

@ -1,13 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/view/quote_view_vm.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/app/invoice/invoice_email_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart';
import 'package:invoiceninja_flutter/ui/quote/view/quote_view_vm.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/client/client_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/data/repositories/quote_repository.dart';
@ -17,49 +17,38 @@ List<Middleware<AppState>> createStoreQuotesMiddleware([
final viewQuoteList = _viewQuoteList();
final viewQuote = _viewQuote();
final editQuote = _editQuote();
final showEmailQuote = _showEmailQuote();
final loadQuotes = _loadQuotes(repository);
final loadQuote = _loadQuote(repository);
final saveQuote = _saveQuote(repository);
final archiveQuote = _archiveQuote(repository);
final deleteQuote = _deleteQuote(repository);
final restoreQuote = _restoreQuote(repository);
final emailQuote = _emailQuote(repository);
final markSentQuote = _markSentQuote(repository);
return [
TypedMiddleware<AppState, ViewQuoteList>(viewQuoteList),
TypedMiddleware<AppState, ViewQuote>(viewQuote),
TypedMiddleware<AppState, EditQuote>(editQuote),
TypedMiddleware<AppState, ShowEmailQuote>(showEmailQuote),
TypedMiddleware<AppState, LoadQuotes>(loadQuotes),
TypedMiddleware<AppState, LoadQuote>(loadQuote),
TypedMiddleware<AppState, SaveQuoteRequest>(saveQuote),
TypedMiddleware<AppState, ArchiveQuoteRequest>(archiveQuote),
TypedMiddleware<AppState, DeleteQuoteRequest>(deleteQuote),
TypedMiddleware<AppState, RestoreQuoteRequest>(restoreQuote),
TypedMiddleware<AppState, EmailQuoteRequest>(emailQuote),
TypedMiddleware<AppState, MarkSentQuoteRequest>(markSentQuote),
];
}
Middleware<AppState> _editQuote() {
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
next(action);
if (action.trackRoute) {
store.dispatch(UpdateCurrentRoute(QuoteEditScreen.route));
}
final quote =
await Navigator.of(action.context).pushNamed(QuoteEditScreen.route);
if (action.completer != null && quote != null) {
action.completer.complete(quote);
}
};
}
Middleware<AppState> _viewQuote() {
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
next(action);
store.dispatch(UpdateCurrentRoute(QuoteViewScreen.route));
Navigator.of(action.context).pushNamed(QuoteViewScreen.route);
await Navigator.of(action.context).pushNamed(QuoteViewScreen.route);
};
}
@ -68,8 +57,34 @@ Middleware<AppState> _viewQuoteList() {
next(action);
store.dispatch(UpdateCurrentRoute(QuoteScreen.route));
Navigator.of(action.context).pushNamedAndRemoveUntil(
QuoteScreen.route, (Route<dynamic> route) => false);
};
}
Navigator.of(action.context).pushNamedAndRemoveUntil(QuoteScreen.route, (Route<dynamic> route) => false);
Middleware<AppState> _editQuote() {
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
next(action);
store.dispatch(UpdateCurrentRoute(QuoteEditScreen.route));
final quote =
await Navigator.of(action.context).pushNamed(QuoteEditScreen.route);
if (action.completer != null && quote != null) {
action.completer.complete(quote);
}
};
}
Middleware<AppState> _showEmailQuote() {
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
next(action);
final emailWasSent = await Navigator.of(action.context).pushNamed(InvoiceEmailScreen.route);
if (action.completer != null && emailWasSent) {
action.completer.complete(null);
}
};
}
@ -78,7 +93,7 @@ Middleware<AppState> _archiveQuote(QuoteRepository repository) {
final origQuote = store.state.quoteState.map[action.quoteId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origQuote, EntityAction.archive)
origQuote, EntityAction.archive)
.then((dynamic quote) {
store.dispatch(ArchiveQuoteSuccess(quote));
if (action.completer != null) {
@ -101,9 +116,10 @@ Middleware<AppState> _deleteQuote(QuoteRepository repository) {
final origQuote = store.state.quoteState.map[action.quoteId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origQuote, EntityAction.delete)
.then((dynamic quote) {
origQuote, EntityAction.delete)
.then((InvoiceEntity quote) {
store.dispatch(DeleteQuoteSuccess(quote));
store.dispatch(LoadClient(clientId: quote.clientId));
if (action.completer != null) {
action.completer.complete(null);
}
@ -124,9 +140,10 @@ Middleware<AppState> _restoreQuote(QuoteRepository repository) {
final origQuote = store.state.quoteState.map[action.quoteId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origQuote, EntityAction.restore)
.then((dynamic quote) {
origQuote, EntityAction.restore)
.then((InvoiceEntity quote) {
store.dispatch(RestoreQuoteSuccess(quote));
store.dispatch(LoadClient(clientId: quote.clientId));
if (action.completer != null) {
action.completer.complete(null);
}
@ -142,11 +159,60 @@ Middleware<AppState> _restoreQuote(QuoteRepository repository) {
};
}
Middleware<AppState> _markSentQuote(QuoteRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final origQuote = store.state.quoteState.map[action.quoteId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origQuote, EntityAction.markSent)
.then((dynamic quote) {
store.dispatch(MarkSentQuoteSuccess(quote));
store.dispatch(LoadClient(clientId: quote.clientId));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(MarkSentQuoteFailure(origQuote));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}
Middleware<AppState> _emailQuote(QuoteRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final origQuote = store.state.quoteState.map[action.quoteId];
/*
repository
.emailQuote(store.state.selectedCompany, store.state.authState,
origQuote, action.template, action.subject, action.body)
.then((void _) {
store.dispatch(EmailQuoteSuccess());
store.dispatch(LoadClient(clientId: origQuote.clientId));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(EmailQuoteFailure(error));
if (action.completer != null) {
action.completer.completeError(error);
}
});
*/
next(action);
};
}
Middleware<AppState> _saveQuote(QuoteRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
repository
.saveData(
store.state.selectedCompany, store.state.authState, action.quote)
store.state.selectedCompany, store.state.authState, action.quote)
.then((dynamic quote) {
if (action.quote.isNew) {
store.dispatch(AddQuoteSuccess(quote));
@ -180,7 +246,7 @@ Middleware<AppState> _loadQuote(QuoteRepository repository) {
store.dispatch(LoadQuoteSuccess(quote));
if (action.completer != null) {
action.completer.complete(null);
action.completer.complete(quote);
}
}).catchError((Object error) {
print(error);
@ -209,19 +275,18 @@ Middleware<AppState> _loadQuotes(QuoteRepository repository) {
}
final int updatedAt =
action.force ? 0 : (state.quoteState.lastUpdated / 1000).round();
action.force ? 0 : (state.quoteState.lastUpdated / 1000).round();
store.dispatch(LoadQuotesRequest());
repository
.loadList(state.selectedCompany, state.authState, updatedAt)
.then((data) {
store.dispatch(LoadQuotesSuccess(data));
if (action.completer != null) {
action.completer.complete(null);
}
if (state.dashboardState.isStale) {
store.dispatch(LoadDashboard());
if (state.quoteState.isStale) {
store.dispatch(LoadQuotes());
}
}).catchError((Object error) {
print(error);

View File

@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_details_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_items_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_details_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_items_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_item_selector.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
@ -97,8 +97,8 @@ class _QuoteEditState extends State<QuoteEdit>
child: TabBarView(
controller: _controller,
children: <Widget>[
InvoiceEditDetailsScreen(),
InvoiceEditItemsScreen(),
QuoteEditDetailsScreen(),
QuoteEditItemsScreen(),
],
),
),

View File

@ -0,0 +1,285 @@
import 'package:invoiceninja_flutter/redux/client/client_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart';
import 'package:invoiceninja_flutter/ui/app/invoice/tax_rate_dropdown.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/ui/app/entity_dropdown.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/date_picker.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_details_vm.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
class QuoteEditDetails extends StatefulWidget {
const QuoteEditDetails({
Key key,
@required this.viewModel,
}) : super(key: key);
final QuoteEditDetailsVM viewModel;
@override
QuoteEditDetailsState createState() => QuoteEditDetailsState();
}
class QuoteEditDetailsState extends State<QuoteEditDetails> {
final _quoteNumberController = TextEditingController();
final _quoteDateController = TextEditingController();
final _poNumberController = TextEditingController();
final _discountController = TextEditingController();
final _partialController = TextEditingController();
final _custom1Controller = TextEditingController();
final _custom2Controller = TextEditingController();
final _surcharge1Controller = TextEditingController();
final _surcharge2Controller = TextEditingController();
List<TextEditingController> _controllers = [];
@override
void didChangeDependencies() {
_controllers = [
_quoteNumberController,
_quoteDateController,
_poNumberController,
_discountController,
_partialController,
_custom1Controller,
_custom2Controller,
_surcharge1Controller,
_surcharge2Controller,
];
_controllers
.forEach((dynamic controller) => controller.removeListener(_onChanged));
final quote = widget.viewModel.quote;
_quoteNumberController.text = quote.invoiceNumber;
_quoteDateController.text = quote.invoiceDate;
_poNumberController.text = quote.poNumber;
_discountController.text = formatNumber(quote.discount, context,
formatNumberType: FormatNumberType.input);
_partialController.text = formatNumber(quote.partial, context,
formatNumberType: FormatNumberType.input);
_custom1Controller.text = quote.customTextValue1;
_custom2Controller.text = quote.customTextValue2;
_surcharge1Controller.text = formatNumber(quote.customValue1, context,
formatNumberType: FormatNumberType.input);
_surcharge2Controller.text = formatNumber(quote.customValue2, context,
formatNumberType: FormatNumberType.input);
_controllers
.forEach((dynamic controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_controllers.forEach((dynamic controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
void _onChanged() {
final quote = widget.viewModel.quote.rebuild((b) => b
..invoiceNumber = widget.viewModel.quote.isNew
? ''
: _quoteNumberController.text.trim()
..poNumber = _poNumberController.text.trim()
..discount = parseDouble(_discountController.text)
..partial = parseDouble(_partialController.text)
..customTextValue1 = _custom1Controller.text.trim()
..customTextValue2 = _custom2Controller.text.trim()
..customValue1 = parseDouble(_surcharge1Controller.text)
..customValue2 = parseDouble(_surcharge2Controller.text));
if (quote != widget.viewModel.quote) {
widget.viewModel.onChanged(quote);
}
}
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final quote = viewModel.quote;
final company = viewModel.company;
return ListView(
children: <Widget>[
FormCard(
children: <Widget>[
quote.isNew
? EntityDropdown(
entityType: EntityType.client,
labelText: localization.client,
initialValue:
viewModel.clientMap[quote.clientId]?.displayName,
entityMap: viewModel.clientMap,
entityList: memoizedDropdownClientList(
viewModel.clientMap, viewModel.clientList),
validator: (String val) => val.trim().isEmpty
? AppLocalization.of(context).pleaseSelectAClient
: null,
onSelected: (clientId) {
viewModel.onChanged(
quote.rebuild((b) => b..clientId = clientId));
},
onAddPressed: (completer) {
viewModel.onAddClientPressed(context, completer);
},
)
: TextFormField(
autocorrect: false,
controller: _quoteNumberController,
decoration: InputDecoration(
labelText: localization.quoteNumber,
),
validator: (String val) => val.trim().isEmpty
? AppLocalization.of(context).pleaseEnterAQuoteNumber
: null,
),
DatePicker(
validator: (String val) => val.trim().isEmpty
? AppLocalization.of(context).pleaseSelectADate
: null,
labelText: localization.quoteDate,
selectedDate: quote.invoiceDate,
onSelected: (date) {
viewModel
.onChanged(quote.rebuild((b) => b..invoiceDate = date));
},
),
DatePicker(
labelText: localization.dueDate,
selectedDate: quote.dueDate,
onSelected: (date) {
viewModel.onChanged(quote.rebuild((b) => b..dueDate = date));
},
),
TextFormField(
controller: _partialController,
decoration: InputDecoration(
labelText: localization.partialDeposit,
),
keyboardType: TextInputType.number,
),
quote.partial != null && quote.partial > 0
? DatePicker(
labelText: localization.partialDueDate,
selectedDate: quote.partialDueDate,
onSelected: (date) {
viewModel.onChanged(
quote.rebuild((b) => b..partialDueDate = date));
},
)
: Container(),
TextFormField(
autocorrect: false,
controller: _poNumberController,
decoration: InputDecoration(
labelText: localization.poNumber,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
child: TextFormField(
controller: _discountController,
decoration: InputDecoration(
labelText: localization.discount,
),
keyboardType: TextInputType.number,
),
),
const SizedBox(
width: 10.0,
),
DropdownButtonHideUnderline(
child: DropdownButton<bool>(
value: quote.isAmountDiscount,
items: [
DropdownMenuItem<bool>(
child: Text(
localization.percent,
style: TextStyle(
color: Colors.grey[600],
),
),
value: false,
),
DropdownMenuItem<bool>(
child: Text(
localization.amount,
style: TextStyle(
color: Colors.grey[600],
),
),
value: true,
)
],
onChanged: (bool value) => viewModel.onChanged(
quote.rebuild((b) => b..isAmountDiscount = value)),
),
)
],
),
CustomField(
controller: _custom1Controller,
labelText: company.getCustomFieldLabel(CustomFieldType.invoice1),
options: company.getCustomFieldValues(CustomFieldType.invoice1),
),
CustomField(
controller: _custom2Controller,
labelText: company.getCustomFieldLabel(CustomFieldType.invoice2),
options: company.getCustomFieldValues(CustomFieldType.invoice2),
),
company.getCustomFieldLabel(CustomFieldType.surcharge1).isNotEmpty
? TextFormField(
controller: _surcharge1Controller,
decoration: InputDecoration(
labelText: company
.getCustomFieldLabel(CustomFieldType.surcharge1),
),
keyboardType: TextInputType.number,
)
: Container(),
company.getCustomFieldLabel(CustomFieldType.surcharge2).isNotEmpty
? TextFormField(
controller: _surcharge2Controller,
decoration: InputDecoration(
labelText: company
.getCustomFieldLabel(CustomFieldType.surcharge2),
),
keyboardType: TextInputType.number,
)
: Container(),
company.enableInvoiceTaxes
? TaxRateDropdown(
taxRates: company.taxRates,
onSelected: (taxRate) =>
viewModel.onChanged(quote.applyTax(taxRate)),
labelText: localization.tax,
initialTaxName: quote.taxName1,
initialTaxRate: quote.taxRate1,
)
: Container(),
company.enableInvoiceTaxes && company.enableSecondTaxRate
? TaxRateDropdown(
taxRates: company.taxRates,
onSelected: (taxRate) =>
viewModel.onChanged(quote.applyTax(taxRate)),
labelText: localization.tax,
initialTaxName: quote.taxName2,
initialTaxRate: quote.taxRate2,
)
: Container(),
],
),
],
);
}
}

View File

@ -0,0 +1,75 @@
import 'dart:async';
import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/client/client_actions.dart';
import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_details.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
class QuoteEditDetailsScreen extends StatelessWidget {
const QuoteEditDetailsScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, QuoteEditDetailsVM>(
converter: (Store<AppState> store) {
return QuoteEditDetailsVM.fromStore(store);
},
builder: (context, vm) {
return QuoteEditDetails(
viewModel: vm,
);
},
);
}
}
class QuoteEditDetailsVM {
final CompanyEntity company;
final InvoiceEntity quote;
final Function(InvoiceEntity) onChanged;
final BuiltMap<int, ClientEntity> clientMap;
final BuiltList<int> clientList;
final Function(BuildContext context, Completer<SelectableEntity> completer) onAddClientPressed;
QuoteEditDetailsVM({
@required this.company,
@required this.quote,
@required this.onChanged,
@required this.clientMap,
@required this.clientList,
@required this.onAddClientPressed,
});
factory QuoteEditDetailsVM.fromStore(Store<AppState> store) {
final AppState state = store.state;
final quote = state.quoteUIState.editing;
return QuoteEditDetailsVM(
company: state.selectedCompany,
quote: quote,
onChanged: (InvoiceEntity quote) =>
store.dispatch(UpdateQuote(quote)),
clientMap: state.clientState.map,
clientList: state.clientState.list,
onAddClientPressed: (context, completer) {
store.dispatch(
EditClient(client: ClientEntity(), context: context, completer: completer, trackRoute: false));
completer.future.then((SelectableEntity client) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: AppLocalization.of(context).createdClient,
)
));
});
},
);
}
}

View File

@ -0,0 +1,314 @@
import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart';
import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart';
import 'package:invoiceninja_flutter/ui/app/invoice/invoice_item_view.dart';
import 'package:invoiceninja_flutter/ui/app/invoice/tax_rate_dropdown.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_items_vm.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
class QuoteEditItems extends StatefulWidget {
const QuoteEditItems({
Key key,
@required this.viewModel,
}) : super(key: key);
final QuoteEditItemsVM viewModel;
@override
_QuoteEditItemsState createState() => _QuoteEditItemsState();
}
class _QuoteEditItemsState extends State<QuoteEditItems> {
InvoiceItemEntity selectedQuoteItem;
void _showQuoteItemEditor(
InvoiceItemEntity quoteItem, BuildContext context) {
showDialog<ItemEditDetails>(
context: context,
builder: (BuildContext context) {
final viewModel = widget.viewModel;
final quote = viewModel.quote;
return ItemEditDetails(
viewModel: viewModel,
key: Key(quoteItem.entityKey),
quoteItem: quoteItem,
index: quote.invoiceItems.indexOf(
quote.invoiceItems.firstWhere((i) => i.id == quoteItem.id)),
);
});
}
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final quote = viewModel.quote;
final quoteItem = quote.invoiceItems.contains(viewModel.quoteItem)
? viewModel.quoteItem
: null;
if (quoteItem != null && quoteItem != selectedQuoteItem) {
selectedQuoteItem = quoteItem;
WidgetsBinding.instance.addPostFrameCallback((duration) {
_showQuoteItemEditor(quoteItem, context);
});
}
if (quote.invoiceItems.isEmpty) {
return Center(
child: Text(
localization.clickPlusToAddItem,
style: TextStyle(
color: Colors.grey,
fontSize: 20.0,
),
),
);
}
final quoteItems =
quote.invoiceItems.map((quoteItem) => InvoiceItemListTile(
invoice: quote,
invoiceItem: quoteItem,
onTap: () => _showQuoteItemEditor(quoteItem, context),
));
return ListView(
children: quoteItems.toList(),
);
}
}
class ItemEditDetails extends StatefulWidget {
const ItemEditDetails({
Key key,
@required this.index,
@required this.quoteItem,
@required this.viewModel,
}) : super(key: key);
final int index;
final InvoiceItemEntity quoteItem;
final QuoteEditItemsVM viewModel;
@override
ItemEditDetailsState createState() => ItemEditDetailsState();
}
class ItemEditDetailsState extends State<ItemEditDetails> {
final _productKeyController = TextEditingController();
final _notesController = TextEditingController();
final _costController = TextEditingController();
final _qtyController = TextEditingController();
final _discountController = TextEditingController();
final _custom1Controller = TextEditingController();
final _custom2Controller = TextEditingController();
List<TextEditingController> _controllers = [];
@override
void initState() {
super.initState();
}
@override
void didChangeDependencies() {
if (_controllers.isNotEmpty) {
return;
}
final quoteItem = widget.quoteItem;
_productKeyController.text = quoteItem.productKey;
_notesController.text = quoteItem.notes;
_costController.text = formatNumber(quoteItem.cost, context,
formatNumberType: FormatNumberType.input);
_qtyController.text = formatNumber(quoteItem.qty, context,
formatNumberType: FormatNumberType.input);
_discountController.text = formatNumber(quoteItem.discount, context,
formatNumberType: FormatNumberType.input);
_custom1Controller.text = quoteItem.customValue1;
_custom2Controller.text = quoteItem.customValue2;
_controllers = [
_productKeyController,
_notesController,
_costController,
_qtyController,
_discountController,
_custom1Controller,
_custom2Controller,
];
_controllers
.forEach((dynamic controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_controllers.forEach((dynamic controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
void _onChanged() {
final quoteItem = widget.quoteItem.rebuild((b) => b
..productKey = _productKeyController.text.trim()
..notes = _notesController.text.trim()
..cost = parseDouble(_costController.text)
..qty = parseDouble(_qtyController.text)
..discount = parseDouble(_discountController.text)
..customValue1 = _custom1Controller.text.trim()
..customValue2 = _custom2Controller.text.trim());
if (quoteItem != widget.quoteItem) {
widget.viewModel.onChangedQuoteItem(quoteItem, widget.index);
}
}
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final quoteItem = widget.quoteItem;
final company = viewModel.company;
void _confirmDelete() {
showDialog<AlertDialog>(
context: context,
builder: (BuildContext context) => AlertDialog(
semanticLabel: localization.areYouSure,
title: Text(localization.areYouSure),
actions: <Widget>[
FlatButton(
child: Text(localization.cancel.toUpperCase()),
onPressed: () {
Navigator.pop(context);
}),
FlatButton(
child: Text(localization.ok.toUpperCase()),
onPressed: () {
widget.viewModel.onRemoveQuoteItemPressed(widget.index);
Navigator.pop(context); // confirmation dialog
Navigator.pop(context); // quote item editor
})
],
),
);
}
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.viewInsets
.bottom, // stay clear of the keyboard
),
child: SingleChildScrollView(
child: FormCard(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ElevatedButton(
color: Colors.red,
icon: Icons.delete,
label: localization.remove,
onPressed: _confirmDelete,
),
SizedBox(
width: 10.0,
),
ElevatedButton(
icon: Icons.check_circle,
label: localization.done,
onPressed: () {
viewModel.onDoneQuoteItemPressed();
Navigator.of(context).pop();
},
),
],
),
TextFormField(
autocorrect: false,
controller: _productKeyController,
decoration: InputDecoration(
labelText: localization.product,
),
),
TextFormField(
autocorrect: false,
controller: _notesController,
maxLines: 4,
decoration: InputDecoration(
labelText: localization.description,
),
),
CustomField(
controller: _custom1Controller,
labelText: company.getCustomFieldLabel(CustomFieldType.product1),
options: company.getCustomFieldValues(CustomFieldType.product1),
),
CustomField(
controller: _custom2Controller,
labelText: company.getCustomFieldLabel(CustomFieldType.product2),
options: company.getCustomFieldValues(CustomFieldType.product2),
),
TextFormField(
controller: _costController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: localization.unitCost,
),
),
company.hasInvoiceField('quantity')
? TextFormField(
controller: _qtyController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: localization.quantity,
),
)
: Container(),
company.hasInvoiceField('discount')
? TextFormField(
controller: _discountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: localization.discount,
),
)
: Container(),
company.enableInvoiceTaxes
? TaxRateDropdown(
taxRates: company.taxRates,
onSelected: (taxRate) => viewModel.onChangedQuoteItem(
quoteItem.applyTax(taxRate), widget.index),
labelText: localization.tax,
initialTaxName: quoteItem.taxName1,
initialTaxRate: quoteItem.taxRate1,
)
: Container(),
company.enableInvoiceTaxes && company.enableSecondTaxRate
? TaxRateDropdown(
taxRates: company.taxRates,
onSelected: (taxRate) => viewModel.onChangedQuoteItem(
quoteItem.applyTax(taxRate), widget.index),
labelText: localization.tax,
initialTaxName: quoteItem.taxName2,
initialTaxRate: quoteItem.taxRate2,
)
: Container(),
],
),
),
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_items.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
class QuoteEditItemsScreen extends StatelessWidget {
const QuoteEditItemsScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, QuoteEditItemsVM>(
converter: (Store<AppState> store) {
return QuoteEditItemsVM.fromStore(store);
},
builder: (context, vm) {
return QuoteEditItems(
viewModel: vm,
);
},
);
}
}
class QuoteEditItemsVM {
final CompanyEntity company;
final InvoiceEntity quote;
final InvoiceItemEntity quoteItem;
final Function(int) onRemoveQuoteItemPressed;
final Function onDoneQuoteItemPressed;
final Function(InvoiceItemEntity, int) onChangedQuoteItem;
QuoteEditItemsVM({
@required this.company,
@required this.quote,
@required this.quoteItem,
@required this.onRemoveQuoteItemPressed,
@required this.onDoneQuoteItemPressed,
@required this.onChangedQuoteItem,
});
factory QuoteEditItemsVM.fromStore(Store<AppState> store) {
final AppState state = store.state;
final quote = state.quoteUIState.editing;
return QuoteEditItemsVM(
company: state.selectedCompany,
quote: quote,
quoteItem: state.quoteUIState.editingItem,
onRemoveQuoteItemPressed: (index) =>
store.dispatch(DeleteQuoteItem(index)),
onDoneQuoteItemPressed: () => store.dispatch(EditQuoteItem()),
onChangedQuoteItem: (quoteItem, index) {
store.dispatch(
UpdateQuoteItem(quoteItem: quoteItem, index: index));
});
}
}

View File

@ -2,13 +2,13 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart';
import 'package:invoiceninja_flutter/ui/invoice/invoice_screen.dart';
import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
@ -55,22 +55,22 @@ class QuoteEditVM {
factory QuoteEditVM.fromStore(Store<AppState> store) {
final AppState state = store.state;
final invoice = state.invoiceUIState.editing;
final quote = state.quoteUIState.editing;
return QuoteEditVM(
company: state.selectedCompany,
isSaving: state.isSaving,
quote: invoice,
quote: quote,
quoteItem: state.invoiceUIState.editingItem,
origQuote: store.state.invoiceState.map[invoice.id],
origQuote: store.state.invoiceState.map[quote.id],
onBackPressed: () =>
store.dispatch(UpdateCurrentRoute(InvoiceScreen.route)),
onSavePressed: (BuildContext context) {
final Completer<InvoiceEntity> completer = Completer<InvoiceEntity>();
store.dispatch(
SaveInvoiceRequest(completer: completer, invoice: invoice));
SaveQuoteRequest(completer: completer, quote: quote));
return completer.future.then((savedInvoice) {
if (invoice.isNew) {
if (quote.isNew) {
Navigator.of(context).pushReplacementNamed(InvoiceViewScreen.route);
} else {
Navigator.of(context).pop(savedInvoice);
@ -85,9 +85,9 @@ class QuoteEditVM {
},
onItemsAdded: (items) {
if (items.length == 1) {
store.dispatch(EditInvoiceItem(items[0]));
store.dispatch(EditQuoteItem(items[0]));
}
store.dispatch(AddInvoiceItems(items));
store.dispatch(AddQuoteItems(items));
},
);
}

View File

@ -67,7 +67,7 @@ class QuoteScreen extends StatelessWidget {
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
store.dispatch(
EditQuote(quote: InvoiceEntity(), context: context));
EditQuote(quote: InvoiceEntity(isQuote: true), context: context));
},
child: Icon(
Icons.add,

View File

@ -41,10 +41,6 @@ Middleware<AppState> _editStub() {
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
next(action);
if (action.trackRoute) {
store.dispatch(UpdateCurrentRoute(StubEditScreen.route));
}
final stub =
await Navigator.of(action.context).pushNamed(StubEditScreen.route);