760 lines
22 KiB
Dart
760 lines
22 KiB
Dart
// Flutter imports:
|
|
import 'package:flutter/material.dart';
|
|
|
|
// Package imports:
|
|
import 'package:flutter_redux/flutter_redux.dart';
|
|
import 'package:invoiceninja_flutter/redux/company/company_selectors.dart';
|
|
import 'package:invoiceninja_flutter/utils/colors.dart';
|
|
import 'package:invoiceninja_flutter/utils/formatting.dart';
|
|
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
|
import 'package:timeago/timeago.dart' as timeago;
|
|
|
|
// Project imports:
|
|
import 'package:invoiceninja_flutter/constants.dart';
|
|
import 'package:invoiceninja_flutter/data/models/entities.dart';
|
|
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/lists/list_filter.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/lists/selected_indicator.dart';
|
|
import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart';
|
|
import 'package:invoiceninja_flutter/ui/settings/settings_list_vm.dart';
|
|
import 'package:invoiceninja_flutter/utils/icons.dart';
|
|
import 'package:invoiceninja_flutter/utils/localization.dart';
|
|
import 'package:invoiceninja_flutter/utils/platforms.dart';
|
|
|
|
class SettingsList extends StatefulWidget {
|
|
const SettingsList({
|
|
Key? key,
|
|
required this.viewModel,
|
|
}) : super(key: key);
|
|
|
|
final SettingsListVM viewModel;
|
|
|
|
@override
|
|
_SettingsListState createState() => _SettingsListState();
|
|
}
|
|
|
|
class _SettingsListState extends State<SettingsList> {
|
|
ScrollController? _scrollController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_scrollController = ScrollController();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController!.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final localization = AppLocalization.of(context);
|
|
final state = widget.viewModel.state;
|
|
final settingsUIState = state.uiState.settingsUIState;
|
|
final showAll = settingsUIState.entityType == EntityType.company;
|
|
|
|
if (state.credentials.token!.isEmpty) {
|
|
return SizedBox();
|
|
}
|
|
|
|
if (!state.userCompany.isAdmin)
|
|
return Stack(
|
|
children: [
|
|
ScrollableListView(
|
|
children: <Widget>[
|
|
SettingsListTile(
|
|
section: kSettingsUserDetails,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsDeviceSettings,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
],
|
|
),
|
|
if (state.isLoading) LinearProgressIndicator(),
|
|
],
|
|
);
|
|
else if (settingsUIState.filter != null || settingsUIState.showNewSettings)
|
|
return SettingsSearch(
|
|
viewModel: widget.viewModel,
|
|
filter: settingsUIState.filter,
|
|
);
|
|
|
|
return Stack(
|
|
children: [
|
|
ScrollableListView(
|
|
scrollController: _scrollController,
|
|
children: <Widget>[
|
|
if (settingsUIState.isFiltered)
|
|
Container(
|
|
color: Colors.orangeAccent,
|
|
child: ListFilterMessage(
|
|
filterEntityType: settingsUIState.entityType,
|
|
filterEntityId: settingsUIState.entityType == EntityType.group
|
|
? settingsUIState.group.id
|
|
: settingsUIState.client.id,
|
|
onPressed: settingsUIState.entityType == EntityType.client
|
|
? widget.viewModel.onViewClientPressed
|
|
: widget.viewModel.onViewGroupPressed,
|
|
onClearPressed: widget.viewModel.onClearSettingsFilterPressed,
|
|
isSettings: true,
|
|
),
|
|
),
|
|
Container(
|
|
color: Theme.of(context).colorScheme.background,
|
|
padding: const EdgeInsets.only(left: 19, top: 16, bottom: 16),
|
|
child: Text(
|
|
localization!.basicSettings,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsCompanyDetails,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsUserDetails,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsLocalization,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsPaymentSettings,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsTaxSettings,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsProducts,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (state.company.isModuleEnabled(EntityType.task))
|
|
SettingsListTile(
|
|
section: kSettingsTasks,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (showAll && state.company.isModuleEnabled(EntityType.expense))
|
|
SettingsListTile(
|
|
section: kSettingsExpenses,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsWorkflowSettings,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
// TODO Re-entable
|
|
/*
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsIntegrations,
|
|
viewModel: viewModel,
|
|
),
|
|
*/
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsImportExport,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsDeviceSettings,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (showAll && state.userCompany.isAdmin)
|
|
SettingsListTile(
|
|
section: kSettingsAccountManagement,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
Container(
|
|
color: Theme.of(context).colorScheme.background,
|
|
padding: const EdgeInsets.only(left: 19, top: 16, bottom: 16),
|
|
child: Text(
|
|
localization.advancedSettings,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
),
|
|
|
|
SettingsListTile(
|
|
section: kSettingsInvoiceDesign,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsCustomFields,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsGeneratedNumbers,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsClientPortal,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsEmailSettings,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsTemplatesAndReminders,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsBankAccounts,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsGroupSettings,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsPaymentLinks,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
/*
|
|
if (showAll)
|
|
SettingsListTile(
|
|
section: kSettingsBuyNowButtons,
|
|
viewModel: viewModel,
|
|
),
|
|
*/
|
|
/*
|
|
SettingsListTile(
|
|
section: kSettingsCreditCardsAndBanks,
|
|
viewModel: viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsDataVisualizations,
|
|
viewModel: viewModel,
|
|
),
|
|
*/
|
|
if (showAll) ...[
|
|
SettingsListTile(
|
|
section: kSettingsSchedules,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
SettingsListTile(
|
|
section: kSettingsUserManagement,
|
|
viewModel: widget.viewModel,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
if (state.isLoading) LinearProgressIndicator(),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class SettingsListTile extends StatefulWidget {
|
|
const SettingsListTile({
|
|
required this.section,
|
|
required this.viewModel,
|
|
});
|
|
|
|
final String section;
|
|
final SettingsListVM viewModel;
|
|
|
|
@override
|
|
State<SettingsListTile> createState() => _SettingsListTileState();
|
|
}
|
|
|
|
class _SettingsListTileState extends State<SettingsListTile> {
|
|
bool _isHovered = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final localization = AppLocalization.of(context)!;
|
|
final store = StoreProvider.of<AppState>(context);
|
|
final state = store.state;
|
|
|
|
IconData? icon;
|
|
if (widget.section == kSettingsDeviceSettings) {
|
|
icon = isMobile(context) ? Icons.phone_android : MdiIcons.desktopClassic;
|
|
} else {
|
|
icon = getSettingIcon(widget.section);
|
|
}
|
|
|
|
final isSelected =
|
|
widget.viewModel.state.uiState.containsRoute('/${widget.section}') &&
|
|
isDesktop(context);
|
|
|
|
final hoverColor = convertHexStringToColor(state.prefState.enableDarkMode
|
|
? kDefaultDarkSelectedColorMenu
|
|
: kDefaultLightSelectedColorMenu);
|
|
|
|
return MouseRegion(
|
|
onEnter: (_) => setState(() => _isHovered = true),
|
|
onExit: (_) => setState(() => _isHovered = false),
|
|
child: Container(
|
|
color: Theme.of(context).cardColor,
|
|
child: SelectedIndicator(
|
|
isSelected: isSelected,
|
|
child: ListTile(
|
|
tileColor: _isHovered && !isSelected ? hoverColor : null,
|
|
dense: isDesktop(context),
|
|
leading: Padding(
|
|
padding: const EdgeInsets.only(left: 6, top: 2),
|
|
child: Icon(icon ?? icon, size: 22),
|
|
),
|
|
title: Text(
|
|
localization.lookup(widget.section),
|
|
style:
|
|
Theme.of(context).textTheme.bodyLarge!.copyWith(fontSize: 14),
|
|
),
|
|
onTap: () =>
|
|
widget.viewModel.loadSection(context, widget.section, 0),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SettingsSearch extends StatelessWidget {
|
|
const SettingsSearch({this.filter, this.viewModel});
|
|
|
|
final SettingsListVM? viewModel;
|
|
final String? filter;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final localization = AppLocalization.of(context)!;
|
|
final store = StoreProvider.of<AppState>(context);
|
|
final company = store.state.company;
|
|
|
|
final map = {
|
|
kSettingsCompanyDetails: [
|
|
[
|
|
'name',
|
|
'id_number',
|
|
'vat_number',
|
|
'website',
|
|
'email',
|
|
'phone',
|
|
'size',
|
|
'industry',
|
|
if (company.hasCustomField(CustomFieldType.company1))
|
|
company.getCustomFieldLabel(CustomFieldType.company1),
|
|
if (company.hasCustomField(CustomFieldType.company2))
|
|
company.getCustomFieldLabel(CustomFieldType.company2),
|
|
if (company.hasCustomField(CustomFieldType.company3))
|
|
company.getCustomFieldLabel(CustomFieldType.company3),
|
|
if (company.hasCustomField(CustomFieldType.company4))
|
|
company.getCustomFieldLabel(CustomFieldType.company4)
|
|
],
|
|
[
|
|
'address',
|
|
'postal_code',
|
|
'country',
|
|
],
|
|
[
|
|
'logo',
|
|
],
|
|
[
|
|
'defaults',
|
|
'payment_terms',
|
|
'invoice_terms',
|
|
'invoice_footer',
|
|
'quote_terms',
|
|
'quote_footer',
|
|
'credit_terms',
|
|
'credit_footer',
|
|
'use_quote_terms#2022-05-17',
|
|
],
|
|
[
|
|
'default_documents',
|
|
]
|
|
],
|
|
kSettingsUserDetails: [
|
|
[
|
|
'first_name',
|
|
'last_name',
|
|
'email',
|
|
'phone',
|
|
'password',
|
|
'accent_color',
|
|
'connect_google',
|
|
'connect_gmail',
|
|
'enable_two_factor',
|
|
],
|
|
[
|
|
'notifications',
|
|
],
|
|
],
|
|
kSettingsLocalization: [
|
|
[
|
|
'currency',
|
|
'language',
|
|
'timezone',
|
|
'date_format',
|
|
'military_time',
|
|
'decimal_comma',
|
|
'first_month_of_the_year',
|
|
],
|
|
[
|
|
'custom_labels',
|
|
],
|
|
],
|
|
kSettingsPaymentSettings: [
|
|
[
|
|
'company_gateways',
|
|
'auto_bill',
|
|
'auto_bill_on',
|
|
'payment_type',
|
|
'online_payment_email',
|
|
'manual_payment_email',
|
|
'use_available_credits',
|
|
'admin_initiated_payments#2022-06-06',
|
|
'allow_over_payment',
|
|
'allow_under_payment',
|
|
'auto_bill_standard_invoices#2023-01-17',
|
|
'client_initiated_payments#2023-03-20',
|
|
]
|
|
],
|
|
kSettingsTaxSettings: [
|
|
[
|
|
'tax_settings',
|
|
'inclusive_taxes',
|
|
'calculate_taxes#2023-06-11',
|
|
],
|
|
],
|
|
kSettingsTaxRates: [
|
|
[
|
|
'tax_rates',
|
|
],
|
|
],
|
|
kSettingsProducts: [
|
|
[
|
|
'track_inventory#2022-06-03',
|
|
'stock_notifications#2022-06-03',
|
|
'show_product_discount',
|
|
'show_product_cost',
|
|
'fill_products',
|
|
'update_products',
|
|
'convert_products',
|
|
],
|
|
],
|
|
kSettingsTasks: [
|
|
[
|
|
'task_settings',
|
|
'auto_start_tasks',
|
|
'show_tasks_table',
|
|
'client_portal',
|
|
'lock_invoiced_tasks#2022-11-30',
|
|
'invoice_task_hours#2023-01-19',
|
|
'allow_billable_task_items#2023-03-22',
|
|
'show_task_item_description#2023-03-22',
|
|
'project_location#2023-06-06',
|
|
],
|
|
],
|
|
kSettingsTaskStatuses: [
|
|
[
|
|
'task_statuses',
|
|
],
|
|
],
|
|
kSettingsExpenses: [
|
|
[
|
|
'should_be_invoiced',
|
|
'mark_paid',
|
|
'inclusive_taxes',
|
|
'convert_currency',
|
|
'notify_vendor_when_paid#2023-01-08',
|
|
],
|
|
],
|
|
kSettingsExpenseCategories: [
|
|
[
|
|
'expense_categories',
|
|
],
|
|
],
|
|
kSettingsWorkflowSettings: [
|
|
[
|
|
'auto_email_invoice',
|
|
'stop_on_unpaid',
|
|
'auto_archive_paid_invoices',
|
|
'auto_archive_cancelled_invoices',
|
|
'lock_invoices',
|
|
],
|
|
[
|
|
'auto_convert',
|
|
],
|
|
],
|
|
kSettingsImportExport: [
|
|
[
|
|
'import',
|
|
'export',
|
|
],
|
|
],
|
|
kSettingsDeviceSettings: [
|
|
[
|
|
'long_press_multiselect',
|
|
'biometric_authentication',
|
|
'enable_flexible_search#2022-07-05',
|
|
'enable_tooltips#2022-07-05',
|
|
'show_pdf_preview',
|
|
'pdf_preview_location#2022-10-24',
|
|
'refresh_data',
|
|
],
|
|
[
|
|
'dark_mode',
|
|
'custom_colors',
|
|
],
|
|
],
|
|
kSettingsAccountManagement: [
|
|
[
|
|
'activate_company',
|
|
'enable_markdown',
|
|
'include_drafts',
|
|
'include_deleted#2022-10-07',
|
|
'api_tokens',
|
|
'api_webhooks',
|
|
'purge_data',
|
|
'delete_company',
|
|
],
|
|
[
|
|
'enabled_modules',
|
|
],
|
|
[
|
|
'google_analytics',
|
|
'matomo_id#2022-12-12',
|
|
],
|
|
[
|
|
'password_timeout',
|
|
'web_session_timeout',
|
|
],
|
|
],
|
|
kSettingsInvoiceDesign: [
|
|
[
|
|
'invoice_design',
|
|
'quote_design',
|
|
'page_size',
|
|
'font_size',
|
|
'primary_font',
|
|
'secondary_font',
|
|
'primary_color',
|
|
'secondary_color',
|
|
'empty_columns',
|
|
'logo_size#2023-01-26',
|
|
'show_paid_stamp#2023-01-29',
|
|
'show_shipping_address#2023-01-29',
|
|
'share_invoice_quote_columns#2023-03-20',
|
|
],
|
|
],
|
|
kSettingsCustomDesigns: [
|
|
[
|
|
'custom_designs',
|
|
],
|
|
],
|
|
kSettingsCustomFields: [
|
|
[
|
|
'custom_fields',
|
|
],
|
|
],
|
|
kSettingsGeneratedNumbers: [
|
|
[
|
|
'number_padding',
|
|
'number_counter',
|
|
'recurring_prefix',
|
|
'reset_counter',
|
|
'invoice_number',
|
|
'client_number',
|
|
'credit_number',
|
|
'payment_number',
|
|
],
|
|
],
|
|
kSettingsClientPortal: [
|
|
[
|
|
'client_portal',
|
|
'dashboard',
|
|
'portal_mode',
|
|
'subdomain',
|
|
'domain',
|
|
'client_document_upload',
|
|
'vendor_document_upload#2022-07-06',
|
|
'accept_purchase_order_number#2023-02-02',
|
|
],
|
|
[
|
|
'client_registration',
|
|
],
|
|
[
|
|
'enable_portal_password',
|
|
'show_accept_invoice_terms',
|
|
'show_accept_quote_terms',
|
|
'require_invoice_signature',
|
|
'require_quote_signature',
|
|
],
|
|
[
|
|
'messages',
|
|
],
|
|
[
|
|
'header',
|
|
'footer',
|
|
if (isSelfHosted(context)) ...[
|
|
'custom_css',
|
|
'custom_javascript',
|
|
],
|
|
],
|
|
],
|
|
kSettingsEmailSettings: [
|
|
[
|
|
'send_from_gmail',
|
|
'email_design',
|
|
'from_name',
|
|
'reply_to_email',
|
|
'reply_to_name',
|
|
'bcc_email',
|
|
'attach_pdf',
|
|
'attach_documents',
|
|
'attach_ubl',
|
|
'email_signature',
|
|
'microsoft',
|
|
'postmark#2023-01-11',
|
|
'mailgun#2023-01-11',
|
|
'email_alignment#2023-01-17',
|
|
'show_email_footer#2023-01-17',
|
|
'enable_e_invoice#2023-06-11,'
|
|
],
|
|
],
|
|
kSettingsTemplatesAndReminders: [
|
|
[
|
|
'template',
|
|
'send_reminders',
|
|
'late_fees',
|
|
]
|
|
],
|
|
kSettingsBankAccounts: [
|
|
[
|
|
'bank_accounts#2022-09-13',
|
|
],
|
|
],
|
|
kSettingsTransactionRules: [
|
|
[
|
|
'transaction_rules#2022-11-21',
|
|
],
|
|
],
|
|
kSettingsGroupSettings: [
|
|
[
|
|
'groups',
|
|
],
|
|
],
|
|
kSettingsPaymentLinks: [
|
|
[
|
|
'payment_links',
|
|
],
|
|
],
|
|
kSettingsSchedules: [
|
|
[
|
|
'schedules#2023-02-15',
|
|
],
|
|
],
|
|
kSettingsUserManagement: [
|
|
[
|
|
'users',
|
|
],
|
|
]
|
|
};
|
|
|
|
if (store.state.settingsUIState.showNewSettings) {
|
|
final sections = <String>[];
|
|
for (var section in map.keys) {
|
|
for (var tab = 0; tab < map[section]!.length; tab++) {
|
|
final fields = map[section]![tab];
|
|
for (var field in fields) {
|
|
final List<String> parts = field.split('#');
|
|
final dateAdded =
|
|
parts.length == 1 ? '' : convertSqlDateToDateTime(parts[1]);
|
|
sections.add('$dateAdded#${parts[0]}#$section#$tab');
|
|
}
|
|
}
|
|
}
|
|
|
|
sections.sort((a, b) {
|
|
if (a.startsWith('#') && b.startsWith('#')) {
|
|
return a.compareTo(b);
|
|
} else if (a.startsWith('#')) {
|
|
return 1;
|
|
} else if (b.startsWith('#')) {
|
|
return -1;
|
|
}
|
|
|
|
return b.compareTo(a);
|
|
});
|
|
|
|
return ScrollableListView(children: [
|
|
for (var parts
|
|
in sections.map((section) => section.split('#').toList()))
|
|
if ((filter ?? '').trim().isEmpty ||
|
|
localization
|
|
.lookup(parts[1])
|
|
.toLowerCase()
|
|
.contains(filter!.toLowerCase()))
|
|
ListTile(
|
|
title: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(localization.lookup(parts[1])),
|
|
Text(
|
|
localization.lookup(parts[2]),
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
if (parts[0].isNotEmpty)
|
|
Flexible(
|
|
child: Text(timeago.format(DateTime.parse(parts[0]),
|
|
locale:
|
|
localeSelector(store.state, twoLetter: true) +
|
|
'_short'))),
|
|
],
|
|
),
|
|
leading: Padding(
|
|
padding: const EdgeInsets.only(left: 6, top: 10),
|
|
child: Icon(getSettingIcon(parts[2]), size: 22),
|
|
),
|
|
onTap: () =>
|
|
viewModel!.loadSection(context, parts[2], parseInt(parts[3])),
|
|
),
|
|
]);
|
|
} else {
|
|
return ScrollableListView(
|
|
children: [
|
|
for (var section in map.keys)
|
|
for (int i = 0; i < map[section]!.length; i++)
|
|
for (var field in map[section]![i])
|
|
if (localization
|
|
.lookup(field.split('#')[0])
|
|
.toLowerCase()
|
|
.contains(filter!.toLowerCase()))
|
|
ListTile(
|
|
title: Text(localization.lookup(field.split('#')[0])),
|
|
leading: Padding(
|
|
padding: const EdgeInsets.only(left: 6, top: 10),
|
|
child: Icon(getSettingIcon(section), size: 22),
|
|
),
|
|
subtitle: Text(localization.lookup(section)),
|
|
onTap: () => viewModel!.loadSection(context, section, i),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
}
|