Files
ezbookkeeping/src/views/base/transactions/TransactionEditPageBase.ts
T

486 lines
20 KiB
TypeScript

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 { NumeralSystem } from '@/core/numeral.ts';
import type { WeekDayValue } from '@/core/datetime.ts';
import type { LocalizedTimezoneInfo } from '@/core/timezone.ts';
import { TransactionType } from '@/core/transaction.ts';
import { TemplateType } from '@/core/template.ts';
import { DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.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 {
getExchangedAmountByRate
} from '@/lib/numeral.ts';
import {
getUtcOffsetByUtcOffsetMinutes,
getTimezoneOffsetMinutes,
getSameDateTimeWithCurrentTimezone,
parseDateTimeFromUnixTimeWithBrowserTimezone,
getCurrentUnixTime
} from '@/lib/datetime.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,
getCurrentNumeralSystemType,
getTimezoneDifferenceDisplayText,
formatAmountToLocalizedNumeralsWithCurrency,
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 setGeoLocationByClickMap = ref<boolean>(false);
const transaction = ref<Transaction | TransactionTemplate>(createNewTransactionModel(transactionDefaultType));
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
const currentTimezoneOffsetMinutes = computed<number>(() => getTimezoneOffsetMinutes(transaction.value.time));
const showAccountBalance = computed<boolean>(() => settingsStore.appSettings.showAccountBalance);
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const defaultAccountId = computed<string>(() => userStore.currentUserDefaultAccountId);
const firstDayOfWeek = computed<WeekDayValue>(() => userStore.currentUserFirstDayOfWeek);
const coordinateDisplayType = computed<number>(() => userStore.currentUserCoordinateDisplayType);
const allTimezones = computed<LocalizedTimezoneInfo[]>(() => getAllTimezones(transaction.value.time, 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 firstVisibleAccountId = computed<string | undefined>(() => allVisibleAccounts.value && allVisibleAccounts.value[0] ? allVisibleAccounts.value[0].id : undefined);
const hasVisibleExpenseCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleExpenseCategories);
const hasVisibleIncomeCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleIncomeCategories);
const hasVisibleTransferCategories = computed<boolean>(() => transactionCategoriesStore.hasVisibleTransferCategories);
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 sourceAmountTitle = computed<string>(() => {
const sourceAccount = allAccountsMap.value[transaction.value.sourceAccountId];
const amountName = tt(sourceAmountName.value);
if (!sourceAccount || sourceAccount.currency === defaultCurrency.value || !transaction.value.sourceAmount || transaction.value.hideAmount) {
return amountName;
}
const fromExchangeRate = exchangeRatesStore.latestExchangeRateMap[sourceAccount.currency];
const toExchangeRate = exchangeRatesStore.latestExchangeRateMap[defaultCurrency.value];
if (!fromExchangeRate || !fromExchangeRate.rate || !toExchangeRate || !toExchangeRate.rate) {
return amountName;
}
let amountInDefaultCurrency = getExchangedAmountByRate(transaction.value.sourceAmount, fromExchangeRate.rate, toExchangeRate.rate);
if (!amountInDefaultCurrency) {
return amountName;
}
amountInDefaultCurrency = Math.trunc(amountInDefaultCurrency);
const displayAmountInDefaultCurrency = getDisplayAmount(amountInDefaultCurrency, transaction.value.hideAmount, defaultCurrency.value);
return amountName + ` (${displayAmountInDefaultCurrency})`;
});
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 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>(() => {
const utcOffset = numeralSystem.value.replaceWesternArabicDigitsToLocalizedDigits(getUtcOffsetByUtcOffsetMinutes(transaction.value.utcOffset));
return `UTC${utcOffset}`;
});
const transactionTimezoneTimeDifference = computed<string>(() => {
return getTimezoneDifferenceDisplayText(transaction.value.time, 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 getCurrentUnixTimeForNewTransaction(): number {
return getSameDateTimeWithCurrentTimezone(parseDateTimeFromUnixTimeWithBrowserTimezone(getCurrentUnixTime())).getUnixTime();
}
function createNewTransactionModel(transactionType?: number): Transaction | TransactionTemplate {
const now: number = getCurrentUnixTimeForNewTransaction();
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(now, currentTimezone));
if (type === TransactionEditPageType.Template) {
newTransaction = TransactionTemplate.createNewTransactionTemplate(newTransaction);
}
return newTransaction;
}
function updateTransactionTime(newTime: number): void {
transaction.value.time = newTime;
updateTransactionTimezone(transaction.value.timeZone ?? '');
}
function updateTransactionTimezone(timezoneName: string): void {
const oldUtcOffset = transaction.value.utcOffset;
for (const timezone of allTimezones.value) {
if (timezone.name === timezoneName) {
transaction.value.timeZone = timezone.name;
transaction.value.utcOffset = timezone.utcOffsetMinutes;
break;
}
}
transaction.value.time = transaction.value.time - (transaction.value.utcOffset - oldUtcOffset) * 60;
}
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, hideAmount: boolean, currencyCode: string): string {
if (hideAmount) {
return formatAmountToLocalizedNumeralsWithCurrency(DISPLAY_HIDDEN_AMOUNT, currencyCode);
}
return formatAmountToLocalizedNumeralsWithCurrency(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;
}
});
return {
// constants
isSupportGeoLocation,
// states
mode,
editId,
addByTemplateId,
duplicateFromId,
clientSessionId,
loading,
submitting,
uploadingPicture,
geoLocationStatus,
setGeoLocationByClickMap,
transaction,
// computed states
numeralSystem,
currentTimezoneOffsetMinutes,
showAccountBalance,
defaultCurrency,
defaultAccountId,
firstDayOfWeek,
coordinateDisplayType,
allTimezones,
allAccounts,
allVisibleAccounts,
allAccountsMap,
allVisibleCategorizedAccounts,
allCategories,
allCategoriesMap,
allTags,
allTagsMap,
firstVisibleAccountId,
hasVisibleExpenseCategories,
hasVisibleIncomeCategories,
hasVisibleTransferCategories,
canAddTransactionPicture,
title,
saveButtonTitle,
cancelButtonTitle,
sourceAmountName,
sourceAmountTitle,
sourceAccountTitle,
transferInAmountTitle,
sourceAccountName,
destinationAccountName,
sourceAccountCurrency,
destinationAccountCurrency,
transactionDisplayTimezone,
transactionTimezoneTimeDifference,
geoLocationStatusInfo,
inputEmptyProblemMessage,
inputIsEmpty,
// functions
createNewTransactionModel,
updateTransactionTime,
updateTransactionTimezone,
swapTransactionData,
getDisplayAmount,
getTransactionPictureUrl
}
}