This commit is contained in:
unknown 2018-06-18 12:49:36 -07:00
parent a40d7946d7
commit 013f8e29f9
22 changed files with 573 additions and 401 deletions

View File

@ -46,10 +46,28 @@ class EditClient implements PersistUI {
EditClient({this.client, this.context});
}
class UpdateClient implements PersistUI {
final ClientEntity client;
UpdateClient(this.client);
}
class AddContact {}
class UpdateContact {
final int index;
final ContactEntity contact;
UpdateContact({this.index, this.contact});
}
class DeleteContact {
final int index;
DeleteContact(this.index);
}
class SaveClientRequest implements StartLoading {
final Completer completer;
final ClientEntity client;
SaveClientRequest(this.completer, this.client);
SaveClientRequest({this.completer, this.client});
}
class SaveClientSuccess implements StopLoading, PersistData {

View File

@ -17,12 +17,34 @@ final editingReducer = combineReducers<ClientEntity>([
TypedReducer<ClientEntity, AddClientSuccess>(_updateEditing),
TypedReducer<ClientEntity, ViewClient>(_updateEditing),
TypedReducer<ClientEntity, EditClient>(_updateEditing),
TypedReducer<ClientEntity, UpdateClient>(_updateEditing),
TypedReducer<ClientEntity, AddContact>(_addContact),
TypedReducer<ClientEntity, DeleteContact>(_removeContact),
TypedReducer<ClientEntity, UpdateContact>(_updateContact),
]);
ClientEntity _updateEditing(ClientEntity client, action) {
return action.client;
}
ClientEntity _addContact(ClientEntity client, AddContact action) {
return client.rebuild((b) => b
..contacts.add(ContactEntity())
);
}
ClientEntity _removeContact(ClientEntity client, DeleteContact action) {
return client.rebuild((b) => b
..contacts.removeAt(action.index)
);
}
ClientEntity _updateContact(ClientEntity client, UpdateContact action) {
return client.rebuild((b) => b
..contacts[action.index] = action.contact
);
}
final clientListReducer = combineReducers<ListUIState>([
TypedReducer<ListUIState, SortClients>(_sortClients),
TypedReducer<ListUIState, FilterClientsByState>(_filterClientsByState),

View File

@ -46,10 +46,28 @@ class EditInvoice implements PersistUI {
EditInvoice({this.invoice, this.context});
}
class UpdateInvoice implements PersistUI {
final InvoiceEntity invoice;
UpdateInvoice(this.invoice);
}
class AddInvoiceItem {}
class UpdateInvoiceItem {
final int index;
final InvoiceItemEntity invoiceItem;
UpdateInvoiceItem({this.index, this.invoiceItem});
}
class DeleteInvoiceItem {
final int index;
DeleteInvoiceItem(this.index);
}
class SaveInvoiceRequest implements StartLoading {
final Completer completer;
final InvoiceEntity invoice;
SaveInvoiceRequest(this.completer, this.invoice);
SaveInvoiceRequest({this.completer, this.invoice});
}
class SaveInvoiceSuccess implements StopLoading, PersistData {

View File

@ -17,6 +17,7 @@ final editingReducer = combineReducers<InvoiceEntity>([
TypedReducer<InvoiceEntity, AddInvoiceSuccess>(_updateEditing),
TypedReducer<InvoiceEntity, ViewInvoice>(_updateEditing),
TypedReducer<InvoiceEntity, EditInvoice>(_updateEditing),
TypedReducer<InvoiceEntity, UpdateInvoice>(_updateEditing),
]);
InvoiceEntity _updateEditing(InvoiceEntity client, action) {

View File

@ -46,7 +46,8 @@ class UpdateProduct implements PersistUI {
class SaveProductRequest implements StartLoading {
final Completer completer;
SaveProductRequest(this.completer);
final ProductEntity product;
SaveProductRequest({this.product, this.completer});
}
class SaveProductSuccess implements StopLoading, PersistData {

View File

@ -100,9 +100,9 @@ Middleware<AppState> _saveProduct(ProductRepository repository) {
return (Store<AppState> store, action, NextDispatcher next) {
repository
.saveData(
store.state.selectedCompany, store.state.authState, store.state.productUIState.selected)
store.state.selectedCompany, store.state.authState, action.product)
.then((product) {
if (store.state.productUIState.selected.isNew()) {
if (action.product.isNew()) {
store.dispatch(AddProductSuccess(product));
} else {
store.dispatch(SaveProductSuccess(product));

View File

@ -54,8 +54,14 @@ class ClientScreen extends StatelessWidget {
floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
// TODO create factory
var client = ClientEntity()
.rebuild((b) => b..contacts.replace([ContactEntity()]));
.rebuild((b) => b..contacts.replace([ContactEntity().rebuild((b) => b
..firstName = ''
..lastName = ''
..email = ''
..phone = ''
)]));
store.dispatch(EditClient(client: client, context: context));
},
child: Icon(Icons.add, color: Colors.white,),

View File

@ -23,16 +23,6 @@ class _ClientEditState extends State<ClientEdit>
with SingleTickerProviderStateMixin {
TabController _controller;
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
static final GlobalKey<ClientEditDetailsState> _detailsKey =
GlobalKey<ClientEditDetailsState>();
/*
static final GlobalKey<ClientEditBillingAddressState> _billingAddressKey =
GlobalKey<ClientEditBillingAddressState>();
static final GlobalKey<ClientEditShippingAddressState> _shippingAddressKey =
GlobalKey<ClientEditShippingAddressState>();
*/
static final GlobalKey<ClientEditContactsState> _contactsKey =
GlobalKey<ClientEditContactsState>();
@override
void initState() {
@ -49,32 +39,12 @@ class _ClientEditState extends State<ClientEdit>
@override
Widget build(BuildContext context) {
var localization = AppLocalization.of(context);
var client = widget.viewModel.client;
List<Widget> editors = [
ClientEditDetails(
client: client,
key: _detailsKey,
),
ClientEditContacts(
client: client,
key: _contactsKey,
),
/*
ClientEditBillingAddress(
client: client,
key: _billingAddressKey,
),
ClientEditShippingAddress(
client: client,
key: _shippingAddressKey,
),
*/
];
var viewModel = widget.viewModel;
var client = viewModel.client;
return WillPopScope(
onWillPop: () async {
widget.viewModel.onBackClicked();
viewModel.onBackClicked();
return true;
},
child: Scaffold(
@ -84,19 +54,13 @@ class _ClientEditState extends State<ClientEdit>
: client.displayName), // Text(localizations.clientDetails),
actions: <Widget>[
SaveIconButton(
isLoading: widget.viewModel.isLoading,
isLoading: viewModel.isLoading,
onPressed: () {
if (! _formKey.currentState.validate()) {
return;
}
_formKey.currentState.save();
var detailsState = _detailsKey.currentState;
//var billingAddressState = _billingAddressKey.currentState;
//var shippingAddressState = _shippingAddressKey.currentState;
var contactState = _contactsKey.currentState;
/*
ClientEntity client = widget.viewModel.client.rebuild((b) => b
..name = detailsState.name
..idNumber = detailsState.idNumber
@ -117,8 +81,9 @@ class _ClientEditState extends State<ClientEdit>
*/
..contacts.replace(
contactState?.getContacts() ?? widget.viewModel.client.contacts));
*/
widget.viewModel.onSaveClicked(context, client);
viewModel.onSaveClicked(context);
},
)
],
@ -147,7 +112,22 @@ class _ClientEditState extends State<ClientEdit>
key: _formKey,
child: TabBarView(
controller: _controller,
children: editors,
children: <Widget>[
ClientEditDetails(
viewModel: widget.viewModel,
),
ClientEditContacts(
viewModel: widget.viewModel,
),
/*
ClientEditBillingAddress(
client: client,
),
ClientEditShippingAddress(
client: client,
),
*/
],
),
),
),

View File

@ -1,79 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:invoiceninja/data/models/models.dart';
import 'package:invoiceninja/ui/app/form_card.dart';
import 'package:invoiceninja/utils/localization.dart';
import '../../app/form_card.dart';
class ClientEditBillingAddress extends StatefulWidget {
ClientEditBillingAddress({
Key key,
@required this.client,
@required this.onChanged,
}) : super(key: key);
final ClientEntity client;
final Function(ClientEntity) onChanged;
@override
ClientEditBillingAddressState createState() =>
new ClientEditBillingAddressState();
}
class ClientEditBillingAddressState extends State<ClientEditBillingAddress>
with AutomaticKeepAliveClientMixin {
String address1;
String address2;
String city;
String state;
String postalCode;
class ClientEditBillingAddressState extends State<ClientEditBillingAddress> {
final _address1Controller = TextEditingController();
final _address2Controller = TextEditingController();
final _cityController = TextEditingController();
final _stateController = TextEditingController();
final _postalCodeController = TextEditingController();
var _controllers = [];
@override
bool get wantKeepAlive => true;
void didChangeDependencies() {
_controllers = [
_address1Controller,
_address2Controller,
_cityController,
_stateController,
_postalCodeController,
];
_controllers.forEach((controller) => controller.removeListener(_onChanged));
var client = widget.client;
_address1Controller.text = client.address1;
_address2Controller.text = client.address2;
_cityController.text = client.city;
_stateController.text = client.state;
_postalCodeController.text = client.postalCode;
_controllers.forEach((controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_controllers.forEach((controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
_onChanged() {
var client = widget.client.rebuild((b) => b
..address1 = _address1Controller.text.trim()
..address2 = _address2Controller.text.trim()
..city = _cityController.text.trim()
..state = _stateController.text.trim()
..postalCode = _postalCodeController.text.trim()
);
if (client != widget.client) {
widget.onChanged(client);
}
}
@override
Widget build(BuildContext context) {
var localization = AppLocalization.of(context);
var client = widget.client;
return ListView(shrinkWrap: true, children: <Widget>[
FormCard(
children: <Widget>[
TextFormField(
autocorrect: false,
onSaved: (value) => address1 = value.trim(),
initialValue: client.address1,
controller: _address1Controller,
decoration: InputDecoration(
labelText: localization.address1,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => address2 = value.trim(),
initialValue: client.address2,
controller: _address2Controller,
decoration: InputDecoration(
labelText: localization.address2,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => city = value.trim(),
initialValue: client.city,
controller: _cityController,
decoration: InputDecoration(
labelText: localization.city,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => state = value.trim(),
initialValue: client.state,
controller: _stateController,
decoration: InputDecoration(
labelText: localization.state,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => postalCode = value.trim(),
initialValue: client.postalCode,
controller: _postalCodeController,
decoration: InputDecoration(
labelText: localization.postalCode,
),

View File

@ -1,93 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:invoiceninja/data/models/models.dart';
import 'package:invoiceninja/ui/app/form_card.dart';
import 'package:invoiceninja/ui/client/edit/client_edit_vm.dart';
import 'package:invoiceninja/utils/localization.dart';
import '../../app/form_card.dart';
class ClientEditContacts extends StatefulWidget {
class ClientEditContacts extends StatelessWidget {
ClientEditContacts({
Key key,
@required this.client,
@required this.viewModel,
}) : super(key: key);
final ClientEntity client;
@override
ClientEditContactsState createState() => new ClientEditContactsState();
}
class ClientEditContactsState extends State<ClientEditContacts>
with AutomaticKeepAliveClientMixin {
List<ContactEntity> contacts;
List<GlobalKey<ContactEditDetailsState>> contactKeys;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
var client = widget.client;
contacts = client.contacts.toList();
contactKeys = client.contacts
.map((contact) => GlobalKey<ContactEditDetailsState>())
.toList();
}
List<ContactEntity> getContacts() {
List<ContactEntity> contacts = [];
contactKeys.forEach((contactKey) {
if (contactKey.currentState != null) {
contacts.add(contactKey.currentState.getContact());
}
});
return contacts;
}
_onAddPressed() {
setState(() {
contacts.add(ContactEntity());
contactKeys.add(GlobalKey<ContactEditDetailsState>());
});
}
_onRemovePressed(GlobalKey<ContactEditDetailsState> key) {
setState(() {
var index = contactKeys.indexOf(key);
contactKeys.removeAt(index);
contacts.removeAt(index);
});
}
final ClientEditVM viewModel;
@override
Widget build(BuildContext context) {
var localization = AppLocalization.of(context);
List<Widget> items = [];
for (var i = 0; i < contacts.length; i++) {
var contact = contacts[i];
var contactKey = contactKeys[i];
items.add(ContactEditDetails(
var client = viewModel.client;
var contacts = client.contacts.map((contact) => ContactEditDetails(
viewModel: viewModel,
isRemoveVisible: client.contacts.length > 1,
contact: contact,
key: contactKey,
onRemovePressed: (key) => _onRemovePressed(key),
isRemoveVisible: contacts.length > 1,
));
}
items.add(Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 4.0),
child: RaisedButton(
elevation: 4.0,
color: Theme.of(context).primaryColor,
child: Text(localization.addContact.toUpperCase()),
onPressed: _onAddPressed,
),
));
index: client.contacts.indexOf(contact)));
return ListView(
children: items,
children: []
..addAll(client.contacts.map((contact) => Container()))
..addAll(contacts)
..add(Padding(
padding: const EdgeInsets.all(12.0),
child: RaisedButton(
elevation: 4.0,
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).secondaryHeaderColor,
child: Text(localization.addContact.toUpperCase()),
onPressed: viewModel.onAddContactClicked,
),
)),
);
}
}
@ -95,13 +44,15 @@ class ClientEditContactsState extends State<ClientEditContacts>
class ContactEditDetails extends StatefulWidget {
ContactEditDetails({
Key key,
@required this.index,
@required this.contact,
@required this.onRemovePressed,
@required this.viewModel,
@required this.isRemoveVisible,
}) : super(key: key);
final int index;
final ContactEntity contact;
final Function(GlobalKey<ContactEditDetailsState>) onRemovePressed;
final ClientEditVM viewModel;
final bool isRemoveVisible;
@override
@ -109,17 +60,55 @@ class ContactEditDetails extends StatefulWidget {
}
class ContactEditDetailsState extends State<ContactEditDetails> {
String _firstName;
String _lastName;
String _email;
String _phone;
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
ContactEntity getContact() {
return widget.contact.rebuild((b) => b
..firstName = _firstName
..lastName = _lastName
..email = _email
..phone = _phone);
var _controllers = [];
@override
void didChangeDependencies() {
_controllers = [
_firstNameController,
_lastNameController,
_emailController,
_phoneController,
];
_controllers.forEach((controller) => controller.removeListener(_onChanged));
var contact = widget.contact;
_firstNameController.text = contact.firstName;
_lastNameController.text = contact.lastName;
_emailController.text = contact.email;
_phoneController.text = contact.phone;
_controllers.forEach((controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_controllers.forEach((controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
_onChanged() {
var contact = widget.contact.rebuild((b) => b
..firstName = _firstNameController.text.trim()
..lastName = _lastNameController.text.trim()
..email = _emailController.text.trim()
..phone = _phoneController.text.trim());
if (contact != widget.contact) {
widget.viewModel.onChangedContact(contact, widget.index);
}
}
@override
@ -141,7 +130,7 @@ class ContactEditDetailsState extends State<ContactEditDetails> {
new FlatButton(
child: Text(localization.ok.toUpperCase()),
onPressed: () {
widget.onRemovePressed(widget.key);
widget.viewModel.onRemoveContactPressed(widget.index);
Navigator.pop(context);
})
],
@ -153,34 +142,32 @@ class ContactEditDetailsState extends State<ContactEditDetails> {
children: <Widget>[
TextFormField(
autocorrect: false,
initialValue: widget.contact.firstName,
onSaved: (value) => _firstName = value.trim(),
controller: _firstNameController,
decoration: InputDecoration(
labelText: localization.firstName,
),
),
TextFormField(
autocorrect: false,
initialValue: widget.contact.lastName,
onSaved: (value) => _lastName = value.trim(),
controller: _lastNameController,
decoration: InputDecoration(
labelText: localization.lastName,
),
),
TextFormField(
autocorrect: false,
initialValue: widget.contact.email,
onSaved: (value) => _email = value.trim(),
controller: _emailController,
decoration: InputDecoration(
labelText: localization.email,
),
keyboardType: TextInputType.emailAddress,
validator: (value) => value.isNotEmpty && ! value.contains('@') ? localization.emailIsInvalid : null,
validator: (value) => value.isNotEmpty && !value.contains('@')
? localization.emailIsInvalid
: null,
),
TextFormField(
autocorrect: false,
initialValue: widget.contact.phone,
onSaved: (value) => _phone = value.trim(),
controller: _phoneController,
decoration: InputDecoration(
labelText: localization.phone,
),

View File

@ -1,36 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:invoiceninja/data/models/models.dart';
import 'package:invoiceninja/ui/client/edit/client_edit_vm.dart';
import 'package:invoiceninja/utils/localization.dart';
import 'package:invoiceninja/ui/app/form_card.dart';
class ClientEditDetails extends StatefulWidget {
ClientEditDetails({
Key key,
@required this.client,
@required this.viewModel,
}) : super(key: key);
final ClientEntity client;
final ClientEditVM viewModel;
@override
ClientEditDetailsState createState() => new ClientEditDetailsState();
}
class ClientEditDetailsState extends State<ClientEditDetails>
with AutomaticKeepAliveClientMixin {
String name;
String idNumber;
String vatNumber;
String website;
String phone;
class ClientEditDetailsState extends State<ClientEditDetails> {
final _nameController = TextEditingController();
final _idNumberController = TextEditingController();
final _vatNumberController = TextEditingController();
final _websiteController = TextEditingController();
final _phoneController = TextEditingController();
var _controllers = [];
@override
bool get wantKeepAlive => true;
void didChangeDependencies() {
List<TextEditingController> _controllers = [
_nameController,
_idNumberController,
_vatNumberController,
_websiteController,
_phoneController,
];
_controllers.forEach((controller) => controller.removeListener(_onChanged));
var client = widget.viewModel.client;
_nameController.text = client.name;
_idNumberController.text = client.idNumber;
_vatNumberController.text = client.vatNumber;
_websiteController.text = client.website;
_phoneController.text = client.workPhone;
_controllers.forEach((controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_controllers.forEach((controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
_onChanged() {
var viewModel = widget.viewModel;
var client = viewModel.client.rebuild((b) => b
..name = _nameController.text.trim()
..idNumber = _idNumberController.text.trim()
..vatNumber = _vatNumberController.text.trim()
..website = _websiteController.text.trim()
..workPhone = _phoneController.text.trim()
);
if (client != viewModel.client) {
viewModel.onChanged(client);
}
}
@override
Widget build(BuildContext context) {
var localization = AppLocalization.of(context);
var client = widget.client;
return ListView(
shrinkWrap: true,
@ -39,32 +86,27 @@ class ClientEditDetailsState extends State<ClientEditDetails>
children: <Widget>[
TextFormField(
autocorrect: false,
onSaved: (value) => name = value.trim(),
initialValue: client.name,
controller: _nameController,
decoration: InputDecoration(
labelText: localization.name,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => idNumber = value.trim(),
initialValue: client.idNumber,
decoration: InputDecoration(
labelText: localization.idNumber,
controller: _idNumberController,
decoration: InputDecoration(labelText: localization.idNumber,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => vatNumber = value.trim(),
initialValue: client.vatNumber,
controller: _vatNumberController,
decoration: InputDecoration(
labelText: localization.vatNumber,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => website = value.trim(),
initialValue: client.website,
controller: _websiteController,
decoration: InputDecoration(
labelText: localization.website,
),
@ -72,8 +114,7 @@ class ClientEditDetailsState extends State<ClientEditDetails>
),
TextFormField(
autocorrect: false,
onSaved: (value) => phone = value.trim(),
initialValue: client.workPhone,
controller: _phoneController,
decoration: InputDecoration(
labelText: localization.phone,
),

View File

@ -8,77 +8,120 @@ class ClientEditShippingAddress extends StatefulWidget {
ClientEditShippingAddress({
Key key,
@required this.client,
@required this.onChanged,
}) : super(key: key);
final ClientEntity client;
final Function(ClientEntity) onChanged;
@override
ClientEditShippingAddressState createState() =>
new ClientEditShippingAddressState();
}
class ClientEditShippingAddressState extends State<ClientEditShippingAddress>
with AutomaticKeepAliveClientMixin {
String shippingAddress1;
String shippingAddress2;
String shippingCity;
String shippingState;
String shippingPostalCode;
class ClientEditShippingAddressState extends State<ClientEditShippingAddress> {
final _shippingAddress1Controller = TextEditingController();
final _shippingAddress2Controller = TextEditingController();
final _shippingCityController = TextEditingController();
final _shippingStateController = TextEditingController();
final _shippingPostalCodeController = TextEditingController();
var _controllers = [];
@override
bool get wantKeepAlive => true;
void didChangeDependencies() {
_controllers = [
_shippingAddress1Controller,
_shippingAddress2Controller,
_shippingCityController,
_shippingStateController,
_shippingPostalCodeController,
];
_controllers.forEach((controller) => controller.removeListener(_onChanged));
var client = widget.client;
_shippingAddress1Controller.text = client.shippingAddress1;
_shippingAddress2Controller.text = client.shippingAddress2;
_shippingCityController.text = client.shippingCity;
_shippingStateController.text = client.shippingState;
_shippingPostalCodeController.text = client.shippingPostalCode;
_controllers.forEach((controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_controllers.forEach((controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
_onChanged() {
var client = widget.client.rebuild((b) => b
..shippingAddress1 = _shippingAddress1Controller.text.trim()
..shippingAddress2 = _shippingAddress2Controller.text.trim()
..shippingCity = _shippingCityController.text.trim()
..shippingState = _shippingStateController.text.trim()
..shippingPostalCode = _shippingPostalCodeController.text.trim()
);
if (client != widget.client) {
widget.onChanged(client);
}
}
@override
Widget build(BuildContext context) {
var localization = AppLocalization.of(context);
var client = widget.client;
return ListView(children: <Widget>[
return ListView(shrinkWrap: true, children: <Widget>[
FormCard(
children: <Widget>[
TextFormField(
autocorrect: false,
onSaved: (value) => shippingAddress1 = value.trim(),
initialValue: client.shippingAddress1,
controller: _shippingAddress1Controller,
decoration: InputDecoration(
labelText: localization.address1,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => shippingAddress2 = value.trim(),
initialValue: client.shippingAddress2,
controller: _shippingAddress2Controller,
decoration: InputDecoration(
labelText: localization.address2,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => shippingCity = value.trim(),
initialValue: client.shippingCity,
controller: _shippingCityController,
decoration: InputDecoration(
labelText: localization.city,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => shippingState = value.trim(),
initialValue: client.shippingState,
controller: _shippingStateController,
decoration: InputDecoration(
labelText: localization.state,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => shippingPostalCode = value.trim(),
initialValue: client.shippingPostalCode,
controller: _shippingPostalCodeController,
decoration: InputDecoration(
labelText: localization.postalCode,
),
keyboardType: TextInputType.phone,
),
],
),
)
]);
}
}

View File

@ -33,12 +33,20 @@ class ClientEditScreen extends StatelessWidget {
class ClientEditVM {
final bool isLoading;
final ClientEntity client;
final Function(BuildContext, ClientEntity) onSaveClicked;
final Function(ClientEntity) onChanged;
final Function() onAddContactClicked;
final Function(int) onRemoveContactPressed;
final Function(ContactEntity, int) onChangedContact;
final Function(BuildContext) onSaveClicked;
final Function onBackClicked;
ClientEditVM({
@required this.isLoading,
@required this.client,
@required this.onAddContactClicked,
@required this.onRemoveContactPressed,
@required this.onChangedContact,
@required this.onChanged,
@required this.onSaveClicked,
@required this.onBackClicked,
});
@ -49,17 +57,28 @@ class ClientEditVM {
return ClientEditVM(
client: client,
isLoading: store.state.isLoading,
onBackClicked: () {
store.dispatch(UpdateCurrentRoute(ClientScreen.route));
onBackClicked: () =>
store.dispatch(UpdateCurrentRoute(ClientScreen.route)),
onAddContactClicked: () => store.dispatch(AddContact()),
onRemoveContactPressed: (index) => store.dispatch(DeleteContact(index)),
onChangedContact: (contact, index) {
print('== ON CHANGED');
print(store.state.clientUIState.selected);
print(contact);
store.dispatch(UpdateContact(contact: contact, index: index));
},
onSaveClicked: (BuildContext context, ClientEntity client) {
onChanged: (ClientEntity client) =>
store.dispatch(UpdateClient(client)),
onSaveClicked: (BuildContext context) {
final Completer<Null> completer = new Completer<Null>();
store.dispatch(SaveClientRequest(completer, client));
store.dispatch(
SaveClientRequest(completer: completer, client: client));
return completer.future.then((_) {
if (client.isNew()) {
Navigator.of(context).pop();
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => ClientViewScreen()));
Navigator
.of(context)
.push(MaterialPageRoute(builder: (_) => ClientViewScreen()));
} else {
Navigator.of(context).pop();
}

View File

@ -41,7 +41,7 @@ class _ClientViewState extends State<ClientView>
return Scaffold(
appBar: AppBar(
title: Text(widget.viewModel.client
.displayName), // Text(localizations.clientDetails),
.displayName ?? ''), // Text(localizations.clientDetails),
bottom: TabBar(
controller: _controller,
//isScrollable: true,

View File

@ -30,7 +30,7 @@ class ClientOverview extends StatelessWidget {
height: 6.0,
),
Text(
client.paidToDate.toStringAsFixed(2),
client.paidToDate?.toStringAsFixed(2) ?? '',
style: TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold,
@ -51,7 +51,7 @@ class ClientOverview extends StatelessWidget {
height: 6.0,
),
Text(
client.balance.toStringAsFixed(2),
client.balance?.toStringAsFixed(2) ?? '',
style: TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold,

View File

@ -61,7 +61,7 @@ class ClientViewVM {
},
onSaveClicked: (BuildContext context, ClientEntity client) {
final Completer<Null> completer = new Completer<Null>();
store.dispatch(SaveClientRequest(completer, client));
store.dispatch(SaveClientRequest(completer: completer, client: client));
return completer.future.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(

View File

@ -22,10 +22,6 @@ class _InvoiceEditState extends State<InvoiceEdit>
with SingleTickerProviderStateMixin {
TabController _controller;
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
static final GlobalKey<InvoiceEditDetailsState> _detailsKey =
GlobalKey<InvoiceEditDetailsState>();
static final GlobalKey<InvoiceEditItemsState> _itemsKey =
GlobalKey<InvoiceEditItemsState>();
@override
void initState() {
@ -48,13 +44,11 @@ class _InvoiceEditState extends State<InvoiceEdit>
List<Widget> editors = [
InvoiceEditDetails(
invoice: invoice,
key: _detailsKey,
clientList: viewModel.clientList,
clientMap: viewModel.clientMap,
),
InvoiceEditItems(
invoice: invoice,
key: _itemsKey,
),
];
@ -87,7 +81,7 @@ class _InvoiceEditState extends State<InvoiceEdit>
itemState?.getItems() ?? widget.viewModel.invoice.items));
*/
widget.viewModel.onSaveClicked(context, invoice);
widget.viewModel.onSaveClicked(context);
},
)
],

View File

@ -11,30 +11,70 @@ class InvoiceEditDetails extends StatefulWidget {
@required this.invoice,
@required this.clientList,
@required this.clientMap,
@required this.onChanged,
}) : super(key: key);
final InvoiceEntity invoice;
final List<int> clientList;
final BuiltMap<int, ClientEntity> clientMap;
final Function(InvoiceEntity) onChanged;
@override
InvoiceEditDetailsState createState() => new InvoiceEditDetailsState();
}
class InvoiceEditDetailsState extends State<InvoiceEditDetails>
with AutomaticKeepAliveClientMixin {
int clientId;
String invoiceDate;
String dueDate;
double partial;
String partialDate;
String invoiceNumber;
String poNumber;
double discount;
bool isAmountDiscount;
class InvoiceEditDetailsState extends State<InvoiceEditDetails> {
final _invoiceNumberController = TextEditingController();
final _poNumberController = TextEditingController();
final _discountController = TextEditingController();
final _partialController = TextEditingController();
var _controllers = [];
@override
bool get wantKeepAlive => true;
void didChangeDependencies() {
_controllers = [
_invoiceNumberController,
_poNumberController,
_discountController,
_partialController,
];
_controllers.forEach((controller) => controller.removeListener(_onChanged));
var invoice = widget.invoice;
_invoiceNumberController.text = invoice.invoiceNumber;
_poNumberController.text = invoice.poNumber;
_discountController.text = invoice.discount?.toStringAsFixed(2) ?? '';
_partialController.text = invoice.partial?.toStringAsFixed(2) ?? '';
_controllers.forEach((controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_controllers.forEach((controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
_onChanged() {
var invoice = widget.invoice.rebuild((b) => b
..poNumber = _poNumberController.text.trim()
..discount = double.tryParse(_discountController.text) ?? 0.0
..partial = double.tryParse(_partialController.text) ?? 0.0
);
if (invoice != widget.invoice) {
widget.onChanged(invoice);
}
}
@override
Widget build(BuildContext context) {
@ -52,24 +92,21 @@ class InvoiceEditDetailsState extends State<InvoiceEditDetails>
entityMap: widget.clientMap,
) : TextFormField(
autocorrect: false,
onSaved: (value) => invoiceNumber = value.trim(),
initialValue: invoice.invoiceNumber,
controller: _invoiceNumberController,
decoration: InputDecoration(
labelText: localization.invoiceNumber,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => poNumber = value.trim(),
initialValue: invoice.poNumber,
controller: _poNumberController,
decoration: InputDecoration(
labelText: localization.poNumber,
),
),
TextFormField(
autocorrect: false,
onSaved: (value) => discount = double.tryParse(value) ?? 0.0,
initialValue: invoice.discount?.toStringAsFixed(2),
controller: _discountController,
decoration: InputDecoration(
labelText: localization.discount,
),
@ -77,8 +114,7 @@ class InvoiceEditDetailsState extends State<InvoiceEditDetails>
),
TextFormField(
autocorrect: false,
onSaved: (value) => partial = double.tryParse(value) ?? 0.0,
initialValue: invoice.partial?.toStringAsFixed(2),
controller: _partialController,
decoration: InputDecoration(
labelText: localization.partial,
),

View File

@ -3,91 +3,19 @@ import 'package:invoiceninja/data/models/models.dart';
import 'package:invoiceninja/utils/localization.dart';
import 'package:invoiceninja/ui/app/form_card.dart';
class InvoiceEditItems extends StatefulWidget {
class InvoiceEditItems extends StatelessWidget {
InvoiceEditItems({
Key key,
@required this.invoice,
@required this.onChanged,
}) : super(key: key);
final InvoiceEntity invoice;
@override
InvoiceEditItemsState createState() => new InvoiceEditItemsState();
}
class InvoiceEditItemsState extends State<InvoiceEditItems>
with AutomaticKeepAliveClientMixin {
List<InvoiceItemEntity> invoiceItems;
List<GlobalKey<ItemEditDetailsState>> invoiceItemKeys;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
var invoice = widget.invoice;
invoiceItems = invoice.invoiceItems.toList();
invoiceItemKeys = invoice.invoiceItems
.map((invoiceItem) => GlobalKey<ItemEditDetailsState>())
.toList();
}
List<InvoiceItemEntity> getItems() {
List<InvoiceItemEntity> invoiceItems = [];
invoiceItemKeys.forEach((invoiceItemKey) {
if (invoiceItemKey.currentState != null) {
invoiceItems.add(invoiceItemKey.currentState.getItem());
}
});
return invoiceItems;
}
_onAddPressed() {
setState(() {
invoiceItems.add(InvoiceItemEntity());
invoiceItemKeys.add(GlobalKey<ItemEditDetailsState>());
});
}
_onRemovePressed(GlobalKey<ItemEditDetailsState> key) {
setState(() {
var index = invoiceItemKeys.indexOf(key);
invoiceItemKeys.removeAt(index);
invoiceItems.removeAt(index);
});
}
final Function(InvoiceEntity) onChanged;
@override
Widget build(BuildContext context) {
var localization = AppLocalization.of(context);
List<Widget> widgets = [];
for (var i = 0; i < invoiceItems.length; i++) {
var invoiceItem = invoiceItems[i];
var invoiceItemKey = invoiceItemKeys[i];
widgets.add(ItemEditDetails(
invoiceItem: invoiceItem,
key: invoiceItemKey,
onRemovePressed: (key) => _onRemovePressed(key),
isRemoveVisible: invoiceItems.length > 1,
));
}
widgets.add(Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 4.0),
child: RaisedButton(
elevation: 4.0,
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).secondaryHeaderColor,
child: Text(localization.addItem.toUpperCase()),
onPressed: _onAddPressed,
),
));
return ListView(
children: widgets,
);
return new Container();
}
}
@ -108,10 +36,54 @@ class ItemEditDetails extends StatefulWidget {
}
class ItemEditDetailsState extends State<ItemEditDetails> {
String _productKey;
String _notes;
double _cost;
double _qty;
final _productKeyController = TextEditingController();
final _notesController = TextEditingController();
final _costController = TextEditingController();
final _qtyController = TextEditingController();
var _controllers = [];
@override
void didChangeDependencies() {
_controllers = [
_productKeyController,
_notesController,
_costController,
_qtyController,
];
_controllers.forEach((controller) => controller.removeListener(_onChanged));
//var client = widget.client;
//_nameController.text = client.name;
_controllers.forEach((controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_controllers.forEach((controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
_onChanged() {
/*
var client = widget.client.rebuild((b) => b
..name = _nameController.text.trim()
);
if (client != widget.client) {
widget.onChanged(client);
}
*/
}
InvoiceItemEntity getItem() {
return widget.invoiceItem.rebuild((b) => b
@ -150,33 +122,25 @@ class ItemEditDetailsState extends State<ItemEditDetails> {
children: <Widget>[
TextFormField(
autocorrect: false,
initialValue: widget.invoiceItem.productKey,
onSaved: (value) => _productKey = value.trim(),
decoration: InputDecoration(
labelText: localization.product,
),
),
TextFormField(
autocorrect: false,
initialValue: widget.invoiceItem.notes,
maxLines: 4,
onSaved: (value) => _notes = value.trim(),
decoration: InputDecoration(
labelText: localization.description,
),
),
TextFormField(
autocorrect: false,
initialValue: widget.invoiceItem.cost?.toStringAsFixed(2),
onSaved: (value) => _cost = double.tryParse(value) ?? 0.0,
decoration: InputDecoration(
labelText: localization.unitCost,
),
),
TextFormField(
autocorrect: false,
initialValue: widget.invoiceItem.qty?.toStringAsFixed(2),
onSaved: (value) => _qty = double.tryParse(value) ?? 0.0,
decoration: InputDecoration(
labelText: localization.quantity,
),

View File

@ -38,23 +38,21 @@ class InvoiceEditVM {
final InvoiceEntity invoice;
final List<int> clientList;
final BuiltMap<int, ClientEntity> clientMap;
final Function onDelete;
final Function(BuildContext, InvoiceEntity) onSaveClicked;
final Function(InvoiceEntity) onChanged;
final Function(BuildContext) onSaveClicked;
final Function(BuildContext, EntityAction) onActionSelected;
final Function onBackClicked;
final bool isLoading;
final bool isDirty;
InvoiceEditVM({
@required this.invoice,
@required this.clientList,
@required this.clientMap,
@required this.onDelete,
@required this.onChanged,
@required this.onSaveClicked,
@required this.onBackClicked,
@required this.onActionSelected,
@required this.isLoading,
@required this.isDirty,
});
factory InvoiceEditVM.fromStore(Store<AppState> store) {
@ -62,53 +60,55 @@ class InvoiceEditVM {
final invoice = state.invoiceUIState.selected;
return InvoiceEditVM(
isLoading: state.isLoading,
isDirty: invoice.isNew(),
invoice: invoice,
clientList: memoizedActiveClientList(state.clientState.map, state.clientState.list),
clientMap: state.clientState.map,
onDelete: () => false,
isLoading: state.isLoading,
invoice: invoice,
clientList: memoizedActiveClientList(
state.clientState.map, state.clientState.list),
clientMap: state.clientState.map,
onBackClicked: () {
store.dispatch(UpdateCurrentRoute(InvoiceScreen.route));
},
onSaveClicked: (BuildContext context, InvoiceEntity invoice) {
final Completer<Null> completer = new Completer<Null>();
store.dispatch(SaveInvoiceRequest(completer, invoice));
return completer.future.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: invoice.isNew()
? AppLocalization.of(context).successfullyCreatedInvoice
: AppLocalization.of(context).successfullyUpdatedInvoice,
),
duration: Duration(seconds: 3)));
onChanged: (InvoiceEntity invoice) {
store.dispatch(UpdateInvoice(invoice));
},
onSaveClicked: (BuildContext context) {
final Completer<Null> completer = new Completer<Null>();
store.dispatch(
SaveInvoiceRequest(completer: completer, invoice: invoice));
return completer.future.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: invoice.isNew()
? AppLocalization.of(context).successfullyCreatedInvoice
: AppLocalization.of(context).successfullyUpdatedInvoice,
),
duration: Duration(seconds: 3)));
});
},
onActionSelected: (BuildContext context, EntityAction action) {
final Completer<Null> completer = new Completer<Null>();
var message = '';
switch (action) {
case EntityAction.archive:
store.dispatch(ArchiveInvoiceRequest(completer, invoice.id));
message = AppLocalization.of(context).successfullyArchivedInvoice;
break;
case EntityAction.delete:
store.dispatch(DeleteInvoiceRequest(completer, invoice.id));
message = AppLocalization.of(context).successfullyDeletedInvoice;
break;
case EntityAction.restore:
store.dispatch(RestoreInvoiceRequest(completer, invoice.id));
message = AppLocalization.of(context).successfullyRestoredInvoice;
break;
}
return completer.future.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: message,
),
duration: Duration(seconds: 3)));
});
});
},
onActionSelected: (BuildContext context, EntityAction action) {
final Completer<Null> completer = new Completer<Null>();
var message = '';
switch (action) {
case EntityAction.archive:
store.dispatch(ArchiveInvoiceRequest(completer, invoice.id));
message = AppLocalization.of(context).successfullyArchivedInvoice;
break;
case EntityAction.delete:
store.dispatch(DeleteInvoiceRequest(completer, invoice.id));
message = AppLocalization.of(context).successfullyDeletedInvoice;
break;
case EntityAction.restore:
store.dispatch(RestoreInvoiceRequest(completer, invoice.id));
message = AppLocalization.of(context).successfullyRestoredInvoice;
break;
}
return completer.future.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: message,
),
duration: Duration(seconds: 3)));
});
}
);
}
}

View File

@ -25,34 +25,35 @@ class _ProductEditState extends State<ProductEdit> {
final _notesController = TextEditingController();
final _costController = TextEditingController();
var _controllers = [];
@override
void didChangeDependencies() {
_productKeyController.removeListener(_onChanged);
_notesController.removeListener(_onChanged);
_costController.removeListener(_onChanged);
_controllers = [
_productKeyController,
_notesController,
_costController,
];
_controllers.forEach((controller) => controller.removeListener(_onChanged));
var product = widget.viewModel.product;
_productKeyController.text = product.productKey;
_notesController.text = product.notes;
_costController.text = product.cost?.toStringAsFixed(2) ?? '';
_productKeyController.addListener(_onChanged);
_notesController.addListener(_onChanged);
_costController.addListener(_onChanged);
_controllers.forEach((controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_productKeyController.removeListener(_onChanged);
_notesController.removeListener(_onChanged);
_costController.removeListener(_onChanged);
_productKeyController.dispose();
_notesController.dispose();
_costController.dispose();
_controllers.forEach((controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}

View File

@ -66,7 +66,7 @@ class ProductEditVM {
},
onSaveClicked: (BuildContext context) {
final Completer<Null> completer = new Completer<Null>();
store.dispatch(SaveProductRequest(completer));
store.dispatch(SaveProductRequest(completer: completer, product: product));
return completer.future.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(