498 lines
15 KiB
Dart
498 lines
15 KiB
Dart
// Dart imports:
|
|
import 'dart:math';
|
|
|
|
// Flutter imports:
|
|
import 'package:flutter/material.dart';
|
|
|
|
// Package imports:
|
|
import 'package:flutter_redux/flutter_redux.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:intl/number_symbols.dart';
|
|
import 'package:intl/number_symbols_data.dart';
|
|
|
|
// Project imports:
|
|
import 'package:invoiceninja_flutter/constants.dart';
|
|
import 'package:invoiceninja_flutter/data/models/group_model.dart';
|
|
import 'package:invoiceninja_flutter/data/models/models.dart';
|
|
import 'package:invoiceninja_flutter/main_app.dart';
|
|
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
|
|
import 'package:invoiceninja_flutter/redux/company/company_selectors.dart';
|
|
import 'package:invoiceninja_flutter/utils/localization.dart';
|
|
|
|
double round(double? value, int precision) {
|
|
if (value == null || value.isNaN) {
|
|
return 0;
|
|
}
|
|
|
|
final int fac = pow(10, precision) as int;
|
|
double result = value * fac;
|
|
|
|
// Workaround for floating point issues:
|
|
// ie. 35 * 1.107 => 38.745
|
|
// ie. .75 * 55.3 => 41.4749999999
|
|
if ('$result'.contains('999999')) {
|
|
result += 0.000001;
|
|
}
|
|
|
|
return result.round() / fac;
|
|
}
|
|
|
|
int? parseInt(String value, {bool zeroIsNull = false}) {
|
|
value = value.replaceAll(RegExp(r'[^0-9\.\-]'), '');
|
|
|
|
final intValue = int.tryParse(value) ?? 0;
|
|
|
|
return (intValue == 0 && zeroIsNull) ? null : intValue;
|
|
}
|
|
|
|
double? parseDouble(String? value, {bool zeroIsNull = false}) {
|
|
// check for comma as decimal separator
|
|
final state = StoreProvider.of<AppState>(navigatorKey.currentContext!).state;
|
|
|
|
if (state.company!.useCommaAsDecimalPlace && value!.contains(',')) {
|
|
value = value.replaceAll('.', '');
|
|
value = value.replaceAll(',', '.');
|
|
}
|
|
|
|
value = value!.replaceAll(RegExp(r'[^0-9\.\-]'), '');
|
|
|
|
final doubleValue = double.tryParse(value) ?? 0.0;
|
|
|
|
return (doubleValue == 0 && zeroIsNull) ? null : doubleValue;
|
|
}
|
|
|
|
enum FormatNumberType {
|
|
money, // $1,000.00
|
|
percent, // 1,000.00%
|
|
int, // 1,000
|
|
double, // 1,000.00
|
|
inputMoney, // 1000.00
|
|
inputAmount, // 1000
|
|
duration,
|
|
}
|
|
|
|
String formatSize(int size) {
|
|
return size > 1000000
|
|
? '${round(size / 1000000, 1).toInt()} MB'
|
|
: '${round(size / 1000, 0).toInt()} KB';
|
|
}
|
|
|
|
String? formatNumber(
|
|
double? value,
|
|
BuildContext? context, {
|
|
String? clientId,
|
|
String? vendorId,
|
|
String? currencyId,
|
|
FormatNumberType? formatNumberType = FormatNumberType.money,
|
|
bool? showCurrencyCode,
|
|
bool zeroIsNull = false,
|
|
bool roundToPrecision = true,
|
|
}) {
|
|
if ((zeroIsNull ||
|
|
formatNumberType == FormatNumberType.inputMoney ||
|
|
formatNumberType == FormatNumberType.inputAmount) &&
|
|
value == 0) {
|
|
return null;
|
|
} else if (value == null) {
|
|
return '';
|
|
}
|
|
|
|
if (formatNumberType == FormatNumberType.duration) {
|
|
return formatDuration(Duration(seconds: value.toInt()));
|
|
}
|
|
|
|
final state = StoreProvider.of<AppState>(context!).state;
|
|
final CompanyEntity company = state.company!;
|
|
final ClientEntity? client = state.clientState.map[clientId];
|
|
final VendorEntity? vendor = state.vendorState.map[vendorId];
|
|
final GroupEntity? group = state.groupState.map[client?.groupId];
|
|
|
|
String? countryId;
|
|
|
|
if ((client?.countryId ?? '').isNotEmpty && client!.hasNameSet) {
|
|
countryId = client.countryId;
|
|
} else if ((vendor?.countryId ?? '').isNotEmpty && vendor!.hasNameSet) {
|
|
countryId = vendor.countryId;
|
|
} else {
|
|
countryId = company.settings.countryId;
|
|
}
|
|
|
|
if (currencyId == kCurrencyAll) {
|
|
currencyId = company.currencyId;
|
|
} else if (currencyId != null && currencyId.isNotEmpty) {
|
|
// do nothing
|
|
} else if (client != null && client.hasCurrency) {
|
|
currencyId = client.currencyId;
|
|
} else if (vendor != null && vendor.hasCurrency) {
|
|
currencyId = vendor.currencyId;
|
|
} else if (group != null && group.hasCurrency) {
|
|
currencyId = group.currencyId;
|
|
} else {
|
|
currencyId = company.currencyId;
|
|
}
|
|
|
|
final CurrencyEntity? currency = state.staticState.currencyMap[currencyId];
|
|
final CurrencyEntity? companyCurrency =
|
|
state.staticState.currencyMap[company.currencyId];
|
|
|
|
final CountryEntity country =
|
|
state.staticState.countryMap[countryId] ?? CountryEntity();
|
|
final CountryEntity companyCountry =
|
|
state.staticState.countryMap[company.settings.countryId] ??
|
|
CountryEntity();
|
|
|
|
if (currency == null) {
|
|
return '';
|
|
}
|
|
|
|
if (formatNumberType == FormatNumberType.money && roundToPrecision) {
|
|
value = round(value, currency.precision);
|
|
}
|
|
|
|
String thousandSeparator = currency.thousandSeparator;
|
|
String decimalSeparator = currency.decimalSeparator;
|
|
bool swapCurrencySymbol = companyCurrency!.swapCurrencySymbol;
|
|
|
|
if (currency.id == kCurrencyEuro) {
|
|
swapCurrencySymbol = companyCountry.swapCurrencySymbol;
|
|
if (country.thousandSeparator != null &&
|
|
country.thousandSeparator.isNotEmpty) {
|
|
thousandSeparator = country.thousandSeparator;
|
|
}
|
|
if (country.decimalSeparator != null &&
|
|
country.decimalSeparator.isNotEmpty) {
|
|
decimalSeparator = country.decimalSeparator;
|
|
}
|
|
}
|
|
|
|
if ([
|
|
FormatNumberType.inputMoney,
|
|
FormatNumberType.inputAmount,
|
|
].contains(formatNumberType)) {
|
|
thousandSeparator = '';
|
|
if (state.company!.useCommaAsDecimalPlace) {
|
|
decimalSeparator = ',';
|
|
} else {
|
|
decimalSeparator = '.';
|
|
}
|
|
}
|
|
|
|
numberFormatSymbols['custom'] = NumberSymbols(
|
|
NAME: 'custom',
|
|
DECIMAL_SEP: decimalSeparator,
|
|
GROUP_SEP: thousandSeparator,
|
|
ZERO_DIGIT: '0',
|
|
PLUS_SIGN: '+',
|
|
MINUS_SIGN: '-',
|
|
CURRENCY_PATTERN: '',
|
|
DECIMAL_PATTERN: '',
|
|
DEF_CURRENCY_CODE: '',
|
|
EXP_SYMBOL: '',
|
|
INFINITY: '',
|
|
NAN: '',
|
|
PERCENT: '',
|
|
PERCENT_PATTERN: '',
|
|
PERMILL: '',
|
|
SCIENTIFIC_PATTERN: '',
|
|
);
|
|
|
|
late NumberFormat formatter;
|
|
String? formatted;
|
|
|
|
if (formatNumberType == FormatNumberType.int) {
|
|
return NumberFormat('#,##0', 'custom').format(value);
|
|
} else if (formatNumberType == FormatNumberType.double) {
|
|
return NumberFormat('#,##0.#####', 'custom').format(value);
|
|
} else if (formatNumberType == FormatNumberType.inputAmount) {
|
|
return NumberFormat('#.#####', 'custom').format(value);
|
|
} else if (formatNumberType == FormatNumberType.inputMoney) {
|
|
if (currency.precision == 0) {
|
|
return NumberFormat('#.#####', 'custom').format(value);
|
|
} else if (currency.precision == 1) {
|
|
return NumberFormat('#.0####', 'custom').format(value);
|
|
} else if (currency.precision == 2) {
|
|
return NumberFormat('#.00###', 'custom').format(value);
|
|
} else if (currency.precision == 3) {
|
|
return NumberFormat('#.000##', 'custom').format(value);
|
|
}
|
|
} else {
|
|
if (formatNumberType == FormatNumberType.percent) {
|
|
formatter = NumberFormat('#,##0.#####', 'custom');
|
|
} else if (currency.precision == 0) {
|
|
formatter = NumberFormat('#,##0.#####', 'custom');
|
|
} else if (currency.precision == 1) {
|
|
formatter = NumberFormat('#,##0.0####', 'custom');
|
|
} else if (currency.precision == 2) {
|
|
formatter = NumberFormat('#,##0.00###', 'custom');
|
|
} else if (currency.precision == 3) {
|
|
formatter = NumberFormat('#,##0.000##', 'custom');
|
|
}
|
|
|
|
formatted = formatter.format(value < 0 ? value * -1 : value);
|
|
|
|
// Ugly workaround to prevent showing negative zero values
|
|
if (formatted == '-0.00') {
|
|
formatted = '0.00';
|
|
} else if (formatted == '-0,00') {
|
|
formatted = '0,00';
|
|
}
|
|
}
|
|
|
|
final prefix = value < 0 ? '-' : '';
|
|
|
|
if (formatNumberType == FormatNumberType.percent) {
|
|
return '$prefix$formatted%';
|
|
} else if ((showCurrencyCode ?? company.settings.showCurrencyCode ?? false) ||
|
|
currency.symbol.isEmpty) {
|
|
return '$prefix$formatted ${currency.code}';
|
|
} else {
|
|
if (swapCurrencySymbol) {
|
|
return '$prefix$formatted ${currency.symbol.trim()}';
|
|
} else {
|
|
return '$prefix${currency.symbol}$formatted';
|
|
}
|
|
}
|
|
}
|
|
|
|
String cleanPhoneNumber(String phoneNumber) {
|
|
return phoneNumber.replaceAll(RegExp(r'\D'), '');
|
|
}
|
|
|
|
String formatURL(String url) {
|
|
if (url.startsWith('http')) {
|
|
return url;
|
|
}
|
|
|
|
return 'http://' + url;
|
|
}
|
|
|
|
String formatAddress(AppState appState,
|
|
{dynamic object, bool isShipping = false, String delimiter = '\n'}) {
|
|
var str = '';
|
|
|
|
final String address1 =
|
|
(isShipping ? object.shippingAddress1 : object.address1) ?? '';
|
|
final String address2 =
|
|
(isShipping ? object.shippingAddress2 : object.address2) ?? '';
|
|
final String city = (isShipping ? object.shippingCity : object.city) ?? '';
|
|
final String state = (isShipping ? object.shippingState : object.state) ?? '';
|
|
final String postalCode =
|
|
(isShipping ? object.shippingPostalCode : object.postalCode) ?? '';
|
|
final String countryId =
|
|
(isShipping ? object.shippingCountryId : object.countryId) ?? '';
|
|
|
|
if (address1.isNotEmpty) {
|
|
str += address1 + delimiter;
|
|
}
|
|
if (address2.isNotEmpty) {
|
|
str += address2 + delimiter;
|
|
}
|
|
|
|
if (city.isNotEmpty) {
|
|
str += city + ', ';
|
|
}
|
|
|
|
if (state.isNotEmpty) {
|
|
str += state + ' ';
|
|
}
|
|
|
|
if (postalCode.isNotEmpty) {
|
|
str += postalCode;
|
|
}
|
|
|
|
if (countryId.isNotEmpty &&
|
|
countryId != appState.company!.settings.countryId) {
|
|
if (str.isNotEmpty) {
|
|
str += delimiter;
|
|
}
|
|
|
|
str += appState.staticState.countryMap[countryId]?.name ?? '';
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
String convertDateTimeToSqlDate([DateTime? date]) {
|
|
date = date ?? DateTime.now();
|
|
return date.toIso8601String().split('T').first;
|
|
}
|
|
|
|
DateTime convertSqlDateToDateTime([String? date]) {
|
|
date = date ?? convertDateTimeToSqlDate();
|
|
final parts = date.split('-');
|
|
return DateTime.utc(
|
|
parseInt(parts[0])!,
|
|
parseInt(parts[1])!,
|
|
parseInt(parts[2])!,
|
|
);
|
|
}
|
|
|
|
DateTime convertTimestampToDate(int? timestamp) =>
|
|
DateTime.fromMillisecondsSinceEpoch((timestamp ?? 0) * 1000, isUtc: true);
|
|
|
|
String convertTimestampToDateString(int? timestamp) =>
|
|
convertTimestampToDate(timestamp).toIso8601String();
|
|
|
|
String formatDuration(Duration? duration, {bool showSeconds = true}) {
|
|
final time = duration.toString().split('.')[0];
|
|
|
|
if (showSeconds) {
|
|
return time;
|
|
} else {
|
|
final parts = time.split(':');
|
|
return '${parts[0]}:${parts[1]}';
|
|
}
|
|
}
|
|
|
|
DateTime convertTimeOfDayToDateTime(TimeOfDay? timeOfDay, [DateTime? dateTime]) {
|
|
dateTime ??= DateTime.now();
|
|
return DateTime(dateTime.year, dateTime.month, dateTime.day,
|
|
timeOfDay?.hour ?? 0, timeOfDay?.minute ?? 0)
|
|
.toUtc();
|
|
}
|
|
|
|
TimeOfDay convertDateTimeToTimeOfDay(DateTime? dateTime) =>
|
|
TimeOfDay(hour: dateTime?.hour ?? 0, minute: dateTime?.minute ?? 0);
|
|
|
|
String formatDateRange(String startDate, String endDate, BuildContext context) {
|
|
final today = DateTime.now();
|
|
|
|
final startDateTime = DateTime.tryParse(startDate)!.toLocal();
|
|
final startFormatter =
|
|
DateFormat(today.year == startDateTime.year ? 'MMM d' : 'MMM d, yyy');
|
|
final startDateTimeString = startFormatter.format(startDateTime);
|
|
|
|
final endDateTime = DateTime.tryParse(endDate)!.toLocal();
|
|
final endFormatter =
|
|
DateFormat(today.year == endDateTime.year ? 'MMM d' : 'MMM d, yyy');
|
|
final endDateTimeString = endFormatter.format(endDateTime);
|
|
|
|
return '$startDateTimeString - $endDateTimeString';
|
|
}
|
|
|
|
String parseDate(String value, BuildContext context) {
|
|
if (value == null || value.isEmpty) {
|
|
return '';
|
|
}
|
|
|
|
final state = StoreProvider.of<AppState>(context).state;
|
|
final CompanyEntity company = state.company!;
|
|
|
|
final dateFormats = state.staticState.dateFormatMap;
|
|
final dateFormatId = (company.settings.dateFormatId ?? '').isNotEmpty
|
|
? company.settings.dateFormatId
|
|
: kDefaultDateFormat;
|
|
|
|
final format = dateFormats[dateFormatId]!.format;
|
|
final formatter = DateFormat(format, localeSelector(state));
|
|
|
|
return convertDateTimeToSqlDate(formatter.parse(value));
|
|
}
|
|
|
|
DateTime? parseTime(String value, BuildContext context) {
|
|
if (value == null || value.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final state = StoreProvider.of<AppState>(context).state;
|
|
final CompanyEntity company = state.company!;
|
|
|
|
final showSeconds = ':'.allMatches(value).length >= 2;
|
|
final enableMilitaryTime = company.settings.enableMilitaryTime;
|
|
String format;
|
|
|
|
format = showSeconds
|
|
? enableMilitaryTime!
|
|
? 'H:mm:ss'
|
|
: 'h:mm:ss a'
|
|
: enableMilitaryTime!
|
|
? 'H:mm'
|
|
: 'h:mm a';
|
|
|
|
final formatter = DateFormat('y-M-D ' + format, localeSelector(state));
|
|
return formatter.parse('2000-01-01 ' + value);
|
|
}
|
|
|
|
String formatDate(String? value, BuildContext? context,
|
|
{bool showDate = true, bool showTime = false, bool showSeconds = true}) {
|
|
if (value == null || value.isEmpty) {
|
|
return '';
|
|
}
|
|
|
|
final state = StoreProvider.of<AppState>(context!).state;
|
|
final CompanyEntity? company = state.company;
|
|
var formattedValue = '';
|
|
|
|
if (showTime) {
|
|
String format;
|
|
if (!showDate) {
|
|
format = showSeconds
|
|
? company!.settings.enableMilitaryTime!
|
|
? 'H:mm:ss'
|
|
: 'h:mm:ss a'
|
|
: company!.settings.enableMilitaryTime!
|
|
? 'H:mm'
|
|
: 'h:mm a';
|
|
} else {
|
|
final dateFormats = state.staticState.dateFormatMap;
|
|
final dateFormatId = (company!.settings.dateFormatId ?? '').isNotEmpty
|
|
? company.settings.dateFormatId
|
|
: kDefaultDateFormat;
|
|
format = dateFormats[dateFormatId]!.format;
|
|
format += ' ' +
|
|
(showSeconds
|
|
? company.settings.enableMilitaryTime!
|
|
? 'H:mm:ss'
|
|
: 'h:mm:ss a'
|
|
: company.settings.enableMilitaryTime!
|
|
? 'H:mm'
|
|
: 'h:mm a');
|
|
}
|
|
final formatter = DateFormat(format, localeSelector(state));
|
|
final parsed = DateTime.tryParse(value.endsWith('Z') ? value : value + 'Z');
|
|
formattedValue = parsed == null ? '' : formatter.format(parsed.toLocal());
|
|
} else {
|
|
final dateFormats = state.staticState.dateFormatMap;
|
|
final formatter = DateFormat(
|
|
dateFormats[company!.settings.dateFormatId]!.format,
|
|
localeSelector(state));
|
|
final parsed = DateTime.tryParse(value);
|
|
formattedValue = parsed == null ? '' : formatter.format(parsed);
|
|
}
|
|
|
|
// Fix double periods in dates in foreign languages #527
|
|
return formattedValue.replaceFirst('..', '.');
|
|
}
|
|
|
|
String formatApiUrl(String? url) {
|
|
url = cleanApiUrl(url);
|
|
|
|
if (url.isEmpty) {
|
|
return '';
|
|
}
|
|
|
|
return url + '/api/v1';
|
|
}
|
|
|
|
String cleanApiUrl(String? url) => (url ?? '')
|
|
.trim()
|
|
.replaceFirst(RegExp(r'/api/v1'), '')
|
|
.replaceFirst(RegExp(r'/$'), '');
|
|
|
|
String? formatCustomValue({String? value, String? field, required BuildContext context}) {
|
|
final localization = AppLocalization.of(context);
|
|
final state = StoreProvider.of<AppState>(context).state;
|
|
final CompanyEntity company = state.company!;
|
|
|
|
switch (company.getCustomFieldType(field)) {
|
|
case kFieldTypeSwitch:
|
|
return value == 'yes' ? localization!.yes : localization!.no;
|
|
break;
|
|
case kFieldTypeDate:
|
|
return formatDate(value, context);
|
|
break;
|
|
default:
|
|
return value;
|
|
}
|
|
}
|