Quotes [WIP]

This commit is contained in:
Hillel Coren 2018-08-22 12:05:46 -07:00
parent 9fa8ff9dde
commit e1f60319e8
36 changed files with 3832 additions and 40 deletions

View File

@ -13,6 +13,7 @@ export 'package:invoiceninja_flutter/data/models/invoice_model.dart';
export 'package:invoiceninja_flutter/data/models/task_model.dart';
export 'package:invoiceninja_flutter/data/models/expense_model.dart';
export 'package:invoiceninja_flutter/data/models/vendor_model.dart';
export 'package:invoiceninja_flutter/data/models/quote_model.dart';
export 'package:invoiceninja_flutter/data/models/static/static_data_model.dart';
export 'package:invoiceninja_flutter/data/models/static/currency_model.dart';
export 'package:invoiceninja_flutter/data/models/static/size_model.dart';

View File

@ -0,0 +1,343 @@
import 'package:built_value/built_value.dart';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/mixins/invoice_mixin.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
part 'quote_model.g.dart';
abstract class QuoteListResponse
implements Built<QuoteListResponse, QuoteListResponseBuilder> {
factory QuoteListResponse([void updates(QuoteListResponseBuilder b)]) =
_$QuoteListResponse;
QuoteListResponse._();
BuiltList<QuoteEntity> get data;
static Serializer<QuoteListResponse> get serializer =>
_$quoteListResponseSerializer;
}
abstract class QuoteItemResponse
implements Built<QuoteItemResponse, QuoteItemResponseBuilder> {
factory QuoteItemResponse([void updates(QuoteItemResponseBuilder b)]) =
_$QuoteItemResponse;
QuoteItemResponse._();
QuoteEntity get data;
static Serializer<QuoteItemResponse> get serializer =>
_$quoteItemResponseSerializer;
}
class QuoteFields {
static const String amount = 'amount';
static const String clientId = 'clientId';
static const String quoteStatusId = 'quoteStatusId';
static const String quoteNumber = 'quoteNumber';
static const String discount = 'discount';
static const String poNumber = 'poNumber';
static const String quoteDate = 'quoteDate';
static const String validUntil = 'validUntil';
static const String terms = 'terms';
static const String partial = 'partial';
static const String partialDueDate = 'partialDueDate';
static const String publicNotes = 'publicNotes';
static const String privateNotes = 'privateNotes';
static const String updatedAt = 'updatedAt';
static const String archivedAt = 'archivedAt';
static const String isDeleted = 'isDeleted';
}
abstract class QuoteEntity extends Object
with BaseEntity, CalculateInvoiceTotal
implements Built<QuoteEntity, QuoteEntityBuilder> {
static int counter = 0;
factory QuoteEntity() {
return _$QuoteEntity._(
id: --QuoteEntity.counter,
amount: 0.0,
clientId: 0,
quoteStatusId: 0,
quoteNumber: '',
discount: 0.0,
poNumber: '',
quoteDate: convertDateTimeToSqlDate(),
validUntil: '',
terms: '',
publicNotes: '',
privateNotes: '',
recurringQuoteId: 0,
taxName1: '',
taxRate1: 0.0,
taxName2: '',
taxRate2: 0.0,
isAmountDiscount: false,
footer: '',
partial: 0.0,
partialDueDate: '',
customValue1: 0.0,
customValue2: 0.0,
customTaxes1: false,
customTaxes2: false,
quoteInvoiceId: 0,
customTextValue1: '',
customTextValue2: '',
isPublic: false,
filename: '',
invoiceItems: BuiltList<InvoiceItemEntity>(),
invitations: BuiltList<InvitationEntity>(),
updatedAt: 0,
archivedAt: 0,
isDeleted: false,
);
}
QuoteEntity._();
QuoteEntity get clone => rebuild((b) => b
..id = --QuoteEntity.counter
..quoteNumber = ''
..isPublic = false);
@override
EntityType get entityType {
return EntityType.invoice;
}
double get amount;
@BuiltValueField(wireName: 'client_id')
int get clientId;
@BuiltValueField(wireName: 'invoice_status_id')
int get quoteStatusId;
@BuiltValueField(wireName: 'invoice_number')
String get quoteNumber;
@override
double get discount;
@BuiltValueField(wireName: 'po_number')
String get poNumber;
@BuiltValueField(wireName: 'invoice_date')
String get quoteDate;
@BuiltValueField(wireName: 'due_date')
String get validUntil;
String get terms;
@BuiltValueField(wireName: 'public_notes')
String get publicNotes;
@BuiltValueField(wireName: 'private_notes')
String get privateNotes;
@BuiltValueField(wireName: 'recurring_invoice_id')
int get recurringQuoteId;
@override
@BuiltValueField(wireName: 'tax_name1')
String get taxName1;
@override
@BuiltValueField(wireName: 'tax_rate1')
double get taxRate1;
@override
@BuiltValueField(wireName: 'tax_name2')
String get taxName2;
@override
@BuiltValueField(wireName: 'tax_rate2')
double get taxRate2;
@override
@BuiltValueField(wireName: 'is_amount_discount')
bool get isAmountDiscount;
@BuiltValueField(wireName: 'invoice_footer')
String get footer;
double get partial;
@BuiltValueField(wireName: 'partial_due_date')
String get partialDueDate;
@override
@BuiltValueField(wireName: 'custom_value1')
double get customValue1;
@override
@BuiltValueField(wireName: 'custom_value2')
double get customValue2;
@override
@BuiltValueField(wireName: 'custom_taxes1')
bool get customTaxes1;
@override
@BuiltValueField(wireName: 'custom_taxes2')
bool get customTaxes2;
@BuiltValueField(wireName: 'quote_invoice_id')
int get quoteInvoiceId;
@BuiltValueField(wireName: 'custom_text_value1')
String get customTextValue1;
@BuiltValueField(wireName: 'custom_text_value2')
String get customTextValue2;
@BuiltValueField(wireName: 'is_public')
bool get isPublic;
String get filename;
@override
@BuiltValueField(wireName: 'invoice_items')
BuiltList<InvoiceItemEntity> get invoiceItems;
BuiltList<InvitationEntity> get invitations;
//String get last_login;
//String get custom_messages;
int compareTo(QuoteEntity quote, String sortField, bool sortAscending) {
int response = 0;
final QuoteEntity quoteA = sortAscending ? this : quote;
final QuoteEntity quoteB = sortAscending ? quote : this;
switch (sortField) {
case QuoteFields.amount:
response = quoteA.amount.compareTo(quoteB.amount);
break;
case QuoteFields.updatedAt:
response = quoteA.updatedAt.compareTo(quoteB.updatedAt);
break;
case QuoteFields.quoteDate:
response = quoteA.quoteDate.compareTo(quoteB.quoteDate);
break;
}
if (response == 0) {
return quoteA.quoteNumber.compareTo(quoteB.quoteNumber);
} else {
return response;
}
}
@override
bool matchesStatuses(BuiltList<EntityStatus> statuses) {
if (statuses.isEmpty) {
return true;
}
for (final status in statuses) {
if (status.id == quoteStatusId) {
return true;
}
if (status.id == kInvoiceStatusPastDue && isPastDue) {
return true;
}
}
return false;
}
@override
bool matchesFilter(String filter) {
if (filter == null || filter.isEmpty) {
return true;
}
if (quoteNumber.toLowerCase().contains(filter)) {
return true;
} else if (customTextValue1.isNotEmpty &&
customTextValue1.toLowerCase().contains(filter)) {
return true;
} else if (customTextValue2.isNotEmpty &&
customTextValue2.toLowerCase().contains(filter)) {
return true;
}
return false;
}
@override
String matchesFilterValue(String filter) {
if (filter == null || filter.isEmpty) {
return null;
}
filter = filter.toLowerCase();
if (customTextValue1.isNotEmpty &&
customTextValue1.toLowerCase().contains(filter)) {
return customTextValue1;
} else if (customTextValue2.isNotEmpty &&
customTextValue2.toLowerCase().contains(filter)) {
return customTextValue2;
}
return null;
}
QuoteEntity applyTax(TaxRateEntity taxRate) {
QuoteEntity invoice = rebuild((b) => b
..taxRate1 = taxRate.rate
..taxName1 = taxRate.name);
if (taxRate.isInclusive) {
invoice = invoice.rebuild((b) => b
..invoiceItems.replace(invoiceItems
.map((item) => item.rebuild(
(b) => b.cost = round(b.cost / (100 + taxRate.rate) * 100, 2)))
.toList()));
}
return invoice;
}
@override
String get listDisplayName {
return quoteNumber;
}
@override
double get listDisplayAmount => null;
@override
FormatNumberType get listDisplayAmountType => FormatNumberType.money;
double get requestedAmount => partial > 0 ? partial : amount;
bool get isPastDue {
if (validUntil.isEmpty) {
return false;
}
return !isDeleted &&
isPublic &&
quoteStatusId != kInvoiceStatusPaid &&
DateTime.tryParse(validUntil)
.isBefore(DateTime.now().subtract(Duration(days: 1)));
}
String get invitationLink => invitations.first?.link;
String get invitationSilentLink => invitations.first?.silentLink;
String get invitationDownloadLink => invitations.first?.downloadLink;
static Serializer<QuoteEntity> get serializer => _$quoteEntitySerializer;
}

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,9 @@ import 'package:invoiceninja_flutter/redux/client/client_state.dart';
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/data/models/quote_model.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
part 'serializers.g.dart';
@ -70,6 +73,8 @@ part 'serializers.g.dart';
TimezoneItemResponse,
TimezoneListResponse,
// STARTER: serializers - do not remove comment
QuoteEntity,
])
final Serializers serializers =
(_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

View File

@ -91,6 +91,9 @@ Serializers _$serializers = (new Serializers().toBuilder()
..add(ProjectEntity.serializer)
..add(ProjectItemResponse.serializer)
..add(ProjectListResponse.serializer)
..add(QuoteEntity.serializer)
..add(QuoteState.serializer)
..add(QuoteUIState.serializer)
..add(SizeEntity.serializer)
..add(SizeItemResponse.serializer)
..add(SizeListResponse.serializer)
@ -254,6 +257,12 @@ Serializers _$serializers = (new Serializers().toBuilder()
..addBuilderFactory(
const FullType(BuiltList, const [const FullType(InvitationEntity)]),
() => new ListBuilder<InvitationEntity>())
..addBuilderFactory(
const FullType(BuiltList, const [const FullType(InvoiceItemEntity)]),
() => new ListBuilder<InvoiceItemEntity>())
..addBuilderFactory(
const FullType(BuiltList, const [const FullType(InvitationEntity)]),
() => new ListBuilder<InvitationEntity>())
..addBuilderFactory(
const FullType(BuiltList, const [const FullType(LanguageEntity)]),
() => new ListBuilder<LanguageEntity>())
@ -323,8 +332,7 @@ Serializers _$serializers = (new Serializers().toBuilder()
const [const FullType(int), const FullType(TimezoneEntity)]),
() => new MapBuilder<int, TimezoneEntity>())
..addBuilderFactory(
const FullType(BuiltMap,
const [const FullType(int), const FullType(DateFormatEntity)]),
const FullType(BuiltMap, const [const FullType(int), const FullType(DateFormatEntity)]),
() => new MapBuilder<int, DateFormatEntity>())
..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(DatetimeFormatEntity)]), () => new MapBuilder<int, DatetimeFormatEntity>())
..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(LanguageEntity)]), () => new MapBuilder<int, LanguageEntity>())
@ -335,5 +343,7 @@ Serializers _$serializers = (new Serializers().toBuilder()
..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(InvoiceEntity)]), () => new MapBuilder<int, InvoiceEntity>())
..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder<int>())
..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(ProductEntity)]), () => new MapBuilder<int, ProductEntity>())
..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder<int>())
..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(QuoteEntity)]), () => new MapBuilder<int, QuoteEntity>())
..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder<int>()))
.build();

View File

@ -0,0 +1,68 @@
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'package:built_collection/built_collection.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 QuoteRepository {
final WebClient webClient;
const QuoteRepository({
this.webClient = const WebClient(),
});
Future<QuoteEntity> loadItem(
CompanyEntity company, AuthState auth, int entityId) async {
final dynamic response = await webClient.get(
'${auth.url}/quotes/$entityId', company.token);
final QuoteItemResponse quoteResponse =
serializers.deserializeWith(QuoteItemResponse.serializer, response);
return quoteResponse.data;
}
Future<BuiltList<QuoteEntity>> loadList(
CompanyEntity company, AuthState auth, int updatedAt) async {
String url = auth.url + '/quotes';
if (updatedAt > 0) {
url += '&updated_at=${updatedAt - 600}';
}
final dynamic response = await webClient.get(url, company.token);
final QuoteListResponse quoteResponse =
serializers.deserializeWith(QuoteListResponse.serializer, response);
return quoteResponse.data;
}
Future<QuoteEntity> saveData(
CompanyEntity company, AuthState auth, QuoteEntity quote,
[EntityAction action]) async {
final data = serializers.serializeWith(QuoteEntity.serializer, quote);
dynamic response;
if (quote.isNew) {
response = await webClient.post(
auth.url + '/quotes',
company.token,
json.encode(data));
} else {
var url = auth.url + '/quotes/' + quote.id.toString();
if (action != null) {
url += '?action=' + action.toString();
}
response = await webClient.put(url, company.token, json.encode(data));
}
final QuoteItemResponse quoteResponse =
serializers.deserializeWith(QuoteItemResponse.serializer, response);
return quoteResponse.data;
}
}

View File

@ -35,6 +35,12 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_middleware.dart';
import 'package:invoiceninja_flutter/ui/invoice/invoice_screen.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/view/quote_view_vm.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_middleware.dart';
void main() async {
final prefs = await SharedPreferences.getInstance();
@ -50,6 +56,8 @@ void main() async {
..addAll(createStoreInvoicesMiddleware())
..addAll(createStorePersistenceMiddleware())
// STARTER: middleware - do not remove comment
..addAll(createStoreQuotesMiddleware())
..addAll([
LoggingMiddleware<dynamic>.printer(),
]));
@ -133,6 +141,13 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
InvoiceEditScreen.route: (context) => InvoiceEditScreen(),
InvoiceEmailScreen.route: (context) => InvoiceEmailScreen(),
// STARTER: routes - do not remove comment
QuoteScreen.route: (context) {
widget.store.dispatch(LoadQuotes());
return QuoteScreen();
},
QuoteViewScreen.route: (context) => QuoteViewScreen(),
QuoteEditScreen.route: (context) => QuoteEditScreen(),
SettingsScreen.route: (context) => SettingsScreen(),
},
);

View File

@ -11,7 +11,9 @@ import 'package:invoiceninja_flutter/redux/product/product_state.dart';
import 'package:invoiceninja_flutter/redux/dashboard/dashboard_state.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
part 'app_state.g.dart';
@ -94,6 +96,9 @@ abstract class AppState implements Built<AppState, AppStateBuilder> {
case EntityType.invoice:
return invoiceUIState;
// STARTER: states switch - do not remove comment
case EntityType.quote:
return quoteUIState;
default:
return null;
}
@ -122,6 +127,11 @@ abstract class AppState implements Built<AppState, AppStateBuilder> {
ListUIState get invoiceListState => uiState.invoiceUIState.listUIState;
// STARTER: state getters - do not remove comment
QuoteState get quoteState => selectedCompanyState.quoteState;
ListUIState get quoteListState => uiState.quoteUIState.listUIState;
QuoteUIState get quoteUIState => uiState.quoteUIState;
@override
String toString() {

View File

@ -8,6 +8,8 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_reducer.dart';
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/quote/quote_reducer.dart';
CompanyState companyReducer(CompanyState state, dynamic action) {
if (action is RefreshData) {
@ -21,6 +23,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
..quoteState.replace(quotesReducer(state.quoteState, action))
);
}

View File

@ -6,6 +6,8 @@ import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
part 'company_state.g.dart';
@ -19,6 +21,8 @@ abstract class CompanyState implements Built<CompanyState, CompanyStateBuilder>
clientState: ClientState(),
invoiceState: InvoiceState(),
// STARTER: constructor - do not remove comment
quoteState: QuoteState(),
);
}
CompanyState._();
@ -31,6 +35,8 @@ abstract class CompanyState implements Built<CompanyState, CompanyStateBuilder>
InvoiceState get invoiceState;
// STARTER: fields - do not remove comment
QuoteState get quoteState;
//factory CompanyState([void updates(CompanyStateBuilder b)]) = _$CompanyState;
static Serializer<CompanyState> get serializer => _$companyStateSerializer;

View File

@ -41,6 +41,9 @@ class _$CompanyStateSerializer implements StructuredSerializer<CompanyState> {
'invoiceState',
serializers.serialize(object.invoiceState,
specifiedType: const FullType(InvoiceState)),
'quoteState',
serializers.serialize(object.quoteState,
specifiedType: const FullType(QuoteState)),
];
if (object.company != null) {
result
@ -83,6 +86,10 @@ class _$CompanyStateSerializer implements StructuredSerializer<CompanyState> {
result.invoiceState.replace(serializers.deserialize(value,
specifiedType: const FullType(InvoiceState)) as InvoiceState);
break;
case 'quoteState':
result.quoteState.replace(serializers.deserialize(value,
specifiedType: const FullType(QuoteState)) as QuoteState);
break;
}
}
@ -101,6 +108,8 @@ class _$CompanyState extends CompanyState {
final ClientState clientState;
@override
final InvoiceState invoiceState;
@override
final QuoteState quoteState;
factory _$CompanyState([void updates(CompanyStateBuilder b)]) =>
(new CompanyStateBuilder()..update(updates)).build();
@ -110,7 +119,8 @@ class _$CompanyState extends CompanyState {
this.dashboardState,
this.productState,
this.clientState,
this.invoiceState})
this.invoiceState,
this.quoteState})
: super._() {
if (dashboardState == null)
throw new BuiltValueNullFieldError('CompanyState', 'dashboardState');
@ -120,6 +130,8 @@ class _$CompanyState extends CompanyState {
throw new BuiltValueNullFieldError('CompanyState', 'clientState');
if (invoiceState == null)
throw new BuiltValueNullFieldError('CompanyState', 'invoiceState');
if (quoteState == null)
throw new BuiltValueNullFieldError('CompanyState', 'quoteState');
}
@override
@ -137,17 +149,20 @@ class _$CompanyState extends CompanyState {
dashboardState == other.dashboardState &&
productState == other.productState &&
clientState == other.clientState &&
invoiceState == other.invoiceState;
invoiceState == other.invoiceState &&
quoteState == other.quoteState;
}
@override
int get hashCode {
return $jf($jc(
$jc(
$jc($jc($jc(0, company.hashCode), dashboardState.hashCode),
productState.hashCode),
clientState.hashCode),
invoiceState.hashCode));
$jc(
$jc($jc($jc(0, company.hashCode), dashboardState.hashCode),
productState.hashCode),
clientState.hashCode),
invoiceState.hashCode),
quoteState.hashCode));
}
@override
@ -157,7 +172,8 @@ class _$CompanyState extends CompanyState {
..add('dashboardState', dashboardState)
..add('productState', productState)
..add('clientState', clientState)
..add('invoiceState', invoiceState))
..add('invoiceState', invoiceState)
..add('quoteState', quoteState))
.toString();
}
}
@ -195,6 +211,12 @@ class CompanyStateBuilder
set invoiceState(InvoiceStateBuilder invoiceState) =>
_$this._invoiceState = invoiceState;
QuoteStateBuilder _quoteState;
QuoteStateBuilder get quoteState =>
_$this._quoteState ??= new QuoteStateBuilder();
set quoteState(QuoteStateBuilder quoteState) =>
_$this._quoteState = quoteState;
CompanyStateBuilder();
CompanyStateBuilder get _$this {
@ -204,6 +226,7 @@ class CompanyStateBuilder
_productState = _$v.productState?.toBuilder();
_clientState = _$v.clientState?.toBuilder();
_invoiceState = _$v.invoiceState?.toBuilder();
_quoteState = _$v.quoteState?.toBuilder();
_$v = null;
}
return this;
@ -230,7 +253,8 @@ class CompanyStateBuilder
dashboardState: dashboardState.build(),
productState: productState.build(),
clientState: clientState.build(),
invoiceState: invoiceState.build());
invoiceState: invoiceState.build(),
quoteState: quoteState.build());
} catch (_) {
String _$failedField;
try {
@ -244,6 +268,8 @@ class CompanyStateBuilder
clientState.build();
_$failedField = 'invoiceState';
invoiceState.build();
_$failedField = 'quoteState';
quoteState.build();
} catch (e) {
throw new BuiltValueNestedFieldError(
'CompanyState', _$failedField, e.toString());

View File

@ -0,0 +1,303 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:built_collection/built_collection.dart';
import 'package:invoiceninja_flutter/redux/app/app_actions.dart';
class ViewQuoteList implements PersistUI {
final BuildContext context;
ViewQuoteList(this.context);
}
class ViewQuote implements PersistUI {
final int quoteId;
final BuildContext context;
ViewQuote({this.quoteId, this.context});
}
class EditQuote implements PersistUI {
final QuoteEntity quote;
final InvoiceItemEntity quoteItem;
final BuildContext context;
final Completer completer;
EditQuote({this.quote, this.context, this.completer, this.quoteItem});
}
class ShowEmailQuote {
final QuoteEntity quote;
final BuildContext context;
final Completer completer;
ShowEmailQuote({this.quote, this.context, this.completer});
}
class EditQuoteItem implements PersistUI {
final InvoiceItemEntity quoteItem;
EditQuoteItem([this.quoteItem]);
}
class UpdateQuote implements PersistUI {
final QuoteEntity quote;
UpdateQuote(this.quote);
}
class LoadQuote {
final Completer completer;
final int quoteId;
LoadQuote({this.completer, this.quoteId});
}
class LoadQuotes {
final Completer completer;
final bool force;
LoadQuotes({this.completer, this.force = false});
}
class LoadQuoteRequest implements StartLoading {}
class LoadQuoteFailure implements StopLoading {
final dynamic error;
LoadQuoteFailure(this.error);
@override
String toString() {
return 'LoadQuoteFailure{error: $error}';
}
}
class LoadQuoteSuccess implements StopLoading, PersistData {
final QuoteEntity quote;
LoadQuoteSuccess(this.quote);
@override
String toString() {
return 'LoadQuoteSuccess{quote: $quote}';
}
}
class LoadQuotesRequest implements StartLoading {}
class LoadQuotesFailure implements StopLoading {
final dynamic error;
LoadQuotesFailure(this.error);
@override
String toString() {
return 'LoadQuotesFailure{error: $error}';
}
}
class LoadQuotesSuccess implements StopLoading, PersistData {
final BuiltList<QuoteEntity> quotes;
LoadQuotesSuccess(this.quotes);
@override
String toString() {
return 'LoadQuotesSuccess{quotes: $quotes}';
}
}
class AddQuoteItem implements PersistUI {
final InvoiceItemEntity quoteItem;
AddQuoteItem({this.quoteItem});
}
class AddQuoteItems implements PersistUI {
final List<InvoiceItemEntity> quoteItems;
AddQuoteItems(this.quoteItems);
}
class UpdateQuoteItem implements PersistUI {
final int index;
final InvoiceItemEntity quoteItem;
UpdateQuoteItem({this.index, this.quoteItem});
}
class DeleteQuoteItem implements PersistUI {
final int index;
DeleteQuoteItem(this.index);
}
class SaveQuoteRequest implements StartSaving {
final Completer completer;
final QuoteEntity quote;
SaveQuoteRequest({this.completer, this.quote});
}
class SaveQuoteSuccess implements StopSaving, PersistData, PersistUI {
final QuoteEntity quote;
SaveQuoteSuccess(this.quote);
}
class AddQuoteSuccess implements StopSaving, PersistData, PersistUI {
final QuoteEntity quote;
AddQuoteSuccess(this.quote);
}
class SaveQuoteFailure implements StopSaving {
final Object error;
SaveQuoteFailure(this.error);
}
class EmailQuoteRequest implements StartSaving {
final Completer completer;
final int quoteId;
final EmailTemplate template;
final String subject;
final String body;
EmailQuoteRequest(
{this.completer, this.quoteId, this.template, this.subject, this.body});
}
class EmailQuoteSuccess implements StopSaving, PersistData {}
class EmailQuoteFailure implements StopSaving {
final dynamic error;
EmailQuoteFailure(this.error);
}
class MarkSentQuoteRequest implements StartSaving {
final Completer completer;
final int quoteId;
MarkSentQuoteRequest(this.completer, this.quoteId);
}
class MarkSentQuoteSuccess implements StopSaving, PersistData {
final QuoteEntity quote;
MarkSentQuoteSuccess(this.quote);
}
class MarkSentQuoteFailure implements StopSaving {
final QuoteEntity quote;
MarkSentQuoteFailure(this.quote);
}
class ArchiveQuoteRequest implements StartSaving {
final Completer completer;
final int quoteId;
ArchiveQuoteRequest(this.completer, this.quoteId);
}
class ArchiveQuoteSuccess implements StopSaving, PersistData {
final QuoteEntity quote;
ArchiveQuoteSuccess(this.quote);
}
class ArchiveQuoteFailure implements StopSaving {
final QuoteEntity quote;
ArchiveQuoteFailure(this.quote);
}
class DeleteQuoteRequest implements StartSaving {
final Completer completer;
final int quoteId;
DeleteQuoteRequest(this.completer, this.quoteId);
}
class DeleteQuoteSuccess implements StopSaving, PersistData {
final QuoteEntity quote;
DeleteQuoteSuccess(this.quote);
}
class DeleteQuoteFailure implements StopSaving {
final QuoteEntity quote;
DeleteQuoteFailure(this.quote);
}
class RestoreQuoteRequest implements StartSaving {
final Completer completer;
final int quoteId;
RestoreQuoteRequest(this.completer, this.quoteId);
}
class RestoreQuoteSuccess implements StopSaving, PersistData {
final QuoteEntity quote;
RestoreQuoteSuccess(this.quote);
}
class RestoreQuoteFailure implements StopSaving {
final QuoteEntity quote;
RestoreQuoteFailure(this.quote);
}
class FilterQuotes {
final String filter;
FilterQuotes(this.filter);
}
class SortQuotes implements PersistUI {
final String field;
SortQuotes(this.field);
}
class FilterQuotesByState implements PersistUI {
final EntityState state;
FilterQuotesByState(this.state);
}
class FilterQuotesByStatus implements PersistUI {
final EntityStatus status;
FilterQuotesByStatus(this.status);
}
class FilterQuotesByClient implements PersistUI {
final int clientId;
FilterQuotesByClient([this.clientId]);
}
class FilterQuoteDropdown {
final String filter;
FilterQuoteDropdown(this.filter);
}
class FilterQuotesByCustom1 implements PersistUI {
final String value;
FilterQuotesByCustom1(this.value);
}
class FilterQuotesByCustom2 implements PersistUI {
final String value;
FilterQuotesByCustom2(this.value);
}

View File

@ -0,0 +1,236 @@
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/product/product_actions.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/view/quote_view_vm.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/data/repositories/quote_repository.dart';
List<Middleware<AppState>> createStoreQuotesMiddleware([
QuoteRepository repository = const QuoteRepository(),
]) {
final viewQuoteList = _viewQuoteList();
final viewQuote = _viewQuote();
final editQuote = _editQuote();
final loadQuotes = _loadQuotes(repository);
final loadQuote = _loadQuote(repository);
final saveQuote = _saveQuote(repository);
final archiveQuote = _archiveQuote(repository);
final deleteQuote = _deleteQuote(repository);
final restoreQuote = _restoreQuote(repository);
return [
TypedMiddleware<AppState, ViewQuoteList>(viewQuoteList),
TypedMiddleware<AppState, ViewQuote>(viewQuote),
TypedMiddleware<AppState, EditQuote>(editQuote),
TypedMiddleware<AppState, LoadQuotes>(loadQuotes),
TypedMiddleware<AppState, LoadQuote>(loadQuote),
TypedMiddleware<AppState, SaveQuoteRequest>(saveQuote),
TypedMiddleware<AppState, ArchiveQuoteRequest>(archiveQuote),
TypedMiddleware<AppState, DeleteQuoteRequest>(deleteQuote),
TypedMiddleware<AppState, RestoreQuoteRequest>(restoreQuote),
];
}
Middleware<AppState> _editQuote() {
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
next(action);
if (action.trackRoute) {
store.dispatch(UpdateCurrentRoute(QuoteEditScreen.route));
}
final quote =
await Navigator.of(action.context).pushNamed(QuoteEditScreen.route);
if (action.completer != null && quote != null) {
action.completer.complete(quote);
}
};
}
Middleware<AppState> _viewQuote() {
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
next(action);
store.dispatch(UpdateCurrentRoute(QuoteViewScreen.route));
Navigator.of(action.context).pushNamed(QuoteViewScreen.route);
};
}
Middleware<AppState> _viewQuoteList() {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
next(action);
store.dispatch(UpdateCurrentRoute(QuoteScreen.route));
Navigator.of(action.context).pushNamedAndRemoveUntil(QuoteScreen.route, (Route<dynamic> route) => false);
};
}
Middleware<AppState> _archiveQuote(QuoteRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final origQuote = store.state.quoteState.map[action.quoteId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origQuote, EntityAction.archive)
.then((dynamic quote) {
store.dispatch(ArchiveQuoteSuccess(quote));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(ArchiveQuoteFailure(origQuote));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}
Middleware<AppState> _deleteQuote(QuoteRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final origQuote = store.state.quoteState.map[action.quoteId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origQuote, EntityAction.delete)
.then((dynamic quote) {
store.dispatch(DeleteQuoteSuccess(quote));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(DeleteQuoteFailure(origQuote));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}
Middleware<AppState> _restoreQuote(QuoteRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final origQuote = store.state.quoteState.map[action.quoteId];
repository
.saveData(store.state.selectedCompany, store.state.authState,
origQuote, EntityAction.restore)
.then((dynamic quote) {
store.dispatch(RestoreQuoteSuccess(quote));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(RestoreQuoteFailure(origQuote));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}
Middleware<AppState> _saveQuote(QuoteRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
repository
.saveData(
store.state.selectedCompany, store.state.authState, action.quote)
.then((dynamic quote) {
if (action.quote.isNew) {
store.dispatch(AddQuoteSuccess(quote));
} else {
store.dispatch(SaveQuoteSuccess(quote));
}
action.completer.complete(quote);
}).catchError((Object error) {
print(error);
store.dispatch(SaveQuoteFailure(error));
action.completer.completeError(error);
});
next(action);
};
}
Middleware<AppState> _loadQuote(QuoteRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final AppState state = store.state;
if (state.isLoading) {
next(action);
return;
}
store.dispatch(LoadQuoteRequest());
repository
.loadItem(state.selectedCompany, state.authState, action.quoteId)
.then((quote) {
store.dispatch(LoadQuoteSuccess(quote));
if (action.completer != null) {
action.completer.complete(null);
}
}).catchError((Object error) {
print(error);
store.dispatch(LoadQuoteFailure(error));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}
Middleware<AppState> _loadQuotes(QuoteRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
final AppState state = store.state;
if (!state.quoteState.isStale && !action.force) {
next(action);
return;
}
if (state.isLoading) {
next(action);
return;
}
final int updatedAt =
action.force ? 0 : (state.quoteState.lastUpdated / 1000).round();
store.dispatch(LoadQuotesRequest());
repository
.loadList(state.selectedCompany, state.authState, updatedAt)
.then((data) {
store.dispatch(LoadQuotesSuccess(data));
if (action.completer != null) {
action.completer.complete(null);
}
if (state.productState.isStale) {
store.dispatch(LoadProducts());
}
}).catchError((Object error) {
print(error);
store.dispatch(LoadQuotesFailure(error));
if (action.completer != null) {
action.completer.completeError(error);
}
});
next(action);
};
}

View File

@ -0,0 +1,194 @@
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/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
EntityUIState quoteUIReducer(QuoteUIState state, dynamic action) {
return state.rebuild((b) => b
..listUIState.replace(quoteListReducer(state.listUIState, action))
..editing.replace(editingReducer(state.editing, action))
..selectedId = selectedIdReducer(state.selectedId, action));
}
Reducer<int> selectedIdReducer = combineReducers([
TypedReducer<int, ViewQuote>(
(int selectedId, dynamic action) => action.quoteId),
TypedReducer<int, AddQuoteSuccess>(
(int selectedId, dynamic action) => action.quote.id),
]);
final editingReducer = combineReducers<QuoteEntity>([
TypedReducer<QuoteEntity, SaveQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, AddQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, RestoreQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, ArchiveQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, DeleteQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, EditQuote>(_updateEditing),
TypedReducer<QuoteEntity, UpdateQuote>(_updateEditing),
TypedReducer<QuoteEntity, SelectCompany>(_clearEditing),
]);
QuoteEntity _clearEditing(QuoteEntity quote, dynamic action) {
return QuoteEntity();
}
QuoteEntity _updateEditing(QuoteEntity quote, dynamic action) {
return action.quote;
}
final quoteListReducer = combineReducers<ListUIState>([
TypedReducer<ListUIState, SortQuotes>(_sortQuotes),
TypedReducer<ListUIState, FilterQuotesByState>(_filterQuotesByState),
TypedReducer<ListUIState, FilterQuotes>(_filterQuotes),
TypedReducer<ListUIState, FilterQuotesByCustom1>(_filterQuotesByCustom1),
TypedReducer<ListUIState, FilterQuotesByCustom2>(_filterQuotesByCustom2),
]);
ListUIState _filterQuotesByCustom1(
ListUIState quoteListState, FilterQuotesByCustom1 action) {
if (quoteListState.custom1Filters.contains(action.value)) {
return quoteListState
.rebuild((b) => b..custom1Filters.remove(action.value));
} else {
return quoteListState.rebuild((b) => b..custom1Filters.add(action.value));
}
}
ListUIState _filterQuotesByCustom2(
ListUIState quoteListState, FilterQuotesByCustom2 action) {
if (quoteListState.custom2Filters.contains(action.value)) {
return quoteListState
.rebuild((b) => b..custom2Filters.remove(action.value));
} else {
return quoteListState.rebuild((b) => b..custom2Filters.add(action.value));
}
}
ListUIState _filterQuotesByState(
ListUIState quoteListState, FilterQuotesByState action) {
if (quoteListState.stateFilters.contains(action.state)) {
return quoteListState.rebuild((b) => b..stateFilters.remove(action.state));
} else {
return quoteListState.rebuild((b) => b..stateFilters.add(action.state));
}
}
ListUIState _filterQuotes(ListUIState quoteListState, FilterQuotes action) {
return quoteListState.rebuild((b) => b..filter = action.filter);
}
ListUIState _sortQuotes(ListUIState quoteListState, SortQuotes action) {
return quoteListState.rebuild((b) => b
..sortAscending = b.sortField != action.field || !b.sortAscending
..sortField = action.field);
}
final quotesReducer = combineReducers<QuoteState>([
TypedReducer<QuoteState, SaveQuoteSuccess>(_updateQuote),
TypedReducer<QuoteState, AddQuoteSuccess>(_addQuote),
TypedReducer<QuoteState, LoadQuotesSuccess>(_setLoadedQuotes),
TypedReducer<QuoteState, LoadQuotesFailure>(_setNoQuotes),
TypedReducer<QuoteState, LoadQuoteSuccess>(_setLoadedQuote),
TypedReducer<QuoteState, ArchiveQuoteRequest>(_archiveQuoteRequest),
TypedReducer<QuoteState, ArchiveQuoteSuccess>(_archiveQuoteSuccess),
TypedReducer<QuoteState, ArchiveQuoteFailure>(_archiveQuoteFailure),
TypedReducer<QuoteState, DeleteQuoteRequest>(_deleteQuoteRequest),
TypedReducer<QuoteState, DeleteQuoteSuccess>(_deleteQuoteSuccess),
TypedReducer<QuoteState, DeleteQuoteFailure>(_deleteQuoteFailure),
TypedReducer<QuoteState, RestoreQuoteRequest>(_restoreQuoteRequest),
TypedReducer<QuoteState, RestoreQuoteSuccess>(_restoreQuoteSuccess),
TypedReducer<QuoteState, RestoreQuoteFailure>(_restoreQuoteFailure),
]);
QuoteState _archiveQuoteRequest(
QuoteState quoteState, ArchiveQuoteRequest action) {
final quote = quoteState.map[action.quoteId]
.rebuild((b) => b..archivedAt = DateTime.now().millisecondsSinceEpoch);
return quoteState.rebuild((b) => b..map[action.quoteId] = quote);
}
QuoteState _archiveQuoteSuccess(
QuoteState quoteState, ArchiveQuoteSuccess action) {
return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote);
}
QuoteState _archiveQuoteFailure(
QuoteState quoteState, ArchiveQuoteFailure action) {
return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote);
}
QuoteState _deleteQuoteRequest(
QuoteState quoteState, DeleteQuoteRequest action) {
final quote = quoteState.map[action.quoteId].rebuild((b) => b
..archivedAt = DateTime.now().millisecondsSinceEpoch
..isDeleted = true);
return quoteState.rebuild((b) => b..map[action.quoteId] = quote);
}
QuoteState _deleteQuoteSuccess(
QuoteState quoteState, DeleteQuoteSuccess action) {
return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote);
}
QuoteState _deleteQuoteFailure(
QuoteState quoteState, DeleteQuoteFailure action) {
return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote);
}
QuoteState _restoreQuoteRequest(
QuoteState quoteState, RestoreQuoteRequest action) {
final quote = quoteState.map[action.quoteId].rebuild((b) => b
..archivedAt = null
..isDeleted = false);
return quoteState.rebuild((b) => b..map[action.quoteId] = quote);
}
QuoteState _restoreQuoteSuccess(
QuoteState quoteState, RestoreQuoteSuccess action) {
return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote);
}
QuoteState _restoreQuoteFailure(
QuoteState quoteState, RestoreQuoteFailure action) {
return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote);
}
QuoteState _addQuote(QuoteState quoteState, AddQuoteSuccess action) {
return quoteState.rebuild((b) => b
..map[action.quote.id] = action.quote
..list.add(action.quote.id));
}
QuoteState _updateQuote(QuoteState quoteState, SaveQuoteSuccess action) {
return quoteState.rebuild((b) => b
..map[action.quote.id] = action.quote);
}
QuoteState _setLoadedQuote(
QuoteState quoteState, LoadQuoteSuccess action) {
return quoteState.rebuild((b) => b
..map[action.quote.id] = action.quote);
}
QuoteState _setNoQuotes(QuoteState quoteState, LoadQuotesFailure action) {
return quoteState;
}
QuoteState _setLoadedQuotes(
QuoteState quoteState, LoadQuotesSuccess action) {
final state = quoteState.rebuild((b) => b
..lastUpdated = DateTime.now().millisecondsSinceEpoch
..map.addAll(Map.fromIterable(
action.quotes,
key: (dynamic item) => item.id,
value: (dynamic item) => item,
)));
return state.rebuild((b) => b..list.replace(state.map.keys));
}

View File

@ -0,0 +1,54 @@
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 memoizedDropdownQuoteList = memo2(
(BuiltMap<int, QuoteEntity> quoteMap, BuiltList<int> quoteList) =>
dropdownQuotesSelector(quoteMap, quoteList));
List<int> dropdownQuotesSelector(
BuiltMap<int, QuoteEntity> quoteMap, BuiltList<int> quoteList) {
final list =
quoteList.where((quoteId) => quoteMap[quoteId].isActive).toList();
list.sort((quoteAId, quoteBId) {
final quoteA = quoteMap[quoteAId];
final quoteB = quoteMap[quoteBId];
return quoteA.compareTo(quoteB, QuoteFields.quoteNumber, true);
});
return list;
}
var memoizedFilteredQuoteList = memo3((BuiltMap<int, QuoteEntity> quoteMap,
BuiltList<int> quoteList, ListUIState quoteListState) =>
filteredQuotesSelector(quoteMap, quoteList, quoteListState));
List<int> filteredQuotesSelector(BuiltMap<int, QuoteEntity> quoteMap,
BuiltList<int> quoteList, ListUIState quoteListState) {
final list = quoteList.where((quoteId) {
final quote = quoteMap[quoteId];
if (!quote.matchesStates(quoteListState.stateFilters)) {
return false;
}
if (quoteListState.custom1Filters.isNotEmpty &&
!quoteListState.custom1Filters.contains(quote.customTextValue1)) {
return false;
}
if (quoteListState.custom2Filters.isNotEmpty &&
!quoteListState.custom2Filters.contains(quote.customTextValue2)) {
return false;
}
return quote.matchesFilter(quoteListState.filter);
}).toList();
list.sort((quoteAId, quoteBId) {
final quoteA = quoteMap[quoteAId];
final quoteB = quoteMap[quoteBId];
return quoteA.compareTo(
quoteB, quoteListState.sortField, quoteListState.sortAscending);
});
return list;
}

View File

@ -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/quote_model.dart';
import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.dart';
import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart';
part 'quote_state.g.dart';
abstract class QuoteState implements Built<QuoteState, QuoteStateBuilder> {
factory QuoteState() {
return _$QuoteState._(
lastUpdated: 0,
map: BuiltMap<int, QuoteEntity>(),
list: BuiltList<int>(),
);
}
QuoteState._();
@nullable
int get lastUpdated;
BuiltMap<int, QuoteEntity> get map;
BuiltList<int> get list;
bool get isStale {
if (! isLoaded) {
return true;
}
return DateTime.now().millisecondsSinceEpoch - lastUpdated > kMillisecondsToRefreshData;
}
bool get isLoaded => lastUpdated != null && lastUpdated > 0;
static Serializer<QuoteState> get serializer => _$quoteStateSerializer;
}
abstract class QuoteUIState extends Object with EntityUIState implements Built<QuoteUIState, QuoteUIStateBuilder> {
factory QuoteUIState() {
return _$QuoteUIState._(
listUIState: ListUIState(QuoteFields.quoteNumber),
editing: QuoteEntity(),
selectedId: 0,
);
}
QuoteUIState._();
@nullable
QuoteEntity get editing;
@override
bool get isCreatingNew => editing.isNew;
static Serializer<QuoteUIState> get serializer => _$quoteUIStateSerializer;
}

View File

@ -0,0 +1,375 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'quote_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
Serializer<QuoteState> _$quoteStateSerializer = new _$QuoteStateSerializer();
Serializer<QuoteUIState> _$quoteUIStateSerializer =
new _$QuoteUIStateSerializer();
class _$QuoteStateSerializer implements StructuredSerializer<QuoteState> {
@override
final Iterable<Type> types = const [QuoteState, _$QuoteState];
@override
final String wireName = 'QuoteState';
@override
Iterable serialize(Serializers serializers, QuoteState object,
{FullType specifiedType = FullType.unspecified}) {
final result = <Object>[
'map',
serializers.serialize(object.map,
specifiedType: const FullType(BuiltMap,
const [const FullType(int), const FullType(QuoteEntity)])),
'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
QuoteState deserialize(Serializers serializers, Iterable serialized,
{FullType specifiedType = FullType.unspecified}) {
final result = new QuoteStateBuilder();
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(QuoteEntity)
])) 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 _$QuoteUIStateSerializer implements StructuredSerializer<QuoteUIState> {
@override
final Iterable<Type> types = const [QuoteUIState, _$QuoteUIState];
@override
final String wireName = 'QuoteUIState';
@override
Iterable serialize(Serializers serializers, QuoteUIState object,
{FullType specifiedType = FullType.unspecified}) {
final result = <Object>[
'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(QuoteEntity)));
}
return result;
}
@override
QuoteUIState deserialize(Serializers serializers, Iterable serialized,
{FullType specifiedType = FullType.unspecified}) {
final result = new QuoteUIStateBuilder();
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(QuoteEntity)) as QuoteEntity);
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 _$QuoteState extends QuoteState {
@override
final int lastUpdated;
@override
final BuiltMap<int, QuoteEntity> map;
@override
final BuiltList<int> list;
factory _$QuoteState([void updates(QuoteStateBuilder b)]) =>
(new QuoteStateBuilder()..update(updates)).build();
_$QuoteState._({this.lastUpdated, this.map, this.list}) : super._() {
if (map == null) throw new BuiltValueNullFieldError('QuoteState', 'map');
if (list == null) throw new BuiltValueNullFieldError('QuoteState', 'list');
}
@override
QuoteState rebuild(void updates(QuoteStateBuilder b)) =>
(toBuilder()..update(updates)).build();
@override
QuoteStateBuilder toBuilder() => new QuoteStateBuilder()..replace(this);
@override
bool operator ==(dynamic other) {
if (identical(other, this)) return true;
if (other is! QuoteState) return false;
return 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('QuoteState')
..add('lastUpdated', lastUpdated)
..add('map', map)
..add('list', list))
.toString();
}
}
class QuoteStateBuilder implements Builder<QuoteState, QuoteStateBuilder> {
_$QuoteState _$v;
int _lastUpdated;
int get lastUpdated => _$this._lastUpdated;
set lastUpdated(int lastUpdated) => _$this._lastUpdated = lastUpdated;
MapBuilder<int, QuoteEntity> _map;
MapBuilder<int, QuoteEntity> get map =>
_$this._map ??= new MapBuilder<int, QuoteEntity>();
set map(MapBuilder<int, QuoteEntity> map) => _$this._map = map;
ListBuilder<int> _list;
ListBuilder<int> get list => _$this._list ??= new ListBuilder<int>();
set list(ListBuilder<int> list) => _$this._list = list;
QuoteStateBuilder();
QuoteStateBuilder get _$this {
if (_$v != null) {
_lastUpdated = _$v.lastUpdated;
_map = _$v.map?.toBuilder();
_list = _$v.list?.toBuilder();
_$v = null;
}
return this;
}
@override
void replace(QuoteState other) {
if (other == null) throw new ArgumentError.notNull('other');
_$v = other as _$QuoteState;
}
@override
void update(void updates(QuoteStateBuilder b)) {
if (updates != null) updates(this);
}
@override
_$QuoteState build() {
_$QuoteState _$result;
try {
_$result = _$v ??
new _$QuoteState._(
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(
'QuoteState', _$failedField, e.toString());
}
rethrow;
}
replace(_$result);
return _$result;
}
}
class _$QuoteUIState extends QuoteUIState {
@override
final QuoteEntity editing;
@override
final int selectedId;
@override
final ListUIState listUIState;
factory _$QuoteUIState([void updates(QuoteUIStateBuilder b)]) =>
(new QuoteUIStateBuilder()..update(updates)).build();
_$QuoteUIState._({this.editing, this.selectedId, this.listUIState})
: super._() {
if (selectedId == null)
throw new BuiltValueNullFieldError('QuoteUIState', 'selectedId');
if (listUIState == null)
throw new BuiltValueNullFieldError('QuoteUIState', 'listUIState');
}
@override
QuoteUIState rebuild(void updates(QuoteUIStateBuilder b)) =>
(toBuilder()..update(updates)).build();
@override
QuoteUIStateBuilder toBuilder() => new QuoteUIStateBuilder()..replace(this);
@override
bool operator ==(dynamic other) {
if (identical(other, this)) return true;
if (other is! QuoteUIState) return false;
return 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('QuoteUIState')
..add('editing', editing)
..add('selectedId', selectedId)
..add('listUIState', listUIState))
.toString();
}
}
class QuoteUIStateBuilder
implements Builder<QuoteUIState, QuoteUIStateBuilder> {
_$QuoteUIState _$v;
QuoteEntityBuilder _editing;
QuoteEntityBuilder get editing =>
_$this._editing ??= new QuoteEntityBuilder();
set editing(QuoteEntityBuilder 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;
QuoteUIStateBuilder();
QuoteUIStateBuilder get _$this {
if (_$v != null) {
_editing = _$v.editing?.toBuilder();
_selectedId = _$v.selectedId;
_listUIState = _$v.listUIState?.toBuilder();
_$v = null;
}
return this;
}
@override
void replace(QuoteUIState other) {
if (other == null) throw new ArgumentError.notNull('other');
_$v = other as _$QuoteUIState;
}
@override
void update(void updates(QuoteUIStateBuilder b)) {
if (updates != null) updates(this);
}
@override
_$QuoteUIState build() {
_$QuoteUIState _$result;
try {
_$result = _$v ??
new _$QuoteUIState._(
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(
'QuoteUIState', _$failedField, e.toString());
}
rethrow;
}
replace(_$result);
return _$result;
}
}

View File

@ -7,6 +7,8 @@ import 'package:invoiceninja_flutter/redux/product/product_reducer.dart';
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/quote/quote_reducer.dart';
UIState uiReducer(UIState state, dynamic action) {
return state.rebuild((b) => b
@ -18,6 +20,8 @@ UIState uiReducer(UIState state, dynamic action) {
..clientUIState.replace(clientUIReducer(state.clientUIState, action))
..invoiceUIState.replace(invoiceUIReducer(state.invoiceUIState, action))
// STARTER: reducer - do not remove comment
..quoteUIState.replace(quoteUIReducer(state.quoteUIState, action))
);
}

View File

@ -5,6 +5,8 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_state.dart';
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/quote/quote_state.dart';
part 'ui_state.g.dart';
@ -19,6 +21,8 @@ abstract class UIState implements Built<UIState, UIStateBuilder> {
clientUIState: ClientUIState(),
invoiceUIState: InvoiceUIState(),
// STARTER: constructor - do not remove comment
quoteUIState: QuoteUIState(),
);
}
UIState._();
@ -34,6 +38,8 @@ abstract class UIState implements Built<UIState, UIStateBuilder> {
String get filter;
// STARTER: properties - do not remove comment
QuoteUIState get quoteUIState;
static Serializer<UIState> get serializer => _$uIStateSerializer;
}

View File

@ -46,6 +46,9 @@ class _$UIStateSerializer implements StructuredSerializer<UIState> {
'invoiceUIState',
serializers.serialize(object.invoiceUIState,
specifiedType: const FullType(InvoiceUIState)),
'quoteUIState',
serializers.serialize(object.quoteUIState,
specifiedType: const FullType(QuoteUIState)),
];
if (object.filter != null) {
result
@ -96,6 +99,10 @@ class _$UIStateSerializer implements StructuredSerializer<UIState> {
result.filter = serializers.deserialize(value,
specifiedType: const FullType(String)) as String;
break;
case 'quoteUIState':
result.quoteUIState.replace(serializers.deserialize(value,
specifiedType: const FullType(QuoteUIState)) as QuoteUIState);
break;
}
}
@ -118,6 +125,8 @@ class _$UIState extends UIState {
final InvoiceUIState invoiceUIState;
@override
final String filter;
@override
final QuoteUIState quoteUIState;
factory _$UIState([void updates(UIStateBuilder b)]) =>
(new UIStateBuilder()..update(updates)).build();
@ -129,7 +138,8 @@ class _$UIState extends UIState {
this.productUIState,
this.clientUIState,
this.invoiceUIState,
this.filter})
this.filter,
this.quoteUIState})
: super._() {
if (selectedCompanyIndex == null)
throw new BuiltValueNullFieldError('UIState', 'selectedCompanyIndex');
@ -143,6 +153,8 @@ class _$UIState extends UIState {
throw new BuiltValueNullFieldError('UIState', 'clientUIState');
if (invoiceUIState == null)
throw new BuiltValueNullFieldError('UIState', 'invoiceUIState');
if (quoteUIState == null)
throw new BuiltValueNullFieldError('UIState', 'quoteUIState');
}
@override
@ -162,7 +174,8 @@ class _$UIState extends UIState {
productUIState == other.productUIState &&
clientUIState == other.clientUIState &&
invoiceUIState == other.invoiceUIState &&
filter == other.filter;
filter == other.filter &&
quoteUIState == other.quoteUIState;
}
@override
@ -172,13 +185,15 @@ class _$UIState extends UIState {
$jc(
$jc(
$jc(
$jc($jc(0, selectedCompanyIndex.hashCode),
currentRoute.hashCode),
enableDarkMode.hashCode),
productUIState.hashCode),
clientUIState.hashCode),
invoiceUIState.hashCode),
filter.hashCode));
$jc(
$jc($jc(0, selectedCompanyIndex.hashCode),
currentRoute.hashCode),
enableDarkMode.hashCode),
productUIState.hashCode),
clientUIState.hashCode),
invoiceUIState.hashCode),
filter.hashCode),
quoteUIState.hashCode));
}
@override
@ -190,7 +205,8 @@ class _$UIState extends UIState {
..add('productUIState', productUIState)
..add('clientUIState', clientUIState)
..add('invoiceUIState', invoiceUIState)
..add('filter', filter))
..add('filter', filter)
..add('quoteUIState', quoteUIState))
.toString();
}
}
@ -234,6 +250,12 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
String get filter => _$this._filter;
set filter(String filter) => _$this._filter = filter;
QuoteUIStateBuilder _quoteUIState;
QuoteUIStateBuilder get quoteUIState =>
_$this._quoteUIState ??= new QuoteUIStateBuilder();
set quoteUIState(QuoteUIStateBuilder quoteUIState) =>
_$this._quoteUIState = quoteUIState;
UIStateBuilder();
UIStateBuilder get _$this {
@ -245,6 +267,7 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
_clientUIState = _$v.clientUIState?.toBuilder();
_invoiceUIState = _$v.invoiceUIState?.toBuilder();
_filter = _$v.filter;
_quoteUIState = _$v.quoteUIState?.toBuilder();
_$v = null;
}
return this;
@ -273,7 +296,8 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
productUIState: productUIState.build(),
clientUIState: clientUIState.build(),
invoiceUIState: invoiceUIState.build(),
filter: filter);
filter: filter,
quoteUIState: quoteUIState.build());
} catch (_) {
String _$failedField;
try {
@ -283,6 +307,9 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
clientUIState.build();
_$failedField = 'invoiceUIState';
invoiceUIState.build();
_$failedField = 'quoteUIState';
quoteUIState.build();
} catch (e) {
throw new BuiltValueNestedFieldError(
'UIState', _$failedField, e.toString());

View File

@ -15,6 +15,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:redux/redux.dart';
import 'package:url_launcher/url_launcher.dart';
// STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
class AppDrawer extends StatelessWidget {
final AppDrawerVM viewModel;
@ -177,6 +179,12 @@ class AppDrawer extends StatelessWidget {
},
),
// STARTER: menu - do not remove comment
ListTile(
leading: Icon(Icons.widgets),
title: Text('Quotes'),
onTap: () => store.dispatch(ViewQuoteList(context)),
),
DrawerTile(
user: user,
icon: FontAwesomeIcons.cog,

View File

@ -0,0 +1,106 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux_starter/ui/app/form_card.dart';
import 'package:flutter_redux_starter/ui/quote/edit/quote_edit_vm.dart';
import 'package:flutter_redux_starter/ui/app/save_icon_button.dart';
class QuoteEdit extends StatefulWidget {
final QuoteEditVM viewModel;
QuoteEdit({
Key key,
@required this.viewModel,
}) : super(key: key);
@override
_QuoteEditState createState() => _QuoteEditState();
}
class _QuoteEditState extends State<QuoteEdit> {
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// STARTER: controllers - do not remove comment
var _controllers = [];
@override
void didChangeDependencies() {
_controllers = [
// STARTER: array - do not remove comment
];
_controllers.forEach((controller) => controller.removeListener(_onChanged));
var quote = widget.viewModel.quote;
// STARTER: read value - do not remove comment
_controllers.forEach((controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
@override
void dispose() {
_controllers.forEach((controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
_onChanged() {
var quote = widget.viewModel.quote.rebuild((b) => b
// STARTER: set value - do not remove comment
);
if (quote != widget.viewModel.quote) {
widget.viewModel.onChanged(quote);
}
}
@override
Widget build(BuildContext context) {
var viewModel = widget.viewModel;
return WillPopScope(
onWillPop: () async {
viewModel.onBackPressed();
return true;
},
child: Scaffold(
appBar: AppBar(
title: Text(viewModel.quote.isNew
? 'New Quote'
: viewModel.quote.displayName),
actions: <Widget>[
Builder(builder: (BuildContext context) {
return SaveIconButton(
isLoading: viewModel.isLoading,
onPressed: () {
if (!_formKey.currentState.validate()) {
return;
}
viewModel.onSavePressed(context);
},
);
}),
],
),
body: Form(
key: _formKey,
child: ListView(
children: <Widget>[
FormCard(
children: <Widget>[
// STARTER: widgets - do not remove comment
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,75 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_starter/redux/ui/ui_actions.dart';
import 'package:flutter_redux_starter/ui/quote/quote_screen.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux_starter/redux/quote/quote_actions.dart';
import 'package:flutter_redux_starter/data/models/quote_model.dart';
import 'package:flutter_redux_starter/ui/quote/edit/quote_edit.dart';
import 'package:flutter_redux_starter/redux/app/app_state.dart';
import 'package:flutter_redux_starter/ui/app/icon_message.dart';
class QuoteEditScreen extends StatelessWidget {
static final String route = '/quote/edit';
QuoteEditScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, QuoteEditVM>(
converter: (Store<AppState> store) {
return QuoteEditVM.fromStore(store);
},
builder: (context, vm) {
return QuoteEdit(
viewModel: vm,
);
},
);
}
}
class QuoteEditVM {
final QuoteEntity quote;
final Function(QuoteEntity) onChanged;
final Function(BuildContext) onSavePressed;
final Function onBackPressed;
final bool isLoading;
QuoteEditVM({
@required this.quote,
@required this.onChanged,
@required this.onSavePressed,
@required this.onBackPressed,
@required this.isLoading,
});
factory QuoteEditVM.fromStore(Store<AppState> store) {
final quote = store.state.quoteUIState.selected;
return QuoteEditVM(
isLoading: store.state.isLoading,
quote: quote,
onChanged: (QuoteEntity quote) {
store.dispatch(UpdateQuote(quote));
},
onBackPressed: () {
store.dispatch(UpdateCurrentRoute(QuoteScreen.route));
},
onSavePressed: (BuildContext context) {
final Completer<Null> completer = new Completer<Null>();
store.dispatch(SaveQuoteRequest(completer: completer, quote: quote));
return completer.future.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: IconMessage(
message: quote.isNew
? 'Successfully Created Quote'
: 'Successfully Updated Quote',
),
duration: Duration(seconds: 3)));
});
},
);
}
}

View File

@ -0,0 +1,112 @@
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/quote/quote_list_item.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_list_vm.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
class QuoteList extends StatelessWidget {
final QuoteListVM viewModel;
const QuoteList({
Key key,
@required this.viewModel,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (!viewModel.isLoaded) {
return LoadingIndicator();
} else if (viewModel.quoteList.isEmpty) {
return Opacity(
opacity: 0.5,
child: Center(
child: Text(
AppLocalization.of(context).noRecordsFound,
style: TextStyle(
fontSize: 18.0,
),
),
),
);
}
return _buildListView(context);
}
void _showMenu(BuildContext context, QuoteEntity quote) async {
final user = viewModel.user;
final message = await showDialog<String>(
context: context,
builder: (BuildContext context) => SimpleDialog(children: <Widget>[
user.canCreate(EntityType.quote)
? ListTile(
leading: Icon(Icons.control_point_duplicate),
title: Text(AppLocalization.of(context).clone),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.clone),
)
: Container(),
Divider(),
user.canEditEntity(quote) && !quote.isActive
? ListTile(
leading: Icon(Icons.restore),
title: Text(AppLocalization.of(context).restore),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.restore),
)
: Container(),
user.canEditEntity(quote) && quote.isActive
? ListTile(
leading: Icon(Icons.archive),
title: Text(AppLocalization.of(context).archive),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.archive),
)
: Container(),
user.canEditEntity(quote) && !quote.isDeleted
? ListTile(
leading: Icon(Icons.delete),
title: Text(AppLocalization.of(context).delete),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.delete),
)
: Container(),
]));
if (message != null) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: message,
)));
}
}
Widget _buildListView(BuildContext context) {
return RefreshIndicator(
onRefresh: () => viewModel.onRefreshed(context),
child: ListView.builder(
itemCount: viewModel.quoteList.length,
itemBuilder: (BuildContext context, index) {
final quoteId = viewModel.quoteList[index];
final quote = viewModel.quoteMap[quoteId];
return Column(children: <Widget>[
QuoteListItem(
user: viewModel.user,
filter: viewModel.filter,
quote: quote,
client: viewModel.clientMap[quote.clientId],
onDismissed: (DismissDirection direction) =>
viewModel.onDismissed(context, quote, direction),
onTap: () => viewModel.onQuoteTap(context, quote),
onLongPress: () => _showMenu(context, quote),
),
Divider(
height: 1.0,
),
]);
}),
);
}
}

View File

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

View File

@ -0,0 +1,138 @@
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/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_selectors.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_list.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
class QuoteListBuilder extends StatelessWidget {
const QuoteListBuilder({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, QuoteListVM>(
converter: QuoteListVM.fromStore,
builder: (context, viewModel) {
return QuoteList(
viewModel: viewModel,
);
},
);
}
}
class QuoteListVM {
final UserEntity user;
final List<int> quoteList;
final BuiltMap<int, QuoteEntity> quoteMap;
final BuiltMap<int, ClientEntity> clientMap;
final String filter;
final bool isLoading;
final bool isLoaded;
final Function(BuildContext, QuoteEntity) onQuoteTap;
final Function(BuildContext, QuoteEntity, DismissDirection) onDismissed;
final Function(BuildContext) onRefreshed;
final Function(BuildContext, QuoteEntity, EntityAction) onEntityAction;
QuoteListVM({
@required this.user,
@required this.quoteList,
@required this.quoteMap,
@required this.clientMap,
@required this.filter,
@required this.isLoading,
@required this.isLoaded,
@required this.onQuoteTap,
@required this.onDismissed,
@required this.onRefreshed,
@required this.onEntityAction,
});
static QuoteListVM fromStore(Store<AppState> store) {
Future<Null> _handleRefresh(BuildContext context) {
if (store.state.isLoading) {
return Future<Null>(null);
}
final completer = snackBarCompleter(
context, AppLocalization.of(context).refreshComplete);
store.dispatch(LoadQuotes(completer: completer, force: true));
return completer.future;
}
final state = store.state;
return QuoteListVM(
user: state.user,
quoteList: memoizedFilteredQuoteList(state.quoteState.map,
state.quoteState.list, state.quoteListState),
quoteMap: state.quoteState.map,
clientMap: state.clientState.map,
isLoading: state.isLoading,
isLoaded: state.quoteState.isLoaded,
filter: state.quoteUIState.listUIState.filter,
onQuoteTap: (context, quote) {
store.dispatch(EditQuote(quote: quote, context: context));
},
onEntityAction: (context, quote, action) {
switch (action) {
case EntityAction.clone:
Navigator.of(context).pop();
store.dispatch(
EditQuote(context: context, quote: quote.clone));
break;
case EntityAction.restore:
store.dispatch(RestoreQuoteRequest(
popCompleter(
context, AppLocalization.of(context).restoredQuote),
quote.id));
break;
case EntityAction.archive:
store.dispatch(ArchiveQuoteRequest(
popCompleter(
context, AppLocalization.of(context).archivedQuote),
quote.id));
break;
case EntityAction.delete:
store.dispatch(DeleteQuoteRequest(
popCompleter(
context, AppLocalization.of(context).deletedQuote),
quote.id));
break;
}
},
onRefreshed: (context) => _handleRefresh(context),
onDismissed: (BuildContext context, QuoteEntity quote,
DismissDirection direction) {
final localization = AppLocalization.of(context);
if (direction == DismissDirection.endToStart) {
if (quote.isDeleted || quote.isArchived) {
store.dispatch(RestoreQuoteRequest(
snackBarCompleter(context, localization.restoredQuote),
quote.id));
} else {
store.dispatch(ArchiveQuoteRequest(
snackBarCompleter(context, localization.archivedQuote),
quote.id));
}
} else if (direction == DismissDirection.startToEnd) {
if (quote.isDeleted) {
store.dispatch(RestoreQuoteRequest(
snackBarCompleter(context, localization.restoredQuote),
quote.id));
} else {
store.dispatch(DeleteQuoteRequest(
snackBarCompleter(context, localization.deletedQuote),
quote.id));
}
}
});
}
}

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.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/quote/quote_list_vm.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/ui/app/app_drawer_vm.dart';
import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart';
class QuoteScreen extends StatelessWidget {
static const String route = '/quote';
@override
Widget build(BuildContext context) {
final store = StoreProvider.of<AppState>(context);
final company = store.state.selectedCompany;
final user = company.user;
final localization = AppLocalization.of(context);
return WillPopScope(
onWillPop: () async => false,
child: Scaffold(
appBar: AppBar(
title: ListFilter(
entityType: EntityType.quote,
onFilterChanged: (value) {
store.dispatch(FilterQuotes(value));
},
),
actions: [
ListFilterButton(
entityType: EntityType.quote,
onFilterPressed: (String value) {
store.dispatch(FilterQuotes(value));
},
),
],
),
drawer: AppDrawerBuilder(),
body: QuoteListBuilder(),
bottomNavigationBar: AppBottomBar(
entityType: EntityType.quote,
onSelectedSortField: (value) => store.dispatch(SortQuotes(value)),
customValues1: company.getCustomFieldValues(CustomFieldType.invoice1,
excludeBlank: true),
customValues2: company.getCustomFieldValues(CustomFieldType.invoice2,
excludeBlank: true),
onSelectedCustom1: (value) =>
store.dispatch(FilterQuotesByCustom1(value)),
onSelectedCustom2: (value) =>
store.dispatch(FilterQuotesByCustom2(value)),
sortFields: [
QuoteFields.quoteNumber,
QuoteFields.quoteDate,
QuoteFields.updatedAt,
],
onSelectedState: (EntityState state, value) {
store.dispatch(FilterQuotesByState(state));
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: user.canCreate(EntityType.quote)
? FloatingActionButton(
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
store.dispatch(
EditQuote(quote: QuoteEntity(), context: context));
},
child: Icon(
Icons.add,
color: Colors.white,
),
tooltip: localization.newQuote,
)
: null,
),
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux_starter/ui/app/actions_menu_button.dart';
import 'package:flutter_redux_starter/ui/quote/view/quote_view_vm.dart';
import 'package:flutter_redux_starter/ui/app/form_card.dart';
class QuoteView extends StatefulWidget {
final QuoteViewVM viewModel;
QuoteView({
Key key,
@required this.viewModel,
}) : super(key: key);
@override
_QuoteViewState createState() => new _QuoteViewState();
}
class _QuoteViewState extends State<QuoteView> {
@override
Widget build(BuildContext context) {
var viewModel = widget.viewModel;
var quote = viewModel.quote;
return Scaffold(
appBar: AppBar(
title: Text(quote.displayName),
actions: quote.isNew
? []
: [
IconButton(
icon: Icon(Icons.edit),
onPressed: () {
viewModel.onEditPressed(context);
},
),
ActionMenuButton(
isLoading: viewModel.isLoading,
entity: quote,
onSelected: viewModel.onActionSelected,
)
],
),
body: FormCard(
children: [
// STARTER: widgets - do not remove comment
]
),
);
}
}

View File

@ -0,0 +1,160 @@
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/client/client_actions.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart';
import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/utils/pdf.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/quote/view/quote_view.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart';
class QuoteViewScreen extends StatelessWidget {
static const String route = '/invoice/view';
const QuoteViewScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, QuoteViewVM>(
distinct: true,
converter: (Store<AppState> store) {
return QuoteViewVM.fromStore(store);
},
builder: (context, viewModel) {
return QuoteView(
viewModel: viewModel,
);
},
);
}
}
class QuoteViewVM {
final CompanyEntity company;
final QuoteEntity quote;
final ClientEntity client;
final bool isSaving;
final bool isDirty;
final Function(BuildContext, EntityAction) onActionSelected;
final Function(BuildContext, [InvoiceItemEntity]) onEditPressed;
final Function(BuildContext) onClientPressed;
final Function(BuildContext) onRefreshed;
final Function onBackPressed;
QuoteViewVM({
@required this.company,
@required this.quote,
@required this.client,
@required this.isSaving,
@required this.isDirty,
@required this.onActionSelected,
@required this.onEditPressed,
@required this.onBackPressed,
@required this.onClientPressed,
@required this.onRefreshed,
});
factory QuoteViewVM.fromStore(Store<AppState> store) {
final state = store.state;
final quote = state.quoteState.map[state.quoteUIState.selectedId];
final client = store.state.clientState.map[quote.clientId];
Future<Null> _handleRefresh(BuildContext context) {
final completer = snackBarCompleter(
context, AppLocalization.of(context).refreshComplete);
store.dispatch(LoadQuotes(completer: completer, force: true));
return completer.future;
}
return QuoteViewVM(
company: state.selectedCompany,
isSaving: state.isSaving,
isDirty: quote.isNew,
quote: quote,
client: client,
onEditPressed: (BuildContext context, [InvoiceItemEntity invoiceItem]) {
final Completer<QuoteEntity> completer =
new Completer<QuoteEntity>();
store.dispatch(EditQuote(
quote: quote,
context: context,
completer: completer,
invoiceItem: invoiceItem));
completer.future.then((invoice) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
message: AppLocalization.of(context).updatedQuote,
)));
});
},
onRefreshed: (context) => _handleRefresh(context),
onBackPressed: () =>
store.dispatch(UpdateCurrentRoute(QuoteScreen.route)),
onClientPressed: (BuildContext context) {
store.dispatch(ViewClient(clientId: client.id, context: context));
},
onActionSelected: (BuildContext context, EntityAction action) {
final localization = AppLocalization.of(context);
switch (action) {
case EntityAction.pdf:
viewPdf(invoice, context);
break;
case EntityAction.markSent:
store.dispatch(MarkSentQuoteRequest(
snackBarCompleter(context, localization.markedQuoteAsSent),
invoice.id));
break;
case EntityAction.emailQuote:
store.dispatch(ShowEmailQuote(
completer:
snackBarCompleter(context, localization.emailedQuote),
invoice: invoice,
context: context));
break;
case EntityAction.archive:
store.dispatch(ArchiveQuoteRequest(
popCompleter(context, localization.archivedQuote),
invoice.id));
break;
case EntityAction.delete:
store.dispatch(DeleteQuoteRequest(
popCompleter(context, localization.deletedQuote),
invoice.id));
break;
case EntityAction.restore:
store.dispatch(RestoreQuoteRequest(
snackBarCompleter(context, localization.restoredQuote),
invoice.id));
break;
case EntityAction.clone:
Navigator.of(context).pop();
store.dispatch(
EditQuote(context: context, invoice: invoice.clone));
break;
}
});
}
@override
bool operator ==(dynamic other) =>
client == other.client &&
company == other.company &&
invoice == other.invoice &&
isSaving == other.isSaving &&
isDirty == other.isDirty;
@override
int get hashCode =>
client.hashCode ^
company.hashCode ^
invoice.hashCode ^
isSaving.hashCode ^
isDirty.hashCode;
}

View File

@ -208,6 +208,12 @@ class AppLocalization {
'payments': 'Payments',
'quote': 'Quote',
'quotes': 'Quotes',
'new_quote': 'New Quote',
'created_quote': 'Successfully created quote',
'updated_quote': 'Successfully updated quote',
'archived_quote': 'Successfully archived quote',
'deleted_quote': 'Successfully deleted quote',
'restored_quote': 'Successfully restored quote',
'expense': 'Expense',
'expenses': 'Expenses',
'vendor': 'Vendor',
@ -7511,6 +7517,23 @@ class AppLocalization {
String get quotes => _localizedValues[locale.languageCode]['quotes'];
String get newQuote => _localizedValues[locale.languageCode]['new_quote'];
String get createdQuote =>
_localizedValues[locale.languageCode]['created_quote'];
String get updatedQuote =>
_localizedValues[locale.languageCode]['updated_quote'];
String get archivedQuote =>
_localizedValues[locale.languageCode]['archived_quote'];
String get deletedQuote =>
_localizedValues[locale.languageCode]['deleted_quote'];
String get restoredQuote =>
_localizedValues[locale.languageCode]['restored_quote'];
String get expense => _localizedValues[locale.languageCode]['expense'];
String get expenses => _localizedValues[locale.languageCode]['expenses'];

View File

@ -189,8 +189,8 @@ else
comment="STARTER: state getters - do not remove comment"
code="${Module}State get ${module}State => selectedCompanyState.${module}State;${lineBreak}"
code="${code}ListUIState get ${module}ListState => this.uiState.${module}UIState.listUIState;${lineBreak}"
code="${code}${Module}UIState get ${module}UIState => this.uiState.${module}UIState;${lineBreak}${lineBreak}"
code="${code}ListUIState get ${module}ListState => uiState.${module}UIState.listUIState;${lineBreak}"
code="${code}${Module}UIState get ${module}UIState => uiState.${module}UIState;${lineBreak}${lineBreak}"
sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/app_state.dart
for (( idx=${#fieldsArray[@]}-1 ; idx>=0 ; idx-- )) ; do

View File

@ -58,5 +58,11 @@ class StubRepository {
url += '?action=' + action.toString();
}
response = await webClient.put(url, company.token, json.encode(data));
}
}
final StubItemResponse stubResponse =
serializers.deserializeWith(StubItemResponse.serializer, response);
return stubResponse.data;
}
}

View File

@ -175,8 +175,7 @@ Middleware<AppState> _loadStub(StubRepository repository) {
store.dispatch(LoadStubRequest());
repository
.loadItem(state.selectedCompany, state.authState, action.stubId,
action.loadActivities)
.loadItem(state.selectedCompany, state.authState, action.stubId)
.then((stub) {
store.dispatch(LoadStubSuccess(stub));

View File

@ -167,15 +167,13 @@ StubState _addStub(StubState stubState, AddStubSuccess action) {
StubState _updateStub(StubState stubState, SaveStubSuccess action) {
return stubState.rebuild((b) => b
..map[action.stub.id] = action.stub.rebuild((b) =>
b..lastUpdatedActivities = DateTime.now().millisecondsSinceEpoch));
..map[action.stub.id] = action.stub);
}
StubState _setLoadedStub(
StubState stubState, LoadStubSuccess action) {
return stubState.rebuild((b) => b
..map[action.stub.id] = action.stub.rebuild((b) =>
b..lastUpdatedActivities = DateTime.now().millisecondsSinceEpoch));
..map[action.stub.id] = action.stub);
}
StubState _setNoStubs(StubState stubState, LoadStubsFailure action) {

View File

@ -1,8 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux_starter/ui/app/actions_menu_button.dart';
import 'package:flutter_redux_starter/ui/stub/view/stub_view_vm.dart';
import 'package:flutter_redux_starter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/actions_menu_button.dart';
import 'package:invoiceninja_flutter/ui/stub/view/stub_view_vm.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
class StubView extends StatefulWidget {
final StubViewVM viewModel;

View File

@ -2,13 +2,13 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_starter/redux/stub/stub_actions.dart';
import 'package:flutter_redux_starter/data/models/stub_model.dart';
import 'package:flutter_redux_starter/data/models/models.dart';
import 'package:flutter_redux_starter/ui/stub/view/stub_view.dart';
import 'package:flutter_redux_starter/redux/app/app_state.dart';
import 'package:flutter_redux_starter/ui/app/icon_message.dart';
import 'package:invoiceninja_flutter/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/stub/stub_actions.dart';
import 'package:invoiceninja_flutter/data/models/stub_model.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/stub/view/stub_view.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/icon_message.dart';
class StubViewScreen extends StatelessWidget {
static final String route = '/stub/view';