diff --git a/src/core/template.ts b/src/core/template.ts index b6b23e8e..dd31ec65 100644 --- a/src/core/template.ts +++ b/src/core/template.ts @@ -5,7 +5,6 @@ type TemplateTypeName = 'Normal' | 'Schedule'; export class TemplateType implements TypeAndName { private static readonly allInstances: TemplateType[] = []; private static readonly allInstancesByType: Record = {}; - private static readonly allInstancesByTypeName: Record = {}; public static readonly Normal = new TemplateType(1, 'Normal'); public static readonly Schedule = new TemplateType(2, 'Schedule'); @@ -19,17 +18,12 @@ export class TemplateType implements TypeAndName { TemplateType.allInstances.push(this); TemplateType.allInstancesByType[type] = this; - TemplateType.allInstancesByTypeName[name] = this; } public static values(): TemplateType[] { return TemplateType.allInstances; } - public static all(): Record { - return TemplateType.allInstancesByTypeName; - } - public static valueOf(type: number): TemplateType | undefined { return TemplateType.allInstancesByType[type]; } diff --git a/src/lib/services.ts b/src/lib/services.ts index 1b4d9447..d31f3e21 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -434,7 +434,7 @@ export default { timeout: DEFAULT_IMPORT_API_TIMEOUT } as ApiRequestConfig); }, - uploadTransactionPicture: ({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId: string }): ApiResponsePromise => { + uploadTransactionPicture: ({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId?: string }): ApiResponsePromise => { return axios.postForm>('v1/transaction/pictures/upload.json', { picture: pictureFile, clientSessionId: clientSessionId diff --git a/src/lib/transaction.ts b/src/lib/transaction.ts index 5906741c..e04d1cb1 100644 --- a/src/lib/transaction.ts +++ b/src/lib/transaction.ts @@ -20,14 +20,14 @@ import { } from './category.ts'; export interface SetTransactionOptions { - type: number; - categoryId: string; - accountId: string; - destinationAccountId: string; - amount: number; - destinationAmount: number; - tagIds: string; - comment: string; + type?: number; + categoryId?: string; + accountId?: string; + destinationAccountId?: string; + amount?: number; + destinationAmount?: number; + tagIds?: string; + comment?: string; } function getDisplayAmount(amount: number, currency: string, hideAmount: boolean, formatAmountWithCurrencyFunc: (value: number | string, currencyCode?: string) => string): string { diff --git a/src/lib/ui/mobile.ts b/src/lib/ui/mobile.ts index 177c0992..6aaadc46 100644 --- a/src/lib/ui/mobile.ts +++ b/src/lib/ui/mobile.ts @@ -37,26 +37,6 @@ export function showAlert(message: string, confirmCallback: ((dialog: Dialog.Dia }); } -export function showConfirm(message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback: ((dialog: Dialog.Dialog, e: Event) => void) | undefined, translateFn: TranslateFunction): void { - f7ready((f7) => { - f7.dialog.create({ - title: translateFn('global.app.title'), - text: translateFn(message), - animate: isEnableAnimate(), - buttons: [ - { - text: translateFn('Cancel'), - onClick: cancelCallback - }, - { - text: translateFn('OK'), - onClick: confirmCallback - } - ] - }).open(); - }); -} - export function showToast(message: string, timeout: number | undefined, translateFn: TranslateFunction): void { f7ready((f7) => { f7.toast.create({ @@ -210,7 +190,7 @@ export function scrollToSelectedItem(parentEl: Framework7Dom, containerSelector: } export function useI18nUIComponents() { - const i18nGlobal = useVueI18n(); + const { t } = useVueI18n(); function routeBackOnError(f7router: Router.Router, errorRef: Ref): void { const unwatch = watch(errorRef, (newValue) => { @@ -228,10 +208,30 @@ export function useI18nUIComponents() { }); } + function showConfirm(message: string, confirmCallback?: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback?: ((dialog: Dialog.Dialog, e: Event) => void) | undefined): void { + f7ready((f7) => { + f7.dialog.create({ + title: t('global.app.title'), + text: t(message), + animate: isEnableAnimate(), + buttons: [ + { + text: t('Cancel'), + onClick: cancelCallback + }, + { + text: t('OK'), + onClick: confirmCallback + } + ] + }).open(); + }); + } + return { - showAlert: (message: string, confirmCallback?: (dialog: Dialog.Dialog, e: Event) => void) => showAlert(message, confirmCallback, i18nGlobal.t), - showConfirm: (message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback?: (dialog: Dialog.Dialog, e: Event) => void): void => showConfirm(message, confirmCallback, cancelCallback, i18nGlobal.t), - showToast: (message: string, timeout?: number): void => showToast(message, timeout, i18nGlobal.t), + showAlert: (message: string, confirmCallback?: (dialog: Dialog.Dialog, e: Event) => void) => showAlert(message, confirmCallback, t), + showConfirm: showConfirm, + showToast: (message: string, timeout?: number): void => showToast(message, timeout, t), routeBackOnError } } diff --git a/src/locales/helper.js b/src/locales/helper.js index f2a2595d..993121d7 100644 --- a/src/locales/helper.js +++ b/src/locales/helper.js @@ -1,29 +1,21 @@ -import { WeekDay, LongDateFormat, ShortDateFormat, LongTimeFormat, ShortTimeFormat, DateRange } from '@/core/datetime.ts'; +import { LongDateFormat, ShortDateFormat, LongTimeFormat, ShortTimeFormat, DateRange } from '@/core/datetime.ts'; import { DecimalSeparator, DigitGroupingSymbol, DigitGroupingType } from '@/core/numeral.ts'; import { CurrencyDisplayType } from '@/core/currency.ts' -import { AccountCategory } from '@/core/account.ts'; import { TransactionTagFilterType } from '@/core/transaction.ts'; -import { UTC_TIMEZONE, ALL_TIMEZONES } from '@/consts/timezone.ts'; import { ALL_CURRENCIES } from '@/consts/currency.ts'; import { KnownErrorCode, SPECIFIED_API_NOT_FOUND_ERRORS, PARAMETERIZED_ERRORS } from '@/consts/api.ts'; import { isString, isNumber, - isBoolean, - copyObjectTo + isBoolean } from '@/lib/common.ts'; import { parseDateFromUnixTime, formatUnixTime, getYear, - getTimezoneOffset, - getTimezoneOffsetMinutes, - getBrowserTimezoneOffset, - getBrowserTimezoneOffsetMinutes, - getTimeDifferenceHoursAndMinutes, getDateTimeFormatType, getRecentMonthDateRanges, isDateRangeMatchFullYears, @@ -31,8 +23,7 @@ import { } from '@/lib/datetime.ts'; import { - formatAmount, - getAdaptiveDisplayAmountRate + formatAmount } from '@/lib/numeral.ts'; import { @@ -40,11 +31,6 @@ import { appendCurrencySymbol } from '@/lib/currency.ts'; -import { - getCategorizedAccountsMap, - getAllFilteredAccountsBalance -} from '@/lib/account.ts'; - function getLocalizedDisplayNameAndType(typeAndNames, translateFn) { const ret = []; @@ -78,10 +64,6 @@ function getCurrencyUnitName(currencyCode, isPlural, translateFn) { return ''; } -function getMonthdayOrdinal(monthDay, translateFn) { - return translateFn(`datetime.monthDayOrdinal.${monthDay}`); -} - function getWeekdayShortName(weekDayName, translateFn) { return translateFn(`datetime.${weekDayName}.short`); } @@ -90,50 +72,6 @@ function getWeekdayLongName(weekDayName, translateFn) { return translateFn(`datetime.${weekDayName}.long`); } -function getMultiMonthdayShortNames(monthDays, translateFn) { - if (!monthDays) { - return ''; - } - - if (monthDays.length === 1) { - return translateFn('format.misc.monthDay', { - ordinal: getMonthdayOrdinal(monthDays[0], translateFn) - }); - } else { - return translateFn('format.misc.monthDays', { - multiMonthDays: joinMultiText(monthDays.map(monthDay => - translateFn('format.misc.eachMonthDayInMonthDays', { - ordinal: getMonthdayOrdinal(monthDay, translateFn) - })), translateFn) - }); - } -} - -function getMultiWeekdayLongNames(weekdayTypes, firstDayOfWeek, translateFn) { - const weekdayTypesMap = {}; - - if (!isNumber(firstDayOfWeek)) { - firstDayOfWeek = WeekDay.DefaultFirstDay.type; - } - - for (let i = 0; i < weekdayTypes.length; i++) { - weekdayTypesMap[weekdayTypes[i]] = true; - } - - const allWeekDays = getAllWeekDays(firstDayOfWeek, translateFn); - const finalWeekdayNames = []; - - for (let i = 0; i < allWeekDays.length; i++) { - const weekDay = allWeekDays[i]; - - if (weekdayTypesMap[weekDay.type]) { - finalWeekdayNames.push(weekDay.displayName); - } - } - - return joinMultiText(finalWeekdayNames, translateFn); -} - function getI18nLongDateFormat(translateFn, formatTypeValue) { const defaultLongDateFormatTypeName = translateFn('default.longDateFormat'); return getDateTimeFormat(translateFn, LongDateFormat.all(), LongDateFormat.values(), 'format.longDate', defaultLongDateFormatTypeName, LongDateFormat.Default, formatTypeValue); @@ -184,110 +122,6 @@ function getDateTimeFormat(translateFn, allFormatMap, allFormatArray, localeForm return translateFn(`${localeFormatPathPrefix}.${type.key}`); } -function getAllTimezones(includeSystemDefault, translateFn) { - const defaultTimezoneOffset = getBrowserTimezoneOffset(); - const defaultTimezoneOffsetMinutes = getBrowserTimezoneOffsetMinutes(); - const allTimezoneInfos = []; - - for (let i = 0; i < ALL_TIMEZONES.length; i++) { - const utcOffset = (ALL_TIMEZONES[i].timezoneName !== UTC_TIMEZONE.timezoneName ? getTimezoneOffset(ALL_TIMEZONES[i].timezoneName) : ''); - const displayName = translateFn(`timezone.${ALL_TIMEZONES[i].displayName}`); - - allTimezoneInfos.push({ - name: ALL_TIMEZONES[i].timezoneName, - utcOffset: utcOffset, - utcOffsetMinutes: getTimezoneOffsetMinutes(ALL_TIMEZONES[i].timezoneName), - displayName: displayName, - displayNameWithUtcOffset: `(UTC${utcOffset}) ${displayName}` - }); - } - - if (includeSystemDefault) { - const defaultDisplayName = translateFn('System Default'); - - allTimezoneInfos.push({ - name: '', - utcOffset: defaultTimezoneOffset, - utcOffsetMinutes: defaultTimezoneOffsetMinutes, - displayName: defaultDisplayName, - displayNameWithUtcOffset: `(UTC${defaultTimezoneOffset}) ${defaultDisplayName}` - }); - } - - allTimezoneInfos.sort(function(c1, c2) { - const utcOffset1 = parseInt(c1.utcOffset.replace(':', '')); - const utcOffset2 = parseInt(c2.utcOffset.replace(':', '')); - - if (utcOffset1 !== utcOffset2) { - return utcOffset1 - utcOffset2; - } - - return c1.displayName.localeCompare(c2.displayName); - }) - - return allTimezoneInfos; -} - -function getTimezoneDifferenceDisplayText(utcOffset, translateFn) { - const defaultTimezoneOffset = getTimezoneOffsetMinutes(); - const offsetTime = getTimeDifferenceHoursAndMinutes(utcOffset - defaultTimezoneOffset); - - if (utcOffset > defaultTimezoneOffset) { - if (offsetTime.offsetMinutes) { - return translateFn('format.misc.hoursMinutesAheadOfDefaultTimezone', { - hours: offsetTime.offsetHours, - minutes: offsetTime.offsetMinutes - }); - } else { - return translateFn('format.misc.hoursAheadOfDefaultTimezone', { - hours: offsetTime.offsetHours - }); - } - } else if (utcOffset < defaultTimezoneOffset) { - if (offsetTime.offsetMinutes) { - return translateFn('format.misc.hoursMinutesBehindDefaultTimezone', { - hours: offsetTime.offsetHours, - minutes: offsetTime.offsetMinutes - }); - } else { - return translateFn('format.misc.hoursBehindDefaultTimezone', { - hours: offsetTime.offsetHours - }); - } - } else { - return translateFn('Same time as default timezone'); - } -} - -function getAllWeekDays(firstDayOfWeek, translateFn) { - const ret = []; - const allWeekDays = WeekDay.values(); - - if (!isNumber(firstDayOfWeek)) { - firstDayOfWeek = WeekDay.DefaultFirstDay.type; - } - - for (let i = firstDayOfWeek; i < allWeekDays.length; i++) { - const weekDay = allWeekDays[i]; - - ret.push({ - type: weekDay.type, - displayName: translateFn(`datetime.${weekDay.name}.long`) - }); - } - - for (let i = 0; i < firstDayOfWeek; i++) { - const weekDay = allWeekDays[i]; - - ret.push({ - type: weekDay.type, - displayName: translateFn(`datetime.${weekDay.name}.long`) - }); - } - - return ret; -} - function getAllDateRanges(scene, includeCustom, includeBillingCycle, translateFn) { const ret = []; const allDateRanges = DateRange.values(); @@ -460,11 +294,6 @@ function getNumberFormatOptions(translateFn, userStore, currencyCode) { }; } -function getFormattedAmount(value, translateFn, userStore, currencyCode) { - const numberFormatOptions = getNumberFormatOptions(translateFn, userStore, currencyCode); - return formatAmount(value, numberFormatOptions); -} - function getCurrentCurrencyDisplayType(translateFn, userStore) { let currencyDisplayType = CurrencyDisplayType.valueOf(userStore.currentUserCurrencyDisplayType); @@ -525,96 +354,10 @@ function getFormattedAmountWithCurrency(value, currencyCode, translateFn, userSt return appendCurrencySymbol(value, currencyDisplayType, currencyCode, currencyUnit, currencyName, isPlural); } -function getAdaptiveAmountRate(amount1, amount2, fromExchangeRate, toExchangeRate, translateFn, userStore) { - const numberFormatOptions = getNumberFormatOptions(translateFn, userStore); - return getAdaptiveDisplayAmountRate(amount1, amount2, fromExchangeRate, toExchangeRate, numberFormatOptions); -} - function getAllTransactionTagFilterTypes(translateFn) { return getLocalizedDisplayNameAndType(TransactionTagFilterType.values(), translateFn); } -function getCategorizedAccountsWithDisplayBalance(allVisibleAccounts, showAccountBalance, defaultCurrency, userStore, settingsStore, exchangeRatesStore, translateFn) { - const ret = []; - const allCategories = AccountCategory.values(); - const categorizedAccounts = copyObjectTo(getCategorizedAccountsMap(allVisibleAccounts), {}); - - for (let i = 0; i < allCategories.length; i++) { - const category = allCategories[i]; - - if (!categorizedAccounts[category.type]) { - continue; - } - - const accountCategory = categorizedAccounts[category.type]; - - if (accountCategory.accounts) { - for (let i = 0; i < accountCategory.accounts.length; i++) { - const account = accountCategory.accounts[i]; - - if (showAccountBalance && account.isAsset) { - account.displayBalance = getFormattedAmountWithCurrency(account.balance, account.currency, translateFn, userStore, settingsStore); - } else if (showAccountBalance && account.isLiability) { - account.displayBalance = getFormattedAmountWithCurrency(-account.balance, account.currency, translateFn, userStore, settingsStore); - } else { - account.displayBalance = '***'; - } - } - } - - if (showAccountBalance) { - const accountsBalance = getAllFilteredAccountsBalance(categorizedAccounts, account => account.category === accountCategory.category); - let totalBalance = 0; - let hasUnCalculatedAmount = false; - - for (let i = 0; i < accountsBalance.length; i++) { - if (accountsBalance[i].currency === defaultCurrency) { - if (accountsBalance[i].isAsset) { - totalBalance += accountsBalance[i].balance; - } else if (accountsBalance[i].isLiability) { - totalBalance -= accountsBalance[i].balance; - } - } else { - const balance = exchangeRatesStore.getExchangedAmount(accountsBalance[i].balance, accountsBalance[i].currency, defaultCurrency); - - if (!isNumber(balance)) { - hasUnCalculatedAmount = true; - continue; - } - - if (accountsBalance[i].isAsset) { - totalBalance += Math.floor(balance); - } else if (accountsBalance[i].isLiability) { - totalBalance -= Math.floor(balance); - } - } - } - - if (hasUnCalculatedAmount) { - totalBalance = totalBalance + '+'; - } - - accountCategory.displayBalance = getFormattedAmountWithCurrency(totalBalance, defaultCurrency, translateFn, userStore, settingsStore); - } else { - accountCategory.displayBalance = '***'; - } - - ret.push(accountCategory); - } - - return ret; -} - -function joinMultiText(textArray, translateFn) { - if (!textArray || !textArray.length) { - return ''; - } - - const separator = translateFn('format.misc.multiTextJoinSeparator'); - - return textArray.join(separator); -} - function getLocalizedError(error) { if (error.errorCode === KnownErrorCode.ApiNotFound && SPECIFIED_API_NOT_FOUND_ERRORS[error.path]) { return { @@ -685,23 +428,15 @@ export function i18nFunctions(i18nGlobal) { return { getWeekdayShortName: (weekDay) => getWeekdayShortName(weekDay, i18nGlobal.t), getWeekdayLongName: (weekDay) => getWeekdayLongName(weekDay, i18nGlobal.t), - getMultiMonthdayShortNames: (monthdays) => getMultiMonthdayShortNames(monthdays, i18nGlobal.t), - getMultiWeekdayLongNames: (weekdayTypes, firstDayOfWeek) => getMultiWeekdayLongNames(weekdayTypes, firstDayOfWeek, i18nGlobal.t), formatUnixTimeToLongDateTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongDateFormat(i18nGlobal.t, userStore.currentUserLongDateFormat) + ' ' + getI18nLongTimeFormat(i18nGlobal.t, userStore.currentUserLongTimeFormat), utcOffset, currentUtcOffset), formatUnixTimeToLongDate: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongDateFormat(i18nGlobal.t, userStore.currentUserLongDateFormat), utcOffset, currentUtcOffset), formatUnixTimeToLongYear: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongYearFormat(i18nGlobal.t, userStore.currentUserLongDateFormat), utcOffset, currentUtcOffset), formatUnixTimeToLongYearMonth: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongYearMonthFormat(i18nGlobal.t, userStore.currentUserLongDateFormat), utcOffset, currentUtcOffset), - formatUnixTimeToLongTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongTimeFormat(i18nGlobal.t, userStore.currentUserLongTimeFormat), utcOffset, currentUtcOffset), formatUnixTimeToShortTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nShortTimeFormat(i18nGlobal.t, userStore.currentUserShortTimeFormat), utcOffset, currentUtcOffset), - getAllTimezones: (includeSystemDefault) => getAllTimezones(includeSystemDefault, i18nGlobal.t), - getTimezoneDifferenceDisplayText: (utcOffset) => getTimezoneDifferenceDisplayText(utcOffset, i18nGlobal.t), getAllDateRanges: (scene, includeCustom, includeBillingCycle) => getAllDateRanges(scene, includeCustom, includeBillingCycle, i18nGlobal.t), getAllRecentMonthDateRanges: (userStore, includeAll, includeCustom) => getAllRecentMonthDateRanges(userStore, includeAll, includeCustom, i18nGlobal.t), getDateRangeDisplayName: (userStore, dateType, startTime, endTime) => getDateRangeDisplayName(userStore, dateType, startTime, endTime, i18nGlobal.t), - formatAmount: (userStore, value, currencyCode) => getFormattedAmount(value, i18nGlobal.t, userStore, currencyCode), formatAmountWithCurrency: (settingsStore, userStore, value, currencyCode) => getFormattedAmountWithCurrency(value, currencyCode, i18nGlobal.t, userStore, settingsStore), - getAdaptiveAmountRate: (userStore, amount1, amount2, fromExchangeRate, toExchangeRate) => getAdaptiveAmountRate(amount1, amount2, fromExchangeRate, toExchangeRate, i18nGlobal.t, userStore), - getAllTransactionTagFilterTypes: () => getAllTransactionTagFilterTypes(i18nGlobal.t), - getCategorizedAccountsWithDisplayBalance: (allVisibleAccounts, showAccountBalance, defaultCurrency, settingsStore, userStore, exchangeRatesStore) => getCategorizedAccountsWithDisplayBalance(allVisibleAccounts, showAccountBalance, defaultCurrency, userStore, settingsStore, exchangeRatesStore, i18nGlobal.t) + getAllTransactionTagFilterTypes: () => getAllTransactionTagFilterTypes(i18nGlobal.t) }; } diff --git a/src/mobile-main.ts b/src/mobile-main.ts index 0f0e24ef..f52f59df 100644 --- a/src/mobile-main.ts +++ b/src/mobile-main.ts @@ -86,7 +86,6 @@ import { getI18nOptions } from '@/locales/helpers.ts'; import { i18nFunctions } from '@/locales/helper.js'; import { showAlert, - showConfirm, showToast, showLoading, hideLoading, @@ -204,7 +203,6 @@ app.directive('TextareaAutoSize', TextareaAutoSize); app.config.globalProperties['$locale'] = i18nFunctions(i18n.global); app.config.globalProperties['$alert'] = (message: string, confirmCallback: ((dialog: Dialog.Dialog, e: Event) => void) | undefined) => showAlert(message, confirmCallback, i18n.global.t); -app.config.globalProperties['$confirm'] = (message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback: ((dialog: Dialog.Dialog, e: Event) => void) | undefined) => showConfirm(message, confirmCallback, cancelCallback, i18n.global.t); app.config.globalProperties['$toast'] = (message: string, timeout: number | undefined) => showToast(message, timeout, i18n.global.t); app.config.globalProperties['$showLoading'] = showLoading; app.config.globalProperties['$hideLoading'] = hideLoading; diff --git a/src/models/transaction_template.ts b/src/models/transaction_template.ts index 2b322391..a30dc8a3 100644 --- a/src/models/transaction_template.ts +++ b/src/models/transaction_template.ts @@ -23,6 +23,18 @@ export class TransactionTemplate extends Transaction implements TransactionTempl this.hidden = hidden; } + public from(other: TransactionTemplate): void { + this.templateType = other.templateType; + this.name = other.name; + + if (this.templateType === TemplateType.Schedule.type) { + this.scheduledFrequencyType = other.scheduledFrequencyType; + this.scheduledFrequency = other.scheduledFrequency; + this.utcOffset = other.utcOffset; + this.timeZone = undefined; + } + } + public toTemplateCreateRequest(clientSessionId: string): TransactionTemplateCreateRequest { return { templateType: this.templateType, diff --git a/src/stores/transaction.ts b/src/stores/transaction.ts index 2d0f106d..013e385f 100644 --- a/src/stores/transaction.ts +++ b/src/stores/transaction.ts @@ -43,7 +43,6 @@ import { countSplitItems } from '@/lib/common.ts'; import { - getCurrentUnixTime, getTimezoneOffsetMinutes, getBrowserTimezoneOffsetMinutes, getActualUnixTimeForStore, @@ -510,21 +509,6 @@ export const useTransactionsStore = defineStore('transactions', () => { clearUserTransactionDraft(); } - function generateNewTransactionModel(type: string): Transaction { - const now: number = getCurrentUnixTime(); - const currentTimezone: string = settingsStore.appSettings.timeZone; - - let defaultType: TransactionType = TransactionType.Expense; - - if (type === TransactionType.Income.toString()) { - defaultType = TransactionType.Income; - } else if (type === TransactionType.Transfer.toString()) { - defaultType = TransactionType.Transfer; - } - - return Transaction.createNewTransaction(defaultType, now, currentTimezone, getTimezoneOffsetMinutes(currentTimezone)); - } - function setTransactionSuitableDestinationAmount(transaction: Transaction, oldValue: number, newValue: number): void { if (transaction.type === TransactionType.Expense || transaction.type === TransactionType.Income) { transaction.destinationAmount = newValue; @@ -1133,7 +1117,7 @@ export const useTransactionsStore = defineStore('transactions', () => { }); } - function uploadTransactionPicture({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId: string }): Promise { + function uploadTransactionPicture({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId?: string }): Promise { return new Promise((resolve, reject) => { services.uploadTransactionPicture({ pictureFile, clientSessionId }).then(response => { const data = response.data; @@ -1183,9 +1167,9 @@ export const useTransactionsStore = defineStore('transactions', () => { }); } - function getTransactionPictureUrl(pictureInfo?: TransactionPictureInfoBasicResponse | null, disableBrowserCache?: boolean | string): string | null { + function getTransactionPictureUrl(pictureInfo?: TransactionPictureInfoBasicResponse | null, disableBrowserCache?: boolean | string): string | undefined { if (!pictureInfo || !pictureInfo.originalUrl) { - return null; + return undefined; } return services.getTransactionPictureUrlWithToken(pictureInfo.originalUrl, disableBrowserCache); @@ -1218,7 +1202,6 @@ export const useTransactionsStore = defineStore('transactions', () => { isTransactionDraftModified, saveTransactionDraft, clearTransactionDraft, - generateNewTransactionModel, setTransactionSuitableDestinationAmount, updateTransactionListInvalidState, resetTransactions, diff --git a/src/views/base/transactions/TransactionEditPageBase.ts b/src/views/base/transactions/TransactionEditPageBase.ts new file mode 100644 index 00000000..d7047983 --- /dev/null +++ b/src/views/base/transactions/TransactionEditPageBase.ts @@ -0,0 +1,450 @@ +import { ref, computed, watch } from 'vue'; + +import { useI18n } from '@/locales/helpers.ts'; + +import { useSettingsStore } from '@/stores/setting.ts'; +import { useUserStore } from '@/stores/user.ts'; +import { useAccountsStore } from '@/stores/account.ts'; +import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; +import { useTransactionTagsStore } from '@/stores/transactionTag.ts'; +import { useTransactionsStore } from '@/stores/transaction.ts'; +import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; + +import type { LocalizedTimezoneInfo } from '@/core/timezone.ts'; +import { CategoryType } from '@/core/category.ts'; +import { TransactionType } from '@/core/transaction.ts'; +import { TemplateType } from '@/core/template.ts'; +import { TRANSACTION_MAX_PICTURE_COUNT } from '@/consts/transaction.ts'; + +import { Account, type CategorizedAccountWithDisplayBalance } from '@/models/account.ts'; +import type { TransactionCategory } from '@/models/transaction_category.ts'; +import type { TransactionTag } from '@/models/transaction_tag.ts'; +import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts'; +import { Transaction } from '@/models/transaction.ts'; +import { TransactionTemplate } from '@/models/transaction_template.ts'; + +import { + isArray +} from '@/lib/common.ts'; + +import { + getUtcOffsetByUtcOffsetMinutes, + getTimezoneOffsetMinutes, + getCurrentUnixTime +} from '@/lib/datetime.ts'; + +import { + getFirstAvailableCategoryId +} from '@/lib/category.ts'; + +export enum TransactionEditPageType { + Transaction = 'transaction', + Template = 'template' +} + +export enum TransactionEditPageMode { + Add = 'add', + Edit = 'edit', + View = 'view' +} + +export enum GeoLocationStatus { + Getting = 'getting', + Success = 'success', + Error = 'error' +} + +export function useTransactionEditPageBase(type: TransactionEditPageType, initMode?: TransactionEditPageMode, transactionDefaultType?: number) { + const { + tt, + getAllTimezones, + getTimezoneDifferenceDisplayText, + formatAmountWithCurrency, + getAdaptiveAmountRate, + getCategorizedAccountsWithDisplayBalance + } = useI18n(); + + const settingsStore = useSettingsStore(); + const userStore = useUserStore(); + const accountsStore = useAccountsStore(); + const transactionCategoriesStore = useTransactionCategoriesStore(); + const transactionTagsStore = useTransactionTagsStore(); + const transactionsStore = useTransactionsStore(); + const exchangeRatesStore = useExchangeRatesStore(); + + const isSupportGeoLocation: boolean = !!navigator.geolocation; + + const mode = ref(initMode ?? TransactionEditPageMode.Add); + const editId = ref(null); + const addByTemplateId = ref(null); + const duplicateFromId = ref(null); + + const clientSessionId = ref(''); + const loading = ref(true); + const submitting = ref(false); + const uploadingPicture = ref(false); + const geoLocationStatus = ref(null); + + const transaction = ref(createNewTransactionModel(transactionDefaultType)); + + const currentTimezoneOffsetMinutes = computed(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone)); + const showAccountBalance = computed(() => settingsStore.appSettings.showAccountBalance); + const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); + const defaultAccountId = computed(() => userStore.currentUserDefaultAccountId); + const firstDayOfWeek = computed(() => userStore.currentUserFirstDayOfWeek); + + const allTimezones = computed(() => getAllTimezones(true)); + const allAccounts = computed(() => accountsStore.allPlainAccounts); + const allVisibleAccounts = computed(() => accountsStore.allVisiblePlainAccounts); + const allAccountsMap = computed>(() => accountsStore.allAccountsMap); + const allVisibleCategorizedAccounts = computed(() => getCategorizedAccountsWithDisplayBalance(allVisibleAccounts.value, showAccountBalance.value)); + const allCategories = computed>(() => transactionCategoriesStore.allTransactionCategories); + const allCategoriesMap = computed>(() => transactionCategoriesStore.allTransactionCategoriesMap); + const allTags = computed(() => transactionTagsStore.allTransactionTags); + const allTagsMap = computed>(() => transactionTagsStore.allTransactionTagsMap); + + const canAddTransactionPicture = computed(() => { + if (type !== TransactionEditPageType.Transaction || (mode.value !== TransactionEditPageMode.Add && mode.value !== TransactionEditPageMode.Edit)) { + return false; + } + + return !isArray(transaction.value.pictures) || transaction.value.pictures.length < TRANSACTION_MAX_PICTURE_COUNT; + }); + + const title = computed(() => { + if (type === TransactionEditPageType.Transaction) { + if (mode.value === TransactionEditPageMode.Add) { + return 'Add Transaction'; + } else if (mode.value === TransactionEditPageMode.Edit) { + return 'Edit Transaction'; + } else { + return 'Transaction Detail'; + } + } else if (type === TransactionEditPageType.Template && (transaction.value as TransactionTemplate).templateType === TemplateType.Normal.type) { + if (mode.value === TransactionEditPageMode.Add) { + return 'Add Transaction Template'; + } else if (mode.value === TransactionEditPageMode.Edit) { + return 'Edit Transaction Template'; + } + } else if (type === TransactionEditPageType.Template && (transaction.value as TransactionTemplate).templateType === TemplateType.Schedule.type) { + if (mode.value === TransactionEditPageMode.Add) { + return 'Add Scheduled Transaction'; + } else if (mode.value === TransactionEditPageMode.Edit) { + return 'Edit Scheduled Transaction'; + } + } + + return ''; + }); + + const saveButtonTitle = computed(() => { + if (mode.value === TransactionEditPageMode.Add) { + return 'Add'; + } else { + return 'Save'; + } + }); + + const cancelButtonTitle = computed(() => { + if (mode.value === TransactionEditPageMode.View) { + return 'Close'; + } else { + return 'Cancel'; + } + }); + + const sourceAmountName = computed(() => { + if (transaction.value.type === TransactionType.Expense) { + return 'Expense Amount'; + } else if (transaction.value.type === TransactionType.Income) { + return 'Income Amount'; + } else if (transaction.value.type === TransactionType.Transfer) { + return 'Transfer Out Amount'; + } else { + return 'Amount'; + } + }); + + const sourceAccountTitle = computed(() => { + if (transaction.value.type === TransactionType.Expense || transaction.value.type === TransactionType.Income) { + return 'Account'; + } else if (transaction.value.type === TransactionType.Transfer) { + return 'Source Account'; + } else { + return 'Account'; + } + }); + + const transferInAmountTitle = computed(() => { + const sourceAccount = allAccountsMap.value[transaction.value.sourceAccountId]; + const destinationAccount = allAccountsMap.value[transaction.value.destinationAccountId]; + + if (!sourceAccount || !destinationAccount || sourceAccount.currency === destinationAccount.currency) { + return tt('Transfer In Amount'); + } + + const fromExchangeRate = exchangeRatesStore.latestExchangeRateMap[sourceAccount.currency]; + const toExchangeRate = exchangeRatesStore.latestExchangeRateMap[destinationAccount.currency]; + const amountRate = getAdaptiveAmountRate(transaction.value.sourceAmount, transaction.value.destinationAmount, fromExchangeRate, toExchangeRate); + + if (!amountRate) { + return tt('Transfer In Amount'); + } + + return tt('Transfer In Amount') + ` (${amountRate})`; + }); + + const hasAvailableExpenseCategories = computed(() => { + if (!allCategories.value || !allCategories.value[CategoryType.Expense] || !allCategories.value[CategoryType.Expense].length) { + return false; + } + + const firstAvailableCategoryId = getFirstAvailableCategoryId(allCategories.value[CategoryType.Expense]); + return firstAvailableCategoryId !== ''; + }); + + const hasAvailableIncomeCategories = computed(() => { + if (!allCategories.value || !allCategories.value[CategoryType.Income] || !allCategories.value[CategoryType.Income].length) { + return false; + } + + const firstAvailableCategoryId = getFirstAvailableCategoryId(allCategories.value[CategoryType.Income]); + return firstAvailableCategoryId !== ''; + }); + + const hasAvailableTransferCategories = computed(() => { + if (!allCategories.value || !allCategories.value[CategoryType.Transfer] || !allCategories.value[CategoryType.Transfer].length) { + return false; + } + + const firstAvailableCategoryId = getFirstAvailableCategoryId(allCategories.value[CategoryType.Transfer]); + return firstAvailableCategoryId !== ''; + }); + + const sourceAccountName = computed(() => { + if (transaction.value.sourceAccountId) { + return Account.findAccountNameById(allAccounts.value, transaction.value.sourceAccountId) || ''; + } else { + return tt('None'); + } + }); + + const destinationAccountName = computed(() => { + if (transaction.value.destinationAccountId) { + return Account.findAccountNameById(allAccounts.value, transaction.value.destinationAccountId) || ''; + } else { + return tt('None'); + } + }); + + const sourceAccountCurrency = computed(() => { + const sourceAccount = allAccountsMap.value[transaction.value.sourceAccountId]; + + if (sourceAccount) { + return sourceAccount.currency; + } + + return defaultCurrency.value; + }); + + const destinationAccountCurrency = computed(() => { + const destinationAccount = allAccountsMap.value[transaction.value.destinationAccountId]; + + if (destinationAccount) { + return destinationAccount.currency; + } + + return defaultCurrency.value; + }); + + const transactionDisplayTimezone = computed(() => { + return `UTC${getUtcOffsetByUtcOffsetMinutes(transaction.value.utcOffset)}`; + }); + + const transactionTimezoneTimeDifference = computed(() => { + return getTimezoneDifferenceDisplayText(transaction.value.utcOffset); + }); + + const geoLocationStatusInfo = computed(() => { + if (geoLocationStatus.value === GeoLocationStatus.Success) { + return ''; + } else if (geoLocationStatus.value === GeoLocationStatus.Getting) { + return tt('Getting Location...'); + } else { + return tt('No Location'); + } + }); + + const inputEmptyProblemMessage = computed(() => { + if (transaction.value.type === TransactionType.Expense) { + if (!transaction.value.expenseCategoryId || transaction.value.expenseCategoryId === '') { + return 'Transaction category cannot be blank'; + } + + if (!transaction.value.sourceAccountId || transaction.value.sourceAccountId === '') { + return 'Transaction account cannot be blank'; + } + } else if (transaction.value.type === TransactionType.Income) { + if (!transaction.value.incomeCategoryId || transaction.value.incomeCategoryId === '') { + return 'Transaction category cannot be blank'; + } + + if (!transaction.value.sourceAccountId || transaction.value.sourceAccountId === '') { + return 'Transaction account cannot be blank'; + } + } else if (transaction.value.type === TransactionType.Transfer) { + if (!transaction.value.transferCategoryId || transaction.value.transferCategoryId === '') { + return 'Transaction category cannot be blank'; + } + + if (!transaction.value.sourceAccountId || transaction.value.sourceAccountId === '') { + return 'Source account cannot be blank'; + } + + if (!transaction.value.destinationAccountId || transaction.value.destinationAccountId === '') { + return 'Destination account cannot be blank'; + } + } + + if (type === 'template' && transaction.value instanceof TransactionTemplate) { + if (!transaction.value.name) { + return 'Template name cannot be blank'; + } + } + + return null; + }); + + const inputIsEmpty = computed(() => { + return !!inputEmptyProblemMessage.value; + }); + + function createNewTransactionModel(transactionType?: number): Transaction | TransactionTemplate { + const now: number = getCurrentUnixTime(); + const currentTimezone: string = settingsStore.appSettings.timeZone; + + let defaultType: TransactionType = TransactionType.Expense; + + if (transactionType === TransactionType.Income) { + defaultType = TransactionType.Income; + } else if (transactionType === TransactionType.Transfer) { + defaultType = TransactionType.Transfer; + } + + let newTransaction: Transaction | TransactionTemplate = Transaction.createNewTransaction(defaultType, now, currentTimezone, getTimezoneOffsetMinutes(currentTimezone)); + + if (type === TransactionEditPageType.Template) { + newTransaction = TransactionTemplate.createNewTransactionTemplate(newTransaction); + } + + return newTransaction; + } + + function swapTransactionData(swapAccount: boolean, swapAmount: boolean): void { + if (swapAccount) { + const oldSourceAccountId = transaction.value.sourceAccountId; + transaction.value.sourceAccountId = transaction.value.destinationAccountId; + transaction.value.destinationAccountId = oldSourceAccountId; + } + + if (swapAmount) { + const oldSourceAmount = transaction.value.sourceAmount; + transaction.value.sourceAmount = transaction.value.destinationAmount; + transaction.value.destinationAmount = oldSourceAmount; + } + } + + function getDisplayAmount(amount: number | string, hideAmount: boolean, currencyCode: string): string { + if (hideAmount) { + return formatAmountWithCurrency('***', currencyCode); + } + + return formatAmountWithCurrency(amount, currencyCode); + } + + function getTransactionPictureUrl(pictureInfo?: TransactionPictureInfoBasicResponse | null): string | undefined { + return transactionsStore.getTransactionPictureUrl(pictureInfo); + } + + watch(() => transaction.value.sourceAmount, (newValue, oldValue) => { + if (mode.value === TransactionEditPageMode.View || loading.value) { + return; + } + + transactionsStore.setTransactionSuitableDestinationAmount(transaction.value, oldValue, newValue); + }); + + watch(() => transaction.value.destinationAmount, (newValue) => { + if (mode.value === TransactionEditPageMode.View || loading.value) { + return; + } + + if (transaction.value.type === TransactionType.Expense || transaction.value.type === TransactionType.Income) { + transaction.value.sourceAmount = newValue; + } + }); + + watch(() => transaction.value.timeZone, (newValue) => { + for (let i = 0; i < allTimezones.value.length; i++) { + if (allTimezones.value[i].name === newValue) { + transaction.value.utcOffset = allTimezones.value[i].utcOffsetMinutes; + break; + } + } + }); + + return { + // constants + isSupportGeoLocation, + // states + mode, + editId, + addByTemplateId, + duplicateFromId, + clientSessionId, + loading, + submitting, + uploadingPicture, + geoLocationStatus, + transaction, + // computed states + currentTimezoneOffsetMinutes, + showAccountBalance, + defaultCurrency, + firstDayOfWeek, + defaultAccountId, + allTimezones, + allAccounts, + allVisibleAccounts, + allAccountsMap, + allVisibleCategorizedAccounts, + allCategories, + allCategoriesMap, + allTags, + allTagsMap, + canAddTransactionPicture, + title, + saveButtonTitle, + cancelButtonTitle, + sourceAmountName, + sourceAccountTitle, + transferInAmountTitle, + hasAvailableExpenseCategories, + hasAvailableIncomeCategories, + hasAvailableTransferCategories, + sourceAccountName, + destinationAccountName, + sourceAccountCurrency, + destinationAccountCurrency, + transactionDisplayTimezone, + transactionTimezoneTimeDifference, + geoLocationStatusInfo, + inputEmptyProblemMessage, + inputIsEmpty, + // functions + createNewTransactionModel, + swapTransactionData, + getDisplayAmount, + getTransactionPictureUrl + } +} diff --git a/src/views/desktop/templates/ListPage.vue b/src/views/desktop/templates/ListPage.vue index e8ba27ae..23477c1c 100644 --- a/src/views/desktop/templates/ListPage.vue +++ b/src/views/desktop/templates/ListPage.vue @@ -139,7 +139,7 @@ - + @@ -149,6 +149,7 @@ import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue'; import SnackBar from '@/components/desktop/SnackBar.vue'; import EditDialog from '@/views/desktop/transactions/list/dialogs/EditDialog.vue'; +import { TransactionEditPageType } from '@/views/base/transactions/TransactionEditPageBase.ts'; import { ref, computed, useTemplateRef } from 'vue'; diff --git a/src/views/desktop/transactions/list/dialogs/EditDialog.vue b/src/views/desktop/transactions/list/dialogs/EditDialog.vue index 59fe1be3..e0adb59d 100644 --- a/src/views/desktop/transactions/list/dialogs/EditDialog.vue +++ b/src/views/desktop/transactions/list/dialogs/EditDialog.vue @@ -1,35 +1,35 @@
- - - + + +
@@ -351,7 +351,7 @@ class="transaction-picture transaction-picture-add" :class="{ 'enabled': !submitting, 'cursor-pointer': !submitting }" color="rgba(0,0,0,0)" @click="showOpenPictureDialog"> - {{ $t('Add Picture') }} + {{ tt('Add Picture') }} @@ -362,23 +362,23 @@
- - {{ $t(saveButtonTitle) }} + + {{ tt(saveButtonTitle) }} {{ $t('Duplicate') }} + v-if="mode === TransactionEditPageMode.View && transaction.type !== TransactionType.ModifyBalance" + @click="duplicate">{{ tt('Duplicate') }} {{ $t('Edit') }} + v-if="mode === TransactionEditPageMode.View && originalTransactionEditable && transaction.type !== TransactionType.ModifyBalance" + @click="edit">{{ tt('Edit') }} - {{ $t('Delete') }} + v-if="mode === TransactionEditPageMode.View && originalTransactionEditable" @click="remove"> + {{ tt('Delete') }} {{ $t(cancelButtonTitle) }} + @click="cancel">{{ tt(cancelButtonTitle) }}
@@ -386,11 +386,24 @@ - + -