This commit is contained in:
Hillel Coren 2018-08-22 21:00:56 -07:00
parent 29ff8f415e
commit 5a3a30dc4e
28 changed files with 780 additions and 2069 deletions

View File

@ -367,7 +367,7 @@ abstract class ActivityEntity implements Built<ActivityEntity, ActivityEntityBui
//ContactEntity contact, //ContactEntity contact,
PaymentEntity payment, PaymentEntity payment,
CreditEntity credit, CreditEntity credit,
//QuoteEntity quote, //InvoiceEntity quote,
TaskEntity task, TaskEntity task,
ExpenseEntity expense, ExpenseEntity expense,
VendorEntity vendor, VendorEntity vendor,

View File

@ -13,7 +13,6 @@ export 'package:invoiceninja_flutter/data/models/invoice_model.dart';
export 'package:invoiceninja_flutter/data/models/task_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/expense_model.dart';
export 'package:invoiceninja_flutter/data/models/vendor_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/static_data_model.dart';
export 'package:invoiceninja_flutter/data/models/static/currency_model.dart'; export 'package:invoiceninja_flutter/data/models/static/currency_model.dart';
export 'package:invoiceninja_flutter/data/models/static/size_model.dart'; export 'package:invoiceninja_flutter/data/models/static/size_model.dart';

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,6 @@ import 'package:invoiceninja_flutter/redux/client/client_state.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_state.dart'; import 'package:invoiceninja_flutter/redux/ui/ui_state.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_state.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_state.dart';
// STARTER: import - do not remove comment // STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/data/models/quote_model.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
@ -73,7 +72,6 @@ part 'serializers.g.dart';
TimezoneItemResponse, TimezoneItemResponse,
TimezoneListResponse, TimezoneListResponse,
// STARTER: serializers - do not remove comment // STARTER: serializers - do not remove comment
QuoteEntity,
]) ])
final Serializers serializers = final Serializers serializers =

View File

@ -91,7 +91,7 @@ Serializers _$serializers = (new Serializers().toBuilder()
..add(ProjectEntity.serializer) ..add(ProjectEntity.serializer)
..add(ProjectItemResponse.serializer) ..add(ProjectItemResponse.serializer)
..add(ProjectListResponse.serializer) ..add(ProjectListResponse.serializer)
..add(QuoteEntity.serializer) ..add(InvoiceEntity.serializer)
..add(QuoteState.serializer) ..add(QuoteState.serializer)
..add(QuoteUIState.serializer) ..add(QuoteUIState.serializer)
..add(SizeEntity.serializer) ..add(SizeEntity.serializer)
@ -344,6 +344,6 @@ Serializers _$serializers = (new Serializers().toBuilder()
..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder<int>()) ..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(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(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(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(BuiltList, const [const FullType(int)]), () => new ListBuilder<int>()))
.build(); .build();

View File

@ -14,18 +14,18 @@ class QuoteRepository {
this.webClient = const WebClient(), this.webClient = const WebClient(),
}); });
Future<QuoteEntity> loadItem( Future<InvoiceEntity> loadItem(
CompanyEntity company, AuthState auth, int entityId) async { CompanyEntity company, AuthState auth, int entityId) async {
final dynamic response = await webClient.get( final dynamic response = await webClient.get(
'${auth.url}/quotes/$entityId', company.token); '${auth.url}/invoices/$entityId', company.token);
final QuoteItemResponse quoteResponse = final InvoiceItemResponse quoteResponse =
serializers.deserializeWith(QuoteItemResponse.serializer, response); serializers.deserializeWith(InvoiceItemResponse.serializer, response);
return quoteResponse.data; return quoteResponse.data;
} }
Future<BuiltList<QuoteEntity>> loadList( Future<BuiltList<InvoiceEntity>> loadList(
CompanyEntity company, AuthState auth, int updatedAt) async { CompanyEntity company, AuthState auth, int updatedAt) async {
String url = auth.url + '/quotes'; String url = auth.url + '/quotes';
@ -35,16 +35,16 @@ class QuoteRepository {
final dynamic response = await webClient.get(url, company.token); final dynamic response = await webClient.get(url, company.token);
final QuoteListResponse quoteResponse = final InvoiceListResponse quoteResponse =
serializers.deserializeWith(QuoteListResponse.serializer, response); serializers.deserializeWith(InvoiceListResponse.serializer, response);
return quoteResponse.data; return quoteResponse.data;
} }
Future<QuoteEntity> saveData( Future<InvoiceEntity> saveData(
CompanyEntity company, AuthState auth, QuoteEntity quote, CompanyEntity company, AuthState auth, InvoiceEntity quote,
[EntityAction action]) async { [EntityAction action]) async {
final data = serializers.serializeWith(QuoteEntity.serializer, quote); final data = serializers.serializeWith(InvoiceEntity.serializer, quote);
dynamic response; dynamic response;
if (quote.isNew) { if (quote.isNew) {
@ -60,8 +60,8 @@ class QuoteRepository {
response = await webClient.put(url, company.token, json.encode(data)); response = await webClient.put(url, company.token, json.encode(data));
} }
final QuoteItemResponse quoteResponse = final InvoiceItemResponse quoteResponse =
serializers.deserializeWith(QuoteItemResponse.serializer, response); serializers.deserializeWith(InvoiceItemResponse.serializer, response);
return quoteResponse.data; return quoteResponse.data;
} }

View File

@ -34,6 +34,7 @@ import 'package:invoiceninja_flutter/redux/product/product_middleware.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_middleware.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_middleware.dart';
import 'package:invoiceninja_flutter/ui/invoice/invoice_screen.dart'; import 'package:invoiceninja_flutter/ui/invoice/invoice_screen.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
// STARTER: import - do not remove comment // STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/ui/quote/quote_screen.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/edit/quote_edit_vm.dart';
@ -41,7 +42,6 @@ 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_actions.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_middleware.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_middleware.dart';
void main() async { void main() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final enableDarkMode = prefs.getBool(kSharedPrefEnableDarkMode); final enableDarkMode = prefs.getBool(kSharedPrefEnableDarkMode);
@ -56,8 +56,7 @@ void main() async {
..addAll(createStoreInvoicesMiddleware()) ..addAll(createStoreInvoicesMiddleware())
..addAll(createStorePersistenceMiddleware()) ..addAll(createStorePersistenceMiddleware())
// STARTER: middleware - do not remove comment // STARTER: middleware - do not remove comment
..addAll(createStoreQuotesMiddleware()) ..addAll(createStoreQuotesMiddleware())
..addAll([ ..addAll([
LoggingMiddleware<dynamic>.printer(), LoggingMiddleware<dynamic>.printer(),
])); ]));
@ -141,13 +140,12 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
InvoiceEditScreen.route: (context) => InvoiceEditScreen(), InvoiceEditScreen.route: (context) => InvoiceEditScreen(),
InvoiceEmailScreen.route: (context) => InvoiceEmailScreen(), InvoiceEmailScreen.route: (context) => InvoiceEmailScreen(),
// STARTER: routes - do not remove comment // STARTER: routes - do not remove comment
QuoteScreen.route: (context) { QuoteScreen.route: (context) {
widget.store.dispatch(LoadQuotes()); widget.store.dispatch(LoadQuotes());
return QuoteScreen(); return QuoteScreen();
}, },
QuoteViewScreen.route: (context) => QuoteViewScreen(), QuoteViewScreen.route: (context) => QuoteViewScreen(),
QuoteEditScreen.route: (context) => QuoteEditScreen(), QuoteEditScreen.route: (context) => QuoteEditScreen(),
SettingsScreen.route: (context) => SettingsScreen(), SettingsScreen.route: (context) => SettingsScreen(),
}, },
); );

View File

@ -212,9 +212,9 @@ Middleware<AppState> _saveInvoice(InvoiceRepository repository) {
return (Store<AppState> store, dynamic action, NextDispatcher next) { return (Store<AppState> store, dynamic action, NextDispatcher next) {
repository repository
.saveData( .saveData(
store.state.selectedCompany, store.state.authState, action.invoice) store.state.selectedCompany, store.state.authState, action.quote)
.then((dynamic invoice) { .then((dynamic invoice) {
if (action.invoice.isNew) { if (action.quote.isNew) {
store.dispatch(AddInvoiceSuccess(invoice)); store.dispatch(AddInvoiceSuccess(invoice));
} else { } else {
store.dispatch(SaveInvoiceSuccess(invoice)); store.dispatch(SaveInvoiceSuccess(invoice));

View File

@ -21,7 +21,7 @@ final editingItemReducer = combineReducers<InvoiceItemEntity>([
InvoiceItemEntity editInvoiceItem( InvoiceItemEntity editInvoiceItem(
InvoiceItemEntity invoiceItem, dynamic action) { InvoiceItemEntity invoiceItem, dynamic action) {
return action.invoiceItem ?? InvoiceItemEntity(); return action.quoteItem ?? InvoiceItemEntity();
} }
Reducer<String> dropdownFilterReducer = combineReducers([ Reducer<String> dropdownFilterReducer = combineReducers([
@ -37,9 +37,9 @@ Reducer<int> selectedIdReducer = combineReducers([
TypedReducer<int, ViewInvoice>( TypedReducer<int, ViewInvoice>(
(int selectedId, dynamic action) => action.invoiceId), (int selectedId, dynamic action) => action.invoiceId),
TypedReducer<int, AddInvoiceSuccess>( TypedReducer<int, AddInvoiceSuccess>(
(int selectedId, dynamic action) => action.invoice.id), (int selectedId, dynamic action) => action.quote.id),
TypedReducer<int, ShowEmailInvoice>( TypedReducer<int, ShowEmailInvoice>(
(int selectedId, dynamic action) => action.invoice.id), (int selectedId, dynamic action) => action.quote.id),
]); ]);
final editingReducer = combineReducers<InvoiceEntity>([ final editingReducer = combineReducers<InvoiceEntity>([
@ -62,7 +62,7 @@ InvoiceEntity _clearEditing(InvoiceEntity client, dynamic action) {
} }
InvoiceEntity _updateEditing(InvoiceEntity invoice, dynamic action) { InvoiceEntity _updateEditing(InvoiceEntity invoice, dynamic action) {
return action.invoice; return action.quote;
} }
InvoiceEntity _addInvoiceItem(InvoiceEntity invoice, AddInvoiceItem action) { InvoiceEntity _addInvoiceItem(InvoiceEntity invoice, AddInvoiceItem action) {
@ -244,7 +244,7 @@ InvoiceState _addInvoice(InvoiceState invoiceState, AddInvoiceSuccess action) {
InvoiceState _updateInvoice(InvoiceState invoiceState, dynamic action) { InvoiceState _updateInvoice(InvoiceState invoiceState, dynamic action) {
return invoiceState return invoiceState
.rebuild((b) => b..map[action.invoice.id] = action.invoice); .rebuild((b) => b..map[action.quote.id] = action.quote);
} }
InvoiceState _setNoInvoices( InvoiceState _setNoInvoices(

View File

@ -19,7 +19,7 @@ class ViewQuote implements PersistUI {
} }
class EditQuote implements PersistUI { class EditQuote implements PersistUI {
final QuoteEntity quote; final InvoiceEntity quote;
final InvoiceItemEntity quoteItem; final InvoiceItemEntity quoteItem;
final BuildContext context; final BuildContext context;
final Completer completer; final Completer completer;
@ -28,7 +28,7 @@ class EditQuote implements PersistUI {
} }
class ShowEmailQuote { class ShowEmailQuote {
final QuoteEntity quote; final InvoiceEntity quote;
final BuildContext context; final BuildContext context;
final Completer completer; final Completer completer;
@ -42,7 +42,7 @@ class EditQuoteItem implements PersistUI {
} }
class UpdateQuote implements PersistUI { class UpdateQuote implements PersistUI {
final QuoteEntity quote; final InvoiceEntity quote;
UpdateQuote(this.quote); UpdateQuote(this.quote);
} }
@ -75,7 +75,7 @@ class LoadQuoteFailure implements StopLoading {
} }
class LoadQuoteSuccess implements StopLoading, PersistData { class LoadQuoteSuccess implements StopLoading, PersistData {
final QuoteEntity quote; final InvoiceEntity quote;
LoadQuoteSuccess(this.quote); LoadQuoteSuccess(this.quote);
@ -99,7 +99,7 @@ class LoadQuotesFailure implements StopLoading {
} }
class LoadQuotesSuccess implements StopLoading, PersistData { class LoadQuotesSuccess implements StopLoading, PersistData {
final BuiltList<QuoteEntity> quotes; final BuiltList<InvoiceEntity> quotes;
LoadQuotesSuccess(this.quotes); LoadQuotesSuccess(this.quotes);
@ -136,19 +136,19 @@ class DeleteQuoteItem implements PersistUI {
class SaveQuoteRequest implements StartSaving { class SaveQuoteRequest implements StartSaving {
final Completer completer; final Completer completer;
final QuoteEntity quote; final InvoiceEntity quote;
SaveQuoteRequest({this.completer, this.quote}); SaveQuoteRequest({this.completer, this.quote});
} }
class SaveQuoteSuccess implements StopSaving, PersistData, PersistUI { class SaveQuoteSuccess implements StopSaving, PersistData, PersistUI {
final QuoteEntity quote; final InvoiceEntity quote;
SaveQuoteSuccess(this.quote); SaveQuoteSuccess(this.quote);
} }
class AddQuoteSuccess implements StopSaving, PersistData, PersistUI { class AddQuoteSuccess implements StopSaving, PersistData, PersistUI {
final QuoteEntity quote; final InvoiceEntity quote;
AddQuoteSuccess(this.quote); AddQuoteSuccess(this.quote);
} }
@ -186,13 +186,13 @@ class MarkSentQuoteRequest implements StartSaving {
} }
class MarkSentQuoteSuccess implements StopSaving, PersistData { class MarkSentQuoteSuccess implements StopSaving, PersistData {
final QuoteEntity quote; final InvoiceEntity quote;
MarkSentQuoteSuccess(this.quote); MarkSentQuoteSuccess(this.quote);
} }
class MarkSentQuoteFailure implements StopSaving { class MarkSentQuoteFailure implements StopSaving {
final QuoteEntity quote; final InvoiceEntity quote;
MarkSentQuoteFailure(this.quote); MarkSentQuoteFailure(this.quote);
} }
@ -205,13 +205,13 @@ class ArchiveQuoteRequest implements StartSaving {
} }
class ArchiveQuoteSuccess implements StopSaving, PersistData { class ArchiveQuoteSuccess implements StopSaving, PersistData {
final QuoteEntity quote; final InvoiceEntity quote;
ArchiveQuoteSuccess(this.quote); ArchiveQuoteSuccess(this.quote);
} }
class ArchiveQuoteFailure implements StopSaving { class ArchiveQuoteFailure implements StopSaving {
final QuoteEntity quote; final InvoiceEntity quote;
ArchiveQuoteFailure(this.quote); ArchiveQuoteFailure(this.quote);
} }
@ -224,13 +224,13 @@ class DeleteQuoteRequest implements StartSaving {
} }
class DeleteQuoteSuccess implements StopSaving, PersistData { class DeleteQuoteSuccess implements StopSaving, PersistData {
final QuoteEntity quote; final InvoiceEntity quote;
DeleteQuoteSuccess(this.quote); DeleteQuoteSuccess(this.quote);
} }
class DeleteQuoteFailure implements StopSaving { class DeleteQuoteFailure implements StopSaving {
final QuoteEntity quote; final InvoiceEntity quote;
DeleteQuoteFailure(this.quote); DeleteQuoteFailure(this.quote);
} }
@ -243,13 +243,13 @@ class RestoreQuoteRequest implements StartSaving {
} }
class RestoreQuoteSuccess implements StopSaving, PersistData { class RestoreQuoteSuccess implements StopSaving, PersistData {
final QuoteEntity quote; final InvoiceEntity quote;
RestoreQuoteSuccess(this.quote); RestoreQuoteSuccess(this.quote);
} }
class RestoreQuoteFailure implements StopSaving { class RestoreQuoteFailure implements StopSaving {
final QuoteEntity quote; final InvoiceEntity quote;
RestoreQuoteFailure(this.quote); RestoreQuoteFailure(this.quote);
} }

View File

@ -20,22 +20,22 @@ Reducer<int> selectedIdReducer = combineReducers([
(int selectedId, dynamic action) => action.quote.id), (int selectedId, dynamic action) => action.quote.id),
]); ]);
final editingReducer = combineReducers<QuoteEntity>([ final editingReducer = combineReducers<InvoiceEntity>([
TypedReducer<QuoteEntity, SaveQuoteSuccess>(_updateEditing), TypedReducer<InvoiceEntity, SaveQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, AddQuoteSuccess>(_updateEditing), TypedReducer<InvoiceEntity, AddQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, RestoreQuoteSuccess>(_updateEditing), TypedReducer<InvoiceEntity, RestoreQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, ArchiveQuoteSuccess>(_updateEditing), TypedReducer<InvoiceEntity, ArchiveQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, DeleteQuoteSuccess>(_updateEditing), TypedReducer<InvoiceEntity, DeleteQuoteSuccess>(_updateEditing),
TypedReducer<QuoteEntity, EditQuote>(_updateEditing), TypedReducer<InvoiceEntity, EditQuote>(_updateEditing),
TypedReducer<QuoteEntity, UpdateQuote>(_updateEditing), TypedReducer<InvoiceEntity, UpdateQuote>(_updateEditing),
TypedReducer<QuoteEntity, SelectCompany>(_clearEditing), TypedReducer<InvoiceEntity, SelectCompany>(_clearEditing),
]); ]);
QuoteEntity _clearEditing(QuoteEntity quote, dynamic action) { InvoiceEntity _clearEditing(InvoiceEntity quote, dynamic action) {
return QuoteEntity(); return InvoiceEntity();
} }
QuoteEntity _updateEditing(QuoteEntity quote, dynamic action) { InvoiceEntity _updateEditing(InvoiceEntity quote, dynamic action) {
return action.quote; return action.quote;
} }

View File

@ -3,52 +3,96 @@ import 'package:built_collection/built_collection.dart';
import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart';
var memoizedDropdownQuoteList = memo2( ClientEntity quoteClientSelector(
(BuiltMap<int, QuoteEntity> quoteMap, BuiltList<int> quoteList) => InvoiceEntity invoice, BuiltMap<int, ClientEntity> clientMap) {
dropdownQuotesSelector(quoteMap, quoteList)); return clientMap[invoice.clientId];
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, var memoizedFilteredQuoteList = memo4(
BuiltList<int> quoteList, ListUIState quoteListState) => (BuiltMap<int, InvoiceEntity> invoiceMap,
filteredQuotesSelector(quoteMap, quoteList, quoteListState)); BuiltList<int> invoiceList,
BuiltMap<int, ClientEntity> clientMap,
ListUIState invoiceListState) =>
filteredQuotesSelector(
invoiceMap, invoiceList, clientMap, invoiceListState));
List<int> filteredQuotesSelector(BuiltMap<int, QuoteEntity> quoteMap, List<int> filteredQuotesSelector(
BuiltList<int> quoteList, ListUIState quoteListState) { BuiltMap<int, InvoiceEntity> invoiceMap,
final list = quoteList.where((quoteId) { BuiltList<int> invoiceList,
final quote = quoteMap[quoteId]; BuiltMap<int, ClientEntity> clientMap,
if (!quote.matchesStates(quoteListState.stateFilters)) { ListUIState invoiceListState) {
final list = invoiceList.where((invoiceId) {
final invoice = invoiceMap[invoiceId];
final client = clientMap[invoice.clientId];
if (client == null || ! client.isActive) {
return false; return false;
} }
if (quoteListState.custom1Filters.isNotEmpty && if (!invoice.matchesStates(invoiceListState.stateFilters)) {
!quoteListState.custom1Filters.contains(quote.customTextValue1)) {
return false; return false;
} }
if (quoteListState.custom2Filters.isNotEmpty && if (!invoice.matchesStatuses(invoiceListState.statusFilters)) {
!quoteListState.custom2Filters.contains(quote.customTextValue2)) {
return false; return false;
} }
return quote.matchesFilter(quoteListState.filter); if (!invoice.matchesFilter(invoiceListState.filter) &&
!client.matchesFilter(invoiceListState.filter)) {
return false;
}
if (invoiceListState.filterClientId != null &&
invoice.clientId != invoiceListState.filterClientId) {
return false;
}
if (invoiceListState.custom1Filters.isNotEmpty &&
!invoiceListState.custom1Filters.contains(invoice.customTextValue1)) {
return false;
}
if (invoiceListState.custom2Filters.isNotEmpty &&
!invoiceListState.custom2Filters.contains(invoice.customTextValue2)) {
return false;
}
return true;
}).toList(); }).toList();
list.sort((quoteAId, quoteBId) { list.sort((invoiceAId, invoiceBId) {
final quoteA = quoteMap[quoteAId]; return invoiceMap[invoiceAId].compareTo(invoiceMap[invoiceBId],
final quoteB = quoteMap[quoteBId]; invoiceListState.sortField, invoiceListState.sortAscending);
return quoteA.compareTo(
quoteB, quoteListState.sortField, quoteListState.sortAscending);
}); });
return list; return list;
} }
var memoizedQuoteStatsForClient = memo4((int clientId,
BuiltMap<int, InvoiceEntity> invoiceMap,
String activeLabel,
String archivedLabel) =>
quoteStatsForClient(clientId, invoiceMap, activeLabel, archivedLabel));
String quoteStatsForClient(
int clientId,
BuiltMap<int, InvoiceEntity> invoiceMap,
String activeLabel,
String archivedLabel) {
int countActive = 0;
int countArchived = 0;
invoiceMap.forEach((invoiceId, invoice) {
if (invoice.clientId == clientId) {
if (invoice.isActive) {
countActive++;
} else if (invoice.isArchived) {
countArchived++;
}
}
});
String str = '';
if (countActive > 0) {
str = '$countActive $activeLabel';
if (countArchived > 0) {
str += '';
}
}
if (countArchived > 0) {
str += '$countArchived $archivedLabel';
}
return str;
}

View File

@ -2,7 +2,7 @@ import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart'; import 'package:built_value/serializer.dart';
import 'package:built_collection/built_collection.dart'; import 'package:built_collection/built_collection.dart';
import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/quote_model.dart'; import 'package:invoiceninja_flutter/data/models/invoice_model.dart';
import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.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/ui/list_ui_state.dart';
@ -13,7 +13,7 @@ abstract class QuoteState implements Built<QuoteState, QuoteStateBuilder> {
factory QuoteState() { factory QuoteState() {
return _$QuoteState._( return _$QuoteState._(
lastUpdated: 0, lastUpdated: 0,
map: BuiltMap<int, QuoteEntity>(), map: BuiltMap<int, InvoiceEntity>(),
list: BuiltList<int>(), list: BuiltList<int>(),
); );
} }
@ -22,7 +22,7 @@ abstract class QuoteState implements Built<QuoteState, QuoteStateBuilder> {
@nullable @nullable
int get lastUpdated; int get lastUpdated;
BuiltMap<int, QuoteEntity> get map; BuiltMap<int, InvoiceEntity> get map;
BuiltList<int> get list; BuiltList<int> get list;
bool get isStale { bool get isStale {
@ -42,15 +42,19 @@ abstract class QuoteUIState extends Object with EntityUIState implements Built<Q
factory QuoteUIState() { factory QuoteUIState() {
return _$QuoteUIState._( return _$QuoteUIState._(
listUIState: ListUIState(QuoteFields.quoteNumber), listUIState: ListUIState(InvoiceFields.invoiceNumber),
editing: QuoteEntity(), editing: InvoiceEntity(),
selectedId: 0, selectedId: 0,
); );
} }
QuoteUIState._(); QuoteUIState._();
@nullable @nullable
QuoteEntity get editing; InvoiceEntity get editing;
@nullable
InvoiceItemEntity get editingItem;
@override @override
bool get isCreatingNew => editing.isNew; bool get isCreatingNew => editing.isNew;

View File

@ -33,7 +33,7 @@ class _$QuoteStateSerializer implements StructuredSerializer<QuoteState> {
'map', 'map',
serializers.serialize(object.map, serializers.serialize(object.map,
specifiedType: const FullType(BuiltMap, specifiedType: const FullType(BuiltMap,
const [const FullType(int), const FullType(QuoteEntity)])), const [const FullType(int), const FullType(InvoiceEntity)])),
'list', 'list',
serializers.serialize(object.list, serializers.serialize(object.list,
specifiedType: specifiedType:
@ -68,7 +68,7 @@ class _$QuoteStateSerializer implements StructuredSerializer<QuoteState> {
result.map.replace(serializers.deserialize(value, result.map.replace(serializers.deserialize(value,
specifiedType: const FullType(BuiltMap, const [ specifiedType: const FullType(BuiltMap, const [
const FullType(int), const FullType(int),
const FullType(QuoteEntity) const FullType(InvoiceEntity)
])) as BuiltMap); ])) as BuiltMap);
break; break;
case 'list': case 'list':
@ -105,7 +105,13 @@ class _$QuoteUIStateSerializer implements StructuredSerializer<QuoteUIState> {
result result
..add('editing') ..add('editing')
..add(serializers.serialize(object.editing, ..add(serializers.serialize(object.editing,
specifiedType: const FullType(QuoteEntity))); specifiedType: const FullType(InvoiceEntity)));
}
if (object.editingItem != null) {
result
..add('editingItem')
..add(serializers.serialize(object.editingItem,
specifiedType: const FullType(InvoiceItemEntity)));
} }
return result; return result;
@ -124,7 +130,12 @@ class _$QuoteUIStateSerializer implements StructuredSerializer<QuoteUIState> {
switch (key) { switch (key) {
case 'editing': case 'editing':
result.editing.replace(serializers.deserialize(value, result.editing.replace(serializers.deserialize(value,
specifiedType: const FullType(QuoteEntity)) as QuoteEntity); specifiedType: const FullType(InvoiceEntity)) as InvoiceEntity);
break;
case 'editingItem':
result.editingItem.replace(serializers.deserialize(value,
specifiedType: const FullType(InvoiceItemEntity))
as InvoiceItemEntity);
break; break;
case 'selectedId': case 'selectedId':
result.selectedId = serializers.deserialize(value, result.selectedId = serializers.deserialize(value,
@ -145,7 +156,7 @@ class _$QuoteState extends QuoteState {
@override @override
final int lastUpdated; final int lastUpdated;
@override @override
final BuiltMap<int, QuoteEntity> map; final BuiltMap<int, InvoiceEntity> map;
@override @override
final BuiltList<int> list; final BuiltList<int> list;
@ -196,10 +207,10 @@ class QuoteStateBuilder implements Builder<QuoteState, QuoteStateBuilder> {
int get lastUpdated => _$this._lastUpdated; int get lastUpdated => _$this._lastUpdated;
set lastUpdated(int lastUpdated) => _$this._lastUpdated = lastUpdated; set lastUpdated(int lastUpdated) => _$this._lastUpdated = lastUpdated;
MapBuilder<int, QuoteEntity> _map; MapBuilder<int, InvoiceEntity> _map;
MapBuilder<int, QuoteEntity> get map => MapBuilder<int, InvoiceEntity> get map =>
_$this._map ??= new MapBuilder<int, QuoteEntity>(); _$this._map ??= new MapBuilder<int, InvoiceEntity>();
set map(MapBuilder<int, QuoteEntity> map) => _$this._map = map; set map(MapBuilder<int, InvoiceEntity> map) => _$this._map = map;
ListBuilder<int> _list; ListBuilder<int> _list;
ListBuilder<int> get list => _$this._list ??= new ListBuilder<int>(); ListBuilder<int> get list => _$this._list ??= new ListBuilder<int>();
@ -255,7 +266,9 @@ class QuoteStateBuilder implements Builder<QuoteState, QuoteStateBuilder> {
class _$QuoteUIState extends QuoteUIState { class _$QuoteUIState extends QuoteUIState {
@override @override
final QuoteEntity editing; final InvoiceEntity editing;
@override
final InvoiceItemEntity editingItem;
@override @override
final int selectedId; final int selectedId;
@override @override
@ -264,7 +277,8 @@ class _$QuoteUIState extends QuoteUIState {
factory _$QuoteUIState([void updates(QuoteUIStateBuilder b)]) => factory _$QuoteUIState([void updates(QuoteUIStateBuilder b)]) =>
(new QuoteUIStateBuilder()..update(updates)).build(); (new QuoteUIStateBuilder()..update(updates)).build();
_$QuoteUIState._({this.editing, this.selectedId, this.listUIState}) _$QuoteUIState._(
{this.editing, this.editingItem, this.selectedId, this.listUIState})
: super._() { : super._() {
if (selectedId == null) if (selectedId == null)
throw new BuiltValueNullFieldError('QuoteUIState', 'selectedId'); throw new BuiltValueNullFieldError('QuoteUIState', 'selectedId');
@ -284,13 +298,16 @@ class _$QuoteUIState extends QuoteUIState {
if (identical(other, this)) return true; if (identical(other, this)) return true;
if (other is! QuoteUIState) return false; if (other is! QuoteUIState) return false;
return editing == other.editing && return editing == other.editing &&
editingItem == other.editingItem &&
selectedId == other.selectedId && selectedId == other.selectedId &&
listUIState == other.listUIState; listUIState == other.listUIState;
} }
@override @override
int get hashCode { int get hashCode {
return $jf($jc($jc($jc(0, editing.hashCode), selectedId.hashCode), return $jf($jc(
$jc($jc($jc(0, editing.hashCode), editingItem.hashCode),
selectedId.hashCode),
listUIState.hashCode)); listUIState.hashCode));
} }
@ -298,6 +315,7 @@ class _$QuoteUIState extends QuoteUIState {
String toString() { String toString() {
return (newBuiltValueToStringHelper('QuoteUIState') return (newBuiltValueToStringHelper('QuoteUIState')
..add('editing', editing) ..add('editing', editing)
..add('editingItem', editingItem)
..add('selectedId', selectedId) ..add('selectedId', selectedId)
..add('listUIState', listUIState)) ..add('listUIState', listUIState))
.toString(); .toString();
@ -308,10 +326,16 @@ class QuoteUIStateBuilder
implements Builder<QuoteUIState, QuoteUIStateBuilder> { implements Builder<QuoteUIState, QuoteUIStateBuilder> {
_$QuoteUIState _$v; _$QuoteUIState _$v;
QuoteEntityBuilder _editing; InvoiceEntityBuilder _editing;
QuoteEntityBuilder get editing => InvoiceEntityBuilder get editing =>
_$this._editing ??= new QuoteEntityBuilder(); _$this._editing ??= new InvoiceEntityBuilder();
set editing(QuoteEntityBuilder editing) => _$this._editing = editing; set editing(InvoiceEntityBuilder editing) => _$this._editing = editing;
InvoiceItemEntityBuilder _editingItem;
InvoiceItemEntityBuilder get editingItem =>
_$this._editingItem ??= new InvoiceItemEntityBuilder();
set editingItem(InvoiceItemEntityBuilder editingItem) =>
_$this._editingItem = editingItem;
int _selectedId; int _selectedId;
int get selectedId => _$this._selectedId; int get selectedId => _$this._selectedId;
@ -328,6 +352,7 @@ class QuoteUIStateBuilder
QuoteUIStateBuilder get _$this { QuoteUIStateBuilder get _$this {
if (_$v != null) { if (_$v != null) {
_editing = _$v.editing?.toBuilder(); _editing = _$v.editing?.toBuilder();
_editingItem = _$v.editingItem?.toBuilder();
_selectedId = _$v.selectedId; _selectedId = _$v.selectedId;
_listUIState = _$v.listUIState?.toBuilder(); _listUIState = _$v.listUIState?.toBuilder();
_$v = null; _$v = null;
@ -353,6 +378,7 @@ class QuoteUIStateBuilder
_$result = _$v ?? _$result = _$v ??
new _$QuoteUIState._( new _$QuoteUIState._(
editing: _editing?.build(), editing: _editing?.build(),
editingItem: _editingItem?.build(),
selectedId: selectedId, selectedId: selectedId,
listUIState: listUIState.build()); listUIState: listUIState.build());
} catch (_) { } catch (_) {
@ -360,6 +386,8 @@ class QuoteUIStateBuilder
try { try {
_$failedField = 'editing'; _$failedField = 'editing';
_editing?.build(); _editing?.build();
_$failedField = 'editingItem';
_editingItem?.build();
_$failedField = 'listUIState'; _$failedField = 'listUIState';
listUIState.build(); listUIState.build();

View File

@ -4,14 +4,13 @@ import 'package:invoiceninja_flutter/redux/client/client_state.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_state.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_state.dart';
import 'package:invoiceninja_flutter/redux/product/product_state.dart'; import 'package:invoiceninja_flutter/redux/product/product_state.dart';
import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; import 'package:invoiceninja_flutter/ui/auth/login_vm.dart';
// STARTER: import - do not remove comment // STARTER: import - do not remove comment
import 'package:invoiceninja_flutter/redux/quote/quote_state.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
part 'ui_state.g.dart'; part 'ui_state.g.dart';
abstract class UIState implements Built<UIState, UIStateBuilder> { abstract class UIState implements Built<UIState, UIStateBuilder> {
factory UIState({bool enableDarkMode}) { factory UIState({bool enableDarkMode}) {
return _$UIState._( return _$UIState._(
selectedCompanyIndex: 0, selectedCompanyIndex: 0,
@ -21,26 +20,29 @@ abstract class UIState implements Built<UIState, UIStateBuilder> {
clientUIState: ClientUIState(), clientUIState: ClientUIState(),
invoiceUIState: InvoiceUIState(), invoiceUIState: InvoiceUIState(),
// STARTER: constructor - do not remove comment // STARTER: constructor - do not remove comment
quoteUIState: QuoteUIState(), quoteUIState: QuoteUIState(),
); );
} }
UIState._(); UIState._();
int get selectedCompanyIndex; int get selectedCompanyIndex;
String get currentRoute; String get currentRoute;
bool get enableDarkMode; bool get enableDarkMode;
ProductUIState get productUIState; ProductUIState get productUIState;
ClientUIState get clientUIState; ClientUIState get clientUIState;
InvoiceUIState get invoiceUIState; InvoiceUIState get invoiceUIState;
@nullable @nullable
String get filter; String get filter;
// STARTER: properties - do not remove comment // STARTER: properties - do not remove comment
QuoteUIState get quoteUIState; QuoteUIState get quoteUIState;
static Serializer<UIState> get serializer => _$uIStateSerializer; static Serializer<UIState> get serializer => _$uIStateSerializer;
} }

View File

@ -1,141 +1,141 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_details_vm.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_details_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_items_vm.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_items_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_item_selector.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_item_selector.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/refresh_icon_button.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/refresh_icon_button.dart';
class InvoiceEdit extends StatefulWidget { class InvoiceEdit extends StatefulWidget {
final InvoiceEditVM viewModel; final InvoiceEditVM viewModel;
const InvoiceEdit({ const InvoiceEdit({
Key key, Key key,
@required this.viewModel, @required this.viewModel,
}) : super(key: key); }) : super(key: key);
@override @override
_InvoiceEditState createState() => _InvoiceEditState(); _InvoiceEditState createState() => _InvoiceEditState();
}
class _InvoiceEditState extends State<InvoiceEdit>
with SingleTickerProviderStateMixin {
TabController _controller;
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
static const kDetailsScreen = 0;
static const kItemScreen = 1;
@override
void initState() {
super.initState();
final invoice = widget.viewModel.invoice;
final invoiceItem = widget.viewModel.invoiceItem;
final index =
invoice.invoiceItems.contains(invoiceItem) ? kItemScreen : kDetailsScreen;
_controller =
TabController(vsync: this, length: 2, initialIndex: index);
} }
@override class _InvoiceEditState extends State<InvoiceEdit>
void dispose() { with SingleTickerProviderStateMixin {
_controller.dispose(); TabController _controller;
super.dispose(); static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
}
@override static const kDetailsScreen = 0;
Widget build(BuildContext context) { static const kItemScreen = 1;
final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final invoice = viewModel.invoice;
return WillPopScope( @override
onWillPop: () async { void initState() {
viewModel.onBackPressed(); super.initState();
return true;
},
child: Scaffold(
appBar: AppBar(
title: Text(invoice.isNew
? localization.newInvoice
: '${localization.invoice} ${viewModel.origInvoice.invoiceNumber}'),
actions: <Widget>[
RefreshIconButton(
icon: Icons.cloud_upload,
tooltip: localization.save,
isVisible: !invoice.isDeleted,
isSaving: widget.viewModel.isSaving,
isDirty: invoice.isNew || invoice != viewModel.origInvoice,
onPressed: () {
if (!_formKey.currentState.validate()) {
return;
}
widget.viewModel.onSavePressed(context); final invoice = widget.viewModel.invoice;
}, final invoiceItem = widget.viewModel.invoiceItem;
)
], final index =
bottom: TabBar( invoice.invoiceItems.contains(invoiceItem) ? kItemScreen : kDetailsScreen;
controller: _controller, _controller =
//isScrollable: true, TabController(vsync: this, length: 2, initialIndex: index);
tabs: [ }
Tab(
text: localization.details, @override
), void dispose() {
Tab( _controller.dispose();
text: localization.items, super.dispose();
), }
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final invoice = viewModel.invoice;
return WillPopScope(
onWillPop: () async {
viewModel.onBackPressed();
return true;
},
child: Scaffold(
appBar: AppBar(
title: Text(invoice.isNew
? localization.newInvoice
: '${localization.invoice} ${viewModel.origInvoice.invoiceNumber}'),
actions: <Widget>[
RefreshIconButton(
icon: Icons.cloud_upload,
tooltip: localization.save,
isVisible: !invoice.isDeleted,
isSaving: widget.viewModel.isSaving,
isDirty: invoice.isNew || invoice != viewModel.origInvoice,
onPressed: () {
if (!_formKey.currentState.validate()) {
return;
}
widget.viewModel.onSavePressed(context);
},
)
], ],
bottom: TabBar(
controller: _controller,
//isScrollable: true,
tabs: [
Tab(
text: localization.details,
),
Tab(
text: localization.items,
),
],
),
), ),
), body: Form(
body: Form( key: _formKey,
key: _formKey, child: TabBarView(
child: TabBarView( controller: _controller,
controller: _controller, children: <Widget>[
children: <Widget>[ InvoiceEditDetailsScreen(),
InvoiceEditDetailsScreen(), InvoiceEditItemsScreen(),
InvoiceEditItemsScreen(), ],
], ),
), ),
), bottomNavigationBar: BottomAppBar(
bottomNavigationBar: BottomAppBar( color: Theme.of(context).primaryColor,
color: Theme.of(context).primaryColor, shape: CircularNotchedRectangle(),
shape: CircularNotchedRectangle(), child: Padding(
child: Padding( padding: const EdgeInsets.all(14.0),
padding: const EdgeInsets.all(14.0), child: Text(
child: Text( '${localization.total}: ${formatNumber(invoice.calculateTotal(viewModel.company.enableInclusiveTaxes), context, clientId: viewModel.invoice.clientId)}',
'${localization.total}: ${formatNumber(invoice.calculateTotal(viewModel.company.enableInclusiveTaxes), context, clientId: viewModel.invoice.clientId)}', style: TextStyle(
style: TextStyle( //color: Theme.of(context).selectedRowColor,
//color: Theme.of(context).selectedRowColor, color: Colors.white,
color: Colors.white, fontSize: 18.0,
fontSize: 18.0, ),
), ),
), ),
), ),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
showDialog<InvoiceItemSelector>(
context: context,
builder: (BuildContext context) {
return InvoiceItemSelector(
onItemsSelected: (items) {
viewModel.onItemsAdded(items);
_controller.animateTo(kItemScreen);
},
);
});
},
child: const Icon(Icons.add, color: Colors.white),
tooltip: localization.addItem,
),
), ),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, );
floatingActionButton: FloatingActionButton( }
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
showDialog<InvoiceItemSelector>(
context: context,
builder: (BuildContext context) {
return InvoiceItemSelector(
onItemsSelected: (items) {
viewModel.onItemsAdded(items);
_controller.animateTo(kItemScreen);
},
);
});
},
child: const Icon(Icons.add, color: Colors.white),
tooltip: localization.addItem,
),
),
);
} }
}

View File

@ -30,7 +30,8 @@ class InvoiceListItem extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localization = AppLocalization.of(context); final localization = AppLocalization.of(context);
final filterMatch = filter != null && filter.isNotEmpty final filterMatch = filter != null && filter.isNotEmpty
? (invoice.matchesFilterValue(filter) ?? client.matchesFilterValue(filter)) ? (invoice.matchesFilterValue(filter) ??
client.matchesFilterValue(filter))
: null; : null;
return DismissibleEntity( return DismissibleEntity(
@ -71,10 +72,15 @@ class InvoiceListItem extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
Text(invoice.isPastDue ? localization.pastDue : localization.lookup('invoice_status_${invoice.invoiceStatusId}'), Text(
invoice.isPastDue
? localization.pastDue
: localization.lookup(
'invoice_status_${invoice.invoiceStatusId}'),
style: TextStyle( style: TextStyle(
color: color: invoice.isPastDue
invoice.isPastDue ? Colors.red : InvoiceStatusColors.colors[invoice.invoiceStatusId], ? Colors.red
: InvoiceStatusColors.colors[invoice.invoiceStatusId],
)), )),
], ],
), ),
@ -85,4 +91,3 @@ class InvoiceListItem extends StatelessWidget {
); );
} }
} }

View File

@ -146,7 +146,7 @@ class InvoiceViewVM {
bool operator ==(dynamic other) => bool operator ==(dynamic other) =>
client == other.client && client == other.client &&
company == other.company && company == other.company &&
invoice == other.invoice && invoice == other.quote &&
isSaving == other.isSaving && isSaving == other.isSaving &&
isDirty == other.isDirty; isDirty == other.isDirty;

View File

@ -1,13 +1,17 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux_starter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_details_vm.dart';
import 'package:flutter_redux_starter/ui/quote/edit/quote_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_items_vm.dart';
import 'package:flutter_redux_starter/ui/app/save_icon_button.dart'; import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_item_selector.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/refresh_icon_button.dart';
class QuoteEdit extends StatefulWidget { class QuoteEdit extends StatefulWidget {
final QuoteEditVM viewModel; final QuoteEditVM viewModel;
QuoteEdit({ const QuoteEdit({
Key key, Key key,
@required this.viewModel, @required this.viewModel,
}) : super(key: key); }) : super(key: key);
@ -16,52 +20,38 @@ class QuoteEdit extends StatefulWidget {
_QuoteEditState createState() => _QuoteEditState(); _QuoteEditState createState() => _QuoteEditState();
} }
class _QuoteEditState extends State<QuoteEdit> { class _QuoteEditState extends State<QuoteEdit>
with SingleTickerProviderStateMixin {
TabController _controller;
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// STARTER: controllers - do not remove comment static const kDetailsScreen = 0;
static const kItemScreen = 1;
var _controllers = [];
@override @override
void didChangeDependencies() { void initState() {
super.initState();
_controllers = [ final invoice = widget.viewModel.quote;
// STARTER: array - do not remove comment final invoiceItem = widget.viewModel.quoteItem;
];
_controllers.forEach((controller) => controller.removeListener(_onChanged)); final index = invoice.invoiceItems.contains(invoiceItem)
? kItemScreen
var quote = widget.viewModel.quote; : kDetailsScreen;
// STARTER: read value - do not remove comment _controller = TabController(vsync: this, length: 2, initialIndex: index);
_controllers.forEach((controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
} }
@override @override
void dispose() { void dispose() {
_controllers.forEach((controller) { _controller.dispose();
controller.removeListener(_onChanged);
controller.dispose();
});
super.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var viewModel = widget.viewModel; final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final invoice = viewModel.quote;
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
@ -70,36 +60,81 @@ class _QuoteEditState extends State<QuoteEdit> {
}, },
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(viewModel.quote.isNew title: Text(invoice.isNew
? 'New Quote' ? localization.newQuote
: viewModel.quote.displayName), : '${localization.quote} ${viewModel.origQuote.invoiceNumber}'),
actions: <Widget>[ actions: <Widget>[
Builder(builder: (BuildContext context) { RefreshIconButton(
return SaveIconButton( icon: Icons.cloud_upload,
isLoading: viewModel.isLoading, tooltip: localization.save,
onPressed: () { isVisible: !invoice.isDeleted,
if (!_formKey.currentState.validate()) { isSaving: widget.viewModel.isSaving,
return; isDirty: invoice.isNew || invoice != viewModel.origQuote,
} onPressed: () {
if (!_formKey.currentState.validate()) {
return;
}
viewModel.onSavePressed(context); widget.viewModel.onSavePressed(context);
}, },
); )
}),
], ],
), bottom: TabBar(
body: Form( controller: _controller,
key: _formKey, //isScrollable: true,
child: ListView( tabs: [
children: <Widget>[ Tab(
FormCard( text: localization.details,
children: <Widget>[ ),
// STARTER: widgets - do not remove comment Tab(
], text: localization.items,
), ),
], ],
), ),
), ),
body: Form(
key: _formKey,
child: TabBarView(
controller: _controller,
children: <Widget>[
InvoiceEditDetailsScreen(),
InvoiceEditItemsScreen(),
],
),
),
bottomNavigationBar: BottomAppBar(
color: Theme.of(context).primaryColor,
shape: CircularNotchedRectangle(),
child: Padding(
padding: const EdgeInsets.all(14.0),
child: Text(
'${localization.total}: ${formatNumber(invoice.calculateTotal(viewModel.company.enableInclusiveTaxes), context, clientId: viewModel.quote.clientId)}',
style: TextStyle(
//color: Theme.of(context).selectedRowColor,
color: Colors.white,
fontSize: 18.0,
),
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
showDialog<InvoiceItemSelector>(
context: context,
builder: (BuildContext context) {
return InvoiceItemSelector(
onItemsSelected: (items) {
viewModel.onItemsAdded(items);
_controller.animateTo(kItemScreen);
},
);
});
},
child: const Icon(Icons.add, color: Colors.white),
tooltip: localization.addItem,
),
), ),
); );
} }

View File

@ -2,18 +2,21 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_starter/redux/ui/ui_actions.dart'; import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:flutter_redux_starter/ui/quote/quote_screen.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart';
import 'package:invoiceninja_flutter/ui/invoice/invoice_screen.dart';
import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'package:flutter_redux_starter/redux/quote/quote_actions.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart';
import 'package:flutter_redux_starter/data/models/quote_model.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:flutter_redux_starter/ui/quote/edit/quote_edit.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit.dart';
import 'package:flutter_redux_starter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:flutter_redux_starter/ui/app/icon_message.dart';
class QuoteEditScreen extends StatelessWidget { class QuoteEditScreen extends StatelessWidget {
static final String route = '/quote/edit'; static const String route = '/quote/edit';
QuoteEditScreen({Key key}) : super(key: key);
const QuoteEditScreen({Key key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -31,45 +34,62 @@ class QuoteEditScreen extends StatelessWidget {
} }
class QuoteEditVM { class QuoteEditVM {
final QuoteEntity quote; final CompanyEntity company;
final Function(QuoteEntity) onChanged; final InvoiceEntity quote;
final InvoiceItemEntity quoteItem;
final InvoiceEntity origQuote;
final Function(BuildContext) onSavePressed; final Function(BuildContext) onSavePressed;
final Function(List<InvoiceItemEntity>) onItemsAdded;
final Function onBackPressed; final Function onBackPressed;
final bool isLoading; final bool isSaving;
QuoteEditVM({ QuoteEditVM({
@required this.company,
@required this.quote, @required this.quote,
@required this.onChanged, @required this.quoteItem,
@required this.origQuote,
@required this.onSavePressed, @required this.onSavePressed,
@required this.onItemsAdded,
@required this.onBackPressed, @required this.onBackPressed,
@required this.isLoading, @required this.isSaving,
}); });
factory QuoteEditVM.fromStore(Store<AppState> store) { factory QuoteEditVM.fromStore(Store<AppState> store) {
final quote = store.state.quoteUIState.selected; final AppState state = store.state;
final invoice = state.invoiceUIState.editing;
return QuoteEditVM( return QuoteEditVM(
isLoading: store.state.isLoading, company: state.selectedCompany,
quote: quote, isSaving: state.isSaving,
onChanged: (QuoteEntity quote) { quote: invoice,
store.dispatch(UpdateQuote(quote)); quoteItem: state.invoiceUIState.editingItem,
}, origQuote: store.state.invoiceState.map[invoice.id],
onBackPressed: () { onBackPressed: () =>
store.dispatch(UpdateCurrentRoute(QuoteScreen.route)); store.dispatch(UpdateCurrentRoute(InvoiceScreen.route)),
},
onSavePressed: (BuildContext context) { onSavePressed: (BuildContext context) {
final Completer<Null> completer = new Completer<Null>(); final Completer<InvoiceEntity> completer = Completer<InvoiceEntity>();
store.dispatch(SaveQuoteRequest(completer: completer, quote: quote)); store.dispatch(
return completer.future.then((_) { SaveInvoiceRequest(completer: completer, invoice: invoice));
Scaffold.of(context).showSnackBar(SnackBar( return completer.future.then((savedInvoice) {
content: IconMessage( if (invoice.isNew) {
message: quote.isNew Navigator.of(context).pushReplacementNamed(InvoiceViewScreen.route);
? 'Successfully Created Quote' } else {
: 'Successfully Updated Quote', Navigator.of(context).pop(savedInvoice);
), }
duration: Duration(seconds: 3))); }).catchError((Object error) {
showDialog<ErrorDialog>(
context: context,
builder: (BuildContext context) {
return ErrorDialog(error);
});
}); });
}, },
onItemsAdded: (items) {
if (items.length == 1) {
store.dispatch(EditInvoiceItem(items[0]));
}
store.dispatch(AddInvoiceItems(items));
},
); );
} }
} }

View File

@ -1,9 +1,11 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/data/models/invoice_model.dart';
import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart'; import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart';
import 'package:invoiceninja_flutter/ui/app/snackbar_row.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/invoice/invoice_list_item.dart';
import 'package:invoiceninja_flutter/ui/invoice/invoice_list_vm.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_list_vm.dart'; import 'package:invoiceninja_flutter/ui/quote/quote_list_vm.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
@ -15,98 +17,167 @@ class QuoteList extends StatelessWidget {
@required this.viewModel, @required this.viewModel,
}) : super(key: key); }) : super(key: key);
@override void _showMenu(
Widget build(BuildContext context) { BuildContext context, InvoiceEntity invoice, ClientEntity client) async {
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 user = viewModel.user;
final message = await showDialog<String>( final message = await showDialog<String>(
context: context, context: context,
builder: (BuildContext context) => SimpleDialog(children: <Widget>[ builder: (BuildContext context) => SimpleDialog(children: <Widget>[
user.canCreate(EntityType.quote) user.canCreate(EntityType.invoice)
? ListTile( ? ListTile(
leading: Icon(Icons.control_point_duplicate), leading: Icon(Icons.control_point_duplicate),
title: Text(AppLocalization.of(context).clone), title: Text(AppLocalization.of(context).clone),
onTap: () => viewModel.onEntityAction( onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.clone), context, invoice, EntityAction.clone),
) )
: Container(), : Container(),
Divider(), user.canEditEntity(invoice) && !invoice.isPublic
user.canEditEntity(quote) && !quote.isActive ? ListTile(
? ListTile( leading: Icon(Icons.publish),
leading: Icon(Icons.restore), title: Text(AppLocalization.of(context).markSent),
title: Text(AppLocalization.of(context).restore), onTap: () => viewModel.onEntityAction(
onTap: () => viewModel.onEntityAction( context, invoice, EntityAction.markSent),
context, quote, EntityAction.restore), )
) : Container(),
: Container(), user.canEditEntity(invoice) && client.hasEmailAddress
user.canEditEntity(quote) && quote.isActive ? ListTile(
? ListTile( leading: Icon(Icons.send),
leading: Icon(Icons.archive), title: Text(AppLocalization.of(context).email),
title: Text(AppLocalization.of(context).archive), onTap: () => viewModel.onEntityAction(
onTap: () => viewModel.onEntityAction( context, invoice, EntityAction.emailInvoice),
context, quote, EntityAction.archive), )
) : Container(),
: Container(), ListTile(
user.canEditEntity(quote) && !quote.isDeleted leading: Icon(Icons.picture_as_pdf),
? ListTile( title: Text(AppLocalization.of(context).pdf),
leading: Icon(Icons.delete), onTap: () => viewModel.onEntityAction(
title: Text(AppLocalization.of(context).delete), context, invoice, EntityAction.pdf),
onTap: () => viewModel.onEntityAction( ),
context, quote, EntityAction.delete), Divider(),
) user.canEditEntity(invoice) && !invoice.isActive
: Container(), ? ListTile(
])); leading: Icon(Icons.restore),
title: Text(AppLocalization.of(context).restore),
onTap: () => viewModel.onEntityAction(
context, invoice, EntityAction.restore),
)
: Container(),
user.canEditEntity(invoice) && invoice.isActive
? ListTile(
leading: Icon(Icons.archive),
title: Text(AppLocalization.of(context).archive),
onTap: () => viewModel.onEntityAction(
context, invoice, EntityAction.archive),
)
: Container(),
user.canEditEntity(invoice) && !invoice.isDeleted
? ListTile(
leading: Icon(Icons.delete),
title: Text(AppLocalization.of(context).delete),
onTap: () => viewModel.onEntityAction(
context, invoice, EntityAction.delete),
)
: Container(),
]));
if (message != null) { if (message != null) {
Scaffold.of(context).showSnackBar(SnackBar( Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow( content: SnackBarRow(
message: message, message: message,
))); )));
} }
} }
Widget _buildListView(BuildContext context) { @override
return RefreshIndicator( Widget build(BuildContext context) {
onRefresh: () => viewModel.onRefreshed(context), final localization = AppLocalization.of(context);
child: ListView.builder( final listState = viewModel.listState;
itemCount: viewModel.quoteList.length, final filteredClientId = listState.filterClientId;
itemBuilder: (BuildContext context, index) { final filteredClient =
final quoteId = viewModel.quoteList[index]; filteredClientId != null ? viewModel.clientMap[filteredClientId] : null;
final quote = viewModel.quoteMap[quoteId];
return Column(children: <Widget>[ return Column(
QuoteListItem( children: <Widget>[
user: viewModel.user, filteredClient != null
filter: viewModel.filter, ? Material(
quote: quote, color: Colors.orangeAccent,
client: viewModel.clientMap[quote.clientId], elevation: 6.0,
onDismissed: (DismissDirection direction) => child: InkWell(
viewModel.onDismissed(context, quote, direction), onTap: () => viewModel.onViewClientFilterPressed(context),
onTap: () => viewModel.onQuoteTap(context, quote), child: Row(
onLongPress: () => _showMenu(context, quote), children: <Widget>[
SizedBox(width: 18.0),
Expanded(
child: Text(
localization.clientsInvoices.replaceFirst(
':client', filteredClient.displayName),
style: TextStyle(
color: Colors.white,
fontSize: 16.0,
),
),
),
IconButton(
icon: Icon(
Icons.close,
color: Colors.white,
),
onPressed: () => viewModel.onClearClientFilterPressed(),
)
],
),
),
)
: Container(),
Expanded(
child: !viewModel.isLoaded
? LoadingIndicator()
: RefreshIndicator(
onRefresh: () => viewModel.onRefreshed(context),
child: viewModel.invoiceList.isEmpty
? Opacity(
opacity: 0.5,
child: Center(
child: Text(
AppLocalization.of(context).noRecordsFound,
style: TextStyle(
fontSize: 18.0,
),
),
), ),
Divider( )
height: 1.0, : ListView.builder(
), shrinkWrap: true,
]); itemCount: viewModel.invoiceList.length,
}), itemBuilder: (BuildContext context, index) {
final invoiceId = viewModel.invoiceList[index];
final invoice = viewModel.invoiceMap[invoiceId];
final client =
viewModel.clientMap[invoice.clientId];
return Column(
children: <Widget>[
InvoiceListItem(
user: viewModel.user,
filter: viewModel.filter,
invoice: invoice,
client: viewModel.clientMap[invoice.clientId],
onDismissed: (DismissDirection direction) =>
viewModel.onDismissed(
context, invoice, direction),
onTap: () =>
viewModel.onInvoiceTap(context, invoice),
onLongPress: () =>
_showMenu(context, invoice, client),
),
Divider(
height: 1.0,
),
],
);
},
),
),
),
],
); );
} }
} }

View File

@ -12,7 +12,7 @@ class QuoteListItem extends StatelessWidget {
final DismissDirectionCallback onDismissed; final DismissDirectionCallback onDismissed;
final GestureTapCallback onTap; final GestureTapCallback onTap;
final GestureTapCallback onLongPress; final GestureTapCallback onLongPress;
final QuoteEntity quote; final InvoiceEntity invoice;
final ClientEntity client; final ClientEntity client;
final String filter; final String filter;
@ -21,7 +21,7 @@ class QuoteListItem extends StatelessWidget {
@required this.onDismissed, @required this.onDismissed,
@required this.onTap, @required this.onTap,
@required this.onLongPress, @required this.onLongPress,
@required this.quote, @required this.invoice,
@required this.client, @required this.client,
@required this.filter, @required this.filter,
}); });
@ -30,12 +30,13 @@ class QuoteListItem extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localization = AppLocalization.of(context); final localization = AppLocalization.of(context);
final filterMatch = filter != null && filter.isNotEmpty final filterMatch = filter != null && filter.isNotEmpty
? (quote.matchesFilterValue(filter) ?? client.matchesFilterValue(filter)) ? (invoice.matchesFilterValue(filter) ??
client.matchesFilterValue(filter))
: null; : null;
return DismissibleEntity( return DismissibleEntity(
user: user, user: user,
entity: quote, entity: invoice,
onDismissed: onDismissed, onDismissed: onDismissed,
child: ListTile( child: ListTile(
onTap: onTap, onTap: onTap,
@ -51,8 +52,8 @@ class QuoteListItem extends StatelessWidget {
), ),
), ),
Text( Text(
formatNumber(quote.amount, context, formatNumber(invoice.amount, context,
clientId: quote.clientId), clientId: invoice.clientId),
style: Theme.of(context).textTheme.title), style: Theme.of(context).textTheme.title),
], ],
), ),
@ -64,25 +65,29 @@ class QuoteListItem extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: filterMatch == null child: filterMatch == null
? Text(quote.quoteNumber) ? Text(invoice.invoiceNumber)
: Text( : Text(
filterMatch, filterMatch,
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
Text(quote.isPastDue ? localization.pastDue : localization.lookup('invoice_status_${quote.quoteStatusId}'), Text(
invoice.isPastDue
? localization.pastDue
: localization.lookup(
'invoice_status_${invoice.invoiceStatusId}'),
style: TextStyle( style: TextStyle(
color: color: invoice.isPastDue
quote.isPastDue ? Colors.red : InvoiceStatusColors.colors[quote.quoteStatusId], ? Colors.red
: InvoiceStatusColors.colors[invoice.invoiceStatusId],
)), )),
], ],
), ),
EntityStateLabel(quote), EntityStateLabel(invoice),
], ],
), ),
), ),
); );
} }
} }

View File

@ -1,28 +1,35 @@
import 'dart:async'; import 'dart:async';
import 'package:redux/redux.dart'; import 'package:built_collection/built_collection.dart';
import 'package:invoiceninja_flutter/redux/client/client_actions.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_selectors.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:built_collection/built_collection.dart'; import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_list.dart';
import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_selectors.dart'; import 'package:invoiceninja_flutter/utils/pdf.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/quote/quote_list.dart'; import 'package:invoiceninja_flutter/ui/invoice/invoice_list.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart';
class QuoteListBuilder extends StatelessWidget { class QuoteListBuilder extends StatelessWidget {
static const String route = '/invoices/edit';
const QuoteListBuilder({Key key}) : super(key: key); const QuoteListBuilder({Key key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StoreConnector<AppState, QuoteListVM>( return StoreConnector<AppState, QuoteListVM>(
//rebuildOnChange: true,
converter: QuoteListVM.fromStore, converter: QuoteListVM.fromStore,
builder: (context, viewModel) { builder: (context, vm) {
return QuoteList( return QuoteList(
viewModel: viewModel, viewModel: vm,
); );
}, },
); );
@ -31,28 +38,34 @@ class QuoteListBuilder extends StatelessWidget {
class QuoteListVM { class QuoteListVM {
final UserEntity user; final UserEntity user;
final List<int> quoteList; final ListUIState listState;
final BuiltMap<int, QuoteEntity> quoteMap; final List<int> invoiceList;
final BuiltMap<int, InvoiceEntity> invoiceMap;
final BuiltMap<int, ClientEntity> clientMap; final BuiltMap<int, ClientEntity> clientMap;
final String filter; final String filter;
final bool isLoading; final bool isLoading;
final bool isLoaded; final bool isLoaded;
final Function(BuildContext, QuoteEntity) onQuoteTap; final Function(BuildContext, InvoiceEntity) onInvoiceTap;
final Function(BuildContext, QuoteEntity, DismissDirection) onDismissed; final Function(BuildContext, InvoiceEntity, DismissDirection) onDismissed;
final Function(BuildContext) onRefreshed; final Function(BuildContext) onRefreshed;
final Function(BuildContext, QuoteEntity, EntityAction) onEntityAction; final Function onClearClientFilterPressed;
final Function(BuildContext) onViewClientFilterPressed;
final Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction;
QuoteListVM({ QuoteListVM({
@required this.user, @required this.user,
@required this.quoteList, @required this.listState,
@required this.quoteMap, @required this.invoiceList,
@required this.invoiceMap,
@required this.clientMap, @required this.clientMap,
@required this.filter,
@required this.isLoading, @required this.isLoading,
@required this.isLoaded, @required this.isLoaded,
@required this.onQuoteTap, @required this.filter,
@required this.onInvoiceTap,
@required this.onDismissed, @required this.onDismissed,
@required this.onRefreshed, @required this.onRefreshed,
@required this.onClearClientFilterPressed,
@required this.onViewClientFilterPressed,
@required this.onEntityAction, @required this.onEntityAction,
}); });
@ -63,7 +76,7 @@ class QuoteListVM {
} }
final completer = snackBarCompleter( final completer = snackBarCompleter(
context, AppLocalization.of(context).refreshComplete); context, AppLocalization.of(context).refreshComplete);
store.dispatch(LoadQuotes(completer: completer, force: true)); store.dispatch(LoadInvoices(completer: completer, force: true));
return completer.future; return completer.future;
} }
@ -71,66 +84,98 @@ class QuoteListVM {
return QuoteListVM( return QuoteListVM(
user: state.user, user: state.user,
quoteList: memoizedFilteredQuoteList(state.quoteState.map, listState: state.invoiceListState,
state.quoteState.list, state.quoteListState), invoiceList: memoizedFilteredQuoteList(
quoteMap: state.quoteState.map, state.invoiceState.map,
state.invoiceState.list,
state.clientState.map,
state.invoiceListState),
invoiceMap: state.invoiceState.map,
clientMap: state.clientState.map, clientMap: state.clientState.map,
isLoading: state.isLoading, isLoading: state.isLoading,
isLoaded: state.quoteState.isLoaded, isLoaded: state.invoiceState.isLoaded && state.clientState.isLoaded,
filter: state.quoteUIState.listUIState.filter, filter: state.invoiceListState.filter,
onQuoteTap: (context, quote) { onInvoiceTap: (context, invoice) {
store.dispatch(EditQuote(quote: quote, context: context)); store.dispatch(ViewInvoice(invoiceId: invoice.id, context: context));
}, },
onEntityAction: (context, quote, action) { onRefreshed: (context) => _handleRefresh(context),
onClearClientFilterPressed: () =>
store.dispatch(FilterInvoicesByClient()),
onViewClientFilterPressed: (BuildContext context) => store.dispatch(
ViewClient(
clientId: state.invoiceListState.filterClientId,
context: context)),
onEntityAction: (context, invoice, action) {
final localization = AppLocalization.of(context);
switch (action) { switch (action) {
case EntityAction.pdf:
Navigator.of(context).pop();
viewPdf(invoice, context);
break;
case EntityAction.markSent:
store.dispatch(MarkSentInvoiceRequest(
popCompleter(
context, localization.markedInvoiceAsSent),
invoice.id));
break;
case EntityAction.emailInvoice:
store.dispatch(ShowEmailInvoice(
completer: popCompleter(
context, localization.emailedInvoice),
invoice: invoice,
context: context));
break;
case EntityAction.clone: case EntityAction.clone:
Navigator.of(context).pop(); Navigator.of(context).pop();
store.dispatch( store.dispatch(
EditQuote(context: context, quote: quote.clone)); EditInvoice(context: context, invoice: invoice.clone));
break; break;
case EntityAction.restore: case EntityAction.restore:
store.dispatch(RestoreQuoteRequest( store.dispatch(RestoreInvoiceRequest(
popCompleter( popCompleter(
context, AppLocalization.of(context).restoredQuote), context, localization.restoredInvoice),
quote.id)); invoice.id));
break; break;
case EntityAction.archive: case EntityAction.archive:
store.dispatch(ArchiveQuoteRequest( store.dispatch(ArchiveInvoiceRequest(
popCompleter( popCompleter(
context, AppLocalization.of(context).archivedQuote), context, localization.archivedInvoice),
quote.id)); invoice.id));
break; break;
case EntityAction.delete: case EntityAction.delete:
store.dispatch(DeleteQuoteRequest( store.dispatch(DeleteInvoiceRequest(
popCompleter( popCompleter(
context, AppLocalization.of(context).deletedQuote), context, localization.deletedInvoice),
quote.id)); invoice.id));
break; break;
} }
}, },
onRefreshed: (context) => _handleRefresh(context), onDismissed: (BuildContext context, InvoiceEntity invoice,
onDismissed: (BuildContext context, QuoteEntity quote,
DismissDirection direction) { DismissDirection direction) {
final localization = AppLocalization.of(context); final localization = AppLocalization.of(context);
if (direction == DismissDirection.endToStart) { if (direction == DismissDirection.endToStart) {
if (quote.isDeleted || quote.isArchived) { if (invoice.isDeleted || invoice.isArchived) {
store.dispatch(RestoreQuoteRequest( store.dispatch(RestoreInvoiceRequest(
snackBarCompleter(context, localization.restoredQuote), snackBarCompleter(
quote.id)); context, localization.restoredInvoice),
invoice.id));
} else { } else {
store.dispatch(ArchiveQuoteRequest( store.dispatch(ArchiveInvoiceRequest(
snackBarCompleter(context, localization.archivedQuote), snackBarCompleter(
quote.id)); context, localization.archivedInvoice),
invoice.id));
} }
} else if (direction == DismissDirection.startToEnd) { } else if (direction == DismissDirection.startToEnd) {
if (quote.isDeleted) { if (invoice.isDeleted) {
store.dispatch(RestoreQuoteRequest( store.dispatch(RestoreInvoiceRequest(
snackBarCompleter(context, localization.restoredQuote), snackBarCompleter(
quote.id)); context, localization.restoredInvoice),
invoice.id));
} else { } else {
store.dispatch(DeleteQuoteRequest( store.dispatch(DeleteInvoiceRequest(
snackBarCompleter(context, localization.deletedQuote), snackBarCompleter(
quote.id)); context, localization.deletedInvoice),
invoice.id));
} }
} }
}); });

View File

@ -53,9 +53,9 @@ class QuoteScreen extends StatelessWidget {
onSelectedCustom2: (value) => onSelectedCustom2: (value) =>
store.dispatch(FilterQuotesByCustom2(value)), store.dispatch(FilterQuotesByCustom2(value)),
sortFields: [ sortFields: [
QuoteFields.quoteNumber, InvoiceFields.invoiceNumber,
QuoteFields.quoteDate, InvoiceFields.invoiceDate,
QuoteFields.updatedAt, InvoiceFields.updatedAt,
], ],
onSelectedState: (EntityState state, value) { onSelectedState: (EntityState state, value) {
store.dispatch(FilterQuotesByState(state)); store.dispatch(FilterQuotesByState(state));
@ -67,7 +67,7 @@ class QuoteScreen extends StatelessWidget {
backgroundColor: Theme.of(context).primaryColorDark, backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () { onPressed: () {
store.dispatch( store.dispatch(
EditQuote(quote: QuoteEntity(), context: context)); EditQuote(quote: InvoiceEntity(), context: context));
}, },
child: Icon( child: Icon(
Icons.add, Icons.add,

View File

@ -16,7 +16,7 @@ import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart'; import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart';
class QuoteViewScreen extends StatelessWidget { class QuoteViewScreen extends StatelessWidget {
static const String route = '/invoice/view'; static const String route = '/quote/view';
const QuoteViewScreen({Key key}) : super(key: key); const QuoteViewScreen({Key key}) : super(key: key);
@ -38,7 +38,7 @@ class QuoteViewScreen extends StatelessWidget {
class QuoteViewVM { class QuoteViewVM {
final CompanyEntity company; final CompanyEntity company;
final QuoteEntity quote; final InvoiceEntity quote;
final ClientEntity client; final ClientEntity client;
final bool isSaving; final bool isSaving;
final bool isDirty; final bool isDirty;
@ -80,13 +80,13 @@ class QuoteViewVM {
quote: quote, quote: quote,
client: client, client: client,
onEditPressed: (BuildContext context, [InvoiceItemEntity invoiceItem]) { onEditPressed: (BuildContext context, [InvoiceItemEntity invoiceItem]) {
final Completer<QuoteEntity> completer = final Completer<InvoiceEntity> completer =
new Completer<QuoteEntity>(); new Completer<InvoiceEntity>();
store.dispatch(EditQuote( store.dispatch(EditQuote(
quote: quote, quote: quote,
context: context, context: context,
completer: completer, completer: completer,
invoiceItem: invoiceItem)); quoteItem: invoiceItem));
completer.future.then((invoice) { completer.future.then((invoice) {
Scaffold.of(context).showSnackBar(SnackBar( Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow( content: SnackBarRow(
@ -104,39 +104,39 @@ class QuoteViewVM {
final localization = AppLocalization.of(context); final localization = AppLocalization.of(context);
switch (action) { switch (action) {
case EntityAction.pdf: case EntityAction.pdf:
viewPdf(invoice, context); viewPdf(quote, context);
break; break;
case EntityAction.markSent: case EntityAction.markSent:
store.dispatch(MarkSentQuoteRequest( store.dispatch(MarkSentQuoteRequest(
snackBarCompleter(context, localization.markedQuoteAsSent), snackBarCompleter(context, localization.markedQuoteAsSent),
invoice.id)); quote.id));
break; break;
case EntityAction.emailQuote: case EntityAction.emailInvoice:
store.dispatch(ShowEmailQuote( store.dispatch(ShowEmailQuote(
completer: completer:
snackBarCompleter(context, localization.emailedQuote), snackBarCompleter(context, localization.emailedQuote),
invoice: invoice, quote: quote,
context: context)); context: context));
break; break;
case EntityAction.archive: case EntityAction.archive:
store.dispatch(ArchiveQuoteRequest( store.dispatch(ArchiveQuoteRequest(
popCompleter(context, localization.archivedQuote), popCompleter(context, localization.archivedQuote),
invoice.id)); quote.id));
break; break;
case EntityAction.delete: case EntityAction.delete:
store.dispatch(DeleteQuoteRequest( store.dispatch(DeleteQuoteRequest(
popCompleter(context, localization.deletedQuote), popCompleter(context, localization.deletedQuote),
invoice.id)); quote.id));
break; break;
case EntityAction.restore: case EntityAction.restore:
store.dispatch(RestoreQuoteRequest( store.dispatch(RestoreQuoteRequest(
snackBarCompleter(context, localization.restoredQuote), snackBarCompleter(context, localization.restoredQuote),
invoice.id)); quote.id));
break; break;
case EntityAction.clone: case EntityAction.clone:
Navigator.of(context).pop(); Navigator.of(context).pop();
store.dispatch( store.dispatch(
EditQuote(context: context, invoice: invoice.clone)); EditQuote(context: context, quote: quote.clone));
break; break;
} }
}); });
@ -146,7 +146,7 @@ class QuoteViewVM {
bool operator ==(dynamic other) => bool operator ==(dynamic other) =>
client == other.client && client == other.client &&
company == other.company && company == other.company &&
invoice == other.invoice && quote == other.quote &&
isSaving == other.isSaving && isSaving == other.isSaving &&
isDirty == other.isDirty; isDirty == other.isDirty;
@ -154,7 +154,7 @@ class QuoteViewVM {
int get hashCode => int get hashCode =>
client.hashCode ^ client.hashCode ^
company.hashCode ^ company.hashCode ^
invoice.hashCode ^ quote.hashCode ^
isSaving.hashCode ^ isSaving.hashCode ^
isDirty.hashCode; isDirty.hashCode;
} }

View File

@ -1,8 +1,8 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux_starter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:flutter_redux_starter/ui/stub/edit/stub_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/stub/edit/stub_edit_vm.dart';
import 'package:flutter_redux_starter/ui/app/save_icon_button.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/refresh_icon_button.dart';
class StubEdit extends StatefulWidget { class StubEdit extends StatefulWidget {
final StubEditVM viewModel; final StubEditVM viewModel;

View File

@ -2,14 +2,14 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_starter/redux/ui/ui_actions.dart'; import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:flutter_redux_starter/ui/stub/stub_screen.dart'; import 'package:invoiceninja_flutter/ui/stub/stub_screen.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; import 'package:invoiceninja_flutter/redux/stub/stub_actions.dart';
import 'package:flutter_redux_starter/data/models/stub_model.dart'; import 'package:invoiceninja_flutter/data/models/stub_model.dart';
import 'package:flutter_redux_starter/ui/stub/edit/stub_edit.dart'; import 'package:invoiceninja_flutter/ui/stub/edit/stub_edit.dart';
import 'package:flutter_redux_starter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:flutter_redux_starter/ui/app/icon_message.dart'; import 'package:invoiceninja_flutter/ui/app/icon_message.dart';
class StubEditScreen extends StatelessWidget { class StubEditScreen extends StatelessWidget {
static final String route = '/stub/edit'; static final String route = '/stub/edit';