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,
PaymentEntity payment,
CreditEntity credit,
//QuoteEntity quote,
//InvoiceEntity quote,
TaskEntity task,
ExpenseEntity expense,
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/expense_model.dart';
export 'package:invoiceninja_flutter/data/models/vendor_model.dart';
export 'package:invoiceninja_flutter/data/models/quote_model.dart';
export 'package:invoiceninja_flutter/data/models/static/static_data_model.dart';
export 'package:invoiceninja_flutter/data/models/static/currency_model.dart';
export 'package:invoiceninja_flutter/data/models/static/size_model.dart';

View File

@ -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/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';
@ -73,7 +72,6 @@ part 'serializers.g.dart';
TimezoneItemResponse,
TimezoneListResponse,
// STARTER: serializers - do not remove comment
QuoteEntity,
])
final Serializers serializers =

View File

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

View File

@ -14,18 +14,18 @@ class QuoteRepository {
this.webClient = const WebClient(),
});
Future<QuoteEntity> loadItem(
Future<InvoiceEntity> loadItem(
CompanyEntity company, AuthState auth, int entityId) async {
final dynamic response = await webClient.get(
'${auth.url}/quotes/$entityId', company.token);
'${auth.url}/invoices/$entityId', company.token);
final QuoteItemResponse quoteResponse =
serializers.deserializeWith(QuoteItemResponse.serializer, response);
final InvoiceItemResponse quoteResponse =
serializers.deserializeWith(InvoiceItemResponse.serializer, response);
return quoteResponse.data;
}
Future<BuiltList<QuoteEntity>> loadList(
Future<BuiltList<InvoiceEntity>> loadList(
CompanyEntity company, AuthState auth, int updatedAt) async {
String url = auth.url + '/quotes';
@ -35,16 +35,16 @@ class QuoteRepository {
final dynamic response = await webClient.get(url, company.token);
final QuoteListResponse quoteResponse =
serializers.deserializeWith(QuoteListResponse.serializer, response);
final InvoiceListResponse quoteResponse =
serializers.deserializeWith(InvoiceListResponse.serializer, response);
return quoteResponse.data;
}
Future<QuoteEntity> saveData(
CompanyEntity company, AuthState auth, QuoteEntity quote,
Future<InvoiceEntity> saveData(
CompanyEntity company, AuthState auth, InvoiceEntity quote,
[EntityAction action]) async {
final data = serializers.serializeWith(QuoteEntity.serializer, quote);
final data = serializers.serializeWith(InvoiceEntity.serializer, quote);
dynamic response;
if (quote.isNew) {
@ -60,8 +60,8 @@ class QuoteRepository {
response = await webClient.put(url, company.token, json.encode(data));
}
final QuoteItemResponse quoteResponse =
serializers.deserializeWith(QuoteItemResponse.serializer, response);
final InvoiceItemResponse quoteResponse =
serializers.deserializeWith(InvoiceItemResponse.serializer, response);
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/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';
@ -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_middleware.dart';
void main() async {
final prefs = await SharedPreferences.getInstance();
final enableDarkMode = prefs.getBool(kSharedPrefEnableDarkMode);
@ -56,8 +56,7 @@ void main() async {
..addAll(createStoreInvoicesMiddleware())
..addAll(createStorePersistenceMiddleware())
// STARTER: middleware - do not remove comment
..addAll(createStoreQuotesMiddleware())
..addAll(createStoreQuotesMiddleware())
..addAll([
LoggingMiddleware<dynamic>.printer(),
]));
@ -141,13 +140,12 @@ 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(),
QuoteScreen.route: (context) {
widget.store.dispatch(LoadQuotes());
return QuoteScreen();
},
QuoteViewScreen.route: (context) => QuoteViewScreen(),
QuoteEditScreen.route: (context) => QuoteEditScreen(),
SettingsScreen.route: (context) => SettingsScreen(),
},
);

View File

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

View File

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

View File

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

View File

@ -20,22 +20,22 @@ Reducer<int> selectedIdReducer = combineReducers([
(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),
final editingReducer = combineReducers<InvoiceEntity>([
TypedReducer<InvoiceEntity, SaveQuoteSuccess>(_updateEditing),
TypedReducer<InvoiceEntity, AddQuoteSuccess>(_updateEditing),
TypedReducer<InvoiceEntity, RestoreQuoteSuccess>(_updateEditing),
TypedReducer<InvoiceEntity, ArchiveQuoteSuccess>(_updateEditing),
TypedReducer<InvoiceEntity, DeleteQuoteSuccess>(_updateEditing),
TypedReducer<InvoiceEntity, EditQuote>(_updateEditing),
TypedReducer<InvoiceEntity, UpdateQuote>(_updateEditing),
TypedReducer<InvoiceEntity, SelectCompany>(_clearEditing),
]);
QuoteEntity _clearEditing(QuoteEntity quote, dynamic action) {
return QuoteEntity();
InvoiceEntity _clearEditing(InvoiceEntity quote, dynamic action) {
return InvoiceEntity();
}
QuoteEntity _updateEditing(QuoteEntity quote, dynamic action) {
InvoiceEntity _updateEditing(InvoiceEntity quote, dynamic action) {
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/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;
ClientEntity quoteClientSelector(
InvoiceEntity invoice, BuiltMap<int, ClientEntity> clientMap) {
return clientMap[invoice.clientId];
}
var memoizedFilteredQuoteList = memo3((BuiltMap<int, QuoteEntity> quoteMap,
BuiltList<int> quoteList, ListUIState quoteListState) =>
filteredQuotesSelector(quoteMap, quoteList, quoteListState));
var memoizedFilteredQuoteList = memo4(
(BuiltMap<int, InvoiceEntity> invoiceMap,
BuiltList<int> invoiceList,
BuiltMap<int, ClientEntity> clientMap,
ListUIState invoiceListState) =>
filteredQuotesSelector(
invoiceMap, invoiceList, clientMap, invoiceListState));
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)) {
List<int> filteredQuotesSelector(
BuiltMap<int, InvoiceEntity> invoiceMap,
BuiltList<int> invoiceList,
BuiltMap<int, ClientEntity> clientMap,
ListUIState invoiceListState) {
final list = invoiceList.where((invoiceId) {
final invoice = invoiceMap[invoiceId];
final client = clientMap[invoice.clientId];
if (client == null || ! client.isActive) {
return false;
}
if (quoteListState.custom1Filters.isNotEmpty &&
!quoteListState.custom1Filters.contains(quote.customTextValue1)) {
if (!invoice.matchesStates(invoiceListState.stateFilters)) {
return false;
}
if (quoteListState.custom2Filters.isNotEmpty &&
!quoteListState.custom2Filters.contains(quote.customTextValue2)) {
if (!invoice.matchesStatuses(invoiceListState.statusFilters)) {
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();
list.sort((quoteAId, quoteBId) {
final quoteA = quoteMap[quoteAId];
final quoteB = quoteMap[quoteBId];
return quoteA.compareTo(
quoteB, quoteListState.sortField, quoteListState.sortAscending);
list.sort((invoiceAId, invoiceBId) {
return invoiceMap[invoiceAId].compareTo(invoiceMap[invoiceBId],
invoiceListState.sortField, invoiceListState.sortAscending);
});
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_collection/built_collection.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/list_ui_state.dart';
@ -13,7 +13,7 @@ abstract class QuoteState implements Built<QuoteState, QuoteStateBuilder> {
factory QuoteState() {
return _$QuoteState._(
lastUpdated: 0,
map: BuiltMap<int, QuoteEntity>(),
map: BuiltMap<int, InvoiceEntity>(),
list: BuiltList<int>(),
);
}
@ -22,7 +22,7 @@ abstract class QuoteState implements Built<QuoteState, QuoteStateBuilder> {
@nullable
int get lastUpdated;
BuiltMap<int, QuoteEntity> get map;
BuiltMap<int, InvoiceEntity> get map;
BuiltList<int> get list;
bool get isStale {
@ -42,15 +42,19 @@ abstract class QuoteUIState extends Object with EntityUIState implements Built<Q
factory QuoteUIState() {
return _$QuoteUIState._(
listUIState: ListUIState(QuoteFields.quoteNumber),
editing: QuoteEntity(),
listUIState: ListUIState(InvoiceFields.invoiceNumber),
editing: InvoiceEntity(),
selectedId: 0,
);
}
QuoteUIState._();
@nullable
QuoteEntity get editing;
InvoiceEntity get editing;
@nullable
InvoiceItemEntity get editingItem;
@override
bool get isCreatingNew => editing.isNew;

View File

@ -33,7 +33,7 @@ class _$QuoteStateSerializer implements StructuredSerializer<QuoteState> {
'map',
serializers.serialize(object.map,
specifiedType: const FullType(BuiltMap,
const [const FullType(int), const FullType(QuoteEntity)])),
const [const FullType(int), const FullType(InvoiceEntity)])),
'list',
serializers.serialize(object.list,
specifiedType:
@ -68,7 +68,7 @@ class _$QuoteStateSerializer implements StructuredSerializer<QuoteState> {
result.map.replace(serializers.deserialize(value,
specifiedType: const FullType(BuiltMap, const [
const FullType(int),
const FullType(QuoteEntity)
const FullType(InvoiceEntity)
])) as BuiltMap);
break;
case 'list':
@ -105,7 +105,13 @@ class _$QuoteUIStateSerializer implements StructuredSerializer<QuoteUIState> {
result
..add('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;
@ -124,7 +130,12 @@ class _$QuoteUIStateSerializer implements StructuredSerializer<QuoteUIState> {
switch (key) {
case 'editing':
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;
case 'selectedId':
result.selectedId = serializers.deserialize(value,
@ -145,7 +156,7 @@ class _$QuoteState extends QuoteState {
@override
final int lastUpdated;
@override
final BuiltMap<int, QuoteEntity> map;
final BuiltMap<int, InvoiceEntity> map;
@override
final BuiltList<int> list;
@ -196,10 +207,10 @@ class QuoteStateBuilder implements Builder<QuoteState, QuoteStateBuilder> {
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;
MapBuilder<int, InvoiceEntity> _map;
MapBuilder<int, InvoiceEntity> get map =>
_$this._map ??= new MapBuilder<int, InvoiceEntity>();
set map(MapBuilder<int, InvoiceEntity> map) => _$this._map = map;
ListBuilder<int> _list;
ListBuilder<int> get list => _$this._list ??= new ListBuilder<int>();
@ -255,7 +266,9 @@ class QuoteStateBuilder implements Builder<QuoteState, QuoteStateBuilder> {
class _$QuoteUIState extends QuoteUIState {
@override
final QuoteEntity editing;
final InvoiceEntity editing;
@override
final InvoiceItemEntity editingItem;
@override
final int selectedId;
@override
@ -264,7 +277,8 @@ class _$QuoteUIState extends QuoteUIState {
factory _$QuoteUIState([void updates(QuoteUIStateBuilder b)]) =>
(new QuoteUIStateBuilder()..update(updates)).build();
_$QuoteUIState._({this.editing, this.selectedId, this.listUIState})
_$QuoteUIState._(
{this.editing, this.editingItem, this.selectedId, this.listUIState})
: super._() {
if (selectedId == null)
throw new BuiltValueNullFieldError('QuoteUIState', 'selectedId');
@ -284,13 +298,16 @@ class _$QuoteUIState extends QuoteUIState {
if (identical(other, this)) return true;
if (other is! QuoteUIState) return false;
return editing == other.editing &&
editingItem == other.editingItem &&
selectedId == other.selectedId &&
listUIState == other.listUIState;
}
@override
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));
}
@ -298,6 +315,7 @@ class _$QuoteUIState extends QuoteUIState {
String toString() {
return (newBuiltValueToStringHelper('QuoteUIState')
..add('editing', editing)
..add('editingItem', editingItem)
..add('selectedId', selectedId)
..add('listUIState', listUIState))
.toString();
@ -308,10 +326,16 @@ class QuoteUIStateBuilder
implements Builder<QuoteUIState, QuoteUIStateBuilder> {
_$QuoteUIState _$v;
QuoteEntityBuilder _editing;
QuoteEntityBuilder get editing =>
_$this._editing ??= new QuoteEntityBuilder();
set editing(QuoteEntityBuilder editing) => _$this._editing = editing;
InvoiceEntityBuilder _editing;
InvoiceEntityBuilder get editing =>
_$this._editing ??= new InvoiceEntityBuilder();
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 get selectedId => _$this._selectedId;
@ -328,6 +352,7 @@ class QuoteUIStateBuilder
QuoteUIStateBuilder get _$this {
if (_$v != null) {
_editing = _$v.editing?.toBuilder();
_editingItem = _$v.editingItem?.toBuilder();
_selectedId = _$v.selectedId;
_listUIState = _$v.listUIState?.toBuilder();
_$v = null;
@ -353,6 +378,7 @@ class QuoteUIStateBuilder
_$result = _$v ??
new _$QuoteUIState._(
editing: _editing?.build(),
editingItem: _editingItem?.build(),
selectedId: selectedId,
listUIState: listUIState.build());
} catch (_) {
@ -360,6 +386,8 @@ class QuoteUIStateBuilder
try {
_$failedField = 'editing';
_editing?.build();
_$failedField = 'editingItem';
_editingItem?.build();
_$failedField = 'listUIState';
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/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';
abstract class UIState implements Built<UIState, UIStateBuilder> {
factory UIState({bool enableDarkMode}) {
return _$UIState._(
selectedCompanyIndex: 0,
@ -21,26 +20,29 @@ abstract class UIState implements Built<UIState, UIStateBuilder> {
clientUIState: ClientUIState(),
invoiceUIState: InvoiceUIState(),
// STARTER: constructor - do not remove comment
quoteUIState: QuoteUIState(),
quoteUIState: QuoteUIState(),
);
}
UIState._();
int get selectedCompanyIndex;
String get currentRoute;
bool get enableDarkMode;
ProductUIState get productUIState;
ClientUIState get clientUIState;
InvoiceUIState get invoiceUIState;
@nullable
String get filter;
// STARTER: properties - do not remove comment
QuoteUIState get quoteUIState;
QuoteUIState get quoteUIState;
static Serializer<UIState> get serializer => _$uIStateSerializer;
}

View File

@ -1,14 +1,14 @@
import 'package:flutter/foundation.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_items_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/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/refresh_icon_button.dart';
import 'package:flutter/foundation.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_items_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/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/refresh_icon_button.dart';
class InvoiceEdit extends StatefulWidget {
class InvoiceEdit extends StatefulWidget {
final InvoiceEditVM viewModel;
const InvoiceEdit({
@ -18,9 +18,9 @@ class InvoiceEdit extends StatefulWidget {
@override
_InvoiceEditState createState() => _InvoiceEditState();
}
}
class _InvoiceEditState extends State<InvoiceEdit>
class _InvoiceEditState extends State<InvoiceEdit>
with SingleTickerProviderStateMixin {
TabController _controller;
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@ -138,4 +138,4 @@ class _InvoiceEditState extends State<InvoiceEdit>
),
);
}
}
}

View File

@ -30,7 +30,8 @@ class InvoiceListItem extends StatelessWidget {
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final filterMatch = filter != null && filter.isNotEmpty
? (invoice.matchesFilterValue(filter) ?? client.matchesFilterValue(filter))
? (invoice.matchesFilterValue(filter) ??
client.matchesFilterValue(filter))
: null;
return DismissibleEntity(
@ -71,10 +72,15 @@ class InvoiceListItem extends StatelessWidget {
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(
color:
invoice.isPastDue ? Colors.red : InvoiceStatusColors.colors[invoice.invoiceStatusId],
color: invoice.isPastDue
? 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) =>
client == other.client &&
company == other.company &&
invoice == other.invoice &&
invoice == other.quote &&
isSaving == other.isSaving &&
isDirty == other.isDirty;

View File

@ -1,13 +1,17 @@
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';
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/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 {
final QuoteEditVM viewModel;
QuoteEdit({
const QuoteEdit({
Key key,
@required this.viewModel,
}) : super(key: key);
@ -16,52 +20,38 @@ class QuoteEdit extends StatefulWidget {
_QuoteEditState createState() => _QuoteEditState();
}
class _QuoteEditState extends State<QuoteEdit> {
class _QuoteEditState extends State<QuoteEdit>
with SingleTickerProviderStateMixin {
TabController _controller;
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// STARTER: controllers - do not remove comment
var _controllers = [];
static const kDetailsScreen = 0;
static const kItemScreen = 1;
@override
void didChangeDependencies() {
void initState() {
super.initState();
_controllers = [
// STARTER: array - do not remove comment
];
final invoice = widget.viewModel.quote;
final invoiceItem = widget.viewModel.quoteItem;
_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();
final index = invoice.invoiceItems.contains(invoiceItem)
? kItemScreen
: kDetailsScreen;
_controller = TabController(vsync: this, length: 2, initialIndex: index);
}
@override
void dispose() {
_controllers.forEach((controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
_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;
final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final invoice = viewModel.quote;
return WillPopScope(
onWillPop: () async {
@ -70,35 +60,80 @@ class _QuoteEditState extends State<QuoteEdit> {
},
child: Scaffold(
appBar: AppBar(
title: Text(viewModel.quote.isNew
? 'New Quote'
: viewModel.quote.displayName),
title: Text(invoice.isNew
? localization.newQuote
: '${localization.quote} ${viewModel.origQuote.invoiceNumber}'),
actions: <Widget>[
Builder(builder: (BuildContext context) {
return SaveIconButton(
isLoading: viewModel.isLoading,
RefreshIconButton(
icon: Icons.cloud_upload,
tooltip: localization.save,
isVisible: !invoice.isDeleted,
isSaving: widget.viewModel.isSaving,
isDirty: invoice.isNew || invoice != viewModel.origQuote,
onPressed: () {
if (!_formKey.currentState.validate()) {
return;
}
viewModel.onSavePressed(context);
widget.viewModel.onSavePressed(context);
},
);
}),
)
],
bottom: TabBar(
controller: _controller,
//isScrollable: true,
tabs: [
Tab(
text: localization.details,
),
Tab(
text: localization.items,
),
],
),
),
body: Form(
key: _formKey,
child: ListView(
child: TabBarView(
controller: _controller,
children: <Widget>[
FormCard(
children: <Widget>[
// STARTER: widgets - do not remove comment
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/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:invoiceninja_flutter/redux/ui/ui_actions.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: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';
import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
class QuoteEditScreen extends StatelessWidget {
static final String route = '/quote/edit';
QuoteEditScreen({Key key}) : super(key: key);
static const String route = '/quote/edit';
const QuoteEditScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -31,44 +34,61 @@ class QuoteEditScreen extends StatelessWidget {
}
class QuoteEditVM {
final QuoteEntity quote;
final Function(QuoteEntity) onChanged;
final CompanyEntity company;
final InvoiceEntity quote;
final InvoiceItemEntity quoteItem;
final InvoiceEntity origQuote;
final Function(BuildContext) onSavePressed;
final Function(List<InvoiceItemEntity>) onItemsAdded;
final Function onBackPressed;
final bool isLoading;
final bool isSaving;
QuoteEditVM({
@required this.company,
@required this.quote,
@required this.onChanged,
@required this.quoteItem,
@required this.origQuote,
@required this.onSavePressed,
@required this.onItemsAdded,
@required this.onBackPressed,
@required this.isLoading,
@required this.isSaving,
});
factory QuoteEditVM.fromStore(Store<AppState> store) {
final quote = store.state.quoteUIState.selected;
final AppState state = store.state;
final invoice = state.invoiceUIState.editing;
return QuoteEditVM(
isLoading: store.state.isLoading,
quote: quote,
onChanged: (QuoteEntity quote) {
store.dispatch(UpdateQuote(quote));
},
onBackPressed: () {
store.dispatch(UpdateCurrentRoute(QuoteScreen.route));
},
company: state.selectedCompany,
isSaving: state.isSaving,
quote: invoice,
quoteItem: state.invoiceUIState.editingItem,
origQuote: store.state.invoiceState.map[invoice.id],
onBackPressed: () =>
store.dispatch(UpdateCurrentRoute(InvoiceScreen.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)));
final Completer<InvoiceEntity> completer = Completer<InvoiceEntity>();
store.dispatch(
SaveInvoiceRequest(completer: completer, invoice: invoice));
return completer.future.then((savedInvoice) {
if (invoice.isNew) {
Navigator.of(context).pushReplacementNamed(InvoiceViewScreen.route);
} else {
Navigator.of(context).pop(savedInvoice);
}
}).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/material.dart';
import 'package:invoiceninja_flutter/data/models/invoice_model.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/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/utils/localization.dart';
@ -15,63 +17,65 @@ class QuoteList extends StatelessWidget {
@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 {
void _showMenu(
BuildContext context, InvoiceEntity invoice, ClientEntity client) async {
final user = viewModel.user;
final message = await showDialog<String>(
context: context,
builder: (BuildContext context) => SimpleDialog(children: <Widget>[
user.canCreate(EntityType.quote)
user.canCreate(EntityType.invoice)
? ListTile(
leading: Icon(Icons.control_point_duplicate),
title: Text(AppLocalization.of(context).clone),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.clone),
context, invoice, EntityAction.clone),
)
: Container(),
user.canEditEntity(invoice) && !invoice.isPublic
? ListTile(
leading: Icon(Icons.publish),
title: Text(AppLocalization.of(context).markSent),
onTap: () => viewModel.onEntityAction(
context, invoice, EntityAction.markSent),
)
: Container(),
user.canEditEntity(invoice) && client.hasEmailAddress
? ListTile(
leading: Icon(Icons.send),
title: Text(AppLocalization.of(context).email),
onTap: () => viewModel.onEntityAction(
context, invoice, EntityAction.emailInvoice),
)
: Container(),
ListTile(
leading: Icon(Icons.picture_as_pdf),
title: Text(AppLocalization.of(context).pdf),
onTap: () => viewModel.onEntityAction(
context, invoice, EntityAction.pdf),
),
Divider(),
user.canEditEntity(quote) && !quote.isActive
user.canEditEntity(invoice) && !invoice.isActive
? ListTile(
leading: Icon(Icons.restore),
title: Text(AppLocalization.of(context).restore),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.restore),
context, invoice, EntityAction.restore),
)
: Container(),
user.canEditEntity(quote) && quote.isActive
user.canEditEntity(invoice) && invoice.isActive
? ListTile(
leading: Icon(Icons.archive),
title: Text(AppLocalization.of(context).archive),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.archive),
context, invoice, EntityAction.archive),
)
: Container(),
user.canEditEntity(quote) && !quote.isDeleted
user.canEditEntity(invoice) && !invoice.isDeleted
? ListTile(
leading: Icon(Icons.delete),
title: Text(AppLocalization.of(context).delete),
onTap: () => viewModel.onEntityAction(
context, quote, EntityAction.delete),
context, invoice, EntityAction.delete),
)
: Container(),
]));
@ -83,30 +87,97 @@ class QuoteList extends StatelessWidget {
}
}
Widget _buildListView(BuildContext context) {
return RefreshIndicator(
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final listState = viewModel.listState;
final filteredClientId = listState.filterClientId;
final filteredClient =
filteredClientId != null ? viewModel.clientMap[filteredClientId] : null;
return Column(
children: <Widget>[
filteredClient != null
? Material(
color: Colors.orangeAccent,
elevation: 6.0,
child: InkWell(
onTap: () => viewModel.onViewClientFilterPressed(context),
child: Row(
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: ListView.builder(
itemCount: viewModel.quoteList.length,
child: viewModel.invoiceList.isEmpty
? Opacity(
opacity: 0.5,
child: Center(
child: Text(
AppLocalization.of(context).noRecordsFound,
style: TextStyle(
fontSize: 18.0,
),
),
),
)
: ListView.builder(
shrinkWrap: true,
itemCount: viewModel.invoiceList.length,
itemBuilder: (BuildContext context, index) {
final quoteId = viewModel.quoteList[index];
final quote = viewModel.quoteMap[quoteId];
return Column(children: <Widget>[
QuoteListItem(
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,
quote: quote,
client: viewModel.clientMap[quote.clientId],
invoice: invoice,
client: viewModel.clientMap[invoice.clientId],
onDismissed: (DismissDirection direction) =>
viewModel.onDismissed(context, quote, direction),
onTap: () => viewModel.onQuoteTap(context, quote),
onLongPress: () => _showMenu(context, quote),
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 GestureTapCallback onTap;
final GestureTapCallback onLongPress;
final QuoteEntity quote;
final InvoiceEntity invoice;
final ClientEntity client;
final String filter;
@ -21,7 +21,7 @@ class QuoteListItem extends StatelessWidget {
@required this.onDismissed,
@required this.onTap,
@required this.onLongPress,
@required this.quote,
@required this.invoice,
@required this.client,
@required this.filter,
});
@ -30,12 +30,13 @@ class QuoteListItem extends StatelessWidget {
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final filterMatch = filter != null && filter.isNotEmpty
? (quote.matchesFilterValue(filter) ?? client.matchesFilterValue(filter))
? (invoice.matchesFilterValue(filter) ??
client.matchesFilterValue(filter))
: null;
return DismissibleEntity(
user: user,
entity: quote,
entity: invoice,
onDismissed: onDismissed,
child: ListTile(
onTap: onTap,
@ -51,8 +52,8 @@ class QuoteListItem extends StatelessWidget {
),
),
Text(
formatNumber(quote.amount, context,
clientId: quote.clientId),
formatNumber(invoice.amount, context,
clientId: invoice.clientId),
style: Theme.of(context).textTheme.title),
],
),
@ -64,25 +65,29 @@ class QuoteListItem extends StatelessWidget {
children: <Widget>[
Expanded(
child: filterMatch == null
? Text(quote.quoteNumber)
? Text(invoice.invoiceNumber)
: Text(
filterMatch,
maxLines: 3,
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(
color:
quote.isPastDue ? Colors.red : InvoiceStatusColors.colors[quote.quoteStatusId],
color: invoice.isPastDue
? Colors.red
: InvoiceStatusColors.colors[invoice.invoiceStatusId],
)),
],
),
EntityStateLabel(quote),
EntityStateLabel(invoice),
],
),
),
);
}
}

View File

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

View File

@ -53,9 +53,9 @@ class QuoteScreen extends StatelessWidget {
onSelectedCustom2: (value) =>
store.dispatch(FilterQuotesByCustom2(value)),
sortFields: [
QuoteFields.quoteNumber,
QuoteFields.quoteDate,
QuoteFields.updatedAt,
InvoiceFields.invoiceNumber,
InvoiceFields.invoiceDate,
InvoiceFields.updatedAt,
],
onSelectedState: (EntityState state, value) {
store.dispatch(FilterQuotesByState(state));
@ -67,7 +67,7 @@ class QuoteScreen extends StatelessWidget {
backgroundColor: Theme.of(context).primaryColorDark,
onPressed: () {
store.dispatch(
EditQuote(quote: QuoteEntity(), context: context));
EditQuote(quote: InvoiceEntity(), context: context));
},
child: Icon(
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';
class QuoteViewScreen extends StatelessWidget {
static const String route = '/invoice/view';
static const String route = '/quote/view';
const QuoteViewScreen({Key key}) : super(key: key);
@ -38,7 +38,7 @@ class QuoteViewScreen extends StatelessWidget {
class QuoteViewVM {
final CompanyEntity company;
final QuoteEntity quote;
final InvoiceEntity quote;
final ClientEntity client;
final bool isSaving;
final bool isDirty;
@ -80,13 +80,13 @@ class QuoteViewVM {
quote: quote,
client: client,
onEditPressed: (BuildContext context, [InvoiceItemEntity invoiceItem]) {
final Completer<QuoteEntity> completer =
new Completer<QuoteEntity>();
final Completer<InvoiceEntity> completer =
new Completer<InvoiceEntity>();
store.dispatch(EditQuote(
quote: quote,
context: context,
completer: completer,
invoiceItem: invoiceItem));
quoteItem: invoiceItem));
completer.future.then((invoice) {
Scaffold.of(context).showSnackBar(SnackBar(
content: SnackBarRow(
@ -104,39 +104,39 @@ class QuoteViewVM {
final localization = AppLocalization.of(context);
switch (action) {
case EntityAction.pdf:
viewPdf(invoice, context);
viewPdf(quote, context);
break;
case EntityAction.markSent:
store.dispatch(MarkSentQuoteRequest(
snackBarCompleter(context, localization.markedQuoteAsSent),
invoice.id));
quote.id));
break;
case EntityAction.emailQuote:
case EntityAction.emailInvoice:
store.dispatch(ShowEmailQuote(
completer:
snackBarCompleter(context, localization.emailedQuote),
invoice: invoice,
quote: quote,
context: context));
break;
case EntityAction.archive:
store.dispatch(ArchiveQuoteRequest(
popCompleter(context, localization.archivedQuote),
invoice.id));
quote.id));
break;
case EntityAction.delete:
store.dispatch(DeleteQuoteRequest(
popCompleter(context, localization.deletedQuote),
invoice.id));
quote.id));
break;
case EntityAction.restore:
store.dispatch(RestoreQuoteRequest(
snackBarCompleter(context, localization.restoredQuote),
invoice.id));
quote.id));
break;
case EntityAction.clone:
Navigator.of(context).pop();
store.dispatch(
EditQuote(context: context, invoice: invoice.clone));
EditQuote(context: context, quote: quote.clone));
break;
}
});
@ -146,7 +146,7 @@ class QuoteViewVM {
bool operator ==(dynamic other) =>
client == other.client &&
company == other.company &&
invoice == other.invoice &&
quote == other.quote &&
isSaving == other.isSaving &&
isDirty == other.isDirty;
@ -154,7 +154,7 @@ class QuoteViewVM {
int get hashCode =>
client.hashCode ^
company.hashCode ^
invoice.hashCode ^
quote.hashCode ^
isSaving.hashCode ^
isDirty.hashCode;
}

View File

@ -1,8 +1,8 @@
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/stub/edit/stub_edit_vm.dart';
import 'package:flutter_redux_starter/ui/app/save_icon_button.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/stub/edit/stub_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/refresh_icon_button.dart';
class StubEdit extends StatefulWidget {
final StubEditVM viewModel;

View File

@ -2,14 +2,14 @@ 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/stub/stub_screen.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/stub/stub_screen.dart';
import 'package:redux/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/ui/stub/edit/stub_edit.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/redux/stub/stub_actions.dart';
import 'package:invoiceninja_flutter/data/models/stub_model.dart';
import 'package:invoiceninja_flutter/ui/stub/edit/stub_edit.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/icon_message.dart';
class StubEditScreen extends StatelessWidget {
static final String route = '/stub/edit';