transaction reconciliation statement supports sorting by account name and category name on desktop version

This commit is contained in:
MaysWind
2025-11-23 00:32:38 +08:00
parent 83bd68e7f4
commit 44dc45de51
4 changed files with 97 additions and 65 deletions

View File

@@ -690,6 +690,22 @@ export interface TransactionReconciliationStatementResponse {
readonly closingBalance: number;
}
export interface TransactionReconciliationStatementResponseItemWithInfo extends TransactionReconciliationStatementResponseItem {
readonly sourceAccount?: Account;
readonly sourceAccountName: string;
readonly destinationAccount?: Account;
readonly category?: TransactionCategory;
readonly categoryName: string;
}
export interface TransactionReconciliationStatementResponseWithInfo {
readonly transactions: TransactionReconciliationStatementResponseItemWithInfo[];
readonly totalInflows: number;
readonly totalOutflows: number;
readonly openingBalance: number;
readonly closingBalance: number;
}
export interface TransactionPageWrapper {
readonly items: Transaction[];
readonly totalCount?: number;

View File

@@ -16,7 +16,8 @@ import type { Account } from '@/models/account.ts';
import type { TransactionCategory } from '@/models/transaction_category.ts';
import type {
TransactionReconciliationStatementResponse,
TransactionReconciliationStatementResponseItem
TransactionReconciliationStatementResponseItemWithInfo,
TransactionReconciliationStatementResponseWithInfo
} from '@/models/transaction.ts';
import { replaceAll } from '@/lib/common.ts';
@@ -48,7 +49,7 @@ export function useReconciliationStatementPageBase() {
const accountId = ref<string>('');
const startTime = ref<number>(0);
const endTime = ref<number>(0);
const reconciliationStatements = ref<TransactionReconciliationStatementResponse | undefined>(undefined);
const reconciliationStatements = ref<TransactionReconciliationStatementResponseWithInfo | undefined>(undefined);
const firstDayOfWeek = computed<WeekDayValue>(() => userStore.currentUserFirstDayOfWeek);
const fiscalYearStart = computed<number>(() => userStore.currentUserFiscalYearStart);
@@ -113,7 +114,34 @@ export function useReconciliationStatementPageBase() {
}
});
function getDisplayTransactionType(transaction: TransactionReconciliationStatementResponseItem): string {
function setReconciliationStatements(response: TransactionReconciliationStatementResponse | undefined) {
if (!response) {
reconciliationStatements.value = undefined;
return;
}
const responseWithInfo: TransactionReconciliationStatementResponseWithInfo = {
transactions: response.transactions.map(transaction => {
const transactionWithInfo: TransactionReconciliationStatementResponseItemWithInfo = {
...transaction,
sourceAccount: allAccountsMap.value[transaction.sourceAccountId],
sourceAccountName: allAccountsMap.value[transaction.sourceAccountId]?.name || '',
destinationAccount: transaction.destinationAccountId && transaction.destinationAccountId !== '0' ? allAccountsMap.value[transaction.destinationAccountId] : undefined,
category: allCategoriesMap.value[transaction.categoryId],
categoryName: allCategoriesMap.value[transaction.categoryId]?.name || ''
};
return transactionWithInfo;
}),
totalInflows: response.totalInflows,
totalOutflows: response.totalOutflows,
openingBalance: response.openingBalance,
closingBalance: response.closingBalance
};
reconciliationStatements.value = responseWithInfo;
}
function getDisplayTransactionType(transaction: TransactionReconciliationStatementResponseItemWithInfo): string {
if (transaction.type === TransactionType.ModifyBalance) {
return tt('Modify Balance');
} else if (transaction.type === TransactionType.Income) {
@@ -131,54 +159,44 @@ export function useReconciliationStatementPageBase() {
}
}
function getDisplayDateTime(transaction: TransactionReconciliationStatementResponseItem): string {
function getDisplayDateTime(transaction: TransactionReconciliationStatementResponseItemWithInfo): string {
return formatUnixTimeToLongDateTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value);
}
function getDisplayDate(transaction: TransactionReconciliationStatementResponseItem): string {
function getDisplayDate(transaction: TransactionReconciliationStatementResponseItemWithInfo): string {
return formatUnixTimeToLongDate(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value);
}
function getDisplayTime(transaction: TransactionReconciliationStatementResponseItem): string {
function getDisplayTime(transaction: TransactionReconciliationStatementResponseItemWithInfo): string {
return formatUnixTimeToShortTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value);
}
function getDisplayTimezone(transaction: TransactionReconciliationStatementResponseItem): string {
function getDisplayTimezone(transaction: TransactionReconciliationStatementResponseItemWithInfo): string {
return `UTC${getUtcOffsetByUtcOffsetMinutes(transaction.utcOffset)}`;
}
function getDisplaySourceAmount(transaction: TransactionReconciliationStatementResponseItem): string {
let currency = defaultCurrency.value;
if (allAccountsMap.value[transaction.sourceAccountId]) {
currency = allAccountsMap.value[transaction.sourceAccountId]!.currency;
}
function getDisplaySourceAmount(transaction: TransactionReconciliationStatementResponseItemWithInfo): string {
const currency = transaction.sourceAccount?.currency ?? defaultCurrency.value;
return formatAmountToLocalizedNumeralsWithCurrency(transaction.sourceAmount, currency);
}
function getDisplayDestinationAmount(transaction: TransactionReconciliationStatementResponseItem): string {
let currency = defaultCurrency.value;
if (allAccountsMap.value[transaction.destinationAccountId]) {
currency = allAccountsMap.value[transaction.destinationAccountId]!.currency;
}
function getDisplayDestinationAmount(transaction: TransactionReconciliationStatementResponseItemWithInfo): string {
const currency = transaction.destinationAccount?.currency ?? defaultCurrency.value;
return formatAmountToLocalizedNumeralsWithCurrency(transaction.destinationAmount, currency);
}
function getDisplayAccountBalance(transaction: TransactionReconciliationStatementResponseItem): string {
function getDisplayAccountBalance(transaction: TransactionReconciliationStatementResponseItemWithInfo): string {
let currency = defaultCurrency.value;
let isLiabilityAccount = false;
if (transaction.type === TransactionType.Transfer && transaction.destinationAccountId === accountId.value) {
if (allAccountsMap.value[transaction.destinationAccountId]) {
currency = allAccountsMap.value[transaction.destinationAccountId]!.currency;
isLiabilityAccount = allAccountsMap.value[transaction.destinationAccountId]!.isLiability;
if (transaction.destinationAccount) {
currency = transaction.destinationAccount.currency;
isLiabilityAccount = transaction.destinationAccount.isLiability;
}
} else if (allAccountsMap.value[transaction.sourceAccountId]) {
currency = allAccountsMap.value[transaction.sourceAccountId]!.currency;
isLiabilityAccount = allAccountsMap.value[transaction.sourceAccountId]!.isLiability;
} else if (transaction.sourceAccount) {
currency = transaction.sourceAccount.currency;
isLiabilityAccount = transaction.sourceAccount.isLiability;
}
if (isLiabilityAccount) {
@@ -211,9 +229,9 @@ export function useReconciliationStatementPageBase() {
const rows = transactions.map(transaction => {
const transactionTime = parseDateTimeFromUnixTime(transaction.time, transaction.utcOffset, currentTimezoneOffsetMinutes.value).getUnixTime();
const type = getDisplayTransactionType(transaction);
let categoryName = allCategoriesMap.value[transaction.categoryId]?.name || '';
let categoryName = transaction.categoryName;
let displayAmount = formatAmountToWesternArabicNumeralsWithoutDigitGrouping(transaction.sourceAmount);
let displayAccountName = allAccountsMap.value[transaction.sourceAccountId]?.name || '';
let displayAccountName = transaction.sourceAccountName;
if (transaction.type === TransactionType.ModifyBalance) {
categoryName = tt('Modify Balance');
@@ -221,8 +239,8 @@ export function useReconciliationStatementPageBase() {
displayAmount = formatAmountToWesternArabicNumeralsWithoutDigitGrouping(transaction.destinationAmount);
}
if (transaction.type === TransactionType.Transfer && allAccountsMap.value[transaction.destinationAccountId]) {
displayAccountName = displayAccountName + ' → ' + (allAccountsMap.value[transaction.destinationAccountId]?.name || '');
if (transaction.type === TransactionType.Transfer && transaction.destinationAccount) {
displayAccountName = displayAccountName + ' → ' + (transaction.destinationAccount?.name || '');
}
let displayAccountBalance = '';
@@ -272,8 +290,6 @@ export function useReconciliationStatementPageBase() {
currentAccountCurrency,
isCurrentLiabilityAccount,
exportFileName,
allAccountsMap,
allCategoriesMap,
displayStartDateTime,
displayEndDateTime,
displayTotalInflows,
@@ -282,6 +298,7 @@ export function useReconciliationStatementPageBase() {
displayOpeningBalance,
displayClosingBalance,
// functions
setReconciliationStatements,
getDisplayTransactionType,
getDisplayDateTime,
getDisplayDate,

View File

@@ -155,18 +155,18 @@
:class="{ 'text-income' : item.type === TransactionType.Income, 'text-expense': item.type === TransactionType.Expense }"
:color="getTransactionTypeColor(item)">{{ getDisplayTransactionType(item) }}</v-chip>
</template>
<template #item.categoryId="{ item }">
<template #item.categoryName="{ item }">
<div class="d-flex align-center">
<ItemIcon size="24px" icon-type="category"
:icon-id="allCategoriesMap[item.categoryId]?.icon ?? ''"
:color="allCategoriesMap[item.categoryId]?.color ?? ''"
v-if="allCategoriesMap[item.categoryId] && allCategoriesMap[item.categoryId]?.color"></ItemIcon>
<v-icon size="24" :icon="mdiPencilBoxOutline" v-else-if="!allCategoriesMap[item.categoryId] || !allCategoriesMap[item.categoryId]?.color" />
:icon-id="item.category?.icon ?? ''"
:color="item.category?.color ?? ''"
v-if="item.category && item.category?.color"></ItemIcon>
<v-icon size="24" :icon="mdiPencilBoxOutline" v-else-if="!item.category || !item.category?.color" />
<span class="ms-2" v-if="item.type === TransactionType.ModifyBalance">
{{ tt('Modify Balance') }}
</span>
<span class="ms-2" v-else-if="item.type !== TransactionType.ModifyBalance && allCategoriesMap[item.categoryId]">
{{ allCategoriesMap[item.categoryId]?.name }}
<span class="ms-2" v-else-if="item.type !== TransactionType.ModifyBalance && item.category">
{{ item.category?.name }}
</span>
</div>
</template>
@@ -175,11 +175,11 @@
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer && item.sourceAccountId !== item.destinationAccountId && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)"></v-icon>
<span v-if="item.type === TransactionType.Transfer && item.sourceAccountId !== item.destinationAccountId && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)">{{ getDisplayDestinationAmount(item) }}</span>
</template>
<template #item.sourceAccountId="{ item }">
<template #item.sourceAccountName="{ item }">
<div class="d-flex align-center">
<span v-if="item.sourceAccountId && allAccountsMap[item.sourceAccountId]">{{ allAccountsMap[item.sourceAccountId]?.name }}</span>
<span v-if="item.sourceAccount">{{ item.sourceAccount?.name }}</span>
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer"></v-icon>
<span v-if="item.type === TransactionType.Transfer && item.destinationAccountId && allAccountsMap[item.destinationAccountId]">{{ allAccountsMap[item.destinationAccountId]?.name }}</span>
<span v-if="item.type === TransactionType.Transfer && item.destinationAccount">{{ item.destinationAccount?.name }}</span>
</div>
</template>
<template #item.accountBalance="{ item }">
@@ -326,8 +326,6 @@ const {
currentAccount,
currentAccountCurrency,
isCurrentLiabilityAccount,
allAccountsMap,
allCategoriesMap,
exportFileName,
displayStartDateTime,
displayEndDateTime,
@@ -336,6 +334,7 @@ const {
displayTotalBalance,
displayOpeningBalance,
displayClosingBalance,
setReconciliationStatements,
getDisplayTransactionType,
getDisplayDateTime,
getDisplayTimezone,
@@ -395,9 +394,9 @@ const dataTableHeaders = computed<object[]>(() => {
headers.push({ key: 'time', value: 'time', title: tt('Transaction Time'), sortable: true, nowrap: true });
headers.push({ key: 'type', value: 'type', title: tt('Type'), sortable: true, nowrap: true });
headers.push({ key: 'categoryId', value: 'categoryId', title: tt('Category'), sortable: true, nowrap: true });
headers.push({ key: 'categoryName', value: 'categoryName', title: tt('Category'), sortable: true, nowrap: true });
headers.push({ key: 'sourceAmount', value: 'sourceAmount', title: tt('Amount'), sortable: true, nowrap: true });
headers.push({ key: 'sourceAccountId', value: 'sourceAccountId', title: tt('Account'), sortable: true, nowrap: true });
headers.push({ key: 'sourceAccountName', value: 'sourceAccountName', title: tt('Account'), sortable: true, nowrap: true });
headers.push({ key: 'accountBalance', value: 'accountBalance', title: tt(accountBalanceName), sortable: true, nowrap: true });
headers.push({ key: 'comment', value: 'comment', title: tt('Description'), sortable: true, nowrap: true });
headers.push({ key: 'operation', title: tt('Operation'), sortable: false, nowrap: true, align: 'end' });
@@ -464,7 +463,7 @@ function open(options: { accountId: string, startTime: number, endTime: number }
endTime: options.endTime
});
}).then(result => {
reconciliationStatements.value = result;
setReconciliationStatements(result);
loading.value = false;
}).catch(error => {
loading.value = false;
@@ -496,7 +495,7 @@ function reload(force: boolean): void {
}
}
reconciliationStatements.value = result;
setReconciliationStatements(result);
loading.value = false;
}).catch(error => {
loading.value = false;

View File

@@ -188,10 +188,10 @@
<div class="item-media">
<div class="transaction-icon display-flex align-items-center">
<ItemIcon icon-type="category"
:icon-id="allCategoriesMap[item.transaction.categoryId]?.icon"
:color="allCategoriesMap[item.transaction.categoryId]?.color"
v-if="allCategoriesMap[item.transaction.categoryId] && allCategoriesMap[item.transaction.categoryId]?.color"></ItemIcon>
<f7-icon v-else-if="!allCategoriesMap[item.transaction.categoryId] || !allCategoriesMap[item.transaction.categoryId]?.color"
:icon-id="item.transaction.category?.icon"
:color="item.transaction.category?.color"
v-if="item.transaction.category && item.transaction.category?.color"></ItemIcon>
<f7-icon v-else-if="!item.transaction.category || !item.transaction.category?.color"
f7="pencil_ellipsis_rectangle">
</f7-icon>
</div>
@@ -203,8 +203,8 @@
<span v-if="item.transaction.type === TransactionType.ModifyBalance">
{{ tt('Modify Balance') }}
</span>
<span v-else-if="item.transaction.type !== TransactionType.ModifyBalance && allCategoriesMap[item.transaction.categoryId]">
{{ allCategoriesMap[item.transaction.categoryId]!.name }}
<span v-else-if="item.transaction.type !== TransactionType.ModifyBalance && item.transaction.category">
{{ item.transaction.category.name }}
</span>
</div>
</div>
@@ -352,7 +352,7 @@ import { AccountType } from '@/core/account.ts';
import { TransactionType } from '@/core/transaction.ts';
import { ChartDateAggregationType } from '@/core/statistics.ts';
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
import { type TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts';
import { type TransactionReconciliationStatementResponseItemWithInfo } from '@/models/transaction.ts';
import { isDefined, isEquals, findDisplayNameByType } from '@/lib/common.ts';
import {
@@ -372,7 +372,7 @@ interface ReconciliationStatementVirtualListItem {
index: number;
type: ReconciliationStatementVirtualListItemType;
displayDate?: string;
transaction?: TransactionReconciliationStatementResponseItem;
transaction?: TransactionReconciliationStatementResponseItemWithInfo;
}
type ReconciliationStatementVirtualListItemType = 'transaction' | 'date';
@@ -402,7 +402,6 @@ const {
allDateAggregationTypes,
currentTimezoneOffsetMinutes,
isCurrentLiabilityAccount,
allCategoriesMap,
currentAccount,
currentAccountCurrency,
displayStartDateTime,
@@ -412,6 +411,7 @@ const {
displayTotalBalance,
displayOpeningBalance,
displayClosingBalance,
setReconciliationStatements,
getDisplayDate,
getDisplayTime,
getDisplayTimezone,
@@ -430,7 +430,7 @@ const loadingError = ref<unknown | null>(null);
const queryDateRangeType = ref<number>(DateRange.ThisMonth.type);
const showAccountBalanceTrendsCharts = ref<boolean>(false);
const chartDataDateAggregationType = ref<number>(ChartDateAggregationType.Day.type);
const transactionToDelete = ref<TransactionReconciliationStatementResponseItem | null>(null);
const transactionToDelete = ref<TransactionReconciliationStatementResponseItemWithInfo | null>(null);
const newClosingBalance = ref<number>(0);
const showDisplayModePopover = ref<boolean>(false);
const showCustomDateRangeSheet = ref<boolean>(false);
@@ -485,7 +485,7 @@ const chartDataDateAggregationTypeDisplayName = computed<string>(() => {
return findDisplayNameByType(allDateAggregationTypes.value, chartDataDateAggregationType.value) || tt('Unknown');
});
function getTransactionDomId(transaction: TransactionReconciliationStatementResponseItem): string {
function getTransactionDomId(transaction: TransactionReconciliationStatementResponseItemWithInfo): string {
return 'transaction_' + transaction.id;
}
@@ -567,7 +567,7 @@ function reload(force: boolean): void {
}
loading.value = false;
reconciliationStatements.value = result;
setReconciliationStatements(result);
}).catch(error => {
loading.value = false;
@@ -581,11 +581,11 @@ function addTransaction(): void {
props.f7router.navigate(`/transaction/add?accountId=${accountId.value}`);
}
function duplicateTransaction(transaction: TransactionReconciliationStatementResponseItem): void {
function duplicateTransaction(transaction: TransactionReconciliationStatementResponseItemWithInfo): void {
props.f7router.navigate(`/transaction/add?id=${transaction.id}&type=${transaction.type}`);
}
function editTransaction(transaction: TransactionReconciliationStatementResponseItem): void {
function editTransaction(transaction: TransactionReconciliationStatementResponseItemWithInfo): void {
props.f7router.navigate(`/transaction/edit?id=${transaction.id}&type=${transaction.type}`);
}
@@ -636,7 +636,7 @@ function updateClosingBalance(balance?: number): void {
props.f7router.navigate(`/transaction/add?${params.join('&')}`);
}
function removeTransaction(transaction: TransactionReconciliationStatementResponseItem | null, confirm: boolean): void {
function removeTransaction(transaction: TransactionReconciliationStatementResponseItemWithInfo | null, confirm: boolean): void {
if (!transaction) {
showAlert('An error occurred');
return;