migrate transaction edit page to composition API and typescript

This commit is contained in:
MaysWind
2025-02-05 00:02:40 +08:00
parent 3e7b3297aa
commit 833e767e6c
12 changed files with 1979 additions and 2264 deletions
-6
View File
@@ -5,7 +5,6 @@ type TemplateTypeName = 'Normal' | 'Schedule';
export class TemplateType implements TypeAndName {
private static readonly allInstances: TemplateType[] = [];
private static readonly allInstancesByType: Record<number, TemplateType> = {};
private static readonly allInstancesByTypeName: Record<string, TemplateType> = {};
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<TemplateTypeName, TemplateType> {
return TemplateType.allInstancesByTypeName;
}
public static valueOf(type: number): TemplateType | undefined {
return TemplateType.allInstancesByType[type];
}
+1 -1
View File
@@ -434,7 +434,7 @@ export default {
timeout: DEFAULT_IMPORT_API_TIMEOUT
} as ApiRequestConfig);
},
uploadTransactionPicture: ({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId: string }): ApiResponsePromise<TransactionPictureInfoBasicResponse> => {
uploadTransactionPicture: ({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId?: string }): ApiResponsePromise<TransactionPictureInfoBasicResponse> => {
return axios.postForm<ApiResponse<TransactionPictureInfoBasicResponse>>('v1/transaction/pictures/upload.json', {
picture: pictureFile,
clientSessionId: clientSessionId
+8 -8
View File
@@ -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 {
+24 -24
View File
@@ -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<T>(f7router: Router.Router, errorRef: Ref<T>): 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
}
}
+4 -269
View File
@@ -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)
};
}
-2
View File
@@ -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;
+12
View File
@@ -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,
+3 -20
View File
@@ -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<TransactionPictureInfoBasicResponse> {
function uploadTransactionPicture({ pictureFile, clientSessionId }: { pictureFile: File, clientSessionId?: string }): Promise<TransactionPictureInfoBasicResponse> {
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,
@@ -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<TransactionEditPageMode>(initMode ?? TransactionEditPageMode.Add);
const editId = ref<string | null>(null);
const addByTemplateId = ref<string | null>(null);
const duplicateFromId = ref<string | null>(null);
const clientSessionId = ref<string>('');
const loading = ref<boolean>(true);
const submitting = ref<boolean>(false);
const uploadingPicture = ref<boolean>(false);
const geoLocationStatus = ref<GeoLocationStatus | null>(null);
const transaction = ref<Transaction | TransactionTemplate>(createNewTransactionModel(transactionDefaultType));
const currentTimezoneOffsetMinutes = computed<number>(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone));
const showAccountBalance = computed<boolean>(() => settingsStore.appSettings.showAccountBalance);
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const defaultAccountId = computed<string>(() => userStore.currentUserDefaultAccountId);
const firstDayOfWeek = computed<number>(() => userStore.currentUserFirstDayOfWeek);
const allTimezones = computed<LocalizedTimezoneInfo[]>(() => getAllTimezones(true));
const allAccounts = computed<Account[]>(() => accountsStore.allPlainAccounts);
const allVisibleAccounts = computed<Account[]>(() => accountsStore.allVisiblePlainAccounts);
const allAccountsMap = computed<Record<string, Account>>(() => accountsStore.allAccountsMap);
const allVisibleCategorizedAccounts = computed<CategorizedAccountWithDisplayBalance[]>(() => getCategorizedAccountsWithDisplayBalance(allVisibleAccounts.value, showAccountBalance.value));
const allCategories = computed<Record<number, TransactionCategory[]>>(() => transactionCategoriesStore.allTransactionCategories);
const allCategoriesMap = computed<Record<string, TransactionCategory>>(() => transactionCategoriesStore.allTransactionCategoriesMap);
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
const allTagsMap = computed<Record<string, TransactionTag>>(() => transactionTagsStore.allTransactionTagsMap);
const canAddTransactionPicture = computed<boolean>(() => {
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<string>(() => {
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<string>(() => {
if (mode.value === TransactionEditPageMode.Add) {
return 'Add';
} else {
return 'Save';
}
});
const cancelButtonTitle = computed<string>(() => {
if (mode.value === TransactionEditPageMode.View) {
return 'Close';
} else {
return 'Cancel';
}
});
const sourceAmountName = computed<string>(() => {
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<string>(() => {
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<string>(() => {
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<boolean>(() => {
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<boolean>(() => {
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<boolean>(() => {
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<string>(() => {
if (transaction.value.sourceAccountId) {
return Account.findAccountNameById(allAccounts.value, transaction.value.sourceAccountId) || '';
} else {
return tt('None');
}
});
const destinationAccountName = computed<string>(() => {
if (transaction.value.destinationAccountId) {
return Account.findAccountNameById(allAccounts.value, transaction.value.destinationAccountId) || '';
} else {
return tt('None');
}
});
const sourceAccountCurrency = computed<string>(() => {
const sourceAccount = allAccountsMap.value[transaction.value.sourceAccountId];
if (sourceAccount) {
return sourceAccount.currency;
}
return defaultCurrency.value;
});
const destinationAccountCurrency = computed<string>(() => {
const destinationAccount = allAccountsMap.value[transaction.value.destinationAccountId];
if (destinationAccount) {
return destinationAccount.currency;
}
return defaultCurrency.value;
});
const transactionDisplayTimezone = computed<string>(() => {
return `UTC${getUtcOffsetByUtcOffsetMinutes(transaction.value.utcOffset)}`;
});
const transactionTimezoneTimeDifference = computed<string>(() => {
return getTimezoneDifferenceDisplayText(transaction.value.utcOffset);
});
const geoLocationStatusInfo = computed<string>(() => {
if (geoLocationStatus.value === GeoLocationStatus.Success) {
return '';
} else if (geoLocationStatus.value === GeoLocationStatus.Getting) {
return tt('Getting Location...');
} else {
return tt('No Location');
}
});
const inputEmptyProblemMessage = computed<string | null>(() => {
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<boolean>(() => {
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
}
}
+2 -1
View File
@@ -139,7 +139,7 @@
</v-col>
</v-row>
<edit-dialog ref="editDialog" type="template" :persistent="true" />
<edit-dialog ref="editDialog" :type="TransactionEditPageType.Template" :persistent="true" />
<confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" />
@@ -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';
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff