mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-15 15:37:33 +08:00
reconciliation statement page / dialog supports account balance trends chart (#184)
This commit is contained in:
@@ -7,6 +7,7 @@ import { useUserStore } from '@/stores/user.ts';
|
||||
import { useAccountsStore } from '@/stores/account.ts';
|
||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||
|
||||
import type { TypeAndDisplayName } from '@/core/base.ts';
|
||||
import { type WeekDayValue, KnownDateTimeFormat } from '@/core/datetime.ts';
|
||||
import { TransactionType } from '@/core/transaction.ts';
|
||||
import { KnownFileType } from '@/core/file.ts';
|
||||
@@ -33,6 +34,8 @@ import {
|
||||
export function useReconciliationStatementPageBase() {
|
||||
const {
|
||||
tt,
|
||||
getAllTrendChartTypes,
|
||||
getAllStatisticsDateAggregationTypesWithShortName,
|
||||
getCurrentDigitGroupingSymbol,
|
||||
formatUnixTimeToLongDateTime,
|
||||
formatUnixTimeToLongDate,
|
||||
@@ -56,6 +59,9 @@ export function useReconciliationStatementPageBase() {
|
||||
const currentTimezoneOffsetMinutes = computed<number>(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone));
|
||||
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
|
||||
|
||||
const allChartTypes = computed<TypeAndDisplayName[]>(() => getAllTrendChartTypes());
|
||||
const allDateAggregationTypes = computed<TypeAndDisplayName[]>(() => getAllStatisticsDateAggregationTypesWithShortName());
|
||||
|
||||
const currentAccount = computed(() => allAccountsMap.value[accountId.value]);
|
||||
const currentAccountCurrency = computed<string>(() => currentAccount.value?.currency ?? defaultCurrency.value);
|
||||
const isCurrentLiabilityAccount = computed<boolean>(() => currentAccount.value?.isLiability ?? false);
|
||||
@@ -266,6 +272,8 @@ export function useReconciliationStatementPageBase() {
|
||||
fiscalYearStart,
|
||||
currentTimezoneOffsetMinutes,
|
||||
defaultCurrency,
|
||||
allChartTypes,
|
||||
allDateAggregationTypes,
|
||||
currentAccount,
|
||||
currentAccountCurrency,
|
||||
isCurrentLiabilityAccount,
|
||||
|
||||
@@ -14,6 +14,34 @@
|
||||
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
|
||||
:icon="true" :disabled="loading"
|
||||
v-if="showAccountBalanceTrendsCharts">
|
||||
<v-icon :icon="mdiTuneVertical" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-subheader :title="tt('Chart Type')"/>
|
||||
<v-list-item :key="type.type"
|
||||
:prepend-icon="chartTypeIconMap[type.type]"
|
||||
:append-icon="chartType === type.type ? mdiCheck : undefined"
|
||||
:title="type.displayName"
|
||||
@click="chartType = type.type"
|
||||
v-for="type in allChartTypes"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-subheader :title="tt('Time Granularity')"/>
|
||||
<v-list-item :prepend-icon="mdiCalendarTodayOutline"
|
||||
:append-icon="chartDataDateAggregationType === undefined ? mdiCheck : undefined"
|
||||
:title="tt('granularity.Daily')"
|
||||
@click="chartDataDateAggregationType = undefined"></v-list-item>
|
||||
<v-list-item :key="dateAggregationType.type"
|
||||
:prepend-icon="chartDataDateAggregationTypeIconMap[dateAggregationType.type]"
|
||||
:append-icon="chartDataDateAggregationType === dateAggregationType.type ? mdiCheck : undefined"
|
||||
:title="dateAggregationType.displayName"
|
||||
@click="chartDataDateAggregationType = dateAggregationType.type"
|
||||
v-for="dateAggregationType in allDateAggregationTypes"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ml-2"
|
||||
:icon="true" :disabled="loading">
|
||||
<v-icon :icon="mdiDotsVertical" />
|
||||
@@ -27,6 +55,15 @@
|
||||
:title="tt('Update Closing Balance')"
|
||||
@click="updateClosingBalance()"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiChartBoxOutline"
|
||||
:title="tt('Show Account Balance Trends')"
|
||||
@click="showAccountBalanceTrendsCharts = true"
|
||||
v-if="!showAccountBalanceTrendsCharts"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiListBoxOutline"
|
||||
:title="tt('Show Transaction List')"
|
||||
@click="showAccountBalanceTrendsCharts = false"
|
||||
v-if="showAccountBalanceTrendsCharts"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiComma"
|
||||
:disabled="!reconciliationStatements || !reconciliationStatements.transactions || reconciliationStatements.transactions.length < 1"
|
||||
@click="exportReconciliationStatements(KnownFileType.CSV)">
|
||||
@@ -109,6 +146,7 @@
|
||||
:no-data-text="loading ? '' : tt('No transaction data')"
|
||||
v-model:items-per-page="countPerPage"
|
||||
v-model:page="currentPage"
|
||||
v-if="!showAccountBalanceTrendsCharts"
|
||||
>
|
||||
<template #item.time="{ item }">
|
||||
<span>{{ getDisplayDateTime(item) }}</span>
|
||||
@@ -187,6 +225,27 @@
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<account-balance-trends-chart
|
||||
:type="chartType"
|
||||
:date-aggregation-type="chartDataDateAggregationType"
|
||||
:fiscal-year-start="fiscalYearStart"
|
||||
:items="[]"
|
||||
:legend-name="isCurrentLiabilityAccount ? tt('Account Outstanding Balance') : tt('Account Balance')"
|
||||
:account-currency="currentAccountCurrency"
|
||||
:skeleton="true"
|
||||
v-if="showAccountBalanceTrendsCharts && loading"
|
||||
/>
|
||||
|
||||
<account-balance-trends-chart
|
||||
:type="chartType"
|
||||
:date-aggregation-type="chartDataDateAggregationType"
|
||||
:fiscal-year-start="fiscalYearStart"
|
||||
:items="reconciliationStatements?.transactions"
|
||||
:legend-name="isCurrentLiabilityAccount ? tt('Account Outstanding Balance') : tt('Account Balance')"
|
||||
:account-currency="currentAccountCurrency"
|
||||
v-if="showAccountBalanceTrendsCharts && !loading"
|
||||
/>
|
||||
|
||||
<v-card-text class="overflow-y-visible">
|
||||
<div class="w-100 d-flex justify-center mt-2 mt-sm-4 mt-md-6 gap-4">
|
||||
<v-btn color="secondary" variant="tonal"
|
||||
@@ -219,6 +278,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||
import { useTransactionsStore } from '@/stores/transaction.ts';
|
||||
|
||||
import { TransactionType } from '@/core/transaction.ts';
|
||||
import { TrendChartType, ChartDateAggregationType } from '@/core/statistics.ts';
|
||||
import { KnownFileType } from '@/core/file.ts';
|
||||
import { Transaction, type TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts';
|
||||
|
||||
@@ -229,7 +289,16 @@ import { startDownloadFile } from '@/lib/ui/common.ts';
|
||||
import {
|
||||
mdiRefresh,
|
||||
mdiArrowRight,
|
||||
mdiTuneVertical,
|
||||
mdiDotsVertical,
|
||||
mdiCheck,
|
||||
mdiChartBoxOutline,
|
||||
mdiListBoxOutline,
|
||||
mdiChartBar,
|
||||
mdiChartAreasplineVariant,
|
||||
mdiCalendarTodayOutline,
|
||||
mdiCalendarMonthOutline,
|
||||
mdiLayersTripleOutline,
|
||||
mdiInvoiceTextPlusOutline,
|
||||
mdiInvoiceTextEditOutline,
|
||||
mdiComma,
|
||||
@@ -258,10 +327,13 @@ const {
|
||||
endTime,
|
||||
reconciliationStatements,
|
||||
currentTimezoneOffsetMinutes,
|
||||
allAccountsMap,
|
||||
allCategoriesMap,
|
||||
fiscalYearStart,
|
||||
allChartTypes,
|
||||
allDateAggregationTypes,
|
||||
currentAccountCurrency,
|
||||
isCurrentLiabilityAccount,
|
||||
allAccountsMap,
|
||||
allCategoriesMap,
|
||||
exportFileName,
|
||||
displayStartDateTime,
|
||||
displayEndDateTime,
|
||||
@@ -283,6 +355,18 @@ const accountsStore = useAccountsStore();
|
||||
const transactionCategoriesStore = useTransactionCategoriesStore();
|
||||
const transactionsStore = useTransactionsStore();
|
||||
|
||||
const chartTypeIconMap = {
|
||||
[TrendChartType.Column.type]: mdiChartBar,
|
||||
[TrendChartType.Area.type]: mdiChartAreasplineVariant,
|
||||
};
|
||||
|
||||
const chartDataDateAggregationTypeIconMap = {
|
||||
[ChartDateAggregationType.Month.type]: mdiCalendarMonthOutline,
|
||||
[ChartDateAggregationType.Quarter.type]: mdiLayersTripleOutline,
|
||||
[ChartDateAggregationType.Year.type]: mdiLayersTripleOutline,
|
||||
[ChartDateAggregationType.FiscalYear.type]: mdiLayersTripleOutline,
|
||||
};
|
||||
|
||||
const amountInputDialog = useTemplateRef<AmountInputDialogType>('amountInputDialog');
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
const editDialog = useTemplateRef<EditDialogType>('editDialog');
|
||||
@@ -291,6 +375,9 @@ const showState = ref<boolean>(false);
|
||||
const loading = ref<boolean>(false);
|
||||
const currentPage = ref<number>(1);
|
||||
const countPerPage = ref<number>(10);
|
||||
const showAccountBalanceTrendsCharts = ref<boolean>(false);
|
||||
const chartType = ref<number>(TrendChartType.Default.type);
|
||||
const chartDataDateAggregationType = ref<number | undefined>(undefined);
|
||||
|
||||
let rejectFunc: ((reason?: unknown) => void) | null = null;
|
||||
|
||||
@@ -371,6 +458,9 @@ function open(options: { accountId: string, startTime: number, endTime: number }
|
||||
reconciliationStatements.value = undefined;
|
||||
currentPage.value = 1;
|
||||
countPerPage.value = 10;
|
||||
showAccountBalanceTrendsCharts.value = false;
|
||||
chartType.value = TrendChartType.Default.type;
|
||||
chartDataDateAggregationType.value = undefined;
|
||||
showState.value = true;
|
||||
loading.value = true;
|
||||
|
||||
|
||||
@@ -716,7 +716,7 @@ import {
|
||||
getMonth,
|
||||
getBrowserTimezoneOffsetMinutes,
|
||||
getActualUnixTimeForStore,
|
||||
getSpecifiedDayFirstUnixTime,
|
||||
getDayFirstUnixTimeBySpecifiedUnixTime,
|
||||
getYearMonthFirstUnixTime,
|
||||
getYearMonthLastUnixTime,
|
||||
getShiftedDateRangeAndDateType,
|
||||
@@ -1275,7 +1275,7 @@ function changeDateFilter(dateRange: TimeRangeAndDateType | number | null): void
|
||||
if (dateRange === DateRange.Custom.type || (isObject(dateRange) && dateRange.dateType === DateRange.Custom.type && !dateRange.minTime && !dateRange.maxTime)) { // Custom
|
||||
if (!query.value.minTime || !query.value.maxTime) {
|
||||
customMaxDatetime.value = getActualUnixTimeForStore(getCurrentUnixTime(), currentTimezoneOffsetMinutes.value, getBrowserTimezoneOffsetMinutes());
|
||||
customMinDatetime.value = getSpecifiedDayFirstUnixTime(customMaxDatetime.value);
|
||||
customMinDatetime.value = getDayFirstUnixTimeBySpecifiedUnixTime(customMaxDatetime.value);
|
||||
} else {
|
||||
customMaxDatetime.value = query.value.maxTime;
|
||||
customMinDatetime.value = query.value.minTime;
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<f7-list strong inset dividers media-list
|
||||
class="skeleton-text margin-vertical transaction-info-list reconciliation-statement-list"
|
||||
v-if="finishQuery && loading">
|
||||
v-if="finishQuery && !showAccountBalanceTrendsCharts && loading">
|
||||
<ul>
|
||||
<f7-list-item chevron-center
|
||||
:key="index"
|
||||
@@ -130,14 +130,14 @@
|
||||
</f7-list>
|
||||
|
||||
<f7-list strong inset dividers class="margin-vertical"
|
||||
v-if="finishQuery && !loading && (!allReconciliationStatementVirtualListItems || !allReconciliationStatementVirtualListItems.length)">
|
||||
v-if="finishQuery && !showAccountBalanceTrendsCharts && !loading && (!allReconciliationStatementVirtualListItems || !allReconciliationStatementVirtualListItems.length)">
|
||||
<f7-list-item :title="tt('No transaction data')"></f7-list-item>
|
||||
</f7-list>
|
||||
|
||||
<f7-list strong inset dividers media-list virtual-list
|
||||
class="margin-vertical transaction-info-list reconciliation-statement-list"
|
||||
:virtual-list-params="{ items: allReconciliationStatementVirtualListItems, renderExternal, height: 'auto' }"
|
||||
v-if="finishQuery && !loading && allReconciliationStatementVirtualListItems && allReconciliationStatementVirtualListItems.length">
|
||||
v-if="finishQuery && !showAccountBalanceTrendsCharts && !loading && allReconciliationStatementVirtualListItems && allReconciliationStatementVirtualListItems.length">
|
||||
<ul>
|
||||
<f7-list-item chevron-center
|
||||
:key="item.index"
|
||||
@@ -231,6 +231,50 @@
|
||||
</ul>
|
||||
</f7-list>
|
||||
|
||||
<f7-card v-if="finishQuery && showAccountBalanceTrendsCharts">
|
||||
<f7-card-header class="no-border display-block">
|
||||
<div class="statistics-chart-header display-flex full-line justify-content-space-between">
|
||||
<div></div>
|
||||
<div class="align-self-flex-end">
|
||||
<span style="margin-right: 4px;">{{ tt('Time Granularity') }}</span>
|
||||
<f7-link :class="{ 'disabled': loading }" href="#" popover-open=".chart-data-date-aggregation-type-popover-menu">{{ chartDataDateAggregationTypeDisplayName }}</f7-link>
|
||||
</div>
|
||||
</div>
|
||||
</f7-card-header>
|
||||
<f7-card-content style="margin-top: -14px" :padding="false">
|
||||
<account-balance-trends-bar-chart
|
||||
:loading="loading"
|
||||
:date-aggregation-type="chartDataDateAggregationType"
|
||||
:fiscal-year-start="fiscalYearStart"
|
||||
:items="reconciliationStatements?.transactions"
|
||||
:account-currency="currentAccountCurrency"
|
||||
/>
|
||||
</f7-card-content>
|
||||
</f7-card>
|
||||
|
||||
<f7-popover class="chart-data-date-aggregation-type-popover-menu"
|
||||
v-model:opened="showChartDataDateAggregationTypePopover">
|
||||
<f7-list dividers>
|
||||
<f7-list-item :title="tt('granularity.Daily')"
|
||||
:class="{ 'list-item-selected': chartDataDateAggregationType === undefined }"
|
||||
key="daily"
|
||||
@click="setChartDataDateAggregationType(undefined)">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="chartDataDateAggregationType === undefined"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
<f7-list-item :title="dateAggregationType.displayName"
|
||||
:class="{ 'list-item-selected': chartDataDateAggregationType === dateAggregationType.type }"
|
||||
:key="dateAggregationType.type"
|
||||
v-for="dateAggregationType in allDateAggregationTypes"
|
||||
@click="setChartDataDateAggregationType(dateAggregationType.type)">
|
||||
<template #after>
|
||||
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="chartDataDateAggregationType === dateAggregationType.type"></f7-icon>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
</f7-popover>
|
||||
|
||||
<date-range-selection-sheet :title="tt('Custom Date Range')"
|
||||
:min-time="startTime"
|
||||
:max-time="endTime"
|
||||
@@ -256,6 +300,8 @@
|
||||
</f7-actions-group>
|
||||
<f7-actions-group>
|
||||
<f7-actions-button :class="{ 'disabled': loading }" @click="reload(true)">{{ tt('Refresh') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': loading }" @click="showAccountBalanceTrendsCharts = true" v-if="!showAccountBalanceTrendsCharts">{{ tt('Show Account Balance Trends') }}</f7-actions-button>
|
||||
<f7-actions-button :class="{ 'disabled': loading }" @click="showAccountBalanceTrendsCharts = false" v-if="showAccountBalanceTrendsCharts">{{ tt('Show Transaction List') }}</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
<f7-actions-group>
|
||||
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
|
||||
@@ -292,7 +338,7 @@ import { TransactionType } from '@/core/transaction.ts';
|
||||
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
|
||||
import { type TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts';
|
||||
|
||||
import { isDefined, isEquals } from '@/lib/common.ts';
|
||||
import { isDefined, isEquals, findDisplayNameByType } from '@/lib/common.ts';
|
||||
import {
|
||||
getCurrentUnixTime,
|
||||
getDateTypeByDateRange,
|
||||
@@ -330,6 +376,7 @@ const {
|
||||
reconciliationStatements,
|
||||
firstDayOfWeek,
|
||||
fiscalYearStart,
|
||||
allDateAggregationTypes,
|
||||
currentTimezoneOffsetMinutes,
|
||||
isCurrentLiabilityAccount,
|
||||
allCategoriesMap,
|
||||
@@ -358,12 +405,15 @@ const finishQuery = ref<boolean>(false);
|
||||
const loading = ref<boolean>(false);
|
||||
const loadingError = ref<unknown | null>(null);
|
||||
const queryDateRangeType = ref<number>(DateRange.ThisMonth.type);
|
||||
const showAccountBalanceTrendsCharts = ref<boolean>(false);
|
||||
const chartDataDateAggregationType = ref<number | undefined>(undefined);
|
||||
const transactionToDelete = ref<TransactionReconciliationStatementResponseItem | null>(null);
|
||||
const newClosingBalance = ref<number>(0);
|
||||
const showCustomDateRangeSheet = ref<boolean>(false);
|
||||
const showNewClosingBalanceSheet = ref<boolean>(false);
|
||||
const showMoreActionSheet = ref<boolean>(false);
|
||||
const showDeleteActionSheet = ref<boolean>(false);
|
||||
const showChartDataDateAggregationTypePopover = ref<boolean>(false);
|
||||
const virtualDataItems = ref<ReconciliationStatementVirtualListData>({
|
||||
items: [],
|
||||
topPosition: 0
|
||||
@@ -407,6 +457,14 @@ const allReconciliationStatementVirtualListItems = computed<ReconciliationStatem
|
||||
return ret;
|
||||
});
|
||||
|
||||
const chartDataDateAggregationTypeDisplayName = computed<string>(() => {
|
||||
if (chartDataDateAggregationType.value === undefined) {
|
||||
return tt('granularity.Daily');
|
||||
}
|
||||
|
||||
return findDisplayNameByType(allDateAggregationTypes.value, chartDataDateAggregationType.value) || tt('Unknown');
|
||||
});
|
||||
|
||||
function getTransactionDomId(transaction: TransactionReconciliationStatementResponseItem): string {
|
||||
return 'transaction_' + transaction.id;
|
||||
}
|
||||
@@ -592,6 +650,11 @@ function removeTransaction(transaction: TransactionReconciliationStatementRespon
|
||||
});
|
||||
}
|
||||
|
||||
function setChartDataDateAggregationType(type: number | undefined): void {
|
||||
chartDataDateAggregationType.value = type;
|
||||
showChartDataDateAggregationTypePopover.value = false;
|
||||
}
|
||||
|
||||
function renderExternal(vl: unknown, vlData: ReconciliationStatementVirtualListData): void {
|
||||
virtualDataItems.value = vlData;
|
||||
}
|
||||
|
||||
@@ -638,7 +638,7 @@ import {
|
||||
getActualUnixTimeForStore,
|
||||
getYear,
|
||||
getMonth,
|
||||
getSpecifiedDayFirstUnixTime,
|
||||
getDayFirstUnixTimeBySpecifiedUnixTime,
|
||||
getYearMonthFirstUnixTime,
|
||||
getYearMonthLastUnixTime,
|
||||
getShiftedDateRangeAndDateType,
|
||||
@@ -1066,7 +1066,7 @@ function changeDateFilter(dateType: number): void {
|
||||
if (dateType === DateRange.Custom.type) { // Custom
|
||||
if (!query.value.minTime || !query.value.maxTime) {
|
||||
customMaxDatetime.value = getActualUnixTimeForStore(getCurrentUnixTime(), currentTimezoneOffsetMinutes.value, getBrowserTimezoneOffsetMinutes());
|
||||
customMinDatetime.value = getSpecifiedDayFirstUnixTime(customMaxDatetime.value);
|
||||
customMinDatetime.value = getDayFirstUnixTimeBySpecifiedUnixTime(customMaxDatetime.value);
|
||||
} else {
|
||||
customMaxDatetime.value = query.value.maxTime;
|
||||
customMinDatetime.value = query.value.minTime;
|
||||
|
||||
Reference in New Issue
Block a user