572 lines
25 KiB
Dart
572 lines
25 KiB
Dart
// Flutter imports:
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_redux/flutter_redux.dart';
|
|
|
|
// Project imports:
|
|
import 'package:invoiceninja_flutter/constants.dart';
|
|
import 'package:invoiceninja_flutter/data/models/models.dart';
|
|
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
|
|
import 'package:invoiceninja_flutter/redux/client/client_selectors.dart';
|
|
import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart';
|
|
import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart';
|
|
import 'package:invoiceninja_flutter/redux/vendor/vendor_selectors.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/autobill_dropdown_menu_item.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/entity_dropdown.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/client_picker.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/date_picker.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/design_picker.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/discount_field.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/project_picker.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/user_picker.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/forms/vendor_picker.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/invoice/tax_rate_dropdown.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart';
|
|
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_details_vm.dart';
|
|
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_tax_details.dart';
|
|
import 'package:invoiceninja_flutter/utils/completers.dart';
|
|
import 'package:invoiceninja_flutter/utils/formatting.dart';
|
|
import 'package:invoiceninja_flutter/utils/localization.dart';
|
|
|
|
class InvoiceEditDetails extends StatefulWidget {
|
|
const InvoiceEditDetails({
|
|
Key? key,
|
|
required this.viewModel,
|
|
this.entityType = EntityType.invoice,
|
|
}) : super(key: key);
|
|
|
|
final EntityEditDetailsVM viewModel;
|
|
final EntityType entityType;
|
|
|
|
@override
|
|
InvoiceEditDetailsState createState() => InvoiceEditDetailsState();
|
|
}
|
|
|
|
class InvoiceEditDetailsState extends State<InvoiceEditDetails> {
|
|
final _invoiceNumberController = TextEditingController();
|
|
final _poNumberController = TextEditingController();
|
|
final _discountController = TextEditingController();
|
|
final _partialController = TextEditingController();
|
|
final _custom1Controller = TextEditingController();
|
|
final _custom2Controller = TextEditingController();
|
|
final _custom3Controller = TextEditingController();
|
|
final _custom4Controller = TextEditingController();
|
|
final _surcharge1Controller = TextEditingController();
|
|
final _surcharge2Controller = TextEditingController();
|
|
final _surcharge3Controller = TextEditingController();
|
|
final _surcharge4Controller = TextEditingController();
|
|
|
|
List<TextEditingController> _controllers = [];
|
|
final _debouncer = Debouncer();
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
_controllers = [
|
|
_invoiceNumberController,
|
|
_poNumberController,
|
|
_discountController,
|
|
_partialController,
|
|
_custom1Controller,
|
|
_custom2Controller,
|
|
_custom3Controller,
|
|
_custom4Controller,
|
|
_surcharge1Controller,
|
|
_surcharge2Controller,
|
|
_surcharge3Controller,
|
|
_surcharge4Controller,
|
|
];
|
|
|
|
_controllers
|
|
.forEach((dynamic controller) => controller.removeListener(_onChanged));
|
|
|
|
final invoice = widget.viewModel.invoice!;
|
|
_invoiceNumberController.text = invoice.number;
|
|
_poNumberController.text = invoice.poNumber;
|
|
_discountController.text = formatNumber(invoice.discount, context,
|
|
formatNumberType: FormatNumberType.inputMoney)!;
|
|
_partialController.text = formatNumber(invoice.partial, context,
|
|
formatNumberType: FormatNumberType.inputMoney)!;
|
|
_custom1Controller.text = invoice.customValue1;
|
|
_custom2Controller.text = invoice.customValue2;
|
|
_custom3Controller.text = invoice.customValue3;
|
|
_custom4Controller.text = invoice.customValue4;
|
|
_surcharge1Controller.text = formatNumber(invoice.customSurcharge1, context,
|
|
formatNumberType: FormatNumberType.inputMoney)!;
|
|
_surcharge2Controller.text = formatNumber(invoice.customSurcharge2, context,
|
|
formatNumberType: FormatNumberType.inputMoney)!;
|
|
_surcharge3Controller.text = formatNumber(invoice.customSurcharge3, context,
|
|
formatNumberType: FormatNumberType.inputMoney)!;
|
|
_surcharge4Controller.text = formatNumber(invoice.customSurcharge4, context,
|
|
formatNumberType: FormatNumberType.inputMoney)!;
|
|
_controllers
|
|
.forEach((dynamic controller) => controller.addListener(_onChanged));
|
|
|
|
super.didChangeDependencies();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controllers.forEach((controller) {
|
|
controller.removeListener(_onChanged);
|
|
controller.dispose();
|
|
});
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
void _onChanged() {
|
|
final invoice = widget.viewModel.invoice!.rebuild((b) => b
|
|
..number = _invoiceNumberController.text.trim()
|
|
..poNumber = _poNumberController.text.trim()
|
|
..discount = parseDouble(_discountController.text)
|
|
..partial = parseDouble(_partialController.text)
|
|
..customValue1 = _custom1Controller.text.trim()
|
|
..customValue2 = _custom2Controller.text.trim()
|
|
..customValue3 = _custom3Controller.text.trim()
|
|
..customValue4 = _custom4Controller.text.trim()
|
|
..customSurcharge1 = parseDouble(_surcharge1Controller.text)
|
|
..customSurcharge2 = parseDouble(_surcharge2Controller.text)
|
|
..customSurcharge3 = parseDouble(_surcharge3Controller.text)
|
|
..customSurcharge4 = parseDouble(_surcharge4Controller.text));
|
|
if (invoice != widget.viewModel.invoice) {
|
|
_debouncer.run(() {
|
|
widget.viewModel.onChanged!(invoice);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final store = StoreProvider.of<AppState>(context);
|
|
final localization = AppLocalization.of(context)!;
|
|
final viewModel = widget.viewModel;
|
|
final state = viewModel.state!;
|
|
final invoice = viewModel.invoice!;
|
|
final company = viewModel.company!;
|
|
final originalInvoice =
|
|
state.getEntity(invoice.entityType, invoice.id) as InvoiceEntity?;
|
|
|
|
final client = state.clientState.get(invoice.clientId);
|
|
final settings = getClientSettings(state, client);
|
|
final terms = widget.entityType == EntityType.quote
|
|
? settings.defaultValidUntil
|
|
: settings.defaultPaymentTerms;
|
|
String? termsString;
|
|
if ((terms ?? '').isNotEmpty) {
|
|
termsString = '${localization.net} $terms';
|
|
}
|
|
|
|
return ScrollableListView(
|
|
children: <Widget>[
|
|
FormCard(
|
|
isLast: !company.calculateTaxes,
|
|
children: <Widget>[
|
|
invoice.isNew
|
|
? invoice.isPurchaseOrder
|
|
? VendorPicker(
|
|
autofocus: true,
|
|
vendorId: invoice.vendorId,
|
|
vendorState: state.vendorState,
|
|
onSelected: (vendor) {
|
|
viewModel.onVendorChanged!(
|
|
context, invoice, vendor as VendorEntity?);
|
|
},
|
|
onAddPressed: (completer) =>
|
|
viewModel.onAddVendorPressed!(context, completer),
|
|
)
|
|
: ClientPicker(
|
|
clientId: invoice.clientId,
|
|
clientState: state.clientState,
|
|
onSelected: (client) => viewModel.onClientChanged!(
|
|
context, invoice, client as ClientEntity?),
|
|
onAddPressed: (completer) =>
|
|
viewModel.onAddClientPressed!(context, completer),
|
|
)
|
|
: DecoratedFormField(
|
|
controller: _invoiceNumberController,
|
|
label: widget.entityType == EntityType.purchaseOrder
|
|
? localization.poNumber
|
|
: widget.entityType == EntityType.credit
|
|
? localization.creditNumber
|
|
: widget.entityType == EntityType.quote
|
|
? localization.quoteNumber
|
|
: localization.invoiceNumber,
|
|
keyboardType: TextInputType.text,
|
|
validator: (String val) => val.trim().isEmpty &&
|
|
invoice.isOld &&
|
|
originalInvoice!.number.isNotEmpty
|
|
? AppLocalization.of(context)!
|
|
.pleaseEnterAnInvoiceNumber
|
|
: null,
|
|
),
|
|
UserPicker(
|
|
userId: invoice.assignedUserId,
|
|
onChanged: (userId) => viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..assignedUserId = userId)),
|
|
),
|
|
if (invoice.isRecurringInvoice) ...[
|
|
AppDropdownButton<String>(
|
|
labelText: localization.frequency,
|
|
value: invoice.frequencyId,
|
|
onChanged: (dynamic value) {
|
|
viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..frequencyId = value));
|
|
},
|
|
items: kFrequencies.entries
|
|
.map((entry) => DropdownMenuItem(
|
|
value: entry.key,
|
|
child: Text(localization.lookup(entry.value)),
|
|
))
|
|
.toList()),
|
|
DatePicker(
|
|
labelText: invoice.lastSentDate.isNotEmpty
|
|
? localization.nextSendDate
|
|
: localization.startDate,
|
|
onSelected: (date, _) => viewModel
|
|
.onChanged!(invoice.rebuild((b) => b..nextSendDate = date)),
|
|
selectedDate: invoice.nextSendDate,
|
|
firstDate: DateTime.now(),
|
|
),
|
|
AppDropdownButton<int>(
|
|
labelText: localization.remainingCycles,
|
|
value: invoice.remainingCycles,
|
|
blankValue: null,
|
|
onChanged: (dynamic value) => viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..remainingCycles = value)),
|
|
items: [
|
|
DropdownMenuItem(
|
|
child: Text(localization.endless),
|
|
value: -1,
|
|
),
|
|
...List<int>.generate(37, (i) => i)
|
|
.map((value) => DropdownMenuItem(
|
|
child: Text('$value'),
|
|
value: value,
|
|
))
|
|
.toList()
|
|
],
|
|
),
|
|
AppDropdownButton<String>(
|
|
labelText: localization.dueDate,
|
|
value: invoice.dueDateDays ?? '',
|
|
onChanged: (dynamic value) => viewModel
|
|
.onChanged!(invoice.rebuild((b) => b..dueDateDays = value)),
|
|
items: [
|
|
DropdownMenuItem(
|
|
child: Text(localization.usePaymentTerms),
|
|
value: 'terms',
|
|
),
|
|
DropdownMenuItem(
|
|
child: Text(localization.dueOnReceipt),
|
|
value: 'on_receipt',
|
|
),
|
|
...List<int>.generate(31, (i) => i + 1)
|
|
.map((value) => DropdownMenuItem(
|
|
child: Text(value == 1
|
|
? localization.firstDayOfTheMonth
|
|
: value == 31
|
|
? localization.lastDayOfTheMonth
|
|
: localization.dayCount
|
|
.replaceFirst(':count', '$value')),
|
|
value: '$value',
|
|
))
|
|
.toList()
|
|
],
|
|
),
|
|
] else ...[
|
|
DatePicker(
|
|
validator: (String val) => val.trim().isEmpty
|
|
? AppLocalization.of(context)!.pleaseSelectADate
|
|
: null,
|
|
labelText: widget.entityType == EntityType.purchaseOrder
|
|
? localization.purchaseOrderDate
|
|
: widget.entityType == EntityType.credit
|
|
? localization.creditDate
|
|
: widget.entityType == EntityType.quote
|
|
? localization.quoteDate
|
|
: localization.invoiceDate,
|
|
selectedDate: invoice.date,
|
|
onSelected: (date, _) {
|
|
viewModel.onChanged!(invoice.rebuild((b) => b..date = date));
|
|
},
|
|
),
|
|
DatePicker(
|
|
labelText: widget.entityType == EntityType.invoice ||
|
|
widget.entityType == EntityType.purchaseOrder
|
|
? localization.dueDate
|
|
: localization.validUntil,
|
|
selectedDate: invoice.dueDate,
|
|
message: termsString,
|
|
onSelected: (date, _) {
|
|
viewModel
|
|
.onChanged!(invoice.rebuild((b) => b..dueDate = date));
|
|
},
|
|
),
|
|
DecoratedFormField(
|
|
label: localization.partialDeposit,
|
|
controller: _partialController,
|
|
keyboardType: TextInputType.numberWithOptions(
|
|
decimal: true, signed: true),
|
|
validator: (String value) {
|
|
final amount = parseDouble(_partialController.text)!;
|
|
final total = invoice.calculateTotal(
|
|
precision: precisionForInvoice(state, invoice));
|
|
if (amount < 0 || (amount != 0 && amount > total)) {
|
|
return localization.partialValue;
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
),
|
|
if (invoice.partial > 0)
|
|
DatePicker(
|
|
labelText: localization.partialDueDate,
|
|
selectedDate: invoice.partialDueDate,
|
|
onSelected: (date, _) {
|
|
viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..partialDueDate = date));
|
|
},
|
|
),
|
|
],
|
|
if (!invoice.isPurchaseOrder)
|
|
DecoratedFormField(
|
|
label: localization.poNumber,
|
|
controller: _poNumberController,
|
|
keyboardType: TextInputType.text,
|
|
),
|
|
DiscountField(
|
|
controller: _discountController,
|
|
value: invoice.discount,
|
|
isAmountDiscount: invoice.isAmountDiscount,
|
|
onTypeChanged: (value) => viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..isAmountDiscount = value)),
|
|
),
|
|
if (invoice.isRecurringInvoice)
|
|
AppDropdownButton<String>(
|
|
labelText: localization.autoBill,
|
|
value: invoice.autoBill,
|
|
selectedItemBuilder: (invoice.autoBill ?? '').isEmpty
|
|
? null
|
|
: (context) => [
|
|
SettingsEntity.AUTO_BILL_ALWAYS,
|
|
SettingsEntity.AUTO_BILL_OPT_OUT,
|
|
SettingsEntity.AUTO_BILL_OPT_IN,
|
|
SettingsEntity.AUTO_BILL_OFF,
|
|
]
|
|
.map((type) => Text(localization.lookup(type)))
|
|
.toList(),
|
|
onChanged: (dynamic value) => viewModel
|
|
.onChanged!(invoice.rebuild((b) => b..autoBill = value)),
|
|
items: [
|
|
SettingsEntity.AUTO_BILL_ALWAYS,
|
|
SettingsEntity.AUTO_BILL_OPT_OUT,
|
|
SettingsEntity.AUTO_BILL_OPT_IN,
|
|
SettingsEntity.AUTO_BILL_OFF,
|
|
]
|
|
.map((value) => DropdownMenuItem(
|
|
child: AutobillDropdownMenuItem(type: value),
|
|
value: value,
|
|
))
|
|
.toList()),
|
|
CustomField(
|
|
controller: _custom1Controller,
|
|
field: CustomFieldType.invoice1,
|
|
value: invoice.customValue1,
|
|
),
|
|
CustomField(
|
|
controller: _custom2Controller,
|
|
field: CustomFieldType.invoice2,
|
|
value: invoice.customValue2,
|
|
),
|
|
CustomField(
|
|
controller: _custom3Controller,
|
|
field: CustomFieldType.invoice3,
|
|
value: invoice.customValue3,
|
|
),
|
|
CustomField(
|
|
controller: _custom4Controller,
|
|
field: CustomFieldType.invoice4,
|
|
value: invoice.customValue4,
|
|
),
|
|
if (company.hasCustomField(CustomFieldType.surcharge1))
|
|
DecoratedFormField(
|
|
label: company.getCustomFieldLabel(CustomFieldType.surcharge1),
|
|
controller: _surcharge1Controller,
|
|
keyboardType: TextInputType.numberWithOptions(
|
|
decimal: true, signed: true),
|
|
),
|
|
if (company.hasCustomField(CustomFieldType.surcharge2))
|
|
DecoratedFormField(
|
|
controller: _surcharge2Controller,
|
|
label: company.getCustomFieldLabel(CustomFieldType.surcharge2),
|
|
keyboardType: TextInputType.numberWithOptions(
|
|
decimal: true, signed: true),
|
|
),
|
|
if (company.hasCustomField(CustomFieldType.surcharge3))
|
|
DecoratedFormField(
|
|
label: company.getCustomFieldLabel(CustomFieldType.surcharge3),
|
|
controller: _surcharge3Controller,
|
|
keyboardType: TextInputType.numberWithOptions(
|
|
decimal: true, signed: true),
|
|
),
|
|
if (company.hasCustomField(CustomFieldType.surcharge4))
|
|
DecoratedFormField(
|
|
controller: _surcharge4Controller,
|
|
label: company.getCustomFieldLabel(CustomFieldType.surcharge4),
|
|
keyboardType: TextInputType.numberWithOptions(
|
|
decimal: true, signed: true),
|
|
),
|
|
if (company.enableFirstInvoiceTaxRate ||
|
|
invoice.taxName1.isNotEmpty)
|
|
TaxRateDropdown(
|
|
onSelected: (taxRate) =>
|
|
viewModel.onChanged!(invoice.applyTax(taxRate)),
|
|
labelText: localization.tax +
|
|
(company.settings.enableInclusiveTaxes!
|
|
? ' - ${localization.inclusive}'
|
|
: ''),
|
|
initialTaxName: invoice.taxName1,
|
|
initialTaxRate: invoice.taxRate1,
|
|
),
|
|
if (company.enableSecondInvoiceTaxRate ||
|
|
invoice.taxName2.isNotEmpty)
|
|
TaxRateDropdown(
|
|
onSelected: (taxRate) => viewModel
|
|
.onChanged!(invoice.applyTax(taxRate, isSecond: true)),
|
|
labelText: localization.tax +
|
|
(company.settings.enableInclusiveTaxes!
|
|
? ' - ${localization.inclusive}'
|
|
: ''),
|
|
initialTaxName: invoice.taxName2,
|
|
initialTaxRate: invoice.taxRate2,
|
|
),
|
|
if (company.enableThirdInvoiceTaxRate ||
|
|
invoice.taxName3.isNotEmpty)
|
|
TaxRateDropdown(
|
|
onSelected: (taxRate) => viewModel
|
|
.onChanged!(invoice.applyTax(taxRate, isThird: true)),
|
|
labelText: localization.tax +
|
|
(company.settings.enableInclusiveTaxes!
|
|
? ' - ${localization.inclusive}'
|
|
: ''),
|
|
initialTaxName: invoice.taxName3,
|
|
initialTaxRate: invoice.taxRate3,
|
|
),
|
|
DesignPicker(
|
|
initialValue: invoice.designId,
|
|
onSelected: (value) => viewModel
|
|
.onChanged!(invoice.rebuild((b) => b..designId = value!.id)),
|
|
),
|
|
if (company.isModuleEnabled(EntityType.project))
|
|
ProjectPicker(
|
|
clientId: invoice.clientId,
|
|
projectId: invoice.projectId,
|
|
onChanged: (projectId) {
|
|
final project = state.projectState.get(projectId);
|
|
final client = state.clientState.get(project.clientId);
|
|
|
|
if (project.isOld && project.clientId != invoice.clientId) {
|
|
viewModel.onClientChanged!(
|
|
context,
|
|
invoice.rebuild((b) => b..projectId = projectId),
|
|
client,
|
|
);
|
|
} else {
|
|
viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..projectId = projectId));
|
|
}
|
|
},
|
|
),
|
|
if (invoice.isPurchaseOrder)
|
|
ClientPicker(
|
|
clientId: invoice.clientId,
|
|
clientState: state.clientState,
|
|
onSelected: (client) {
|
|
viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..clientId = client?.id ?? ''));
|
|
},
|
|
)
|
|
else if (company.isModuleEnabled(EntityType.vendor))
|
|
EntityDropdown(
|
|
entityType: EntityType.vendor,
|
|
entityId: invoice.vendorId,
|
|
labelText: localization.vendor,
|
|
entityList: memoizedDropdownVendorList(
|
|
state.vendorState.map,
|
|
state.vendorState.list,
|
|
state.userState.map,
|
|
state.staticState),
|
|
onSelected: (vendor) => viewModel.onChanged!(
|
|
invoice.rebuild((b) => b.vendorId = vendor?.id ?? ''),
|
|
),
|
|
onCreateNew: (completer, name) {
|
|
store.dispatch(SaveVendorRequest(
|
|
vendor: VendorEntity().rebuild((b) => b..name = name),
|
|
completer: completer));
|
|
},
|
|
),
|
|
DecoratedFormField(
|
|
key: ValueKey('__exchange_rate_${invoice.clientId}__'),
|
|
label: localization.exchangeRate,
|
|
initialValue: formatNumber(invoice.exchangeRate, context,
|
|
formatNumberType: FormatNumberType.inputAmount),
|
|
onChanged: (value) => viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..exchangeRate = parseDouble(value))),
|
|
keyboardType:
|
|
TextInputType.numberWithOptions(decimal: true, signed: true),
|
|
),
|
|
if (company.hasTaxes)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: SwitchListTile(
|
|
activeColor: Theme.of(context).colorScheme.secondary,
|
|
title: Text(localization.inclusiveTaxes),
|
|
dense: true,
|
|
value: invoice.usesInclusiveTaxes,
|
|
onChanged: (value) {
|
|
viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..usesInclusiveTaxes = value));
|
|
},
|
|
),
|
|
),
|
|
if (invoice.isInvoice)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: SwitchListTile(
|
|
activeColor: Theme.of(context).colorScheme.secondary,
|
|
title: Text(localization.autoBillEnabled),
|
|
dense: true,
|
|
value: invoice.autoBillEnabled,
|
|
onChanged: (value) {
|
|
viewModel.onChanged!(
|
|
invoice.rebuild((b) => b..autoBillEnabled = value));
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (company.calculateTaxes)
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 20),
|
|
child: AppButton(
|
|
label: localization.taxDetails.toUpperCase(),
|
|
onPressed: invoice.hasClient
|
|
? () {
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (context) => InvoiceTaxDetails(
|
|
invoice: invoice,
|
|
));
|
|
}
|
|
: null,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|