migrate transaction statistics page to composition API and typescript

This commit is contained in:
MaysWind
2025-01-25 14:52:57 +08:00
parent 8207373a05
commit a27a2556aa
7 changed files with 1316 additions and 1272 deletions
+1 -1
View File
@@ -119,7 +119,7 @@ export class ChartDataType implements TypeAndName {
return this.availableAnalysisTypes[analysisType] || false; return this.availableAnalysisTypes[analysisType] || false;
} }
public static values(analysisType: StatisticsAnalysisType | undefined): ChartDataType[] { public static values(analysisType?: StatisticsAnalysisType): ChartDataType[] {
if (analysisType === undefined) { if (analysisType === undefined) {
return ChartDataType.allInstances; return ChartDataType.allInstances;
} }
+12 -2
View File
@@ -1,4 +1,4 @@
import type { TypeAndDisplayName } from '@/core/base.ts'; import type { TypeAndName, TypeAndDisplayName } from '@/core/base.ts';
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export function isFunction(val: unknown): val is Function { export function isFunction(val: unknown): val is Function {
@@ -37,7 +37,7 @@ export function isBoolean(val: unknown): val is boolean {
return typeof(val) === 'boolean'; return typeof(val) === 'boolean';
} }
export function isYearMonth(val: unknown): boolean { export function isYearMonth(val: unknown): val is string {
if (!isString(val)) { if (!isString(val)) {
return false; return false;
} }
@@ -312,6 +312,16 @@ export function getItemByKeyValue<T>(src: Record<string, T>[] | Record<string, R
return null; return null;
} }
export function findNameByType(items: TypeAndName[], type: number): string | null {
for (const item of items) {
if (item.type === type) {
return item.name;
}
}
return null;
}
export function findDisplayNameByType(items: TypeAndDisplayName[], type: number): string | null { export function findDisplayNameByType(items: TypeAndDisplayName[], type: number): string | null {
for (const item of items) { for (const item of items) {
if (item.type === type) { if (item.type === type) {
+54 -1
View File
@@ -105,13 +105,17 @@ import {
isPM, isPM,
formatUnixTime, formatUnixTime,
formatCurrentTime, formatCurrentTime,
parseDateFromUnixTime,
getYear,
getTimezoneOffset, getTimezoneOffset,
getTimezoneOffsetMinutes, getTimezoneOffsetMinutes,
getBrowserTimezoneOffset, getBrowserTimezoneOffset,
getBrowserTimezoneOffsetMinutes, getBrowserTimezoneOffsetMinutes,
getTimeDifferenceHoursAndMinutes, getTimeDifferenceHoursAndMinutes,
getDateTimeFormatType, getDateTimeFormatType,
getRecentMonthDateRanges getRecentMonthDateRanges,
isDateRangeMatchFullYears,
isDateRangeMatchFullMonths
} from '@/lib/datetime.ts'; } from '@/lib/datetime.ts';
import { import {
@@ -1206,6 +1210,54 @@ export function useI18n() {
} }
} }
function formatDateRange(dateType: number, startTime: number, endTime: number): string {
if (dateType === DateRange.All.type) {
return t(DateRange.All.name);
}
const allDateRanges = DateRange.values();
for (let i = 0; i < allDateRanges.length; i++) {
const dateRange = allDateRanges[i];
if (dateRange && dateRange.type !== DateRange.Custom.type && dateRange.type === dateType && dateRange.name) {
return t(dateRange.name);
}
}
if (isDateRangeMatchFullYears(startTime, endTime)) {
const format = getLocalizedShortYearFormat();
const displayStartTime = formatUnixTime(startTime, format);
const displayEndTime = formatUnixTime(endTime, format);
return displayStartTime !== displayEndTime ? `${displayStartTime} ~ ${displayEndTime}` : displayStartTime;
}
if (isDateRangeMatchFullMonths(startTime, endTime)) {
const format = getLocalizedShortYearMonthFormat();
const displayStartTime = formatUnixTime(startTime, format);
const displayEndTime = formatUnixTime(endTime, format);
return displayStartTime !== displayEndTime ? `${displayStartTime} ~ ${displayEndTime}` : displayStartTime;
}
const startTimeYear = getYear(parseDateFromUnixTime(startTime));
const endTimeYear = getYear(parseDateFromUnixTime(endTime));
const format = getLocalizedShortDateFormat();
const displayStartTime = formatUnixTime(startTime, format);
const displayEndTime = formatUnixTime(endTime, format);
if (displayStartTime === displayEndTime) {
return displayStartTime;
} else if (startTimeYear === endTimeYear) {
const displayShortEndTime = formatUnixTime(endTime, getLocalizedShortMonthDayFormat());
return `${displayStartTime} ~ ${displayShortEndTime}`;
}
return `${displayStartTime} ~ ${displayEndTime}`;
}
function getTimezoneDifferenceDisplayText(utcOffset: number): string { function getTimezoneDifferenceDisplayText(utcOffset: number): string {
const defaultTimezoneOffset = getTimezoneOffsetMinutes(); const defaultTimezoneOffset = getTimezoneOffsetMinutes();
const offsetTime = getTimeDifferenceHoursAndMinutes(utcOffset - defaultTimezoneOffset); const offsetTime = getTimeDifferenceHoursAndMinutes(utcOffset - defaultTimezoneOffset);
@@ -1486,6 +1538,7 @@ export function useI18n() {
formatUnixTimeToLongTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongTimeFormat(), utcOffset, currentUtcOffset), formatUnixTimeToLongTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongTimeFormat(), utcOffset, currentUtcOffset),
formatUnixTimeToShortTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortTimeFormat(), utcOffset, currentUtcOffset), formatUnixTimeToShortTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortTimeFormat(), utcOffset, currentUtcOffset),
formatYearQuarter, formatYearQuarter,
formatDateRange,
getTimezoneDifferenceDisplayText, getTimezoneDifferenceDisplayText,
appendDigitGroupingSymbol: getNumberWithDigitGroupingSymbol, appendDigitGroupingSymbol: getNumberWithDigitGroupingSymbol,
parseAmount: getParsedAmountNumber, parseAmount: getParsedAmountNumber,
+21 -4
View File
@@ -101,7 +101,24 @@ interface WritableTransactioTrendsAnalysisDataItem {
items: TransactionTrendsAnalysisDataAmount[]; items: TransactionTrendsAnalysisDataAmount[];
} }
export interface TransactionStatisticsFilter { export interface TransactionStatisticsPartialFilter {
chartDataType?: number;
categoricalChartType?: number;
categoricalChartDateType?: number;
categoricalChartStartTime?: number;
categoricalChartEndTime?: number;
trendChartType?: number;
trendChartDateType?: number;
trendChartStartYearMonth?: string;
trendChartEndYearMonth?: string;
filterAccountIds?: Record<string, boolean>;
filterCategoryIds?: Record<string, boolean>;
tagIds?: string;
tagFilterType?: number;
sortingType?: number;
}
export interface TransactionStatisticsFilter extends TransactionStatisticsPartialFilter {
chartDataType: number; chartDataType: number;
categoricalChartType: number; categoricalChartType: number;
categoricalChartDateType: number; categoricalChartDateType: number;
@@ -672,7 +689,7 @@ export const useStatisticsStore = defineStore('statistics', () => {
transactionStatisticsStateInvalid.value = true; transactionStatisticsStateInvalid.value = true;
} }
function initTransactionStatisticsFilter(analysisType: StatisticsAnalysisType, filter?: TransactionStatisticsFilter): void { function initTransactionStatisticsFilter(analysisType: StatisticsAnalysisType, filter?: TransactionStatisticsPartialFilter): void {
if (filter && isInteger(filter.chartDataType)) { if (filter && isInteger(filter.chartDataType)) {
transactionStatisticsFilter.value.chartDataType = filter.chartDataType; transactionStatisticsFilter.value.chartDataType = filter.chartDataType;
} else { } else {
@@ -810,7 +827,7 @@ export const useStatisticsStore = defineStore('statistics', () => {
} }
} }
function updateTransactionStatisticsFilter(filter: TransactionStatisticsFilter): boolean { function updateTransactionStatisticsFilter(filter: TransactionStatisticsPartialFilter): boolean {
let changed = false; let changed = false;
if (filter && isInteger(filter.chartDataType) && transactionStatisticsFilter.value.chartDataType !== filter.chartDataType) { if (filter && isInteger(filter.chartDataType) && transactionStatisticsFilter.value.chartDataType !== filter.chartDataType) {
@@ -943,7 +960,7 @@ export const useStatisticsStore = defineStore('statistics', () => {
return querys.join('&'); return querys.join('&');
} }
function getTransactionListPageParams(analysisType: StatisticsAnalysisType, itemId: string, dateRange: TimeRangeAndDateType): string { function getTransactionListPageParams(analysisType: StatisticsAnalysisType, itemId: string, dateRange?: TimeRangeAndDateType): string {
const querys: string[] = []; const querys: string[] = [];
if (transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type if (transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type
@@ -0,0 +1,243 @@
import { ref, computed } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import { useSettingsStore } from '@/stores/setting.ts';
import { useUserStore } from '@/stores/user.ts';
import { type TransactionStatisticsFilter, useStatisticsStore } from '@/stores/statistics.ts';
import type { TypeAndDisplayName } from '@/core/base.ts';
import { type LocalizedDateRange, DateRangeScene, DateRange } from '@/core/datetime.ts';
import { StatisticsAnalysisType, ChartDataType, ChartSortingType, ChartDateAggregationType } from '@/core/statistics.ts';
import type { TransactionCategoricalAnalysisData, TransactionTrendsAnalysisData } from '@/models/transaction.ts';
import { limitText, findNameByType, findDisplayNameByType } from '@/lib/common.ts';
import { getYearMonthFirstUnixTime, getYearMonthLastUnixTime } from '@/lib/datetime.ts';
export function useStatisticsTransactionPageBase() {
const {
tt,
getAllDateRanges,
getAllStatisticsSortingTypes,
getAllStatisticsDateAggregationTypes,
formatUnixTimeToLongDateTime,
formatUnixTimeToLongYearMonth,
formatDateRange,
formatAmountWithCurrency
} = useI18n();
const settingsStore = useSettingsStore();
const userStore = useUserStore();
const statisticsStore = useStatisticsStore();
const loading = ref<boolean>(true);
const analysisType = ref<StatisticsAnalysisType>(StatisticsAnalysisType.CategoricalAnalysis);
const trendDateAggregationType = ref<number>(ChartDateAggregationType.Default.type);
const showAccountBalance = computed<boolean>(() => settingsStore.appSettings.showAccountBalance);
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const firstDayOfWeek = computed<number>(() => userStore.currentUserFirstDayOfWeek);
const allDateRanges = computed<LocalizedDateRange[]>(() => {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
return getAllDateRanges(DateRangeScene.Normal, true);
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return getAllDateRanges(DateRangeScene.TrendAnalysis, true);
} else {
return [];
}
});
const allSortingTypes = computed<TypeAndDisplayName[]>(() => getAllStatisticsSortingTypes());
const allDateAggregationTypes = computed<TypeAndDisplayName[]>(() => getAllStatisticsDateAggregationTypes());
const query = computed<TransactionStatisticsFilter>(() => statisticsStore.transactionStatisticsFilter);
const queryChartDataCategory = computed<string>(() => statisticsStore.categoricalAnalysisChartDataCategory);
const queryDateType = computed<number | null>(() => {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
return query.value.categoricalChartDateType;
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return query.value.trendChartDateType;
} else {
return null;
}
});
const queryStartTime = computed<string>(() => {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
return formatUnixTimeToLongDateTime(query.value.categoricalChartStartTime);
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return formatUnixTimeToLongYearMonth(getYearMonthFirstUnixTime(query.value.trendChartStartYearMonth));
} else {
return '';
}
});
const queryEndTime = computed<string>(() => {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
return formatUnixTimeToLongDateTime(query.value.categoricalChartEndTime);
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return formatUnixTimeToLongYearMonth(getYearMonthLastUnixTime(query.value.trendChartEndYearMonth));
} else {
return '';
}
});
const queryDateRangeName = computed<string>(() => {
if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type ||
query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
return tt(DateRange.All.name);
}
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
return formatDateRange(query.value.categoricalChartDateType, query.value.categoricalChartStartTime, query.value.categoricalChartEndTime);
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return formatDateRange(query.value.trendChartDateType, getYearMonthFirstUnixTime(query.value.trendChartStartYearMonth), getYearMonthLastUnixTime(query.value.trendChartEndYearMonth));
} else {
return '';
}
});
const queryChartDataTypeName = computed<string>(() => {
const queryChartDataTypeName = findNameByType(ChartDataType.values(), query.value.chartDataType) || 'Statistics';
return tt(queryChartDataTypeName);
});
const querySortingTypeName = computed<string>(() => {
const querySortingTypeName = findNameByType(ChartSortingType.values(), query.value.sortingType) || 'System Default';
return tt(querySortingTypeName);
});
const queryTrendDateAggregationTypeName = computed<string>(() => findDisplayNameByType(allDateAggregationTypes.value, trendDateAggregationType.value) || '');
const isQueryDateRangeChanged = computed<boolean>(() => {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type ||
query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
return false;
}
return !!query.value.categoricalChartStartTime || !!query.value.categoricalChartEndTime;
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return !!query.value.trendChartStartYearMonth || !!query.value.trendChartEndYearMonth;
} else {
return false;
}
});
const canShiftDateRange = computed<boolean>(() => {
if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
return false;
}
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
return query.value.categoricalChartDateType !== DateRange.All.type;
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return query.value.trendChartDateType !== DateRange.All.type;
} else {
return false;
}
});
const showCustomDateRange = computed<boolean>(() => {
if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
return query.value.categoricalChartDateType === DateRange.Custom.type && !!query.value.categoricalChartStartTime && !!query.value.categoricalChartEndTime;
} else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return query.value.trendChartDateType === DateRange.Custom.type && !!query.value.trendChartStartYearMonth && !!query.value.trendChartEndYearMonth;
} else {
return false;
}
});
const showAmountInChart = computed<boolean>(() => {
if (!showAccountBalance.value
&& (query.value.chartDataType === ChartDataType.AccountTotalAssets.type || query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type)) {
return false;
}
return true;
});
const totalAmountName = computed<string>(() => {
if (query.value.chartDataType === ChartDataType.IncomeByAccount.type
|| query.value.chartDataType === ChartDataType.IncomeByPrimaryCategory.type
|| query.value.chartDataType === ChartDataType.IncomeBySecondaryCategory.type) {
return tt('Total Income');
} else if (query.value.chartDataType === ChartDataType.ExpenseByAccount.type
|| query.value.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type
|| query.value.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type) {
return tt('Total Expense');
} else if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type) {
return tt('Total Assets');
} else if (query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
return tt('Total Liabilities');
}
return tt('Total Amount');
});
const showTotalAmountInTrendsChart = computed<boolean>(() => {
return query.value.chartDataType !== ChartDataType.TotalExpense.type &&
query.value.chartDataType !== ChartDataType.TotalIncome.type &&
query.value.chartDataType !== ChartDataType.TotalBalance.type;
});
const translateNameInTrendsChart = computed<boolean>(() => {
return query.value.chartDataType === ChartDataType.TotalExpense.type ||
query.value.chartDataType === ChartDataType.TotalIncome.type ||
query.value.chartDataType === ChartDataType.TotalBalance.type;
});
const categoricalAnalysisData = computed<TransactionCategoricalAnalysisData>(() => statisticsStore.categoricalAnalysisData);
const trendsAnalysisData = computed<TransactionTrendsAnalysisData | null>(() => statisticsStore.trendsAnalysisData);
function getDisplayAmount(amount: number, currency: string, textLimit?: number): string {
const finalAmount = formatAmountWithCurrency(amount, currency) as string;
if (!showAccountBalance.value
&& (query.value.chartDataType === ChartDataType.AccountTotalAssets.type
|| query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type)
) {
return '***';
}
if (textLimit) {
return limitText(finalAmount, textLimit);
}
return finalAmount;
}
return {
// states
loading,
analysisType,
trendDateAggregationType,
// computed states
showAccountBalance,
defaultCurrency,
firstDayOfWeek,
allDateRanges,
allSortingTypes,
allDateAggregationTypes,
query,
queryChartDataCategory,
queryDateType,
queryStartTime,
queryEndTime,
queryDateRangeName,
queryChartDataTypeName,
querySortingTypeName,
queryTrendDateAggregationTypeName,
isQueryDateRangeChanged,
canShiftDateRange,
showCustomDateRange,
showAmountInChart,
totalAmountName,
showTotalAmountInTrendsChart,
translateNameInTrendsChart,
categoricalAnalysisData,
trendsAnalysisData,
// functions
getDisplayAmount
};
}
File diff suppressed because it is too large Load Diff
+269 -401
View File
@@ -1,7 +1,7 @@
<template> <template>
<f7-page ptr @ptr:refresh="reload" @page:afterin="onPageAfterIn"> <f7-page ptr @ptr:refresh="reload" @page:afterin="onPageAfterIn">
<f7-navbar> <f7-navbar>
<f7-nav-left :back-link="$t('Back')"></f7-nav-left> <f7-nav-left :back-link="tt('Back')"></f7-nav-left>
<f7-nav-title> <f7-nav-title>
<f7-link popover-open=".chart-data-type-popover-menu"> <f7-link popover-open=".chart-data-type-popover-menu">
<span>{{ queryChartDataTypeName }}</span> <span>{{ queryChartDataTypeName }}</span>
@@ -18,38 +18,38 @@
@popover:open="scrollPopoverToSelectedItem"> @popover:open="scrollPopoverToSelectedItem">
<f7-list dividers> <f7-list dividers>
<f7-list-group> <f7-list-group>
<f7-list-item group-title :title="$t('Categorical Analysis')" /> <f7-list-item group-title :title="tt('Categorical Analysis')" />
<f7-list-item :title="$t(dataType.name)" <f7-list-item :title="tt(dataType.name)"
:class="{ 'list-item-selected': analysisType === allAnalysisTypes.CategoricalAnalysis && query.chartDataType === dataType.type }" :class="{ 'list-item-selected': analysisType === StatisticsAnalysisType.CategoricalAnalysis && query.chartDataType === dataType.type }"
:key="dataType.type" :key="dataType.type"
v-for="dataType in allChartDataTypes" v-for="dataType in ChartDataType.all()"
v-show="dataType.isAvailableAnalysisType(allAnalysisTypes.CategoricalAnalysis)" v-show="dataType.isAvailableAnalysisType(StatisticsAnalysisType.CategoricalAnalysis)"
@click="setChartDataType(allAnalysisTypes.CategoricalAnalysis, dataType.type)"> @click="setChartDataType(StatisticsAnalysisType.CategoricalAnalysis, dataType.type)">
<template #after> <template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="analysisType === allAnalysisTypes.CategoricalAnalysis && query.chartDataType === dataType.type"></f7-icon> <f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="analysisType === StatisticsAnalysisType.CategoricalAnalysis && query.chartDataType === dataType.type"></f7-icon>
</template> </template>
</f7-list-item> </f7-list-item>
</f7-list-group> </f7-list-group>
<f7-list-group> <f7-list-group>
<f7-list-item group-title :title="$t('Trend Analysis')" /> <f7-list-item group-title :title="tt('Trend Analysis')" />
<f7-list-item :title="$t(dataType.name)" <f7-list-item :title="tt(dataType.name)"
:class="{ 'list-item-selected': analysisType === allAnalysisTypes.TrendAnalysis && query.chartDataType === dataType.type }" :class="{ 'list-item-selected': analysisType === StatisticsAnalysisType.TrendAnalysis && query.chartDataType === dataType.type }"
:key="dataType.type" :key="dataType.type"
v-for="dataType in allChartDataTypes" v-for="dataType in ChartDataType.all()"
v-show="dataType.isAvailableAnalysisType(allAnalysisTypes.TrendAnalysis)" v-show="dataType.isAvailableAnalysisType(StatisticsAnalysisType.TrendAnalysis)"
@click="setChartDataType(allAnalysisTypes.TrendAnalysis, dataType.type)"> @click="setChartDataType(StatisticsAnalysisType.TrendAnalysis, dataType.type)">
<template #after> <template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="analysisType === allAnalysisTypes.TrendAnalysis && query.chartDataType === dataType.type"></f7-icon> <f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="analysisType === StatisticsAnalysisType.TrendAnalysis && query.chartDataType === dataType.type"></f7-icon>
</template> </template>
</f7-list-item> </f7-list-item>
</f7-list-group> </f7-list-group>
</f7-list> </f7-list>
</f7-popover> </f7-popover>
<f7-card v-if="analysisType === allAnalysisTypes.CategoricalAnalysis && query.categoricalChartType === allCategoricalChartTypes.Pie.type"> <f7-card v-if="analysisType === StatisticsAnalysisType.CategoricalAnalysis && query.categoricalChartType === CategoricalChartType.Pie.type">
<f7-card-header class="no-border display-block"> <f7-card-header class="no-border display-block">
<div class="statistics-chart-header full-line text-align-right"> <div class="statistics-chart-header full-line text-align-right">
<span style="margin-right: 4px;">{{ $t('Sort by') }}</span> <span style="margin-right: 4px;">{{ tt('Sort by') }}</span>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link> <f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link>
</div> </div>
</f7-card-header> </f7-card-header>
@@ -80,7 +80,7 @@
percent-field="percent" percent-field="percent"
hidden-field="hidden" hidden-field="hidden"
v-else-if="!loading" v-else-if="!loading"
@click="clickPieChartItem" @click="onClickPieChartItem"
> >
<text class="statistics-pie-chart-total-amount-title" v-if="categoricalAnalysisData.items && categoricalAnalysisData.items.length"> <text class="statistics-pie-chart-total-amount-title" v-if="categoricalAnalysisData.items && categoricalAnalysisData.items.length">
{{ totalAmountName }} {{ totalAmountName }}
@@ -89,25 +89,25 @@
{{ getDisplayAmount(categoricalAnalysisData.totalAmount, defaultCurrency, 16) }} {{ getDisplayAmount(categoricalAnalysisData.totalAmount, defaultCurrency, 16) }}
</text> </text>
<text class="statistics-pie-chart-total-no-data" cy="50%" v-if="!categoricalAnalysisData.items || !categoricalAnalysisData.items.length"> <text class="statistics-pie-chart-total-no-data" cy="50%" v-if="!categoricalAnalysisData.items || !categoricalAnalysisData.items.length">
{{ $t('No data') }} {{ tt('No data') }}
</text> </text>
</pie-chart> </pie-chart>
</f7-card-content> </f7-card-content>
</f7-card> </f7-card>
<f7-card v-else-if="analysisType === allAnalysisTypes.CategoricalAnalysis && query.categoricalChartType === allCategoricalChartTypes.Bar.type"> <f7-card v-else-if="analysisType === StatisticsAnalysisType.CategoricalAnalysis && query.categoricalChartType === CategoricalChartType.Bar.type">
<f7-card-header class="no-border display-block"> <f7-card-header class="no-border display-block">
<div class="statistics-chart-header display-flex full-line justify-content-space-between"> <div class="statistics-chart-header display-flex full-line justify-content-space-between">
<div> <div>
{{ totalAmountName }} {{ totalAmountName }}
</div> </div>
<div class="align-self-flex-end"> <div class="align-self-flex-end">
<span style="margin-right: 4px;">{{ $t('Sort by') }}</span> <span style="margin-right: 4px;">{{ tt('Sort by') }}</span>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link> <f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link>
</div> </div>
</div> </div>
<div class="display-flex full-line"> <div class="display-flex full-line">
<div :class="{ 'statistics-list-item-overview-amount': true, 'text-expense': query.chartDataType === allChartDataTypes.ExpenseByAccount.type || query.chartDataType === allChartDataTypes.ExpenseByPrimaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseBySecondaryCategory.type, 'text-income': query.chartDataType === allChartDataTypes.IncomeByAccount.type || query.chartDataType === allChartDataTypes.IncomeByPrimaryCategory.type || query.chartDataType === allChartDataTypes.IncomeBySecondaryCategory.type }"> <div :class="{ 'statistics-list-item-overview-amount': true, 'text-expense': query.chartDataType === ChartDataType.ExpenseByAccount.type || query.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type || query.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type, 'text-income': query.chartDataType === ChartDataType.IncomeByAccount.type || query.chartDataType === ChartDataType.IncomeByPrimaryCategory.type || query.chartDataType === ChartDataType.IncomeBySecondaryCategory.type }">
<span v-if="!loading && categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length"> <span v-if="!loading && categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length">
{{ getDisplayAmount(categoricalAnalysisData.totalAmount, defaultCurrency) }} {{ getDisplayAmount(categoricalAnalysisData.totalAmount, defaultCurrency) }}
</span> </span>
@@ -147,7 +147,7 @@
</f7-list> </f7-list>
<f7-list v-else-if="!loading && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length)"> <f7-list v-else-if="!loading && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length)">
<f7-list-item :title="$t('No transaction data')"></f7-list-item> <f7-list-item :title="tt('No transaction data')"></f7-list-item>
</f7-list> </f7-list>
<f7-list v-else-if="!loading && categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length"> <f7-list v-else-if="!loading && categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length">
@@ -169,12 +169,12 @@
<template #title> <template #title>
<div class="statistics-list-item-text"> <div class="statistics-list-item-text">
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
<small class="statistics-percent" v-if="item.percent >= 0">{{ getDisplayPercent(item.percent, 2, '&lt;0.01') }}</small> <small class="statistics-percent" v-if="item.percent >= 0">{{ formatPercent(item.percent, 2, '&lt;0.01') }}</small>
</div> </div>
</template> </template>
<template #after> <template #after>
<span>{{ getDisplayAmount(item.totalAmount, (item.currency || defaultCurrency)) }}</span> <span>{{ getDisplayAmount(item.totalAmount, defaultCurrency) }}</span>
</template> </template>
<template #inner-end> <template #inner-end>
@@ -189,12 +189,12 @@
</f7-card-content> </f7-card-content>
</f7-card> </f7-card>
<f7-card v-else-if="analysisType === allAnalysisTypes.TrendAnalysis"> <f7-card v-else-if="analysisType === StatisticsAnalysisType.TrendAnalysis">
<f7-card-header class="no-border display-block"> <f7-card-header class="no-border display-block">
<div class="statistics-chart-header display-flex full-line justify-content-space-between"> <div class="statistics-chart-header display-flex full-line justify-content-space-between">
<div></div> <div></div>
<div class="align-self-flex-end"> <div class="align-self-flex-end">
<span style="margin-right: 4px;">{{ $t('Sort by') }}</span> <span style="margin-right: 4px;">{{ tt('Sort by') }}</span>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link> <f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link>
</div> </div>
</div> </div>
@@ -214,7 +214,7 @@
value-field="totalAmount" value-field="totalAmount"
hidden-field="hidden" hidden-field="hidden"
display-orders-field="displayOrders" display-orders-field="displayOrders"
@click="clickTrendChartItem" @click="onClickTrendChartItem"
/> />
</f7-card-content> </f7-card-content>
</f7-card> </f7-card>
@@ -235,18 +235,18 @@
</f7-popover> </f7-popover>
<f7-toolbar tabbar bottom class="toolbar-item-auto-size"> <f7-toolbar tabbar bottom class="toolbar-item-auto-size">
<f7-link :class="{ 'disabled': reloading || !canShiftDateRange(query) }" @click="shiftDateRange(query, -1)"> <f7-link :class="{ 'disabled': reloading || !canShiftDateRange }" @click="shiftDateRange(-1)">
<f7-icon f7="arrow_left_square"></f7-icon> <f7-icon f7="arrow_left_square"></f7-icon>
</f7-link> </f7-link>
<f7-link :class="{ 'tabbar-text-with-ellipsis': true, 'disabled': reloading || query.chartDataType === allChartDataTypes.AccountTotalAssets.type || query.chartDataType === allChartDataTypes.AccountTotalLiabilities.type }" popover-open=".date-popover-menu"> <f7-link :class="{ 'tabbar-text-with-ellipsis': true, 'disabled': reloading || query.chartDataType === ChartDataType.AccountTotalAssets.type || query.chartDataType === ChartDataType.AccountTotalLiabilities.type }" popover-open=".date-popover-menu">
<span :class="{ 'tabbar-item-changed': query.maxTime > 0 || query.minTime > 0 }">{{ dateRangeName(query) }}</span> <span :class="{ 'tabbar-item-changed': isQueryDateRangeChanged }">{{ queryDateRangeName }}</span>
</f7-link> </f7-link>
<f7-link :class="{ 'disabled': reloading || !canShiftDateRange(query) }" @click="shiftDateRange(query, 1)"> <f7-link :class="{ 'disabled': reloading || !canShiftDateRange }" @click="shiftDateRange(1)">
<f7-icon f7="arrow_right_square"></f7-icon> <f7-icon f7="arrow_right_square"></f7-icon>
</f7-link> </f7-link>
<f7-link :class="{ 'tabbar-text-with-ellipsis': true, 'disabled': reloading }" popover-open=".date-aggregation-popover-menu" <f7-link :class="{ 'tabbar-text-with-ellipsis': true, 'disabled': reloading }" popover-open=".date-aggregation-popover-menu"
v-if="analysisType === allAnalysisTypes.TrendAnalysis"> v-if="analysisType === StatisticsAnalysisType.TrendAnalysis">
<span :class="{ 'tabbar-item-changed': trendDateAggregationType !== defaultTrendDateAggregationType }">{{ queryTrendDateAggregationTypeName }}</span> <span :class="{ 'tabbar-item-changed': trendDateAggregationType !== ChartDateAggregationType.Default.type }">{{ queryTrendDateAggregationTypeName }}</span>
</f7-link> </f7-link>
<f7-link class="tabbar-text-with-ellipsis" :key="chartType.type" <f7-link class="tabbar-text-with-ellipsis" :key="chartType.type"
v-for="chartType in allChartTypes" @click="setChartType(chartType.type)"> v-for="chartType in allChartTypes" @click="setChartType(chartType.type)">
@@ -261,13 +261,13 @@
<f7-list-item :title="dateRange.displayName" <f7-list-item :title="dateRange.displayName"
:class="{ 'list-item-selected': queryDateType === dateRange.type }" :class="{ 'list-item-selected': queryDateType === dateRange.type }"
:key="dateRange.type" :key="dateRange.type"
v-for="dateRange in allDateRangesArray" v-for="dateRange in allDateRanges"
@click="setDateFilter(dateRange.type)"> @click="setDateFilter(dateRange.type)">
<template #after> <template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="queryDateType === dateRange.type"></f7-icon> <f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="queryDateType === dateRange.type"></f7-icon>
</template> </template>
<template #footer> <template #footer>
<div v-if="dateRange.type === allDateRanges.Custom.type && showCustomDateRange()"> <div v-if="dateRange.type === DateRange.Custom.type && showCustomDateRange">
<span>{{ queryStartTime }}</span> <span>{{ queryStartTime }}</span>
<span>&nbsp;-&nbsp;</span> <span>&nbsp;-&nbsp;</span>
<br/> <br/>
@@ -294,14 +294,14 @@
</f7-list> </f7-list>
</f7-popover> </f7-popover>
<date-range-selection-sheet :title="$t('Custom Date Range')" <date-range-selection-sheet :title="tt('Custom Date Range')"
:min-time="query.categoricalChartStartTime" :min-time="query.categoricalChartStartTime"
:max-time="query.categoricalChartEndTime" :max-time="query.categoricalChartEndTime"
v-model:show="showCustomDateRangeSheet" v-model:show="showCustomDateRangeSheet"
@dateRange:change="setCustomDateFilter"> @dateRange:change="setCustomDateFilter">
</date-range-selection-sheet> </date-range-selection-sheet>
<month-range-selection-sheet :title="$t('Custom Date Range')" <month-range-selection-sheet :title="tt('Custom Date Range')"
:min-time="query.trendChartStartYearMonth" :min-time="query.trendChartStartYearMonth"
:max-time="query.trendChartEndYearMonth" :max-time="query.trendChartEndYearMonth"
v-model:show="showCustomMonthRangeSheet" v-model:show="showCustomMonthRangeSheet"
@@ -310,29 +310,33 @@
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group> <f7-actions-group>
<f7-actions-button @click="filterAccounts">{{ $t('Filter Accounts') }}</f7-actions-button> <f7-actions-button @click="filterAccounts">{{ tt('Filter Accounts') }}</f7-actions-button>
<f7-actions-button @click="filterCategories">{{ $t('Filter Transaction Categories') }}</f7-actions-button> <f7-actions-button @click="filterCategories">{{ tt('Filter Transaction Categories') }}</f7-actions-button>
<f7-actions-button @click="filterTags">{{ $t('Filter Transaction Tags') }}</f7-actions-button> <f7-actions-button @click="filterTags">{{ tt('Filter Transaction Tags') }}</f7-actions-button>
</f7-actions-group> </f7-actions-group>
<f7-actions-group> <f7-actions-group>
<f7-actions-button @click="settings">{{ $t('Settings') }}</f7-actions-button> <f7-actions-button @click="settings">{{ tt('Settings') }}</f7-actions-button>
</f7-actions-group> </f7-actions-group>
<f7-actions-group> <f7-actions-group>
<f7-actions-button bold close>{{ $t('Cancel') }}</f7-actions-button> <f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
</f7-actions-group> </f7-actions-group>
</f7-actions> </f7-actions>
</f7-page> </f7-page>
</template> </template>
<script> <script setup lang="ts">
import { mapStores } from 'pinia'; import { ref, computed } from 'vue';
import { useSettingsStore } from '@/stores/setting.ts'; import type { Router } from 'framework7/types';
import { useUserStore } from '@/stores/user.ts';
import { useI18n } from '@/locales/helpers.ts';
import { useStatisticsTransactionPageBase } from '@/views/base/statistics/StatisticsTransactionPageBase.ts';
import { useAccountsStore } from '@/stores/account.ts'; import { useAccountsStore } from '@/stores/account.ts';
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useStatisticsStore } from '@/stores/statistics.ts'; import { useStatisticsStore } from '@/stores/statistics.ts';
import { DateRangeScene, DateRange } from '@/core/datetime.ts'; import type { TypeAndDisplayName } from '@/core/base.ts';
import { type TimeRangeAndDateType, DateRangeScene, DateRange } from '@/core/datetime.ts';
import { import {
StatisticsAnalysisType, StatisticsAnalysisType,
CategoricalChartType, CategoricalChartType,
@@ -340,7 +344,8 @@ import {
ChartSortingType, ChartSortingType,
ChartDateAggregationType ChartDateAggregationType
} from '@/core/statistics.ts'; } from '@/core/statistics.ts';
import { getNameByKeyValue, limitText } from '@/lib/common.ts';
import { isString, isNumber } from '@/lib/common.ts';
import { formatPercent } from '@/lib/numeral.ts'; import { formatPercent } from '@/lib/numeral.ts';
import { import {
getYearAndMonthFromUnixTime, getYearAndMonthFromUnixTime,
@@ -350,338 +355,245 @@ import {
getDateTypeByDateRange, getDateTypeByDateRange,
getDateRangeByDateType getDateRangeByDateType
} from '@/lib/datetime.ts'; } from '@/lib/datetime.ts';
import { scrollToSelectedItem } from '@/lib/ui/mobile.ts'; import { type Framework7Dom, useI18nUIComponents, scrollToSelectedItem } from '@/lib/ui/mobile.ts';
export default { const props = defineProps<{
props: [ f7router: Router.Router;
'f7router' }>();
],
data() { const { tt, getAllCategoricalChartTypes } = useI18n();
return { const { showToast, routeBackOnError } = useI18nUIComponents();
loading: true,
loadingError: null, const {
reloading: false, loading,
analysisType: StatisticsAnalysisType.CategoricalAnalysis, analysisType,
trendDateAggregationType: ChartDateAggregationType.Default.type, trendDateAggregationType,
showChartDataTypePopover: false, defaultCurrency,
showSortingTypePopover: false, firstDayOfWeek,
showDatePopover: false, allDateRanges,
showDateAggregationPopover: false, allSortingTypes,
showCustomDateRangeSheet: false, allDateAggregationTypes,
showCustomMonthRangeSheet: false, query,
showMoreActionSheet: false queryChartDataCategory,
}; queryDateType,
}, queryStartTime,
computed: { queryEndTime,
...mapStores(useSettingsStore, useUserStore, useAccountsStore, useTransactionCategoriesStore, useStatisticsStore), queryDateRangeName,
defaultCurrency() { queryChartDataTypeName,
return this.userStore.currentUserDefaultCurrency; querySortingTypeName,
}, queryTrendDateAggregationTypeName,
defaultTrendDateAggregationType() { isQueryDateRangeChanged,
return ChartDateAggregationType.Default.type; canShiftDateRange,
}, showCustomDateRange,
firstDayOfWeek() { showAmountInChart,
return this.userStore.currentUserFirstDayOfWeek; totalAmountName,
}, translateNameInTrendsChart,
query() { categoricalAnalysisData,
return this.statisticsStore.transactionStatisticsFilter; trendsAnalysisData,
}, getDisplayAmount
queryChartDataCategory() { } = useStatisticsTransactionPageBase();
return this.statisticsStore.categoricalAnalysisChartDataCategory;
}, const accountsStore = useAccountsStore();
queryChartType: { const transactionCategoriesStore = useTransactionCategoriesStore();
get: function () { const statisticsStore = useStatisticsStore();
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) {
return this.query.categoricalChartType; const loadingError = ref<unknown | null>(null);
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) { const reloading = ref<boolean>(false);
return this.query.trendChartType; const showChartDataTypePopover = ref<boolean>(false);
} else { const showSortingTypePopover = ref<boolean>(false);
return null; const showDatePopover = ref<boolean>(false);
} const showDateAggregationPopover = ref<boolean>(false);
}, const showCustomDateRangeSheet = ref<boolean>(false);
set: function(value) { const showCustomMonthRangeSheet = ref<boolean>(false);
this.setChartType(value); const showMoreActionSheet = ref<boolean>(false);
}
}, const allChartTypes = computed<TypeAndDisplayName[]>(() => {
queryChartDataTypeName() { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
const queryChartDataTypeName = getNameByKeyValue(ChartDataType.values(), this.query.chartDataType, 'type', 'name', 'Statistics'); return getAllCategoricalChartTypes();
return this.$t(queryChartDataTypeName);
},
querySortingTypeName() {
const querySortingTypeName = getNameByKeyValue(ChartSortingType.values(), this.query.sortingType, 'type', 'name', 'System Default');
return this.$t(querySortingTypeName);
},
queryDateType() {
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) {
return this.query.categoricalChartDateType;
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) {
return this.query.trendChartDateType;
} else {
return null;
}
},
queryTrendDateAggregationTypeName() {
return getNameByKeyValue(this.allDateAggregationTypes, this.trendDateAggregationType, 'type', 'displayName', '');
},
queryStartTime() {
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) {
return this.$locale.formatUnixTimeToLongDateTime(this.userStore, this.query.categoricalChartStartTime);
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) {
return this.$locale.formatUnixTimeToLongYearMonth(this.userStore, getYearMonthFirstUnixTime(this.query.trendChartStartYearMonth));
} else {
return '';
}
},
queryEndTime() {
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) {
return this.$locale.formatUnixTimeToLongDateTime(this.userStore, this.query.categoricalChartEndTime);
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) {
return this.$locale.formatUnixTimeToLongYearMonth(this.userStore, getYearMonthLastUnixTime(this.query.trendChartEndYearMonth));
} else {
return '';
}
},
allAnalysisTypes() {
return StatisticsAnalysisType;
},
allChartTypes() {
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) {
return this.$locale.getAllCategoricalChartTypes();
} else { } else {
return []; return [];
} }
}, });
allCategoricalChartTypes() {
return CategoricalChartType.all(); const queryChartType = computed<number | undefined>({
}, get: () => {
allChartDataTypes() { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
return ChartDataType.all(); return query.value.categoricalChartType;
}, } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
allSortingTypes() { return query.value.trendChartType;
return this.$locale.getAllStatisticsSortingTypes();
},
allDateAggregationTypes() {
return this.$locale.getAllStatisticsDateAggregationTypes();
},
allDateRanges() {
return DateRange.all();
},
allDateRangesArray() {
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) {
return this.$locale.getAllDateRanges(DateRangeScene.Normal, true);
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) {
return this.$locale.getAllDateRanges(DateRangeScene.TrendAnalysis, true);
} else { } else {
return []; return undefined;
} }
}, },
showAccountBalance() { set: (value: number | undefined) => {
return this.settingsStore.appSettings.showAccountBalance; setChartType(value);
}, }
totalAmountName() { });
if (this.query.chartDataType === ChartDataType.IncomeByAccount.type
|| this.query.chartDataType === ChartDataType.IncomeByPrimaryCategory.type function getTransactionItemLinkUrl(itemId: string, dateRange?: TimeRangeAndDateType): string {
|| this.query.chartDataType === ChartDataType.IncomeBySecondaryCategory.type) { return `/transaction/list?${statisticsStore.getTransactionListPageParams(analysisType.value, itemId, dateRange)}`;
return this.$t('Total Income');
} else if (this.query.chartDataType === ChartDataType.ExpenseByAccount.type
|| this.query.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type
|| this.query.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type) {
return this.$t('Total Expense');
} else if (this.query.chartDataType === ChartDataType.AccountTotalAssets.type) {
return this.$t('Total Assets');
} else if (this.query.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
return this.$t('Total Liabilities');
} }
return this.$t('Total Amount'); function init(): void {
}, statisticsStore.initTransactionStatisticsFilter(analysisType.value);
categoricalAnalysisData() {
return this.statisticsStore.categoricalAnalysisData;
},
trendsAnalysisData() {
return this.statisticsStore.trendsAnalysisData;
},
translateNameInTrendsChart() {
return this.query.chartDataType === ChartDataType.TotalExpense.type ||
this.query.chartDataType === ChartDataType.TotalIncome.type ||
this.query.chartDataType === ChartDataType.TotalBalance.type;
},
showAmountInChart() {
if (!this.showAccountBalance
&& (this.query.chartDataType === ChartDataType.AccountTotalAssets.type || this.query.chartDataType === ChartDataType.AccountTotalLiabilities.type)) {
return false;
}
return true;
}
},
created() {
const self = this;
self.statisticsStore.initTransactionStatisticsFilter(self.analysisType);
Promise.all([ Promise.all([
self.accountsStore.loadAllAccounts({ force: false }), accountsStore.loadAllAccounts({ force: false }),
self.transactionCategoriesStore.loadAllCategories({ force: false }) transactionCategoriesStore.loadAllCategories({ force: false })
]).then(() => { ]).then(() => {
if (self.analysisType === StatisticsAnalysisType.CategoricalAnalysis) { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
return self.statisticsStore.loadCategoricalAnalysis({ return statisticsStore.loadCategoricalAnalysis({
force: false force: false
}); }) as Promise<unknown>;
} else if (self.analysisType === StatisticsAnalysisType.TrendAnalysis) { } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
return self.statisticsStore.loadTrendAnalysis({ return statisticsStore.loadTrendAnalysis({
force: false force: false
}); }) as Promise<unknown>;
} else {
return Promise.reject('An error occurred');
} }
}).then(() => { }).then(() => {
self.loading = false; loading.value = false;
}).catch(error => { }).catch(error => {
if (error.processed) { if (error.processed) {
self.loading = false; loading.value = false;
} else { } else {
self.loadingError = error; loadingError.value = error;
self.$toast(error.message || error); showToast(error.message || error);
} }
}); });
},
methods: {
onPageAfterIn() {
if (this.statisticsStore.transactionStatisticsStateInvalid && !this.loading) {
this.reload(null);
} }
this.$routeBackOnError(this.f7router, 'loadingError'); function reload(done?: () => void): void {
},
reload(done) {
const self = this;
const force = !!done; const force = !!done;
let dispatchPromise = null; let dispatchPromise: Promise<unknown> | null = null;
self.reloading = true; reloading.value = true;
if (self.query.chartDataType === ChartDataType.ExpenseByAccount.type || if (query.value.chartDataType === ChartDataType.ExpenseByAccount.type ||
self.query.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type || query.value.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type ||
self.query.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type || query.value.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type ||
self.query.chartDataType === ChartDataType.IncomeByAccount.type || query.value.chartDataType === ChartDataType.IncomeByAccount.type ||
self.query.chartDataType === ChartDataType.IncomeByPrimaryCategory.type || query.value.chartDataType === ChartDataType.IncomeByPrimaryCategory.type ||
self.query.chartDataType === ChartDataType.IncomeBySecondaryCategory.type || query.value.chartDataType === ChartDataType.IncomeBySecondaryCategory.type ||
self.query.chartDataType === ChartDataType.TotalExpense.type || query.value.chartDataType === ChartDataType.TotalExpense.type ||
self.query.chartDataType === ChartDataType.TotalIncome.type || query.value.chartDataType === ChartDataType.TotalIncome.type ||
self.query.chartDataType === ChartDataType.TotalBalance.type) { query.value.chartDataType === ChartDataType.TotalBalance.type) {
if (self.analysisType === StatisticsAnalysisType.CategoricalAnalysis) { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
dispatchPromise = self.statisticsStore.loadCategoricalAnalysis({ dispatchPromise = statisticsStore.loadCategoricalAnalysis({
force: force force: force
}); });
} else if (self.analysisType === StatisticsAnalysisType.TrendAnalysis) { } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
dispatchPromise = self.statisticsStore.loadTrendAnalysis({ dispatchPromise = statisticsStore.loadTrendAnalysis({
force: force force: force
}); });
} }
} else if (self.query.chartDataType === ChartDataType.AccountTotalAssets.type || } else if (query.value.chartDataType === ChartDataType.AccountTotalAssets.type ||
self.query.chartDataType === ChartDataType.AccountTotalLiabilities.type) { query.value.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
dispatchPromise = self.accountsStore.loadAllAccounts({ dispatchPromise = accountsStore.loadAllAccounts({
force: force force: force
}); });
} }
if (dispatchPromise) { if (dispatchPromise) {
dispatchPromise.then(() => { dispatchPromise.then(() => {
self.reloading = false; reloading.value = false;
if (done) { done?.();
done();
}
if (force) { if (force) {
self.$toast('Data has been updated'); showToast('Data has been updated');
} }
}).catch(error => { }).catch(error => {
self.reloading = false; reloading.value = false;
if (done) { done?.();
done();
}
if (!error.processed) { if (!error.processed) {
self.$toast(error.message || error); showToast(error.message || error);
} }
}); });
} else { } else {
self.reloading = false; reloading.value = false;
} }
}, }
setChartType(chartType) {
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) { function setChartType(type?: number): void {
this.statisticsStore.updateTransactionStatisticsFilter({ if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
categoricalChartType: chartType statisticsStore.updateTransactionStatisticsFilter({
categoricalChartType: type
}); });
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) { } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
this.statisticsStore.updateTransactionStatisticsFilter({ statisticsStore.updateTransactionStatisticsFilter({
trendChartType: chartType trendChartType: type
}); });
} }
}, }
setChartDataType(analysisType, chartDataType) {
function setChartDataType(type: number, chartDataType: number): void {
let analysisTypeChanged = false; let analysisTypeChanged = false;
if (this.analysisType !== analysisType) { if (analysisType.value !== type) {
if (!ChartDataType.isAvailableForAnalysisType(this.queryChartDataType, analysisType)) { if (!ChartDataType.isAvailableForAnalysisType(query.value.chartDataType, type)) {
this.statisticsStore.updateTransactionStatisticsFilter({ statisticsStore.updateTransactionStatisticsFilter({
chartDataType: ChartDataType.Default.type chartDataType: ChartDataType.Default.type
}); });
} }
this.analysisType = analysisType; analysisType.value = type;
this.statisticsStore.updateTransactionStatisticsInvalidState(true); statisticsStore.updateTransactionStatisticsInvalidState(true);
analysisTypeChanged = true; analysisTypeChanged = true;
} }
this.statisticsStore.updateTransactionStatisticsFilter({ statisticsStore.updateTransactionStatisticsFilter({
chartDataType: chartDataType chartDataType: chartDataType
}); });
this.showChartDataTypePopover = false; showChartDataTypePopover.value = false;
if (analysisTypeChanged) { if (analysisTypeChanged) {
this.reload(null); reload();
} }
}, }
setSortingType(sortingType) {
if (sortingType < ChartSortingType.Amount.type || sortingType > ChartSortingType.Name.type) { function setSortingType(type: number): void {
this.showSortingTypePopover = false; if (type < ChartSortingType.Amount.type || type > ChartSortingType.Name.type) {
showSortingTypePopover.value = false;
return; return;
} }
this.statisticsStore.updateTransactionStatisticsFilter({ statisticsStore.updateTransactionStatisticsFilter({
sortingType: sortingType sortingType: type
}); });
this.showSortingTypePopover = false; showSortingTypePopover.value = false;
}, }
setTrendDateAggregationType(aggregationType) {
this.trendDateAggregationType = aggregationType; function setTrendDateAggregationType(type: number): void {
this.showDateAggregationPopover = false; trendDateAggregationType.value = type;
}, showDateAggregationPopover.value = false;
setDateFilter(dateType) { }
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) {
if (dateType === this.allDateRanges.Custom.type) { // Custom function setDateFilter(dateType: number): void {
this.showCustomDateRangeSheet = true; if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
this.showDatePopover = false; if (dateType === DateRange.Custom.type) { // Custom
showCustomDateRangeSheet.value = true;
showDatePopover.value = false;
return; return;
} else if (this.query.categoricalChartDateType === dateType) { } else if (query.value.categoricalChartDateType === dateType) {
return; return;
} }
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) { } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
if (dateType === this.allDateRanges.Custom.type) { // Custom if (dateType === DateRange.Custom.type) { // Custom
this.showCustomMonthRangeSheet = true; showCustomMonthRangeSheet.value = true;
this.showDatePopover = false; showDatePopover.value = false;
return; return;
} else if (this.query.trendChartDateType === dateType) { } else if (query.value.trendChartDateType === dateType) {
return; return;
} }
} }
const dateRange = getDateRangeByDateType(dateType, this.firstDayOfWeek); const dateRange = getDateRangeByDateType(dateType, firstDayOfWeek.value);
if (!dateRange) { if (!dateRange) {
return; return;
@@ -689,100 +601,80 @@ export default {
let changed = false; let changed = false;
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
changed = this.statisticsStore.updateTransactionStatisticsFilter({ changed = statisticsStore.updateTransactionStatisticsFilter({
categoricalChartDateType: dateRange.dateType, categoricalChartDateType: dateRange.dateType,
categoricalChartStartTime: dateRange.minTime, categoricalChartStartTime: dateRange.minTime,
categoricalChartEndTime: dateRange.maxTime categoricalChartEndTime: dateRange.maxTime
}); });
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) { } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
changed = this.statisticsStore.updateTransactionStatisticsFilter({ changed = statisticsStore.updateTransactionStatisticsFilter({
trendChartDateType: dateRange.dateType, trendChartDateType: dateRange.dateType,
trendChartStartYearMonth: getYearAndMonthFromUnixTime(dateRange.minTime), trendChartStartYearMonth: getYearAndMonthFromUnixTime(dateRange.minTime),
trendChartEndYearMonth: getYearAndMonthFromUnixTime(dateRange.maxTime) trendChartEndYearMonth: getYearAndMonthFromUnixTime(dateRange.maxTime)
}); });
} }
this.showDatePopover = false; showDatePopover.value = false;
if (changed) { if (changed) {
this.reload(null); reload();
} }
}, }
setCustomDateFilter(startTime, endTime) {
function setCustomDateFilter(startTime: number | string, endTime: number | string): void {
if (!startTime || !endTime) { if (!startTime || !endTime) {
return; return;
} }
let changed = false; let changed = false;
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis && isNumber(startTime) && isNumber(endTime)) {
const chartDateType = getDateTypeByDateRange(startTime, endTime, this.firstDayOfWeek, DateRangeScene.Normal); const chartDateType = getDateTypeByDateRange(startTime, endTime, firstDayOfWeek.value, DateRangeScene.Normal);
changed = this.statisticsStore.updateTransactionStatisticsFilter({ changed = statisticsStore.updateTransactionStatisticsFilter({
categoricalChartDateType: chartDateType, categoricalChartDateType: chartDateType,
categoricalChartStartTime: startTime, categoricalChartStartTime: startTime,
categoricalChartEndTime: endTime categoricalChartEndTime: endTime
}); });
this.showCustomDateRangeSheet = false; showCustomDateRangeSheet.value = false;
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) { } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis && isString(startTime) && isString(endTime)) {
const chartDateType = getDateTypeByDateRange(getYearMonthFirstUnixTime(startTime), getYearMonthLastUnixTime(endTime), this.firstDayOfWeek, DateRangeScene.TrendAnalysis); const chartDateType = getDateTypeByDateRange(getYearMonthFirstUnixTime(startTime), getYearMonthLastUnixTime(endTime), firstDayOfWeek.value, DateRangeScene.TrendAnalysis);
changed = this.statisticsStore.updateTransactionStatisticsFilter({ changed = statisticsStore.updateTransactionStatisticsFilter({
trendChartDateType: chartDateType, trendChartDateType: chartDateType,
trendChartStartYearMonth: startTime, trendChartStartYearMonth: startTime,
trendChartEndYearMonth: endTime trendChartEndYearMonth: endTime
}); });
this.showCustomMonthRangeSheet = false; showCustomMonthRangeSheet.value = false;
} }
if (changed) { if (changed) {
this.reload(null); reload();
} }
},
showCustomDateRange() {
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) {
return this.query.categoricalChartDateType === this.allDateRanges.Custom.type && this.query.categoricalChartStartTime && this.query.categoricalChartEndTime;
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) {
return this.query.trendChartDateType === this.allDateRanges.Custom.type && this.query.trendChartStartYearMonth && this.query.trendChartEndYearMonth;
} else {
return false;
}
},
canShiftDateRange(query) {
if (query.chartDataType === ChartDataType.AccountTotalAssets.type || query.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
return false;
} }
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) { function shiftDateRange(scale: number): void {
return query.categoricalChartDateType !== this.allDateRanges.All.type; if (query.value.categoricalChartDateType === DateRange.All.type) {
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) {
return query.trendChartDateType !== this.allDateRanges.All.type;
} else {
return false;
}
},
shiftDateRange(query, scale) {
if (this.query.categoricalChartDateType === this.allDateRanges.All.type) {
return; return;
} }
let changed = false; let changed = false;
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) { if (analysisType.value === StatisticsAnalysisType.CategoricalAnalysis) {
const newDateRange = getShiftedDateRangeAndDateType(query.categoricalChartStartTime, query.categoricalChartEndTime, scale, this.firstDayOfWeek, DateRangeScene.Normal); const newDateRange = getShiftedDateRangeAndDateType(query.value.categoricalChartStartTime, query.value.categoricalChartEndTime, scale, firstDayOfWeek.value, DateRangeScene.Normal);
changed = this.statisticsStore.updateTransactionStatisticsFilter({ changed = statisticsStore.updateTransactionStatisticsFilter({
categoricalChartDateType: newDateRange.dateType, categoricalChartDateType: newDateRange.dateType,
categoricalChartStartTime: newDateRange.minTime, categoricalChartStartTime: newDateRange.minTime,
categoricalChartEndTime: newDateRange.maxTime categoricalChartEndTime: newDateRange.maxTime
}); });
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) { } else if (analysisType.value === StatisticsAnalysisType.TrendAnalysis) {
const newDateRange = getShiftedDateRangeAndDateType(getYearMonthFirstUnixTime(query.trendChartStartYearMonth), getYearMonthLastUnixTime(query.trendChartEndYearMonth), scale, this.firstDayOfWeek, DateRangeScene.TrendAnalysis); const newDateRange = getShiftedDateRangeAndDateType(getYearMonthFirstUnixTime(query.value.trendChartStartYearMonth), getYearMonthLastUnixTime(query.value.trendChartEndYearMonth), scale, firstDayOfWeek.value, DateRangeScene.TrendAnalysis);
changed = this.statisticsStore.updateTransactionStatisticsFilter({ changed = statisticsStore.updateTransactionStatisticsFilter({
trendChartDateType: newDateRange.dateType, trendChartDateType: newDateRange.dateType,
trendChartStartYearMonth: getYearAndMonthFromUnixTime(newDateRange.minTime), trendChartStartYearMonth: getYearAndMonthFromUnixTime(newDateRange.minTime),
trendChartEndYearMonth: getYearAndMonthFromUnixTime(newDateRange.maxTime) trendChartEndYearMonth: getYearAndMonthFromUnixTime(newDateRange.maxTime)
@@ -790,71 +682,47 @@ export default {
} }
if (changed) { if (changed) {
this.reload(null); reload();
} }
},
dateRangeName(query) {
if (this.query.chartDataType === ChartDataType.AccountTotalAssets.type ||
this.query.chartDataType === ChartDataType.AccountTotalLiabilities.type) {
return this.$t(this.allDateRanges.All.name);
} }
if (this.analysisType === StatisticsAnalysisType.CategoricalAnalysis) { function filterAccounts(): void {
return this.$locale.getDateRangeDisplayName(this.userStore, query.categoricalChartDateType, query.categoricalChartStartTime, query.categoricalChartEndTime); props.f7router.navigate('/settings/filter/account?type=statisticsCurrent');
} else if (this.analysisType === StatisticsAnalysisType.TrendAnalysis) {
return this.$locale.getDateRangeDisplayName(this.userStore, query.trendChartDateType, getYearMonthFirstUnixTime(query.trendChartStartYearMonth), getYearMonthLastUnixTime(query.trendChartEndYearMonth));
} else {
return '';
} }
},
clickPieChartItem(item) { function filterCategories(): void {
this.f7router.navigate(this.getTransactionItemLinkUrl(item.id)); props.f7router.navigate('/settings/filter/category?type=statisticsCurrent');
}, }
clickTrendChartItem(item) {
this.f7router.navigate(this.getTransactionItemLinkUrl(item.itemId, item.dateRange)); function filterTags(): void {
}, props.f7router.navigate('/settings/filter/tag?type=statisticsCurrent');
filterAccounts() { }
this.f7router.navigate('/settings/filter/account?type=statisticsCurrent');
}, function settings(): void {
filterCategories() { props.f7router.navigate('/statistic/settings');
this.f7router.navigate('/settings/filter/category?type=statisticsCurrent'); }
},
filterTags() { function scrollPopoverToSelectedItem(event: { $el: Framework7Dom }): void {
this.f7router.navigate('/settings/filter/tag?type=statisticsCurrent');
},
settings() {
this.f7router.navigate('/statistic/settings');
},
scrollPopoverToSelectedItem(event) {
scrollToSelectedItem(event.$el, '.popover-inner', 'li.list-item-selected'); scrollToSelectedItem(event.$el, '.popover-inner', 'li.list-item-selected');
},
getDisplayAmount(amount, currency, textLimit) {
amount = this.getDisplayCurrency(amount, currency);
if (!this.showAccountBalance
&& (this.query.chartDataType === ChartDataType.AccountTotalAssets.type
|| this.query.chartDataType === ChartDataType.AccountTotalLiabilities.type)
) {
return '***';
} }
if (textLimit) { function onClickPieChartItem(item: Record<string, unknown>): void {
return limitText(amount, textLimit); props.f7router.navigate(getTransactionItemLinkUrl(item['id'] as string));
} }
return amount; function onClickTrendChartItem(item: { itemId: string, dateRange: TimeRangeAndDateType }): void {
}, props.f7router.navigate(getTransactionItemLinkUrl(item.itemId, item.dateRange));
getDisplayCurrency(value, currencyCode) {
return this.$locale.formatAmountWithCurrency(this.settingsStore, this.userStore, value, currencyCode);
},
getDisplayPercent(value, precision, lowPrecisionValue) {
return formatPercent(value, precision, lowPrecisionValue);
},
getTransactionItemLinkUrl(itemId, dateRange) {
return `/transaction/list?${this.statisticsStore.getTransactionListPageParams(this.analysisType, itemId, dateRange)}`;
} }
function onPageAfterIn(): void {
if (statisticsStore.transactionStatisticsStateInvalid && !loading.value) {
reload();
} }
};
routeBackOnError(props.f7router, loadingError);
}
init();
</script> </script>
<style> <style>