mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-14 06:57:35 +08:00
305 lines
12 KiB
Vue
305 lines
12 KiB
Vue
<template>
|
|
<v-data-table
|
|
fixed-header
|
|
fixed-footer
|
|
multi-sort
|
|
item-value="index"
|
|
:class="{ 'insights-explorer-table': true, 'text-sm': true, 'disabled': loading, 'loading-skeleton': loading }"
|
|
:headers="dataTableHeaders"
|
|
:items="filteredTransactions"
|
|
:hover="true"
|
|
v-model:items-per-page="itemsPerPage"
|
|
v-model:page="currentPage"
|
|
>
|
|
<template #item.time="{ item }">
|
|
<span>{{ getDisplayDateTime(item) }}</span>
|
|
<v-chip class="ms-1" variant="flat" color="secondary" size="x-small"
|
|
v-if="!isSameAsDefaultTimezoneOffsetMinutes(item)">{{ getDisplayTimezone(item) }}</v-chip>
|
|
<v-tooltip activator="parent" v-if="!isSameAsDefaultTimezoneOffsetMinutes(item)">{{ getDisplayTimeInDefaultTimezone(item) }}</v-tooltip>
|
|
</template>
|
|
<template #item.type="{ item }">
|
|
<v-chip label variant="outlined" size="x-small"
|
|
:class="{ 'text-income' : item.type === TransactionType.Income, 'text-expense': item.type === TransactionType.Expense }"
|
|
:color="getTransactionTypeColor(item)">{{ getDisplayTransactionType(item) }}</v-chip>
|
|
</template>
|
|
<template #item.secondaryCategoryName="{ item }">
|
|
<div class="d-flex align-center">
|
|
<ItemIcon size="24px" icon-type="category"
|
|
:icon-id="item.secondaryCategory?.icon ?? ''"
|
|
:color="item.secondaryCategory?.color ?? ''"
|
|
v-if="item.secondaryCategory?.color"></ItemIcon>
|
|
<v-icon size="24" :icon="mdiPencilBoxOutline" v-else-if="!item.secondaryCategory || !item.secondaryCategory?.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 && item.secondaryCategory">
|
|
{{ item.secondaryCategory?.name }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
<template #item.sourceAmount="{ item }">
|
|
<span :class="{ 'text-expense': item.type === TransactionType.Expense, 'text-income': item.type === TransactionType.Income }">{{ getDisplaySourceAmount(item) }}</span>
|
|
<v-icon class="icon-with-direction mx-1" size="13" :icon="mdiArrowRight" v-if="item.type === TransactionType.Transfer && item.sourceAccount?.id !== item.destinationAccount?.id && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)"></v-icon>
|
|
<span v-if="item.type === TransactionType.Transfer && item.sourceAccount?.id !== item.destinationAccount?.id && getDisplaySourceAmount(item) !== getDisplayDestinationAmount(item)">{{ getDisplayDestinationAmount(item) }}</span>
|
|
</template>
|
|
<template #item.sourceAccountName="{ item }">
|
|
<div class="d-flex align-center">
|
|
<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.destinationAccount">{{ item.destinationAccount?.name }}</span>
|
|
</div>
|
|
</template>
|
|
<template #no-data>
|
|
<div v-if="loading && (!filteredTransactions || filteredTransactions.length < 1)">
|
|
<div class="ms-1" style="padding-top: 3px; padding-bottom: 3px" :key="itemIdx" v-for="itemIdx in skeletonData">
|
|
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
{{ tt('No transaction data') }}
|
|
</div>
|
|
</template>
|
|
<template #bottom>
|
|
<div class="title-and-toolbar d-flex align-center justify-center text-no-wrap mt-2 mb-4">
|
|
<pagination-buttons :disabled="loading"
|
|
:totalPageCount="totalPageCount"
|
|
v-model="currentPage">
|
|
</pagination-buttons>
|
|
</div>
|
|
</template>
|
|
</v-data-table>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import PaginationButtons from '@/components/desktop/PaginationButtons.vue';
|
|
|
|
import { ref, computed } from 'vue';
|
|
|
|
import { useI18n } from '@/locales/helpers.ts';
|
|
|
|
import { useUserStore } from '@/stores/user.ts';
|
|
import { useExplorersStore } from '@/stores/explorer.ts';
|
|
|
|
import type { NumeralSystem } from '@/core/numeral.ts';
|
|
import { TransactionType } from '@/core/transaction.ts';
|
|
|
|
import {
|
|
type TransactionInsightDataItem
|
|
} from '@/models/transaction.ts';
|
|
|
|
import {
|
|
getUtcOffsetByUtcOffsetMinutes,
|
|
getTimezoneOffsetMinutes,
|
|
parseDateTimeFromUnixTimeWithTimezoneOffset
|
|
} from '@/lib/datetime.ts';
|
|
|
|
import {
|
|
mdiArrowRight,
|
|
mdiPencilBoxOutline
|
|
} from '@mdi/js';
|
|
|
|
interface InsightsExplorerDataTableTabProps {
|
|
loading?: boolean;
|
|
countPerPage: number;
|
|
}
|
|
|
|
const props = defineProps<InsightsExplorerDataTableTabProps>();
|
|
const emit = defineEmits<{
|
|
(e: 'update:countPerPage', value: number): void;
|
|
}>();
|
|
|
|
const {
|
|
tt,
|
|
getCurrentNumeralSystemType,
|
|
formatDateTimeToLongDateTime,
|
|
formatDateTimeToGregorianDefaultDateTime,
|
|
formatAmountToWesternArabicNumeralsWithoutDigitGrouping,
|
|
formatAmountToLocalizedNumeralsWithCurrency
|
|
} = useI18n();
|
|
|
|
const userStore = useUserStore();
|
|
const explorersStore = useExplorersStore();
|
|
|
|
const currentPage = ref<number>(1);
|
|
|
|
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
|
|
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
|
|
|
|
const filteredTransactions = computed<TransactionInsightDataItem[]>(() => explorersStore.filteredTransactions);
|
|
|
|
const itemsPerPage = computed<number>({
|
|
get: () => props.countPerPage,
|
|
set: (value: number) => emit('update:countPerPage', value)
|
|
})
|
|
|
|
const skeletonData = computed<number[]>(() => {
|
|
const data: number[] = [];
|
|
|
|
for (let i = 0; i < itemsPerPage.value; i++) {
|
|
data.push(i);
|
|
}
|
|
|
|
return data;
|
|
});
|
|
|
|
const totalPageCount = computed<number>(() => {
|
|
if (!filteredTransactions.value || filteredTransactions.value.length < 1) {
|
|
return 1;
|
|
}
|
|
|
|
const count = filteredTransactions.value.length;
|
|
return Math.ceil(count / itemsPerPage.value);
|
|
});
|
|
|
|
const dataTableHeaders = computed<object[]>(() => {
|
|
const headers: 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: 'secondaryCategoryName', value: 'secondaryCategoryName', title: tt('Category'), sortable: true, nowrap: true });
|
|
headers.push({ key: 'sourceAmount', value: 'sourceAmount', title: tt('Amount'), sortable: true, nowrap: true });
|
|
headers.push({ key: 'sourceAccountName', value: 'sourceAccountName', title: tt('Account'), sortable: true, nowrap: true });
|
|
headers.push({ key: 'comment', value: 'comment', title: tt('Description'), sortable: true, nowrap: true });
|
|
return headers;
|
|
});
|
|
|
|
function getDisplayDateTime(transaction: TransactionInsightDataItem): string {
|
|
const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset);
|
|
return formatDateTimeToLongDateTime(dateTime);
|
|
}
|
|
|
|
function isSameAsDefaultTimezoneOffsetMinutes(transaction: TransactionInsightDataItem): boolean {
|
|
return transaction.utcOffset === getTimezoneOffsetMinutes(transaction.time);
|
|
}
|
|
|
|
function getDisplayTimezone(transaction: TransactionInsightDataItem): string {
|
|
return `UTC${getUtcOffsetByUtcOffsetMinutes(transaction.utcOffset)}`;
|
|
}
|
|
|
|
function getDisplayTimeInDefaultTimezone(transaction: TransactionInsightDataItem): string {
|
|
const timezoneOffsetMinutes = getTimezoneOffsetMinutes(transaction.time);
|
|
const dateTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, timezoneOffsetMinutes);
|
|
const utcOffset = numeralSystem.value.replaceWesternArabicDigitsToLocalizedDigits(getUtcOffsetByUtcOffsetMinutes(timezoneOffsetMinutes));
|
|
return `${formatDateTimeToLongDateTime(dateTime)} (UTC${utcOffset})`;
|
|
}
|
|
|
|
function getDisplayTransactionType(transaction: TransactionInsightDataItem): string {
|
|
if (transaction.type === TransactionType.ModifyBalance) {
|
|
return tt('Modify Balance');
|
|
} else if (transaction.type === TransactionType.Income) {
|
|
return tt('Income');
|
|
} else if (transaction.type === TransactionType.Expense) {
|
|
return tt('Expense');
|
|
} else if (transaction.type === TransactionType.Transfer) {
|
|
return tt('Transfer');
|
|
} else {
|
|
return tt('Unknown');
|
|
}
|
|
}
|
|
|
|
function getTransactionTypeColor(transaction: TransactionInsightDataItem): string | undefined {
|
|
if (transaction.type === TransactionType.ModifyBalance) {
|
|
return 'secondary';
|
|
} else if (transaction.type === TransactionType.Income) {
|
|
return undefined;
|
|
} else if (transaction.type === TransactionType.Expense) {
|
|
return undefined;
|
|
} else if (transaction.type === TransactionType.Transfer) {
|
|
return 'primary';
|
|
} else {
|
|
return 'default';
|
|
}
|
|
}
|
|
|
|
function getDisplaySourceAmount(transaction: TransactionInsightDataItem): string {
|
|
let currency = defaultCurrency.value;
|
|
|
|
if (transaction.sourceAccount) {
|
|
currency = transaction.sourceAccount.currency;
|
|
}
|
|
|
|
return formatAmountToLocalizedNumeralsWithCurrency(transaction.sourceAmount, currency);
|
|
}
|
|
|
|
function getDisplayDestinationAmount(transaction: TransactionInsightDataItem): string {
|
|
let currency = defaultCurrency.value;
|
|
|
|
if (transaction.destinationAccount) {
|
|
currency = transaction.destinationAccount.currency;
|
|
}
|
|
|
|
return formatAmountToLocalizedNumeralsWithCurrency(transaction.destinationAmount, currency);
|
|
}
|
|
|
|
function buildExportResults(): { headers: string[], data: string[][] } | undefined {
|
|
if (!filteredTransactions.value) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
headers: [
|
|
tt('Transaction Time'),
|
|
tt('Type'),
|
|
tt('Category'),
|
|
tt('Amount'),
|
|
tt('Account'),
|
|
tt('Description')
|
|
],
|
|
data: filteredTransactions.value
|
|
.map(transaction => {
|
|
const transactionTime = parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transaction.utcOffset);
|
|
const type = getDisplayTransactionType(transaction);
|
|
|
|
let categoryName = transaction.secondaryCategoryName;
|
|
let displayAmount = formatAmountToWesternArabicNumeralsWithoutDigitGrouping(transaction.sourceAmount);
|
|
let displayAccountName = transaction.sourceAccountName;
|
|
|
|
if (transaction.type === TransactionType.ModifyBalance) {
|
|
categoryName = tt('Modify Balance');
|
|
} else if (transaction.type === TransactionType.Transfer && transaction.sourceAccount?.id !== transaction.destinationAccount?.id && getDisplaySourceAmount(transaction) !== getDisplayDestinationAmount(transaction)) {
|
|
displayAmount = displayAmount + ' → ' + formatAmountToWesternArabicNumeralsWithoutDigitGrouping(transaction.destinationAmount);
|
|
}
|
|
|
|
if (transaction.type === TransactionType.Transfer && transaction.destinationAccount) {
|
|
displayAccountName = displayAccountName + ' → ' + (transaction.destinationAccount?.name || '');
|
|
}
|
|
|
|
const description = transaction.comment || '';
|
|
|
|
return [
|
|
formatDateTimeToGregorianDefaultDateTime(transactionTime),
|
|
type,
|
|
categoryName,
|
|
displayAmount,
|
|
displayAccountName,
|
|
description
|
|
];
|
|
}
|
|
)
|
|
};
|
|
}
|
|
|
|
defineExpose({
|
|
buildExportResults
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.v-table.insights-explorer-table > .v-table__wrapper > table {
|
|
th:not(:last-child),
|
|
td:not(:last-child) {
|
|
width: auto !important;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
th:last-child,
|
|
td:last-child {
|
|
width: 100% !important;
|
|
}
|
|
}
|
|
|
|
.v-table.insights-explorer-table.loading-skeleton tr.v-data-table-rows-no-data > td {
|
|
padding: 0;
|
|
}
|
|
</style>
|