invoice/lib/ui/settings/company_details.dart

536 lines
22 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/redux/payment_term/payment_term_selectors.dart';
import 'package:invoiceninja_flutter/redux/static/static_selectors.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/app_form.dart';
import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart';
import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart';
import 'package:invoiceninja_flutter/ui/app/resources/cached_image.dart';
import 'package:invoiceninja_flutter/ui/settings/company_details_vm.dart';
import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart';
import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/dialogs.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/utils/web_stub.dart'
if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart';
class CompanyDetails extends StatefulWidget {
const CompanyDetails({
Key key,
@required this.viewModel,
}) : super(key: key);
final CompanyDetailsVM viewModel;
@override
_CompanyDetailsState createState() => _CompanyDetailsState();
}
class _CompanyDetailsState extends State<CompanyDetails>
with SingleTickerProviderStateMixin {
static final GlobalKey<FormState> _formKey =
GlobalKey<FormState>(debugLabel: '_companyDetails');
final FocusScopeNode _focusNode = FocusScopeNode();
TabController _controller;
bool autoValidate = false;
final _debouncer = Debouncer();
final _nameController = TextEditingController();
final _idNumberController = TextEditingController();
final _vatNumberController = TextEditingController();
final _emailController = TextEditingController();
final _websiteController = TextEditingController();
final _phoneController = TextEditingController();
final _address1Controller = TextEditingController();
final _address2Controller = TextEditingController();
final _cityController = TextEditingController();
final _stateController = TextEditingController();
final _postalCodeController = TextEditingController();
final _taskRateController = TextEditingController();
final _custom1Controller = TextEditingController();
final _custom2Controller = TextEditingController();
final _custom3Controller = TextEditingController();
final _custom4Controller = TextEditingController();
final _invoiceTermsController = TextEditingController();
final _invoiceFooterController = TextEditingController();
final _quoteTermsController = TextEditingController();
final _quoteFooterController = TextEditingController();
final _creditTermsController = TextEditingController();
final _creditFooterController = TextEditingController();
List<TextEditingController> _controllers = [];
@override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 4);
}
@override
void dispose() {
_controller.dispose();
_controllers.forEach((dynamic controller) {
controller.removeListener(_onSettingsChanged);
controller.dispose();
});
super.dispose();
}
@override
void didChangeDependencies() {
_controllers = [
_nameController,
_idNumberController,
_vatNumberController,
_emailController,
_websiteController,
_phoneController,
_address1Controller,
_address2Controller,
_cityController,
_stateController,
_postalCodeController,
_taskRateController,
_custom1Controller,
_custom2Controller,
_custom3Controller,
_custom4Controller,
_invoiceFooterController,
_invoiceTermsController,
_quoteFooterController,
_quoteTermsController,
_creditFooterController,
_creditTermsController,
];
_controllers.forEach(
(dynamic controller) => controller.removeListener(_onSettingsChanged));
final viewModel = widget.viewModel;
final settings = viewModel.settings;
_nameController.text = settings.name;
_idNumberController.text = settings.idNumber;
_vatNumberController.text = settings.vatNumber;
_emailController.text = settings.email;
_websiteController.text = settings.website;
_phoneController.text = settings.phone;
_address1Controller.text = settings.address1;
_address2Controller.text = settings.address2;
_cityController.text = settings.city;
_stateController.text = settings.state;
_postalCodeController.text = settings.postalCode;
_taskRateController.text = formatNumber(settings.defaultTaskRate, context,
formatNumberType: FormatNumberType.input);
_custom1Controller.text = settings.customValue1;
_custom2Controller.text = settings.customValue2;
_custom3Controller.text = settings.customValue3;
_custom4Controller.text = settings.customValue4;
_invoiceTermsController.text = settings.defaultInvoiceTerms;
_invoiceFooterController.text = settings.defaultInvoiceFooter;
_quoteTermsController.text = settings.defaultQuoteTerms;
_quoteFooterController.text = settings.defaultQuoteFooter;
_creditFooterController.text = settings.defaultCreditFooter;
_creditTermsController.text = settings.defaultCreditTerms;
_controllers.forEach(
(dynamic controller) => controller.addListener(_onSettingsChanged));
super.didChangeDependencies();
}
void _onSettingsChanged() {
_debouncer.run(() {
final settings = widget.viewModel.settings.rebuild((b) => b
..name = _nameController.text.trim()
..idNumber = _idNumberController.text.trim()
..vatNumber = _vatNumberController.text.trim()
..phone = _phoneController.text.trim()
..email = _emailController.text.trim()
..website = _websiteController.text.trim()
..address1 = _address1Controller.text.trim()
..address2 = _address2Controller.text.trim()
..city = _cityController.text.trim()
..state = _stateController.text.trim()
..postalCode = _postalCodeController.text.trim()
..defaultTaskRate =
parseDouble(_taskRateController.text, zeroIsNull: true)
..customValue1 = _custom1Controller.text.trim()
..customValue2 = _custom2Controller.text.trim()
..customValue3 = _custom3Controller.text.trim()
..customValue4 = _custom4Controller.text.trim()
..defaultInvoiceFooter = _invoiceFooterController.text.trim()
..defaultInvoiceTerms = _invoiceTermsController.text.trim()
..defaultQuoteFooter = _quoteFooterController.text.trim()
..defaultQuoteTerms = _quoteTermsController.text.trim()
..defaultCreditFooter = _creditFooterController.text.trim()
..defaultCreditTerms = _creditTermsController.text.trim());
if (settings != widget.viewModel.settings) {
widget.viewModel.onSettingsChanged(settings);
}
});
}
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final state = viewModel.state;
final company = viewModel.company;
final settings = viewModel.settings;
return EditScaffold(
title: localization.companyDetails,
onSavePressed: viewModel.onSavePressed,
appBarBottom: TabBar(
key: ValueKey(state.settingsUIState.updatedAt),
controller: _controller,
tabs: [
Tab(
text: localization.details,
),
Tab(
text: localization.logo,
),
Tab(
text: localization.address,
),
Tab(
text: localization.defaults,
),
],
),
body: AppTabForm(
focusNode: _focusNode,
formKey: _formKey,
tabController: _controller,
children: <Widget>[
ListView(
children: <Widget>[
FormCard(
children: <Widget>[
DecoratedFormField(
label: localization.name,
controller: _nameController,
validator: (val) => val.isEmpty || val.trim().isEmpty
? localization.pleaseEnterAName
: null,
autovalidate: autoValidate,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
DecoratedFormField(
label: localization.idNumber,
controller: _idNumberController,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
DecoratedFormField(
label: localization.vatNumber,
controller: _vatNumberController,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
DecoratedFormField(
label: localization.website,
controller: _websiteController,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
DecoratedFormField(
label: localization.email,
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
DecoratedFormField(
label: localization.phone,
controller: _phoneController,
keyboardType: TextInputType.phone,
//textInputAction: TextInputAction.next,
//onFieldSubmitted: (String value) => _node.nextFocus(),
),
CustomField(
controller: _custom1Controller,
field: company.customFields[CustomFieldType.company1],
value: settings.customValue1,
),
CustomField(
controller: _custom2Controller,
field: company.customFields[CustomFieldType.company2],
value: settings.customValue2,
),
CustomField(
controller: _custom3Controller,
field: company.customFields[CustomFieldType.company3],
value: settings.customValue3,
),
CustomField(
controller: _custom4Controller,
field: company.customFields[CustomFieldType.company4],
value: settings.customValue4,
),
],
),
if (!state.settingsUIState.isFiltered)
FormCard(
children: <Widget>[
AppDropdownButton(
showUseDefault: true,
value: company.sizeId,
labelText: localization.size,
items: memoizedSizeList(state.staticState.sizeMap)
.map((sizeId) => DropdownMenuItem(
child: Text(
state.staticState.sizeMap[sizeId].name),
value: sizeId,
))
.toList(),
onChanged: (dynamic sizeId) => viewModel.onCompanyChanged(
company.rebuild((b) => b..sizeId = sizeId),
),
showBlank: true,
),
EntityDropdown(
key: ValueKey('__industry_${company.industryId}__'),
entityType: EntityType.industry,
entityList:
memoizedIndustryList(state.staticState.industryMap),
labelText: localization.industry,
entityId: company.industryId,
onSelected: (SelectableEntity industry) =>
viewModel.onCompanyChanged(
company.rebuild((b) => b..industryId = industry?.id),
),
allowClearing: true,
),
],
),
],
),
Padding(
padding: const EdgeInsets.all(30),
child: Column(
children: <Widget>[
Builder(
builder: (context) {
return Row(
children: <Widget>[
if ('${settings.companyLogo ?? ''}'.isNotEmpty) ...[
Expanded(
child: AppButton(
width: double.infinity,
color: Colors.redAccent,
label: localization.delete,
iconData: Icons.delete,
onPressed: () {
confirmCallback(
context: context,
callback: () =>
viewModel.onDeleteLogo(context));
},
),
),
SizedBox(width: 20),
],
Expanded(
child: AppButton(
width: double.infinity,
label: localization.uploadLogo,
iconData: Icons.cloud_upload,
onPressed: () async {
String path;
if (kIsWeb) {
path = await webFilePicker();
} else {
final image = await ImagePicker().getImage(
source: kReleaseMode
? ImageSource.gallery
: ImageSource.camera);
if (image != null) {
path = image.path;
}
}
if (path != null) {
viewModel.onUploadLogo(context, path);
}
},
),
)
],
);
},
),
if ('${settings.companyLogo ?? ''}'.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: CachedImage(
width: double.infinity,
url: settings.companyLogo,
//url: '${settings.logoUrl}?clear_cache=${state.selectedCompany.updatedAt}',
)),
],
),
),
ListView(
children: <Widget>[
FormCard(
children: <Widget>[
DecoratedFormField(
label: localization.address1,
controller: _address1Controller,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
DecoratedFormField(
label: localization.address2,
controller: _address2Controller,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
DecoratedFormField(
label: localization.city,
controller: _cityController,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
DecoratedFormField(
label: localization.state,
controller: _stateController,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
DecoratedFormField(
label: localization.postalCode,
controller: _postalCodeController,
textInputAction: TextInputAction.next,
onFieldSubmitted: (String value) => _focusNode.nextFocus(),
),
EntityDropdown(
allowClearing: state.settingsUIState.isFiltered,
key: ValueKey('__country_${settings.countryId}__'),
entityType: EntityType.country,
entityList:
memoizedCountryList(state.staticState.countryMap),
labelText: localization.country,
entityId: settings.countryId,
onSelected: (SelectableEntity country) =>
viewModel.onSettingsChanged(settings
.rebuild((b) => b..countryId = country?.id)),
),
],
)
],
),
ListView(
children: <Widget>[
FormCard(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
EntityDropdown(
key: ValueKey(
'__payment_type_${settings.defaultPaymentTypeId}__'),
entityType: EntityType.paymentType,
entityList: memoizedPaymentTypeList(
state.staticState.paymentTypeMap),
labelText: localization.paymentType,
entityId: settings.defaultPaymentTypeId,
onSelected: (paymentType) => viewModel.onSettingsChanged(
settings.rebuild(
(b) => b..defaultPaymentTypeId = paymentType?.id)),
allowClearing: true,
),
AppDropdownButton<String>(
showUseDefault: true,
showBlank: true,
labelText: localization.paymentTerm,
items: memoizedDropdownPaymentTermList(
state.paymentTermState.map,
state.paymentTermState.list)
.map((paymentTermId) {
final paymentTerm =
state.paymentTermState.map[paymentTermId];
return DropdownMenuItem<String>(
child: Text(paymentTerm.name),
value: paymentTerm.numDays.toString(),
);
}).toList(),
value: '${settings.defaultPaymentTerms}',
onChanged: (dynamic numDays) {
print('## onChanged: $numDays');
viewModel.onSettingsChanged(settings.rebuild((b) => b
..defaultPaymentTerms =
numDays == null ? null : '$numDays'));
},
),
if (!state.uiState.settingsUIState.isFiltered)
Padding(
padding: const EdgeInsets.only(top: 25, bottom: 10),
child: AppButton(
iconData: Icons.settings,
label: localization.configurePaymentTerms.toUpperCase(),
onPressed: () =>
viewModel.onConfigurePaymentTermsPressed(context),
),
),
/* TODO Re-enable with tasks
DecoratedFormField(
label: localization.taskRate,
controller: _taskRateController,
keyboardType:
TextInputType.numberWithOptions(decimal: true),
),
*/
],
),
FormCard(
children: <Widget>[
DecoratedFormField(
label: localization.invoiceTerms,
controller: _invoiceTermsController,
maxLines: 4,
),
DecoratedFormField(
label: localization.invoiceFooter,
controller: _invoiceFooterController,
maxLines: 4,
),
DecoratedFormField(
label: localization.quoteTerms,
controller: _quoteTermsController,
maxLines: 4,
),
DecoratedFormField(
label: localization.quoteFooter,
controller: _quoteFooterController,
maxLines: 4,
),
DecoratedFormField(
label: localization.creditTerms,
controller: _creditTermsController,
maxLines: 4,
),
DecoratedFormField(
label: localization.creditFooter,
controller: _creditFooterController,
maxLines: 4,
),
],
)
],
),
],
),
);
}
}