Quotes [WIP]
This commit is contained in:
parent
9fa8ff9dde
commit
e1f60319e8
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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($jc(0, company.hashCode), dashboardState.hashCode),
|
||||
productState.hashCode),
|
||||
clientState.hashCode),
|
||||
invoiceState.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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -170,6 +183,7 @@ class _$UIState extends UIState {
|
|||
return $jf($jc(
|
||||
$jc(
|
||||
$jc(
|
||||
$jc(
|
||||
$jc(
|
||||
$jc(
|
||||
$jc($jc(0, selectedCompanyIndex.hashCode),
|
||||
|
|
@ -178,7 +192,8 @@ class _$UIState extends UIState {
|
|||
productUIState.hashCode),
|
||||
clientUIState.hashCode),
|
||||
invoiceUIState.hashCode),
|
||||
filter.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());
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -59,4 +59,10 @@ class StubRepository {
|
|||
}
|
||||
response = await webClient.put(url, company.token, json.encode(data));
|
||||
}
|
||||
|
||||
final StubItemResponse stubResponse =
|
||||
serializers.deserializeWith(StubItemResponse.serializer, response);
|
||||
|
||||
return stubResponse.data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in New Issue