diff --git a/.idea/runConfigurations/main_dart.xml b/.idea/runConfigurations/main_dart.xml index aab7b5cd8..934fdfcff 100644 --- a/.idea/runConfigurations/main_dart.xml +++ b/.idea/runConfigurations/main_dart.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/lib/data/models/serializers.dart b/lib/data/models/serializers.dart index 28e2f7a9f..dd4a48ceb 100644 --- a/lib/data/models/serializers.dart +++ b/lib/data/models/serializers.dart @@ -23,6 +23,8 @@ import 'package:invoiceninja_flutter/redux/ui/ui_state.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_state.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/vendor/vendor_state.dart'; + import 'package:invoiceninja_flutter/redux/task/task_state.dart'; import 'package:invoiceninja_flutter/redux/project/project_state.dart'; import 'package:invoiceninja_flutter/redux/payment/payment_state.dart'; @@ -76,6 +78,8 @@ part 'serializers.g.dart'; TimezoneItemResponse, TimezoneListResponse, // STARTER: serializers - do not remove comment +VendorEntity, + TaskEntity, ProjectEntity, PaymentEntity, diff --git a/lib/data/models/serializers.g.dart b/lib/data/models/serializers.g.dart index 0f1120e90..c5c17fa2d 100644 --- a/lib/data/models/serializers.g.dart +++ b/lib/data/models/serializers.g.dart @@ -127,6 +127,8 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(VendorEntity.serializer) ..add(VendorItemResponse.serializer) ..add(VendorListResponse.serializer) + ..add(VendorState.serializer) + ..add(VendorUIState.serializer) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(ActivityEntity)]), () => new ListBuilder()) @@ -363,5 +365,7 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(ProjectEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(TaskEntity)]), () => new MapBuilder()) + ..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder()) + ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(VendorEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder())) .build(); diff --git a/lib/data/repositories/vendor_repository.dart b/lib/data/repositories/vendor_repository.dart new file mode 100644 index 000000000..4d234ff92 --- /dev/null +++ b/lib/data/repositories/vendor_repository.dart @@ -0,0 +1,70 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:core'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/serializers.dart'; +import 'package:invoiceninja_flutter/redux/auth/auth_state.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/data/web_client.dart'; + +class VendorRepository { + + const VendorRepository({ + this.webClient = const WebClient(), + }); + + final WebClient webClient; + + Future loadItem( + CompanyEntity company, AuthState auth, int entityId) async { + final dynamic response = await webClient.get( + '${auth.url}/vendors/$entityId', company.token); + + final VendorItemResponse vendorResponse = + serializers.deserializeWith(VendorItemResponse.serializer, response); + + return vendorResponse.data; + } + + Future> loadList( + CompanyEntity company, AuthState auth, int updatedAt) async { + String url = auth.url + '/vendors?'; + + if (updatedAt > 0) { + url += '&updated_at=${updatedAt - kUpdatedAtBufferSeconds}'; + } + + final dynamic response = await webClient.get(url, company.token); + + final VendorListResponse vendorResponse = + serializers.deserializeWith(VendorListResponse.serializer, response); + + return vendorResponse.data; + } + + Future saveData( + CompanyEntity company, AuthState auth, VendorEntity vendor, + [EntityAction action]) async { + final data = serializers.serializeWith(VendorEntity.serializer, vendor); + dynamic response; + + if (vendor.isNew) { + response = await webClient.post( + auth.url + '/vendors', + company.token, + json.encode(data)); + } else { + var url = auth.url + '/vendors/' + vendor.id.toString(); + if (action != null) { + url += '?action=' + action.toString(); + } + response = await webClient.put(url, company.token, json.encode(data)); + } + + final VendorItemResponse vendorResponse = + serializers.deserializeWith(VendorItemResponse.serializer, response); + + return vendorResponse.data; + } +} diff --git a/lib/main.dart b/lib/main.dart index 7f88a5d7f..c1dd59b35 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,6 +43,12 @@ import 'package:local_auth/local_auth.dart'; //import 'package:quick_actions/quick_actions.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/ui/vendor/vendor_screen.dart'; +import 'package:invoiceninja_flutter/ui/vendor/edit/vendor_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/vendor/view/vendor_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_middleware.dart'; + import 'package:invoiceninja_flutter/ui/task/task_screen.dart'; import 'package:invoiceninja_flutter/ui/task/edit/task_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/task/view/task_view_vm.dart'; @@ -91,6 +97,8 @@ void main() async { ..addAll(createStoreInvoicesMiddleware()) ..addAll(createStorePersistenceMiddleware()) // STARTER: middleware - do not remove comment +..addAll(createStoreVendorsMiddleware()) + ..addAll(createStoreTasksMiddleware()) ..addAll(createStoreProjectsMiddleware()) ..addAll(createStorePaymentsMiddleware()) @@ -299,6 +307,13 @@ class InvoiceNinjaAppState extends State { InvoiceEditScreen.route: (context) => InvoiceEditScreen(), InvoiceEmailScreen.route: (context) => InvoiceEmailScreen(), // STARTER: routes - do not remove comment +VendorScreen.route: (context) { +widget.store.dispatch(LoadVendors()); +return VendorScreen(); +}, +VendorViewScreen.route: (context) => VendorViewScreen(), +VendorEditScreen.route: (context) => VendorEditScreen(), + TaskScreen.route: (context) { widget.store.dispatch(LoadTasks()); return TaskScreen(); diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index 9636b9847..4410abb88 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -13,6 +13,8 @@ import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/vendor/vendor_state.dart'; + import 'package:invoiceninja_flutter/redux/task/task_state.dart'; import 'package:invoiceninja_flutter/redux/project/project_state.dart'; @@ -107,6 +109,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceUIState; // STARTER: states switch - do not remove comment +case EntityType.vendor: +return vendorUIState; + case EntityType.task: return taskUIState; @@ -145,6 +150,11 @@ abstract class AppState implements Built { ListUIState get invoiceListState => uiState.invoiceUIState.listUIState; // STARTER: state getters - do not remove comment +VendorState get vendorState => selectedCompanyState.vendorState; +ListUIState get vendorListState => uiState.vendorUIState.listUIState; +VendorUIState get vendorUIState => uiState.vendorUIState; + + TaskState get taskState => selectedCompanyState.taskState; ListUIState get taskListState => uiState.taskUIState.listUIState; diff --git a/lib/redux/company/company_reducer.dart b/lib/redux/company/company_reducer.dart index 4e229d962..f4485f0c9 100644 --- a/lib/redux/company/company_reducer.dart +++ b/lib/redux/company/company_reducer.dart @@ -9,6 +9,8 @@ import 'package:invoiceninja_flutter/redux/dashboard/dashboard_reducer.dart'; import 'package:invoiceninja_flutter/redux/company/company_actions.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/vendor/vendor_reducer.dart'; + import 'package:invoiceninja_flutter/redux/task/task_reducer.dart'; import 'package:invoiceninja_flutter/redux/project/project_reducer.dart'; @@ -29,6 +31,8 @@ CompanyState companyReducer(CompanyState state, dynamic action) { ..productState.replace(productsReducer(state.productState, action)) ..invoiceState.replace(invoicesReducer(state.invoiceState, action)) // STARTER: reducer - do not remove comment +..vendorState.replace(vendorsReducer(state.vendorState, action)) + ..taskState.replace(tasksReducer(state.taskState, action)) ..projectState.replace(projectsReducer(state.projectState, action)) ..paymentState.replace(paymentsReducer(state.paymentState, action)) diff --git a/lib/redux/company/company_state.dart b/lib/redux/company/company_state.dart index 518a38f00..6eba752fe 100644 --- a/lib/redux/company/company_state.dart +++ b/lib/redux/company/company_state.dart @@ -7,6 +7,8 @@ import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/vendor/vendor_state.dart'; + import 'package:invoiceninja_flutter/redux/task/task_state.dart'; import 'package:invoiceninja_flutter/redux/project/project_state.dart'; @@ -27,6 +29,8 @@ abstract class CompanyState clientState: ClientState(), invoiceState: InvoiceState(), // STARTER: constructor - do not remove comment +vendorState: VendorState(), + taskState: TaskState(), projectState: ProjectState(), paymentState: PaymentState(), @@ -48,6 +52,8 @@ abstract class CompanyState InvoiceState get invoiceState; // STARTER: fields - do not remove comment +VendorState get vendorState; + TaskState get taskState; ProjectState get projectState; diff --git a/lib/redux/company/company_state.g.dart b/lib/redux/company/company_state.g.dart index cc215319f..53fc1676d 100644 --- a/lib/redux/company/company_state.g.dart +++ b/lib/redux/company/company_state.g.dart @@ -44,6 +44,9 @@ class _$CompanyStateSerializer implements StructuredSerializer { 'invoiceState', serializers.serialize(object.invoiceState, specifiedType: const FullType(InvoiceState)), + 'vendorState', + serializers.serialize(object.vendorState, + specifiedType: const FullType(VendorState)), 'taskState', serializers.serialize(object.taskState, specifiedType: const FullType(TaskState)), @@ -98,6 +101,10 @@ class _$CompanyStateSerializer implements StructuredSerializer { result.invoiceState.replace(serializers.deserialize(value, specifiedType: const FullType(InvoiceState)) as InvoiceState); break; + case 'vendorState': + result.vendorState.replace(serializers.deserialize(value, + specifiedType: const FullType(VendorState)) as VendorState); + break; case 'taskState': result.taskState.replace(serializers.deserialize(value, specifiedType: const FullType(TaskState)) as TaskState); @@ -133,6 +140,8 @@ class _$CompanyState extends CompanyState { @override final InvoiceState invoiceState; @override + final VendorState vendorState; + @override final TaskState taskState; @override final ProjectState projectState; @@ -150,6 +159,7 @@ class _$CompanyState extends CompanyState { this.productState, this.clientState, this.invoiceState, + this.vendorState, this.taskState, this.projectState, this.paymentState, @@ -167,6 +177,9 @@ class _$CompanyState extends CompanyState { if (invoiceState == null) { throw new BuiltValueNullFieldError('CompanyState', 'invoiceState'); } + if (vendorState == null) { + throw new BuiltValueNullFieldError('CompanyState', 'vendorState'); + } if (taskState == null) { throw new BuiltValueNullFieldError('CompanyState', 'taskState'); } @@ -197,6 +210,7 @@ class _$CompanyState extends CompanyState { productState == other.productState && clientState == other.clientState && invoiceState == other.invoiceState && + vendorState == other.vendorState && taskState == other.taskState && projectState == other.projectState && paymentState == other.paymentState && @@ -212,11 +226,13 @@ class _$CompanyState extends CompanyState { $jc( $jc( $jc( - $jc($jc(0, company.hashCode), - dashboardState.hashCode), - productState.hashCode), - clientState.hashCode), - invoiceState.hashCode), + $jc( + $jc($jc(0, company.hashCode), + dashboardState.hashCode), + productState.hashCode), + clientState.hashCode), + invoiceState.hashCode), + vendorState.hashCode), taskState.hashCode), projectState.hashCode), paymentState.hashCode), @@ -231,6 +247,7 @@ class _$CompanyState extends CompanyState { ..add('productState', productState) ..add('clientState', clientState) ..add('invoiceState', invoiceState) + ..add('vendorState', vendorState) ..add('taskState', taskState) ..add('projectState', projectState) ..add('paymentState', paymentState) @@ -272,6 +289,12 @@ class CompanyStateBuilder set invoiceState(InvoiceStateBuilder invoiceState) => _$this._invoiceState = invoiceState; + VendorStateBuilder _vendorState; + VendorStateBuilder get vendorState => + _$this._vendorState ??= new VendorStateBuilder(); + set vendorState(VendorStateBuilder vendorState) => + _$this._vendorState = vendorState; + TaskStateBuilder _taskState; TaskStateBuilder get taskState => _$this._taskState ??= new TaskStateBuilder(); @@ -304,6 +327,7 @@ class CompanyStateBuilder _productState = _$v.productState?.toBuilder(); _clientState = _$v.clientState?.toBuilder(); _invoiceState = _$v.invoiceState?.toBuilder(); + _vendorState = _$v.vendorState?.toBuilder(); _taskState = _$v.taskState?.toBuilder(); _projectState = _$v.projectState?.toBuilder(); _paymentState = _$v.paymentState?.toBuilder(); @@ -337,6 +361,7 @@ class CompanyStateBuilder productState: productState.build(), clientState: clientState.build(), invoiceState: invoiceState.build(), + vendorState: vendorState.build(), taskState: taskState.build(), projectState: projectState.build(), paymentState: paymentState.build(), @@ -354,6 +379,8 @@ class CompanyStateBuilder clientState.build(); _$failedField = 'invoiceState'; invoiceState.build(); + _$failedField = 'vendorState'; + vendorState.build(); _$failedField = 'taskState'; taskState.build(); _$failedField = 'projectState'; diff --git a/lib/redux/ui/ui_reducer.dart b/lib/redux/ui/ui_reducer.dart index e97ef3309..1a459b75a 100644 --- a/lib/redux/ui/ui_reducer.dart +++ b/lib/redux/ui/ui_reducer.dart @@ -9,6 +9,8 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_reducer.dart'; import 'package:redux/redux.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/vendor/vendor_reducer.dart'; + import 'package:invoiceninja_flutter/redux/task/task_reducer.dart'; import 'package:invoiceninja_flutter/redux/project/project_reducer.dart'; @@ -34,6 +36,8 @@ UIState uiReducer(UIState state, dynamic action) { ..dashboardUIState .replace(dashboardUIReducer(state.dashboardUIState, action)) // STARTER: reducer - do not remove comment +..vendorUIState.replace(vendorUIReducer(state.vendorUIState, action)) + ..taskUIState.replace(taskUIReducer(state.taskUIState, action)) ..projectUIState.replace(projectUIReducer(state.projectUIState, action)) ..paymentUIState.replace(paymentUIReducer(state.paymentUIState, action)) diff --git a/lib/redux/ui/ui_state.dart b/lib/redux/ui/ui_state.dart index 5e6084af8..b5a1cdc27 100644 --- a/lib/redux/ui/ui_state.dart +++ b/lib/redux/ui/ui_state.dart @@ -8,6 +8,8 @@ import 'package:invoiceninja_flutter/redux/product/product_state.dart'; import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/vendor/vendor_state.dart'; + import 'package:invoiceninja_flutter/redux/task/task_state.dart'; import 'package:invoiceninja_flutter/redux/project/project_state.dart'; @@ -33,6 +35,8 @@ abstract class UIState implements Built { clientUIState: ClientUIState(), invoiceUIState: InvoiceUIState(), // STARTER: constructor - do not remove comment +vendorUIState: VendorUIState(), + taskUIState: TaskUIState(), projectUIState: ProjectUIState(), paymentUIState: PaymentUIState(), @@ -66,6 +70,8 @@ abstract class UIState implements Built { String get filter; // STARTER: properties - do not remove comment +VendorUIState get vendorUIState; + TaskUIState get taskUIState; ProjectUIState get projectUIState; diff --git a/lib/redux/ui/ui_state.g.dart b/lib/redux/ui/ui_state.g.dart index 06c1371af..985f325d7 100644 --- a/lib/redux/ui/ui_state.g.dart +++ b/lib/redux/ui/ui_state.g.dart @@ -61,6 +61,9 @@ class _$UIStateSerializer implements StructuredSerializer { 'invoiceUIState', serializers.serialize(object.invoiceUIState, specifiedType: const FullType(InvoiceUIState)), + 'vendorUIState', + serializers.serialize(object.vendorUIState, + specifiedType: const FullType(VendorUIState)), 'taskUIState', serializers.serialize(object.taskUIState, specifiedType: const FullType(TaskUIState)), @@ -140,6 +143,10 @@ class _$UIStateSerializer implements StructuredSerializer { result.filter = serializers.deserialize(value, specifiedType: const FullType(String)) as String; break; + case 'vendorUIState': + result.vendorUIState.replace(serializers.deserialize(value, + specifiedType: const FullType(VendorUIState)) as VendorUIState); + break; case 'taskUIState': result.taskUIState.replace(serializers.deserialize(value, specifiedType: const FullType(TaskUIState)) as TaskUIState); @@ -187,6 +194,8 @@ class _$UIState extends UIState { @override final String filter; @override + final VendorUIState vendorUIState; + @override final TaskUIState taskUIState; @override final ProjectUIState projectUIState; @@ -210,6 +219,7 @@ class _$UIState extends UIState { this.clientUIState, this.invoiceUIState, this.filter, + this.vendorUIState, this.taskUIState, this.projectUIState, this.paymentUIState, @@ -245,6 +255,9 @@ class _$UIState extends UIState { if (invoiceUIState == null) { throw new BuiltValueNullFieldError('UIState', 'invoiceUIState'); } + if (vendorUIState == null) { + throw new BuiltValueNullFieldError('UIState', 'vendorUIState'); + } if (taskUIState == null) { throw new BuiltValueNullFieldError('UIState', 'taskUIState'); } @@ -281,6 +294,7 @@ class _$UIState extends UIState { clientUIState == other.clientUIState && invoiceUIState == other.invoiceUIState && filter == other.filter && + vendorUIState == other.vendorUIState && taskUIState == other.taskUIState && projectUIState == other.projectUIState && paymentUIState == other.paymentUIState && @@ -304,22 +318,24 @@ class _$UIState extends UIState { $jc( $jc( $jc( - 0, - selectedCompanyIndex + $jc( + 0, + selectedCompanyIndex + .hashCode), + currentRoute .hashCode), - currentRoute + enableDarkMode .hashCode), - enableDarkMode + requireAuthentication .hashCode), - requireAuthentication - .hashCode), - emailPayment.hashCode), - autoStartTasks.hashCode), - dashboardUIState.hashCode), - productUIState.hashCode), - clientUIState.hashCode), - invoiceUIState.hashCode), - filter.hashCode), + emailPayment.hashCode), + autoStartTasks.hashCode), + dashboardUIState.hashCode), + productUIState.hashCode), + clientUIState.hashCode), + invoiceUIState.hashCode), + filter.hashCode), + vendorUIState.hashCode), taskUIState.hashCode), projectUIState.hashCode), paymentUIState.hashCode), @@ -340,6 +356,7 @@ class _$UIState extends UIState { ..add('clientUIState', clientUIState) ..add('invoiceUIState', invoiceUIState) ..add('filter', filter) + ..add('vendorUIState', vendorUIState) ..add('taskUIState', taskUIState) ..add('projectUIState', projectUIState) ..add('paymentUIState', paymentUIState) @@ -407,6 +424,12 @@ class UIStateBuilder implements Builder { String get filter => _$this._filter; set filter(String filter) => _$this._filter = filter; + VendorUIStateBuilder _vendorUIState; + VendorUIStateBuilder get vendorUIState => + _$this._vendorUIState ??= new VendorUIStateBuilder(); + set vendorUIState(VendorUIStateBuilder vendorUIState) => + _$this._vendorUIState = vendorUIState; + TaskUIStateBuilder _taskUIState; TaskUIStateBuilder get taskUIState => _$this._taskUIState ??= new TaskUIStateBuilder(); @@ -446,6 +469,7 @@ class UIStateBuilder implements Builder { _clientUIState = _$v.clientUIState?.toBuilder(); _invoiceUIState = _$v.invoiceUIState?.toBuilder(); _filter = _$v.filter; + _vendorUIState = _$v.vendorUIState?.toBuilder(); _taskUIState = _$v.taskUIState?.toBuilder(); _projectUIState = _$v.projectUIState?.toBuilder(); _paymentUIState = _$v.paymentUIState?.toBuilder(); @@ -485,6 +509,7 @@ class UIStateBuilder implements Builder { clientUIState: clientUIState.build(), invoiceUIState: invoiceUIState.build(), filter: filter, + vendorUIState: vendorUIState.build(), taskUIState: taskUIState.build(), projectUIState: projectUIState.build(), paymentUIState: paymentUIState.build(), @@ -501,6 +526,8 @@ class UIStateBuilder implements Builder { _$failedField = 'invoiceUIState'; invoiceUIState.build(); + _$failedField = 'vendorUIState'; + vendorUIState.build(); _$failedField = 'taskUIState'; taskUIState.build(); _$failedField = 'projectUIState'; diff --git a/lib/redux/vendor/vendor_actions.dart b/lib/redux/vendor/vendor_actions.dart new file mode 100644 index 000000000..b49d498d6 --- /dev/null +++ b/lib/redux/vendor/vendor_actions.dart @@ -0,0 +1,226 @@ +import 'dart:async'; +import 'package:flutter/widgets.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; + +class ViewVendorList implements PersistUI { + ViewVendorList(this.context); + + final BuildContext context; +} + +class ViewVendor implements PersistUI { + ViewVendor({this.vendorId, this.context}); + + final int vendorId; + final BuildContext context; +} + +class EditVendor implements PersistUI { + EditVendor({this.vendor, this.context, this.completer, this.trackRoute = true}); + + final VendorEntity vendor; + final BuildContext context; + final Completer completer; + final bool trackRoute; +} + +class UpdateVendor implements PersistUI { + UpdateVendor(this.vendor); + + final VendorEntity vendor; +} + +class LoadVendor { + LoadVendor({this.completer, this.vendorId, this.loadActivities = false}); + + final Completer completer; + final int vendorId; + final bool loadActivities; +} + +class LoadVendorActivity { + LoadVendorActivity({this.completer, this.vendorId}); + + final Completer completer; + final int vendorId; +} + +class LoadVendors { + LoadVendors({this.completer, this.force = false}); + + final Completer completer; + final bool force; +} + +class LoadVendorRequest implements StartLoading {} + +class LoadVendorFailure implements StopLoading { + LoadVendorFailure(this.error); + + final dynamic error; + + @override + String toString() { + return 'LoadVendorFailure{error: $error}'; + } +} + +class LoadVendorSuccess implements StopLoading, PersistData { + LoadVendorSuccess(this.vendor); + + final VendorEntity vendor; + + @override + String toString() { + return 'LoadVendorSuccess{vendor: $vendor}'; + } +} + +class LoadVendorsRequest implements StartLoading {} + +class LoadVendorsFailure implements StopLoading { + LoadVendorsFailure(this.error); + + final dynamic error; + + @override + String toString() { + return 'LoadVendorsFailure{error: $error}'; + } +} + +class LoadVendorsSuccess implements StopLoading, PersistData { + LoadVendorsSuccess(this.vendors); + + final BuiltList vendors; + + @override + String toString() { + return 'LoadVendorsSuccess{vendors: $vendors}'; + } +} + + +class SaveVendorRequest implements StartSaving { + SaveVendorRequest({this.completer, this.vendor}); + + final Completer completer; + final VendorEntity vendor; +} + +class SaveVendorSuccess implements StopSaving, PersistData, PersistUI { + SaveVendorSuccess(this.vendor); + + final VendorEntity vendor; +} + +class AddVendorSuccess implements StopSaving, PersistData, PersistUI { + AddVendorSuccess(this.vendor); + + final VendorEntity vendor; +} + +class SaveVendorFailure implements StopSaving { + SaveVendorFailure (this.error); + + final Object error; +} + +class ArchiveVendorRequest implements StartSaving { + ArchiveVendorRequest(this.completer, this.vendorId); + + final Completer completer; + final int vendorId; +} + +class ArchiveVendorSuccess implements StopSaving, PersistData { + ArchiveVendorSuccess(this.vendor); + + final VendorEntity vendor; +} + +class ArchiveVendorFailure implements StopSaving { + ArchiveVendorFailure(this.vendor); + + final VendorEntity vendor; +} + +class DeleteVendorRequest implements StartSaving { + DeleteVendorRequest(this.completer, this.vendorId); + + final Completer completer; + final int vendorId; +} + +class DeleteVendorSuccess implements StopSaving, PersistData { + DeleteVendorSuccess(this.vendor); + + final VendorEntity vendor; +} + +class DeleteVendorFailure implements StopSaving { + DeleteVendorFailure(this.vendor); + + final VendorEntity vendor; +} + +class RestoreVendorRequest implements StartSaving { + RestoreVendorRequest(this.completer, this.vendorId); + + final Completer completer; + final int vendorId; +} + +class RestoreVendorSuccess implements StopSaving, PersistData { + RestoreVendorSuccess(this.vendor); + + final VendorEntity vendor; +} + +class RestoreVendorFailure implements StopSaving { + RestoreVendorFailure(this.vendor); + + final VendorEntity vendor; +} + + + + +class FilterVendors { + FilterVendors(this.filter); + + final String filter; +} + +class SortVendors implements PersistUI { + SortVendors(this.field); + + final String field; +} + +class FilterVendorsByState implements PersistUI { + FilterVendorsByState(this.state); + + final EntityState state; +} + +class FilterVendorsByCustom1 implements PersistUI { + FilterVendorsByCustom1(this.value); + + final String value; +} + +class FilterVendorsByCustom2 implements PersistUI { + FilterVendorsByCustom2(this.value); + + final String value; +} + +class FilterVendorsByEntity implements PersistUI { + FilterVendorsByEntity({this.entityId, this.entityType}); + + final int entityId; + final EntityType entityType; +} diff --git a/lib/redux/vendor/vendor_middleware.dart b/lib/redux/vendor/vendor_middleware.dart new file mode 100644 index 000000000..e71453525 --- /dev/null +++ b/lib/redux/vendor/vendor_middleware.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.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/vendor/vendor_screen.dart'; +import 'package:invoiceninja_flutter/ui/vendor/edit/vendor_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/vendor/view/vendor_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/repositories/vendor_repository.dart'; + +List> createStoreVendorsMiddleware([ + VendorRepository repository = const VendorRepository(), +]) { + final viewVendorList = _viewVendorList(); + final viewVendor = _viewVendor(); + final editVendor = _editVendor(); + final loadVendors = _loadVendors(repository); + final loadVendor = _loadVendor(repository); + final saveVendor = _saveVendor(repository); + final archiveVendor = _archiveVendor(repository); + final deleteVendor = _deleteVendor(repository); + final restoreVendor = _restoreVendor(repository); + + return [ + TypedMiddleware(viewVendorList), + TypedMiddleware(viewVendor), + TypedMiddleware(editVendor), + TypedMiddleware(loadVendors), + TypedMiddleware(loadVendor), + TypedMiddleware(saveVendor), + TypedMiddleware(archiveVendor), + TypedMiddleware(deleteVendor), + TypedMiddleware(restoreVendor), + ]; +} + +Middleware _editVendor() { + return (Store store, dynamic action, NextDispatcher next) async { + next(action); + + store.dispatch(UpdateCurrentRoute(VendorEditScreen.route)); + final vendor = + await Navigator.of(action.context).pushNamed(VendorEditScreen.route); + + if (action.completer != null && vendor != null) { + action.completer.complete(vendor); + } + }; +} + +Middleware _viewVendor() { + return (Store store, dynamic action, NextDispatcher next) async { + next(action); + + store.dispatch(UpdateCurrentRoute(VendorViewScreen.route)); + Navigator.of(action.context).pushNamed(VendorViewScreen.route); + }; +} + +Middleware _viewVendorList() { + return (Store store, dynamic action, NextDispatcher next) { + next(action); + + store.dispatch(UpdateCurrentRoute(VendorScreen.route)); + + Navigator.of(action.context).pushNamedAndRemoveUntil(VendorScreen.route, (Route route) => false); + }; +} + +Middleware _archiveVendor(VendorRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final origVendor = store.state.vendorState.map[action.vendorId]; + repository + .saveData(store.state.selectedCompany, store.state.authState, + origVendor, EntityAction.archive) + .then((VendorEntity vendor) { + store.dispatch(ArchiveVendorSuccess(vendor)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(ArchiveVendorFailure(origVendor)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _deleteVendor(VendorRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final origVendor = store.state.vendorState.map[action.vendorId]; + repository + .saveData(store.state.selectedCompany, store.state.authState, + origVendor, EntityAction.delete) + .then((VendorEntity vendor) { + store.dispatch(DeleteVendorSuccess(vendor)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(DeleteVendorFailure(origVendor)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _restoreVendor(VendorRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final origVendor = store.state.vendorState.map[action.vendorId]; + repository + .saveData(store.state.selectedCompany, store.state.authState, + origVendor, EntityAction.restore) + .then((VendorEntity vendor) { + store.dispatch(RestoreVendorSuccess(vendor)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(RestoreVendorFailure(origVendor)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _saveVendor(VendorRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + repository + .saveData( + store.state.selectedCompany, store.state.authState, action.vendor) + .then((VendorEntity vendor) { + if (action.vendor.isNew) { + store.dispatch(AddVendorSuccess(vendor)); + } else { + store.dispatch(SaveVendorSuccess(vendor)); + } + action.completer.complete(vendor); + }).catchError((Object error) { + print(error); + store.dispatch(SaveVendorFailure(error)); + action.completer.completeError(error); + }); + + next(action); + }; +} + +Middleware _loadVendor(VendorRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final AppState state = store.state; + + if (state.isLoading) { + next(action); + return; + } + + store.dispatch(LoadVendorRequest()); + repository + .loadItem(state.selectedCompany, state.authState, action.vendorId) + .then((vendor) { + store.dispatch(LoadVendorSuccess(vendor)); + + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(LoadVendorFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _loadVendors(VendorRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final AppState state = store.state; + + if (!state.vendorState.isStale && !action.force) { + next(action); + return; + } + + if (state.isLoading) { + next(action); + return; + } + + final int updatedAt = (state.vendorState.lastUpdated / 1000).round(); + + store.dispatch(LoadVendorsRequest()); + repository + .loadList(state.selectedCompany, state.authState, updatedAt) + .then((data) { + store.dispatch(LoadVendorsSuccess(data)); + + if (action.completer != null) { + action.completer.complete(null); + } + /* + if (state.productState.isStale) { + store.dispatch(LoadProducts()); + } + */ + }).catchError((Object error) { + print(error); + store.dispatch(LoadVendorsFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} diff --git a/lib/redux/vendor/vendor_reducer.dart b/lib/redux/vendor/vendor_reducer.dart new file mode 100644 index 000000000..b856d5411 --- /dev/null +++ b/lib/redux/vendor/vendor_reducer.dart @@ -0,0 +1,197 @@ +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/company/company_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_state.dart'; + +EntityUIState vendorUIReducer(VendorUIState state, dynamic action) { + return state.rebuild((b) => b + ..listUIState.replace(vendorListReducer(state.listUIState, action)) + ..editing.replace(editingReducer(state.editing, action)) + ..selectedId = selectedIdReducer(state.selectedId, action)); +} + +Reducer selectedIdReducer = combineReducers([ + TypedReducer( + (int selectedId, dynamic action) => action.vendorId), + TypedReducer( + (int selectedId, dynamic action) => action.vendor.id), +]); + +final editingReducer = combineReducers([ + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_clearEditing), +]); + +VendorEntity _clearEditing(VendorEntity vendor, dynamic action) { + return VendorEntity(); +} + +VendorEntity _updateEditing(VendorEntity vendor, dynamic action) { + return action.vendor; +} + + +final vendorListReducer = combineReducers([ + TypedReducer(_sortVendors), + TypedReducer(_filterVendorsByState), + TypedReducer(_filterVendors), + TypedReducer(_filterVendorsByCustom1), + TypedReducer(_filterVendorsByCustom2), + TypedReducer(_filterVendorsByClient), +]); + +ListUIState _filterVendorsByClient( + ListUIState vendorListState, FilterVendorsByEntity action) { + return vendorListState.rebuild((b) => b + ..filterEntityId = action.entityId + ..filterEntityType = action.entityType); +} + +ListUIState _filterVendorsByCustom1( + ListUIState vendorListState, FilterVendorsByCustom1 action) { + if (vendorListState.custom1Filters.contains(action.value)) { + return vendorListState + .rebuild((b) => b..custom1Filters.remove(action.value)); + } else { + return vendorListState.rebuild((b) => b..custom1Filters.add(action.value)); + } +} + +ListUIState _filterVendorsByCustom2( + ListUIState vendorListState, FilterVendorsByCustom2 action) { + if (vendorListState.custom2Filters.contains(action.value)) { + return vendorListState + .rebuild((b) => b..custom2Filters.remove(action.value)); + } else { + return vendorListState.rebuild((b) => b..custom2Filters.add(action.value)); + } +} + +ListUIState _filterVendorsByState( + ListUIState vendorListState, FilterVendorsByState action) { + if (vendorListState.stateFilters.contains(action.state)) { + return vendorListState.rebuild((b) => b..stateFilters.remove(action.state)); + } else { + return vendorListState.rebuild((b) => b..stateFilters.add(action.state)); + } +} + +ListUIState _filterVendors(ListUIState vendorListState, FilterVendors action) { + return vendorListState.rebuild((b) => b..filter = action.filter); +} + +ListUIState _sortVendors(ListUIState vendorListState, SortVendors action) { + return vendorListState.rebuild((b) => b + ..sortAscending = b.sortField != action.field || !b.sortAscending + ..sortField = action.field); +} + +final vendorsReducer = combineReducers([ + TypedReducer(_updateVendor), + TypedReducer(_addVendor), + TypedReducer(_setLoadedVendors), + TypedReducer(_setLoadedVendor), + TypedReducer(_archiveVendorRequest), + TypedReducer(_archiveVendorSuccess), + TypedReducer(_archiveVendorFailure), + TypedReducer(_deleteVendorRequest), + TypedReducer(_deleteVendorSuccess), + TypedReducer(_deleteVendorFailure), + TypedReducer(_restoreVendorRequest), + TypedReducer(_restoreVendorSuccess), + TypedReducer(_restoreVendorFailure), +]); + +VendorState _archiveVendorRequest( + VendorState vendorState, ArchiveVendorRequest action) { + final vendor = vendorState.map[action.vendorId] + .rebuild((b) => b..archivedAt = DateTime.now().millisecondsSinceEpoch); + + return vendorState.rebuild((b) => b..map[action.vendorId] = vendor); +} + +VendorState _archiveVendorSuccess( + VendorState vendorState, ArchiveVendorSuccess action) { + return vendorState.rebuild((b) => b..map[action.vendor.id] = action.vendor); +} + +VendorState _archiveVendorFailure( + VendorState vendorState, ArchiveVendorFailure action) { + return vendorState.rebuild((b) => b..map[action.vendor.id] = action.vendor); +} + +VendorState _deleteVendorRequest( + VendorState vendorState, DeleteVendorRequest action) { + final vendor = vendorState.map[action.vendorId].rebuild((b) => b + ..archivedAt = DateTime.now().millisecondsSinceEpoch + ..isDeleted = true); + + return vendorState.rebuild((b) => b..map[action.vendorId] = vendor); +} + +VendorState _deleteVendorSuccess( + VendorState vendorState, DeleteVendorSuccess action) { + return vendorState.rebuild((b) => b..map[action.vendor.id] = action.vendor); +} + +VendorState _deleteVendorFailure( + VendorState vendorState, DeleteVendorFailure action) { + return vendorState.rebuild((b) => b..map[action.vendor.id] = action.vendor); +} + +VendorState _restoreVendorRequest( + VendorState vendorState, RestoreVendorRequest action) { + final vendor = vendorState.map[action.vendorId].rebuild((b) => b + ..archivedAt = null + ..isDeleted = false); + return vendorState.rebuild((b) => b..map[action.vendorId] = vendor); +} + +VendorState _restoreVendorSuccess( + VendorState vendorState, RestoreVendorSuccess action) { + return vendorState.rebuild((b) => b..map[action.vendor.id] = action.vendor); +} + +VendorState _restoreVendorFailure( + VendorState vendorState, RestoreVendorFailure action) { + return vendorState.rebuild((b) => b..map[action.vendor.id] = action.vendor); +} + +VendorState _addVendor(VendorState vendorState, AddVendorSuccess action) { + return vendorState.rebuild((b) => b + ..map[action.vendor.id] = action.vendor + ..list.add(action.vendor.id)); +} + +VendorState _updateVendor(VendorState vendorState, SaveVendorSuccess action) { + return vendorState.rebuild((b) => b + ..map[action.vendor.id] = action.vendor); +} + +VendorState _setLoadedVendor( + VendorState vendorState, LoadVendorSuccess action) { + return vendorState.rebuild((b) => b + ..map[action.vendor.id] = action.vendor); +} + +VendorState _setLoadedVendors( + VendorState vendorState, LoadVendorsSuccess action) { + final state = vendorState.rebuild((b) => b + ..lastUpdated = DateTime.now().millisecondsSinceEpoch + ..map.addAll(Map.fromIterable( + action.vendors, + key: (dynamic item) => item.id, + value: (dynamic item) => item, + ))); + + return state.rebuild((b) => b..list.replace(state.map.keys)); +} diff --git a/lib/redux/vendor/vendor_selectors.dart b/lib/redux/vendor/vendor_selectors.dart new file mode 100644 index 000000000..271313390 --- /dev/null +++ b/lib/redux/vendor/vendor_selectors.dart @@ -0,0 +1,74 @@ +import 'package:memoize/memoize.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; + +var memoizedDropdownVendorList = memo3( + (BuiltMap vendorMap, BuiltList vendorList, + int clientId) => + dropdownVendorsSelector(vendorMap, vendorList, clientId)); + +List dropdownVendorsSelector(BuiltMap vendorMap, + BuiltList vendorList, int clientId) { + final list = vendorList.where((vendorId) { + final vendor = vendorMap[vendorId]; + /* + if (clientId != null && clientId > 0 && vendor.clientId != clientId) { + return false; + } + */ + return vendor.isActive; + }).toList(); + + list.sort((vendorAId, vendorBId) { + final vendorA = vendorMap[vendorAId]; + final vendorB = vendorMap[vendorBId]; + return vendorA.compareTo(vendorB, VendorFields.name, true); + }); + + return list; +} + +var memoizedFilteredVendorList = memo3((BuiltMap vendorMap, + BuiltList vendorList, ListUIState vendorListState) => + filteredVendorsSelector(vendorMap, vendorList, vendorListState)); + +List filteredVendorsSelector(BuiltMap vendorMap, + BuiltList vendorList, ListUIState vendorListState) { + final list = vendorList.where((vendorId) { + final vendor = vendorMap[vendorId]; + if (!vendor.matchesStates(vendorListState.stateFilters)) { + return false; + } + /* + if (vendorListState.filterEntityId != null && + vendor.clientId != vendorListState.filterEntityId) { + return false; + } + */ + if (vendorListState.custom1Filters.isNotEmpty && + !vendorListState.custom1Filters.contains(vendor.customValue1)) { + return false; + } + if (vendorListState.custom2Filters.isNotEmpty && + !vendorListState.custom2Filters.contains(vendor.customValue2)) { + return false; + } + /* + if (vendorListState.filterEntityId != null && + vendor.entityId != vendorListState.filterEntityId) { + return false; + } + */ + return vendor.matchesFilter(vendorListState.filter); + }).toList(); + + list.sort((vendorAId, vendorBId) { + final vendorA = vendorMap[vendorAId]; + final vendorB = vendorMap[vendorBId]; + return vendorA.compareTo( + vendorB, vendorListState.sortField, vendorListState.sortAscending); + }); + + return list; +} diff --git a/lib/redux/vendor/vendor_state.dart b/lib/redux/vendor/vendor_state.dart new file mode 100644 index 000000000..1158d0d95 --- /dev/null +++ b/lib/redux/vendor/vendor_state.dart @@ -0,0 +1,59 @@ +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/vendor_model.dart'; +import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; + +part 'vendor_state.g.dart'; + +abstract class VendorState implements Built { + + factory VendorState() { + return _$VendorState._( + lastUpdated: 0, + map: BuiltMap(), + list: BuiltList(), + ); + } + VendorState._(); + + @nullable + int get lastUpdated; + + BuiltMap get map; + BuiltList get list; + + bool get isStale { + if (! isLoaded) { + return true; + } + + return DateTime.now().millisecondsSinceEpoch - lastUpdated > kMillisecondsToRefreshData; + } + + bool get isLoaded => lastUpdated != null && lastUpdated > 0; + + static Serializer get serializer => _$vendorStateSerializer; +} + +abstract class VendorUIState extends Object with EntityUIState implements Built { + + factory VendorUIState() { + return _$VendorUIState._( + listUIState: ListUIState(VendorFields.name), + editing: VendorEntity(), + selectedId: 0, + ); + } + VendorUIState._(); + + @nullable + VendorEntity get editing; + + @override + bool get isCreatingNew => editing.isNew; + + static Serializer get serializer => _$vendorUIStateSerializer; +} \ No newline at end of file diff --git a/lib/redux/vendor/vendor_state.g.dart b/lib/redux/vendor/vendor_state.g.dart new file mode 100644 index 000000000..52337c5f9 --- /dev/null +++ b/lib/redux/vendor/vendor_state.g.dart @@ -0,0 +1,388 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'vendor_state.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +// ignore_for_file: always_put_control_body_on_new_line +// ignore_for_file: annotate_overrides +// ignore_for_file: avoid_annotating_with_dynamic +// ignore_for_file: avoid_catches_without_on_clauses +// ignore_for_file: avoid_returning_this +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: omit_local_variable_types +// ignore_for_file: prefer_expression_function_bodies +// ignore_for_file: sort_constructors_first +// ignore_for_file: unnecessary_const +// ignore_for_file: unnecessary_new +// ignore_for_file: test_types_in_equals + +Serializer _$vendorStateSerializer = new _$VendorStateSerializer(); +Serializer _$vendorUIStateSerializer = + new _$VendorUIStateSerializer(); + +class _$VendorStateSerializer implements StructuredSerializer { + @override + final Iterable types = const [VendorState, _$VendorState]; + @override + final String wireName = 'VendorState'; + + @override + Iterable serialize(Serializers serializers, VendorState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'map', + serializers.serialize(object.map, + specifiedType: const FullType(BuiltMap, + const [const FullType(int), const FullType(VendorEntity)])), + 'list', + serializers.serialize(object.list, + specifiedType: + const FullType(BuiltList, const [const FullType(int)])), + ]; + if (object.lastUpdated != null) { + result + ..add('lastUpdated') + ..add(serializers.serialize(object.lastUpdated, + specifiedType: const FullType(int))); + } + + return result; + } + + @override + VendorState deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new VendorStateBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'lastUpdated': + result.lastUpdated = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'map': + result.map.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltMap, const [ + const FullType(int), + const FullType(VendorEntity) + ])) as BuiltMap); + break; + case 'list': + result.list.replace(serializers.deserialize(value, + specifiedType: + const FullType(BuiltList, const [const FullType(int)])) + as BuiltList); + break; + } + } + + return result.build(); + } +} + +class _$VendorUIStateSerializer implements StructuredSerializer { + @override + final Iterable types = const [VendorUIState, _$VendorUIState]; + @override + final String wireName = 'VendorUIState'; + + @override + Iterable serialize(Serializers serializers, VendorUIState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'selectedId', + serializers.serialize(object.selectedId, + specifiedType: const FullType(int)), + 'listUIState', + serializers.serialize(object.listUIState, + specifiedType: const FullType(ListUIState)), + ]; + if (object.editing != null) { + result + ..add('editing') + ..add(serializers.serialize(object.editing, + specifiedType: const FullType(VendorEntity))); + } + + return result; + } + + @override + VendorUIState deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new VendorUIStateBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'editing': + result.editing.replace(serializers.deserialize(value, + specifiedType: const FullType(VendorEntity)) as VendorEntity); + break; + case 'selectedId': + result.selectedId = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'listUIState': + result.listUIState.replace(serializers.deserialize(value, + specifiedType: const FullType(ListUIState)) as ListUIState); + break; + } + } + + return result.build(); + } +} + +class _$VendorState extends VendorState { + @override + final int lastUpdated; + @override + final BuiltMap map; + @override + final BuiltList list; + + factory _$VendorState([void updates(VendorStateBuilder b)]) => + (new VendorStateBuilder()..update(updates)).build(); + + _$VendorState._({this.lastUpdated, this.map, this.list}) : super._() { + if (map == null) { + throw new BuiltValueNullFieldError('VendorState', 'map'); + } + if (list == null) { + throw new BuiltValueNullFieldError('VendorState', 'list'); + } + } + + @override + VendorState rebuild(void updates(VendorStateBuilder b)) => + (toBuilder()..update(updates)).build(); + + @override + VendorStateBuilder toBuilder() => new VendorStateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is VendorState && + lastUpdated == other.lastUpdated && + map == other.map && + list == other.list; + } + + @override + int get hashCode { + return $jf( + $jc($jc($jc(0, lastUpdated.hashCode), map.hashCode), list.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('VendorState') + ..add('lastUpdated', lastUpdated) + ..add('map', map) + ..add('list', list)) + .toString(); + } +} + +class VendorStateBuilder implements Builder { + _$VendorState _$v; + + int _lastUpdated; + int get lastUpdated => _$this._lastUpdated; + set lastUpdated(int lastUpdated) => _$this._lastUpdated = lastUpdated; + + MapBuilder _map; + MapBuilder get map => + _$this._map ??= new MapBuilder(); + set map(MapBuilder map) => _$this._map = map; + + ListBuilder _list; + ListBuilder get list => _$this._list ??= new ListBuilder(); + set list(ListBuilder list) => _$this._list = list; + + VendorStateBuilder(); + + VendorStateBuilder get _$this { + if (_$v != null) { + _lastUpdated = _$v.lastUpdated; + _map = _$v.map?.toBuilder(); + _list = _$v.list?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(VendorState other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$VendorState; + } + + @override + void update(void updates(VendorStateBuilder b)) { + if (updates != null) updates(this); + } + + @override + _$VendorState build() { + _$VendorState _$result; + try { + _$result = _$v ?? + new _$VendorState._( + lastUpdated: lastUpdated, map: map.build(), list: list.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'map'; + map.build(); + _$failedField = 'list'; + list.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'VendorState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$VendorUIState extends VendorUIState { + @override + final VendorEntity editing; + @override + final int selectedId; + @override + final ListUIState listUIState; + + factory _$VendorUIState([void updates(VendorUIStateBuilder b)]) => + (new VendorUIStateBuilder()..update(updates)).build(); + + _$VendorUIState._({this.editing, this.selectedId, this.listUIState}) + : super._() { + if (selectedId == null) { + throw new BuiltValueNullFieldError('VendorUIState', 'selectedId'); + } + if (listUIState == null) { + throw new BuiltValueNullFieldError('VendorUIState', 'listUIState'); + } + } + + @override + VendorUIState rebuild(void updates(VendorUIStateBuilder b)) => + (toBuilder()..update(updates)).build(); + + @override + VendorUIStateBuilder toBuilder() => new VendorUIStateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is VendorUIState && + editing == other.editing && + selectedId == other.selectedId && + listUIState == other.listUIState; + } + + @override + int get hashCode { + return $jf($jc($jc($jc(0, editing.hashCode), selectedId.hashCode), + listUIState.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('VendorUIState') + ..add('editing', editing) + ..add('selectedId', selectedId) + ..add('listUIState', listUIState)) + .toString(); + } +} + +class VendorUIStateBuilder + implements Builder { + _$VendorUIState _$v; + + VendorEntityBuilder _editing; + VendorEntityBuilder get editing => + _$this._editing ??= new VendorEntityBuilder(); + set editing(VendorEntityBuilder editing) => _$this._editing = editing; + + int _selectedId; + int get selectedId => _$this._selectedId; + set selectedId(int selectedId) => _$this._selectedId = selectedId; + + ListUIStateBuilder _listUIState; + ListUIStateBuilder get listUIState => + _$this._listUIState ??= new ListUIStateBuilder(); + set listUIState(ListUIStateBuilder listUIState) => + _$this._listUIState = listUIState; + + VendorUIStateBuilder(); + + VendorUIStateBuilder get _$this { + if (_$v != null) { + _editing = _$v.editing?.toBuilder(); + _selectedId = _$v.selectedId; + _listUIState = _$v.listUIState?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(VendorUIState other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$VendorUIState; + } + + @override + void update(void updates(VendorUIStateBuilder b)) { + if (updates != null) updates(this); + } + + @override + _$VendorUIState build() { + _$VendorUIState _$result; + try { + _$result = _$v ?? + new _$VendorUIState._( + editing: _editing?.build(), + selectedId: selectedId, + listUIState: listUIState.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'editing'; + _editing?.build(); + + _$failedField = 'listUIState'; + listUIState.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'VendorUIState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} diff --git a/lib/ui/app/app_drawer.dart b/lib/ui/app/app_drawer.dart index 81bfa6574..f1d2f48d8 100644 --- a/lib/ui/app/app_drawer.dart +++ b/lib/ui/app/app_drawer.dart @@ -19,6 +19,8 @@ import 'package:redux/redux.dart'; import 'package:cached_network_image/cached_network_image.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; + import 'package:invoiceninja_flutter/redux/task/task_actions.dart'; import 'package:invoiceninja_flutter/redux/project/project_actions.dart'; import 'package:invoiceninja_flutter/redux/payment/payment_actions.dart'; @@ -213,7 +215,6 @@ class AppDrawer extends StatelessWidget { quote: InvoiceEntity(isQuote: true), context: context)); }, ), - // STARTER: menu - do not remove comment DrawerTile( company: company, entityType: EntityType.project, @@ -239,45 +240,19 @@ class AppDrawer extends StatelessWidget { context: context)); }, ), - ListTile( - dense: true, - leading: Icon(FontAwesomeIcons.building, size: 22.0), - title: Text('Vendors & Expenses'), - onTap: () { - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - semanticLabel: 'Vendors & Expenses', - title: Text('Vendors & Expenses'), - content: RichText( - text: TextSpan( - children: [ - TextSpan( - style: aboutTextStyle, - text: localization.thanksForPatience + ' ', - ), - _LinkTextSpan( - style: linkStyle, - url: getLegacyAppURL(context), - text: localization.legacyMobileApp, - ), - TextSpan( - style: aboutTextStyle, - text: '.', - ), - ], - ), - ), - actions: [ - FlatButton( - child: Text(localization.ok.toUpperCase()), - onPressed: () => Navigator.pop(context), - ) - ], - ), - ); + DrawerTile( + company: company, + entityType: EntityType.vendor, + icon: getEntityIcon(EntityType.vendor), + title: localization.vendors, + onTap: () => store.dispatch(ViewVendorList(context)), + onCreateTap: () { + navigator.pop(); + store.dispatch( + EditVendor(vendor: VendorEntity(), context: context)); }, ), + // STARTER: menu - do not remove comment DrawerTile( company: company, icon: FontAwesomeIcons.cog, diff --git a/lib/ui/vendor/edit/vendor_edit.dart b/lib/ui/vendor/edit/vendor_edit.dart new file mode 100644 index 000000000..d43d490fa --- /dev/null +++ b/lib/ui/vendor/edit/vendor_edit.dart @@ -0,0 +1,208 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/vendor/edit/vendor_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/buttons/action_icon_button.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class VendorEdit extends StatefulWidget { + const VendorEdit({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final VendorEditVM viewModel; + + @override + _VendorEditState createState() => _VendorEditState(); +} + +class _VendorEditState extends State { + static final GlobalKey _formKey = GlobalKey(); + + // STARTER: controllers - do not remove comment +final _nameController = TextEditingController(); + +final _address1Controller = TextEditingController(); + +final _address2Controller = TextEditingController(); + +final _cityController = TextEditingController(); + +final _stateController = TextEditingController(); + +final _postalCodeController = TextEditingController(); + + + List _controllers = []; + + @override + void didChangeDependencies() { + + _controllers = [ + // STARTER: array - do not remove comment +_nameController, + +_address1Controller, + +_address2Controller, + +_cityController, + +_stateController, + +_postalCodeController, + + ]; + + _controllers.forEach((controller) => controller.removeListener(_onChanged)); + + final vendor = widget.viewModel.vendor; + // STARTER: read value - do not remove comment +_nameController.text = vendor.name; + +_address1Controller.text = vendor.address1; + +_address2Controller.text = vendor.address2; + +_cityController.text = vendor.city; + +_stateController.text = vendor.state; + +_postalCodeController.text = vendor.postalCode; + + + _controllers.forEach((controller) => controller.addListener(_onChanged)); + + super.didChangeDependencies(); + } + + @override + void dispose() { + _controllers.forEach((controller) { + controller.removeListener(_onChanged); + controller.dispose(); + }); + + super.dispose(); + } + + void _onChanged() { + final vendor = widget.viewModel.vendor.rebuild((b) => b + // STARTER: set value - do not remove comment +..name = _nameController.text.trim() + +..address1 = _address1Controller.text.trim() + +..address2 = _address2Controller.text.trim() + +..city = _cityController.text.trim() + +..state = _stateController.text.trim() + +..postalCode = _postalCodeController.text.trim() + + ); + if (vendor != widget.viewModel.vendor) { + widget.viewModel.onChanged(vendor); + } + } + + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final localization = AppLocalization.of(context); + final vendor = viewModel.vendor; + + return WillPopScope( + onWillPop: () async { + viewModel.onBackPressed(); + return true; + }, + child: Scaffold( + appBar: AppBar( + title: Text(viewModel.vendor.isNew + ? localization.newVendor + : localization.editVendor), + actions: [ + ActionIconButton( + icon: Icons.cloud_upload, + tooltip: localization.save, + isVisible: !vendor.isDeleted, + isDirty: vendor.isNew || vendor != viewModel.origVendor, + isSaving: viewModel.isSaving, + onPressed: () { + if (! _formKey.currentState.validate()) { + return; + } + viewModel.onSavePressed(context); + }, + ), + ], + ), + body: Form( + key: _formKey, + child: Builder(builder: (BuildContext context) { + return ListView( + children: [ + FormCard( + children: [ + // STARTER: widgets - do not remove comment +TextFormField( +controller: _nameController, +autocorrect: false, +decoration: InputDecoration( +labelText: 'Name', +), +), + +TextFormField( +controller: _address1Controller, +autocorrect: false, +decoration: InputDecoration( +labelText: 'Address1', +), +), + +TextFormField( +controller: _address2Controller, +autocorrect: false, +decoration: InputDecoration( +labelText: 'Address2', +), +), + +TextFormField( +controller: _cityController, +autocorrect: false, +decoration: InputDecoration( +labelText: 'City', +), +), + +TextFormField( +controller: _stateController, +autocorrect: false, +decoration: InputDecoration( +labelText: 'State', +), +), + +TextFormField( +controller: _postalCodeController, +autocorrect: false, +decoration: InputDecoration( +labelText: 'PostalCode', +), +), + + ], + ), + ], + ); + }) + ), + ), + ); + } +} diff --git a/lib/ui/vendor/edit/vendor_edit_vm.dart b/lib/ui/vendor/edit/vendor_edit_vm.dart new file mode 100644 index 000000000..db1899b56 --- /dev/null +++ b/lib/ui/vendor/edit/vendor_edit_vm.dart @@ -0,0 +1,99 @@ +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/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/vendor/vendor_screen.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; +import 'package:invoiceninja_flutter/ui/vendor/view/vendor_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; +import 'package:invoiceninja_flutter/data/models/vendor_model.dart'; +import 'package:invoiceninja_flutter/ui/vendor/edit/vendor_edit.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class VendorEditScreen extends StatelessWidget { + const VendorEditScreen({Key key}) : super(key: key); + static const String route = '/vendor/edit'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return VendorEditVM.fromStore(store); + }, + builder: (context, viewModel) { + return VendorEdit( + viewModel: viewModel, + ); + }, + ); + } +} + +class VendorEditVM { + VendorEditVM({ + @required this.state, + @required this.vendor, + @required this.company, + @required this.onChanged, + @required this.isSaving, + @required this.origVendor, + @required this.onSavePressed, + @required this.onBackPressed, + @required this.isLoading, + }); + + factory VendorEditVM.fromStore(Store store) { + final vendor = store.state.vendorUIState.editing; + final state = store.state; + + return VendorEditVM( + state: state, + isLoading: state.isLoading, + isSaving: state.isSaving, + origVendor: state.vendorState.map[vendor.id], + vendor: vendor, + company: state.selectedCompany, + onChanged: (VendorEntity vendor) { + store.dispatch(UpdateVendor(vendor)); + }, + onBackPressed: () { + if (state.uiState.currentRoute.contains(VendorScreen.route)) { + store.dispatch(UpdateCurrentRoute(vendor.isNew ? VendorScreen.route : VendorViewScreen.route)); + } + }, + onSavePressed: (BuildContext context) { + final Completer completer = new Completer(); + store.dispatch(SaveVendorRequest(completer: completer, vendor: vendor)); + return completer.future.then((_) { + return completer.future.then((savedVendor) { + store.dispatch(UpdateCurrentRoute(VendorViewScreen.route)); + if (vendor.isNew) { + Navigator.of(context).pushReplacementNamed(VendorViewScreen.route); + } else { + Navigator.of(context).pop(savedVendor); + } + }).catchError((Object error) { + showDialog( + context: context, + builder: (BuildContext context) { + return ErrorDialog(error); + }); + }); + }); + }, + ); + } + + final VendorEntity vendor; + final CompanyEntity company; + final Function(VendorEntity) onChanged; + final Function(BuildContext) onSavePressed; + final Function onBackPressed; + final bool isLoading; + final bool isSaving; + final VendorEntity origVendor; + final AppState state; +} diff --git a/lib/ui/vendor/vendor_list.dart b/lib/ui/vendor/vendor_list.dart new file mode 100644 index 000000000..accba1153 --- /dev/null +++ b/lib/ui/vendor/vendor_list.dart @@ -0,0 +1,212 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.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/vendor/vendor_list_item.dart'; +import 'package:invoiceninja_flutter/ui/vendor/vendor_list_vm.dart'; +import 'package:invoiceninja_flutter/utils/icons.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class VendorList extends StatelessWidget { + const VendorList({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final VendorListVM viewModel; + + void _showMenu( + BuildContext context, VendorEntity vendor, ClientEntity client) async { + if (vendor == null || client == null) { + return; + } + + final user = viewModel.user; + final message = await showDialog( + context: context, + builder: (BuildContext dialogContext) => SimpleDialog( + children: vendor + .getEntityActions(user: user, client: client, includeEdit: true) + .map((entityAction) { + if (entityAction == null) { + return Divider(); + } else { + return ListTile( + leading: Icon(getEntityActionIcon(entityAction)), + title: Text(AppLocalization.of(context) + .lookup(entityAction.toString())), + onTap: () { + Navigator.of(dialogContext).pop(); + viewModel.onEntityAction(context, vendor, entityAction); + }, + ); + } + }).toList())); + + 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.filterEntityId; + final filteredClient = + filteredClientId != null ? viewModel.clientMap[filteredClientId] : null; + */ + + return Column( + children: [ + + Expanded( + child: !viewModel.isLoaded + ? LoadingIndicator() + : RefreshIndicator( + onRefresh: () => viewModel.onRefreshed(context), + child: viewModel.vendorList.isEmpty + ? Opacity( + opacity: 0.5, + child: Center( + child: Text( + AppLocalization.of(context).noRecordsFound, + style: TextStyle( + fontSize: 18.0, + ), + ), + ), + ) + : ListView.builder( + shrinkWrap: true, + itemCount: viewModel.vendorList.length, + itemBuilder: (BuildContext context, index) { + final vendorId = viewModel.vendorList[index]; + final vendor = viewModel.vendorMap[vendorId]; + return Column( + children: [ + VendorListItem( + user: viewModel.user, + filter: viewModel.filter, + vendor: vendor, + onTap: () => + viewModel.onVendorTap(context, vendor), + onEntityAction: (EntityAction action) { + if (action == EntityAction.more) { + _showMenu(context, vendor, null); + } else { + viewModel.onEntityAction( + context, vendor, action); + } + }, + onLongPress: () => + _showMenu(context, vendor, null), + ), + Divider( + height: 1.0, + ), + ], + ); + }, + ), + ), + ), + + + /* + filteredClient != null + ? Material( + color: Colors.orangeAccent, + elevation: 6.0, + child: InkWell( + onTap: () => viewModel.onViewEntityFilterPressed(context), + child: Row( + children: [ + SizedBox(width: 18.0), + Expanded( + child: Text( + '${localization.filteredBy} ${filteredClient.listDisplayName}', + style: TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + ), + ), + IconButton( + icon: Icon( + Icons.close, + color: Colors.white, + ), + onPressed: () => viewModel.onClearEntityFilterPressed(), + ) + ], + ), + ), + ) + : Container(), + Expanded( + child: !viewModel.isLoaded + ? LoadingIndicator() + : RefreshIndicator( + onRefresh: () => viewModel.onRefreshed(context), + child: viewModel.vendorList.isEmpty + ? Opacity( + opacity: 0.5, + child: Center( + child: Text( + AppLocalization.of(context).noRecordsFound, + style: TextStyle( + fontSize: 18.0, + ), + ), + ), + ) + : ListView.builder( + shrinkWrap: true, + itemCount: viewModel.vendorList.length, + itemBuilder: (BuildContext context, index) { + final vendorId = viewModel.vendorList[index]; + final vendor = viewModel.vendorMap[vendorId]; + final client = + viewModel.clientMap[vendor.clientId] ?? + ClientEntity(); + return Column( + children: [ + VendorListItem( + user: viewModel.user, + filter: viewModel.filter, + vendor: vendor, + client: + viewModel.clientMap[vendor.clientId] ?? + ClientEntity(), + onTap: () => + viewModel.onVendorTap(context, vendor), + onEntityAction: (EntityAction action) { + if (action == EntityAction.more) { + _showMenu(context, vendor, client); + } else { + viewModel.onEntityAction( + context, vendor, action); + } + }, + onLongPress: () => + _showMenu(context, vendor, client), + ), + Divider( + height: 1.0, + ), + ], + ); + }, + ), + ), + ),*/ + ], + ); + } +} diff --git a/lib/ui/vendor/vendor_list_item.dart b/lib/ui/vendor/vendor_list_item.dart new file mode 100644 index 000000000..a06bc6859 --- /dev/null +++ b/lib/ui/vendor/vendor_list_item.dart @@ -0,0 +1,85 @@ +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'; + +class VendorListItem extends StatelessWidget { + + const VendorListItem({ + @required this.user, + @required this.onEntityAction, + @required this.onTap, + @required this.onLongPress, + //@required this.onCheckboxChanged, + @required this.vendor, + @required this.filter, + }); + + final UserEntity user; + final Function(EntityAction) onEntityAction; + final GestureTapCallback onTap; + final GestureTapCallback onLongPress; + //final ValueChanged onCheckboxChanged; + final VendorEntity vendor; + final String filter; + + static final vendorItemKey = (int id) => Key('__vendor_item_${id}__'); + + @override + Widget build(BuildContext context) { + final filterMatch = filter != null && filter.isNotEmpty + ? vendor.matchesFilterValue(filter) + : null; + final subtitle = filterMatch; + + return DismissibleEntity( + user: user, + entity: vendor, + onEntityAction: onEntityAction, + child: ListTile( + onTap: onTap, + onLongPress: onLongPress, + /* + leading: Checkbox( + //key: NinjaKeys.vendorItemCheckbox(vendor.id), + value: true, + //onChanged: onCheckboxChanged, + onChanged: (value) { + return true; + }, + ), + */ + title: Container( + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + Expanded( + child: Text( + vendor.name, + //key: NinjaKeys.clientItemClientKey(client.id), + style: Theme.of(context).textTheme.title, + ), + ), + Text(formatNumber(vendor.listDisplayAmount, context), + style: Theme.of(context).textTheme.title), + ], + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + subtitle != null && subtitle.isNotEmpty ? + Text( + subtitle, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ) : Container(), + EntityStateLabel(vendor), + ], + ), + ), + ); + } +} diff --git a/lib/ui/vendor/vendor_list_vm.dart b/lib/ui/vendor/vendor_list_vm.dart new file mode 100644 index 000000000..1abbaa712 --- /dev/null +++ b/lib/ui/vendor/vendor_list_vm.dart @@ -0,0 +1,129 @@ +import 'dart:async'; +import 'package:redux/redux.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/redux/client/client_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_selectors.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/vendor/vendor_list.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; + +class VendorListBuilder extends StatelessWidget { + const VendorListBuilder({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: VendorListVM.fromStore, + builder: (context, viewModel) { + return VendorList( + viewModel: viewModel, + ); + }, + ); + } +} + +class VendorListVM { + VendorListVM({ + @required this.user, + @required this.vendorList, + @required this.vendorMap, + @required this.filter, + @required this.isLoading, + @required this.isLoaded, + @required this.onVendorTap, + @required this.listState, + @required this.onRefreshed, + @required this.onEntityAction, + @required this.onClearEntityFilterPressed, + @required this.onViewEntityFilterPressed, + }); + + static VendorListVM fromStore(Store store) { + Future _handleRefresh(BuildContext context) { + if (store.state.isLoading) { + return Future(null); + } + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(LoadVendors(completer: completer, force: true)); + return completer.future; + } + + final state = store.state; + + return VendorListVM( + user: state.user, + listState: state.vendorListState, + vendorList: memoizedFilteredVendorList(state.vendorState.map, + state.vendorState.list, state.vendorListState), + vendorMap: state.vendorState.map, + isLoading: state.isLoading, + isLoaded: state.vendorState.isLoaded, + filter: state.vendorUIState.listUIState.filter, + onClearEntityFilterPressed: () => + store.dispatch(FilterVendorsByEntity()), + onViewEntityFilterPressed: (BuildContext context) => store.dispatch( + ViewClient( + clientId: state.vendorListState.filterEntityId, + context: context)), + onVendorTap: (context, vendor) { + store.dispatch(ViewVendor(vendorId: vendor.id, context: context)); + }, + onEntityAction: (context, vendor, action) { + switch (action) { + case EntityAction.edit: + store.dispatch( + EditVendor(context: context, vendor: vendor)); + break; + case EntityAction.clone: + Navigator.of(context).pop(); + store.dispatch( + EditVendor(context: context, vendor: vendor.clone)); + break; + case EntityAction.restore: + store.dispatch(RestoreVendorRequest( + snackBarCompleter( + context, AppLocalization.of(context).restoredVendor), + vendor.id)); + break; + case EntityAction.archive: + store.dispatch(ArchiveVendorRequest( + snackBarCompleter( + context, AppLocalization.of(context).archivedVendor), + vendor.id)); + break; + case EntityAction.delete: + store.dispatch(DeleteVendorRequest( + snackBarCompleter( + context, AppLocalization.of(context).deletedVendor), + vendor.id)); + break; + } + }, + onRefreshed: (context) => _handleRefresh(context), + ); + } + + final UserEntity user; + final List vendorList; + final BuiltMap vendorMap; + final ListUIState listState; + final String filter; + final bool isLoading; + final bool isLoaded; + final Function(BuildContext, VendorEntity) onVendorTap; + final Function(BuildContext) onRefreshed; + final Function(BuildContext, VendorEntity, EntityAction) onEntityAction; + final Function onClearEntityFilterPressed; + final Function(BuildContext) onViewEntityFilterPressed; + +} diff --git a/lib/ui/vendor/vendor_screen.dart b/lib/ui/vendor/vendor_screen.dart new file mode 100644 index 000000000..098a651de --- /dev/null +++ b/lib/ui/vendor/vendor_screen.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/list_filter.dart'; +import 'package:invoiceninja_flutter/ui/app/list_filter_button.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/vendor/vendor_list_vm.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/app_drawer_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart'; + +class VendorScreen extends StatelessWidget { + static const String route = '/vendor'; + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final company = store.state.selectedCompany; + final user = company.user; + final localization = AppLocalization.of(context); + + return WillPopScope( + onWillPop: () async { + store.dispatch(ViewDashboard(context)); + return false; + }, + child: Scaffold( + appBar: AppBar( + title: ListFilter( + entityType: EntityType.vendor, + onFilterChanged: (value) { + store.dispatch(FilterVendors(value)); + }, + ), + actions: [ + ListFilterButton( + entityType: EntityType.vendor, + onFilterPressed: (String value) { + store.dispatch(FilterVendors(value)); + }, + ), + ], + ), + drawer: AppDrawerBuilder(), + body: VendorListBuilder(), + bottomNavigationBar: AppBottomBar( + entityType: EntityType.vendor, + onSelectedSortField: (value) => store.dispatch(SortVendors(value)), + customValues1: company.getCustomFieldValues(CustomFieldType.vendor1, + excludeBlank: true), + customValues2: company.getCustomFieldValues(CustomFieldType.vendor2, + excludeBlank: true), + onSelectedCustom1: (value) => + store.dispatch(FilterVendorsByCustom1(value)), + onSelectedCustom2: (value) => + store.dispatch(FilterVendorsByCustom2(value)), + sortFields: [ + VendorFields.updatedAt, + ], + onSelectedState: (EntityState state, value) { + store.dispatch(FilterVendorsByState(state)); + }, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, + floatingActionButton: user.canCreate(EntityType.vendor) + ? FloatingActionButton( + //key: Key(VendorKeys.vendorScreenFABKeyString), + backgroundColor: Theme.of(context).primaryColorDark, + onPressed: () { + store.dispatch( + EditVendor(vendor: VendorEntity(), context: context)); + }, + child: Icon( + Icons.add, + color: Colors.white, + ), + tooltip: localization.newVendor, + ) + : null, + ), + ); + } +} diff --git a/lib/ui/vendor/view/vendor_view.dart b/lib/ui/vendor/view/vendor_view.dart new file mode 100644 index 000000000..76e289c3e --- /dev/null +++ b/lib/ui/vendor/view/vendor_view.dart @@ -0,0 +1,53 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/ui/app/actions_menu_button.dart'; +import 'package:invoiceninja_flutter/ui/vendor/view/vendor_view_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; + +class VendorView extends StatefulWidget { + + const VendorView({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final VendorViewVM viewModel; + + @override + _VendorViewState createState() => new _VendorViewState(); +} + +class _VendorViewState extends State { + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final vendor = viewModel.vendor; + + return Scaffold( + appBar: AppBar( + title: Text(vendor.name), + actions: vendor.isNew + ? [] + : [ + IconButton( + icon: Icon(Icons.edit), + onPressed: () { + viewModel.onEditPressed(context); + }, + ), + ActionMenuButton( + user: viewModel.company.user, + isSaving: viewModel.isSaving, + entity: vendor, + onSelected: viewModel.onActionSelected, + ), + ], + ), + body: FormCard( + children: [ + // STARTER: widgets - do not remove comment + ] + ), + ); + } +} diff --git a/lib/ui/vendor/view/vendor_view_vm.dart b/lib/ui/vendor/view/vendor_view_vm.dart new file mode 100644 index 000000000..e64e27c98 --- /dev/null +++ b/lib/ui/vendor/view/vendor_view_vm.dart @@ -0,0 +1,108 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/ui/vendor/vendor_screen.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:redux/redux.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; +import 'package:invoiceninja_flutter/data/models/vendor_model.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/vendor/view/vendor_view.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class VendorViewScreen extends StatelessWidget { + const VendorViewScreen({Key key}) : super(key: key); + static const String route = '/vendor/view'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return VendorViewVM.fromStore(store); + }, + builder: (context, vm) { + return VendorView( + viewModel: vm, + ); + }, + ); + } +} + +class VendorViewVM { + + VendorViewVM({ + @required this.state, + @required this.vendor, + @required this.company, + @required this.onActionSelected, + @required this.onEditPressed, + @required this.onBackPressed, + @required this.onRefreshed, + @required this.isSaving, + @required this.isLoading, + @required this.isDirty, + }); + + factory VendorViewVM.fromStore(Store store) { + final state = store.state; + final vendor = state.vendorState.map[state.vendorUIState.selectedId]; + + Future _handleRefresh(BuildContext context) { + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(LoadVendor(completer: completer, vendorId: vendor.id)); + return completer.future; + } + + return VendorViewVM( + state: state, + company: state.selectedCompany, + isSaving: state.isSaving, + isLoading: state.isLoading, + isDirty: vendor.isNew, + vendor: vendor, + onEditPressed: (BuildContext context) { + store.dispatch(EditVendor(vendor: vendor, context: context)); + }, + onRefreshed: (context) => _handleRefresh(context), + onBackPressed: () { + if (state.uiState.currentRoute.contains(VendorScreen.route)) { + store.dispatch(UpdateCurrentRoute(VendorScreen.route)); + } + }, + onActionSelected: (BuildContext context, EntityAction action) { + final localization = AppLocalization.of(context); + switch (action) { + case EntityAction.archive: + store.dispatch(ArchiveVendorRequest( + popCompleter(context, localization.archivedVendor), + vendor.id)); + break; + case EntityAction.delete: + store.dispatch(DeleteVendorRequest( + popCompleter(context, localization.deletedVendor), + vendor.id)); + break; + case EntityAction.restore: + store.dispatch(RestoreVendorRequest( + snackBarCompleter(context, localization.restoredVendor), + vendor.id)); + break; + } + }); + } + + final AppState state; + final VendorEntity vendor; + final CompanyEntity company; + final Function(BuildContext, EntityAction) onActionSelected; + final Function(BuildContext) onEditPressed; + final Function onBackPressed; + final Function(BuildContext) onRefreshed; + final bool isSaving; + final bool isLoading; + final bool isDirty; +} diff --git a/lib/utils/localization.dart b/lib/utils/localization.dart index f18f29c03..845b73e18 100644 --- a/lib/utils/localization.dart +++ b/lib/utils/localization.dart @@ -88,9 +88,6 @@ class AppLocalization { 'hosted_login': 'Hosted Login', 'selfhost_login': 'Selfhost Login', 'google_login': 'Google Login', - 'thanks_for_patience': - 'Thank for your patience while we work to implement these features.\n\nWe hope to have them completed in the next few months.\n\nUntil then we\'ll continue to support the', - 'legacy_mobile_app': 'legacy mobile app', 'today': 'Today', 'custom_range': 'Custom', 'date_range': 'Date Range', @@ -11465,12 +11462,6 @@ class AppLocalization { String get googleLogin => _localizedValues[locale.toString()]['google_login']; - String get thanksForPatience => - _localizedValues[locale.toString()]['thanks_for_patience']; - - String get legacyMobileApp => - _localizedValues[locale.toString()]['legacy_mobile_app']; - String get today => _localizedValues[locale.toString()]['today']; String get customRange => _localizedValues[locale.toString()]['custom_range'];