reconciliation statement page / dialog supports account balance trends chart (#184)

This commit is contained in:
MaysWind
2025-08-04 01:22:36 +08:00
parent 15d1d269ae
commit 14b4e40039
26 changed files with 917 additions and 29 deletions
@@ -0,0 +1,183 @@
import { computed } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import {
type UnixTimeRange,
type YearUnixTime,
type YearQuarterUnixTime,
type YearMonthUnixTime,
YearMonthDayUnixTime,
} from '@/core/datetime.ts';
import type { FiscalYearUnixTime } from '@/core/fiscalyear.ts';
import { ChartDateAggregationType } from '@/core/statistics.ts';
import type { TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts';
import { isDefined, isArray } from '@/lib/common.ts';
import {
getYearAndMonthFromUnixTime,
getYearFirstUnixTimeBySpecifiedUnixTime,
getQuarterFirstUnixTimeBySpecifiedUnixTime,
getMonthFirstUnixTimeBySpecifiedUnixTime,
getDayFirstUnixTimeBySpecifiedUnixTime,
getAllDaysStartAndEndUnixTimes,
getFiscalYearStartUnixTime
} from '@/lib/datetime.ts';
import { getAllDateRangesByYearMonthRange } from '@/lib/statistics.ts';
export interface AccountBalanceUnixTimeAndBalanceRange extends UnixTimeRange {
minUnixTimeBalance: number;
maxUnixTimeBalance: number;
}
export interface AccountBalanceTrendsChartItem {
displayDate: string;
amount: number;
}
export interface CommonAccountBalanceTrendsChartProps {
items: TransactionReconciliationStatementResponseItem[] | undefined;
dateAggregationType?: number;
fiscalYearStart: number;
accountCurrency: string;
}
export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTrendsChartProps) {
const { formatUnixTimeToShortDate, formatUnixTimeToShortYear, formatUnixTimeToShortYearMonth, formatUnixTimeToYearQuarter, formatUnixTimeToFiscalYear } = useI18n();
const dataDateRange = computed<AccountBalanceUnixTimeAndBalanceRange | null>(() => {
if (!props.items || props.items.length < 1) {
return null;
}
let minUnixTime = Number.MAX_SAFE_INTEGER, maxUnixTime = 0;
let minUnixTimeBalance = 0, maxUnixTimeBalance = 0;
for (let i = 0; i < props.items.length; i++) {
const item = props.items[i];
if (item.time < minUnixTime) {
minUnixTime = item.time;
minUnixTimeBalance = item.accountBalance;
}
if (item.time > maxUnixTime) {
maxUnixTime = item.time;
maxUnixTimeBalance = item.accountBalance;
}
}
if (minUnixTime >= Number.MAX_SAFE_INTEGER || maxUnixTime <= 0) {
return null;
}
return {
minUnixTime: minUnixTime,
maxUnixTime: maxUnixTime,
minUnixTimeBalance: minUnixTimeBalance,
maxUnixTimeBalance: maxUnixTimeBalance
};
});
const allDateRanges = computed<YearUnixTime[] | FiscalYearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[] | YearMonthDayUnixTime[]>(() => {
if (!dataDateRange.value) {
return [];
}
if (!isDefined(props.dateAggregationType)) {
return getAllDaysStartAndEndUnixTimes(dataDateRange.value.minUnixTime, dataDateRange.value.maxUnixTime);
} else {
const startYearMonth = getYearAndMonthFromUnixTime(dataDateRange.value.minUnixTime);
const endYearMonth = getYearAndMonthFromUnixTime(dataDateRange.value.maxUnixTime);
return getAllDateRangesByYearMonthRange(startYearMonth, endYearMonth, props.fiscalYearStart, props.dateAggregationType);
}
});
const allDataItems = computed<AccountBalanceTrendsChartItem[]>(() => {
const ret: AccountBalanceTrendsChartItem[] = [];
if (!dataDateRange.value || !allDateRanges.value || allDateRanges.value.length < 1 || !props.items || props.items.length < 1) {
return ret;
}
const dayDataItemsMap: Record<number, TransactionReconciliationStatementResponseItem[]> = {};
for (let i = 0; i < props.items.length; i++) {
const dateItem = props.items[i];
let dateRangeMinUnixTime = 0;
if (props.dateAggregationType === ChartDateAggregationType.Year.type) {
dateRangeMinUnixTime = getYearFirstUnixTimeBySpecifiedUnixTime(dateItem.time);
} else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type) {
dateRangeMinUnixTime = getFiscalYearStartUnixTime(dateItem.time, props.fiscalYearStart);
} else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type) {
dateRangeMinUnixTime = getQuarterFirstUnixTimeBySpecifiedUnixTime(dateItem.time);
} else if (props.dateAggregationType === ChartDateAggregationType.Month.type) {
dateRangeMinUnixTime = getMonthFirstUnixTimeBySpecifiedUnixTime(dateItem.time);
} else {
dateRangeMinUnixTime = getDayFirstUnixTimeBySpecifiedUnixTime(dateItem.time);
}
const dataItems: TransactionReconciliationStatementResponseItem[] = dayDataItemsMap[dateRangeMinUnixTime] || [];
dataItems.push(dateItem);
dayDataItemsMap[dateRangeMinUnixTime] = dataItems;
}
let lastAmount = dataDateRange.value.minUnixTimeBalance;
for (let i = 0; i < allDateRanges.value.length; i++) {
const dateRange = allDateRanges.value[i];
const dataItems = dayDataItemsMap[dateRange.minUnixTime];
let displayDate = '';
if (props.dateAggregationType === ChartDateAggregationType.Year.type) {
displayDate = formatUnixTimeToShortYear(dateRange.minUnixTime);
} else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type) {
displayDate = formatUnixTimeToFiscalYear(dateRange.minUnixTime);
} else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type) {
displayDate = formatUnixTimeToYearQuarter(dateRange.minUnixTime);
} else if (props.dateAggregationType === ChartDateAggregationType.Month.type) {
displayDate = formatUnixTimeToShortYearMonth(dateRange.minUnixTime);
} else {
displayDate = formatUnixTimeToShortDate(dateRange.minUnixTime);
}
if (isArray(dataItems)) {
let lastUnixTime = 0;
for (let i = 0; i < dataItems.length; i++) {
const dataItem = dataItems[i];
if (dataItem.time >= lastUnixTime) {
lastUnixTime = dataItem.time;
lastAmount = dataItem.accountBalance;
}
}
}
ret.push({
displayDate: displayDate,
amount: lastAmount
});
}
return ret;
});
const allDisplayDateRanges = computed<string[]>(() => {
if (!allDataItems.value || allDataItems.value.length < 1) {
return [];
}
return allDataItems.value.map(item => item.displayDate);
});
return {
// computed states
allDateRanges,
allDataItems,
allDisplayDateRanges
};
}
@@ -0,0 +1,184 @@
<template>
<v-chart autoresize class="account-balance-trends-chart-container" :class="{ 'transition-in': skeleton }" :option="chartOptions"/>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useTheme } from 'vuetify';
import type { CallbackDataParams } from 'echarts/types/dist/shared';
import { useI18n } from '@/locales/helpers.ts';
import { type CommonAccountBalanceTrendsChartProps, useAccountBalanceTrendsChartBase } from '@/components/base/AccountBalanceTrendsChartBase.ts'
import type { ColorValue } from '@/core/color.ts';
import { ThemeType } from '@/core/theme.ts';
import { TrendChartType } from '@/core/statistics.ts';
import { DEFAULT_CHART_COLORS } from '@/consts/color.ts';
interface DesktopAccountBalanceTrendsChartProps extends CommonAccountBalanceTrendsChartProps {
legendName: string;
skeleton?: boolean;
type?: number;
}
interface AccountBalanceTrendsChartDataItem {
id: string;
name: string;
itemStyle: {
color: ColorValue;
};
selected: boolean;
type: string;
areaStyle?: object;
stack: string;
animation: boolean;
data: number[];
}
const props = defineProps<DesktopAccountBalanceTrendsChartProps>();
const theme = useTheme();
const { formatAmountWithCurrency } = useI18n();
const { allDataItems, allDisplayDateRanges } = useAccountBalanceTrendsChartBase(props);
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
const allSeries = computed<AccountBalanceTrendsChartDataItem[]>(() => {
const series: AccountBalanceTrendsChartDataItem = {
id: 'accountBalance',
name: props.legendName,
itemStyle: {
color: `#${DEFAULT_CHART_COLORS[0]}`
},
selected: true,
type: 'line',
stack: 'a',
animation: !props.skeleton,
data: []
};
if (props.type === TrendChartType.Area.type) {
series.areaStyle = {};
} else if (props.type === TrendChartType.Column.type) {
series.type = 'bar';
}
for (let i = 0; i < allDataItems.value.length; i++) {
const item = allDataItems.value[i];
series.data.push(item.amount);
}
return [series];
});
const yAxisWidth = computed<number>(() => {
let maxValue = Number.MIN_SAFE_INTEGER;
let minValue = Number.MAX_SAFE_INTEGER;
let width = 90;
if (!allSeries.value || !allSeries.value.length) {
return width;
}
for (let i = 0; i < allSeries.value.length; i++) {
for (let j = 0; j < allSeries.value[i].data.length; j++) {
const value = allSeries.value[i].data[j];
if (value > maxValue) {
maxValue = value;
}
if (value < minValue) {
minValue = value;
}
}
}
const maxValueText = formatAmountWithCurrency(maxValue, props.accountCurrency);
const minValueText = formatAmountWithCurrency(minValue, props.accountCurrency);
const maxLengthText = maxValueText.length > minValueText.length ? maxValueText : minValueText;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (context) {
context.font = '12px Arial';
const textMetrics = context.measureText(maxLengthText);
const actualWidth = Math.round(textMetrics.width) + 20;
if (actualWidth >= 200) {
width = 200;
} if (actualWidth > 90) {
width = actualWidth;
}
}
return width;
});
const chartOptions = computed<object>(() => {
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: isDarkMode.value ? '#333' : '#fff',
color: isDarkMode.value ? '#eee' : '#333'
},
},
backgroundColor: isDarkMode.value ? '#333' : '#fff',
borderColor: isDarkMode.value ? '#333' : '#fff',
textStyle: {
color: isDarkMode.value ? '#eee' : '#333'
},
formatter: (params: CallbackDataParams[]) => {
const amount = params[0].data as number;
const value = formatAmountWithCurrency(amount, props.accountCurrency);
return `${params[0].name}<br/>`
+ '<div><span class="chart-pointer" style="background-color: #' + DEFAULT_CHART_COLORS[0] + '"></span>'
+ `<span>${props.legendName}</span><span style="margin-left: 20px; float: right">${value}</span><br/>`
+ '</div>';
}
},
grid: {
left: yAxisWidth.value,
right: 20
},
xAxis: [
{
type: 'category',
data: allDisplayDateRanges.value
}
],
yAxis: [
{
type: 'value',
axisLabel: {
formatter: (value: string) => {
return formatAmountWithCurrency(value, props.accountCurrency);
}
},
axisPointer: {
label: {
formatter: (params: CallbackDataParams) => {
return formatAmountWithCurrency(Math.floor(params.value as number), props.accountCurrency);
}
}
}
}
],
series: allSeries.value
};
});
</script>
<style scoped>
.account-balance-trends-chart-container {
width: 100%;
height: 400px;
margin-top: 10px;
}
</style>
@@ -0,0 +1,133 @@
<template>
<f7-list class="skeleton-text margin-top-half" media-list v-if="loading">
<f7-list-item class="account-balance-trends-list-item" title="Date Range" after="0.00 USD"
:key="itemIdx" v-for="itemIdx in [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]">
<template #media>
<f7-icon f7="app_fill"></f7-icon>
</template>
<template #inner>
<div class="display-flex padding-top-half">
<div class="account-balance-percent-line width-100">
<f7-progressbar :progress="0"></f7-progressbar>
</div>
</div>
</template>
</f7-list-item>
</f7-list>
<f7-list v-else-if="!loading && (!allVirtualListItems || !allVirtualListItems.length)">
<f7-list-item :title="tt('No transaction data')"></f7-list-item>
</f7-list>
<f7-list class="margin-top-half" media-list virtual-list :virtual-list-params="{ items: allVirtualListItems, renderExternal, height: 'auto' }"
:key="`account-balance-trends-${dateAggregationType}`"
v-else-if="!loading && allVirtualListItems && allVirtualListItems.length > 0">
<ul>
<f7-list-item class="account-balance-trends-list-item"
:key="item.index"
:style="`top: ${virtualDataItems.topPosition}px`"
:virtual-list-index="item.index"
:title="item.displayDate"
:after="formatAmountWithCurrency(item.amount, accountCurrency)"
v-for="item in virtualDataItems.items"
>
<template #media>
<f7-icon f7="calendar"></f7-icon>
</template>
<template #inner>
<div class="display-flex padding-top-half">
<div class="account-balance-percent-line" :style="{ 'width': item.percent + '%' }">
<f7-progressbar :progress="100" :style="{ '--f7-progressbar-progress-color': (item.color ? item.color : '') } "></f7-progressbar>
</div>
<div class="account-balance-percent-line" :style="{ 'width': (100.0 - item.percent) + '%' }"
v-if="item.percent < 100.0">
<f7-progressbar :progress="0"></f7-progressbar>
</div>
</div>
</template>
</f7-list-item>
</ul>
</f7-list>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
import {
type AccountBalanceTrendsChartItem,
type CommonAccountBalanceTrendsChartProps,
useAccountBalanceTrendsChartBase
} from '@/components/base/AccountBalanceTrendsChartBase.ts'
import type { ColorValue } from '@/core/color.ts';
import { DEFAULT_CHART_COLORS } from '@/consts/color.ts';
interface MobileAccountBalanceTrendsChartItem extends AccountBalanceTrendsChartItem {
index: number;
percent: number;
color: ColorValue;
}
interface MobileAccountBalanceTrendsChartProps extends CommonAccountBalanceTrendsChartProps {
loading?: boolean;
}
interface MobileAccountBalanceTrendsChartVirtualListData {
items: MobileAccountBalanceTrendsChartItem[],
topPosition: number
}
const props = defineProps<MobileAccountBalanceTrendsChartProps>();
const { tt, formatAmountWithCurrency } = useI18n();
const { allDataItems } = useAccountBalanceTrendsChartBase(props);
const virtualDataItems = ref<MobileAccountBalanceTrendsChartVirtualListData>({
items: [],
topPosition: 0
});
const allVirtualListItems = computed<MobileAccountBalanceTrendsChartItem[]>(() => {
const ret: MobileAccountBalanceTrendsChartItem[] = [];
let maxAmount = 0;
for (let i = 0; i < allDataItems.value.length; i++) {
const dataItem = allDataItems.value[i];
if (dataItem.amount > maxAmount) {
maxAmount = dataItem.amount;
}
const finalDataItem: MobileAccountBalanceTrendsChartItem = {
index: i,
displayDate: dataItem.displayDate,
amount: dataItem.amount,
color: `#${DEFAULT_CHART_COLORS[0]}`,
percent: 0.0
};
ret.push(finalDataItem);
}
for (let i = 0; i < ret.length; i++) {
if (maxAmount > 0 && ret[i].amount > 0) {
ret[i].percent = 100.0 * ret[i].amount / maxAmount;
} else {
ret[i].percent = 0.0;
}
}
return ret;
});
function renderExternal(vl: unknown, vlData: MobileAccountBalanceTrendsChartVirtualListData): void {
virtualDataItems.value = vlData;
}
</script>
<style>
.account-balance-trends-list-item .account-balance-percent-line {
--f7-progressbar-bg-color: #f8f8f8;
}
</style>
+26
View File
@@ -20,6 +20,12 @@ export interface YearMonthRange {
readonly endYearMonth: Year0BasedMonth;
}
export interface YearMonthDay {
readonly year: number;
readonly month: number; // 1-based (1 = January, 12 = December)
readonly day: number;
}
export interface TimeRange {
readonly minTime: number;
readonly maxTime: number;
@@ -136,6 +142,26 @@ export class YearMonthUnixTime implements Year0BasedMonth, UnixTimeRange {
}
}
export class YearMonthDayUnixTime implements YearMonthDay, UnixTimeRange {
public readonly year: number;
public readonly month: number;
public readonly day: number;
public readonly minUnixTime: number;
public readonly maxUnixTime: number;
private constructor(year: number, month: number, day: number, minUnixTime: number, maxUnixTime: number) {
this.year = year;
this.month = month;
this.day = day
this.minUnixTime = minUnixTime;
this.maxUnixTime = maxUnixTime;
}
public static of(yearMonthDay: YearMonthDay, minUnixTime: number, maxUnixTime: number): YearMonthDayUnixTime {
return new YearMonthDayUnixTime(yearMonthDay.year, yearMonthDay.month, yearMonthDay.day, minUnixTime, maxUnixTime);
}
}
export type MonthValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
export class Month {
+10 -8
View File
@@ -151,23 +151,25 @@ export class ChartSortingType implements TypeAndName {
}
}
export class ChartDateAggregationType implements TypeAndName {
export class ChartDateAggregationType {
private static readonly allInstances: ChartDateAggregationType[] = [];
private static readonly allInstancesByType: Record<number, ChartDateAggregationType> = {};
public static readonly Month = new ChartDateAggregationType(0, 'Aggregate by Month');
public static readonly Quarter = new ChartDateAggregationType(1, 'Aggregate by Quarter');
public static readonly Year = new ChartDateAggregationType(2, 'Aggregate by Year');
public static readonly FiscalYear = new ChartDateAggregationType(3, 'Aggregate by Fiscal Year');
public static readonly Month = new ChartDateAggregationType(0, 'Monthly', 'Aggregate by Month');
public static readonly Quarter = new ChartDateAggregationType(1, 'Quarterly', 'Aggregate by Quarter');
public static readonly Year = new ChartDateAggregationType(2, 'Yearly', 'Aggregate by Year');
public static readonly FiscalYear = new ChartDateAggregationType(3, 'FiscalYearly', 'Aggregate by Fiscal Year');
public static readonly Default = ChartDateAggregationType.Month;
public readonly type: number;
public readonly name: string;
public readonly shortName: string;
public readonly fullName: string;
private constructor(type: number, name: string) {
private constructor(type: number, shortName: string, fullName: string) {
this.type = type;
this.name = name;
this.shortName = shortName;
this.fullName = fullName;
ChartDateAggregationType.allInstances.push(this);
ChartDateAggregationType.allInstancesByType[type] = this;
+2
View File
@@ -99,6 +99,7 @@ import MonthlyTrendsChart from '@/components/desktop/MonthlyTrendsChart.vue';
import DateRangeSelectionDialog from '@/components/desktop/DateRangeSelectionDialog.vue';
import MonthSelectionDialog from '@/components/desktop/MonthSelectionDialog.vue';
import MonthRangeSelectionDialog from '@/components/desktop/MonthRangeSelectionDialog.vue';
import AccountBalanceTrendsChart from '@/components/desktop/AccountBalanceTrendsChart.vue';
import SwitchToMobileDialog from '@/components/desktop/SwitchToMobileDialog.vue';
import '@/styles/desktop/template/vuetify/index.scss';
@@ -525,6 +526,7 @@ app.component('MonthlyTrendsChart', MonthlyTrendsChart);
app.component('DateRangeSelectionDialog', DateRangeSelectionDialog);
app.component('MonthSelectionDialog', MonthSelectionDialog);
app.component('MonthRangeSelectionDialog', MonthRangeSelectionDialog);
app.component('AccountBalanceTrendsChart', AccountBalanceTrendsChart);
app.component('SwitchToMobileDialog', SwitchToMobileDialog);
app.mount('#app');
+65 -10
View File
@@ -17,6 +17,7 @@ import {
type TimeFormat,
YearQuarterUnixTime,
YearMonthUnixTime,
YearMonthDayUnixTime,
Month,
WeekDay,
MeridiemIndicator,
@@ -222,6 +223,10 @@ export function getYear(date: SupportedDate): number {
return moment(date).year();
}
export function getQuarter(date: SupportedDate): number {
return moment(date).quarter();
}
export function getMonth(date: SupportedDate): number {
return moment(date).month() + 1;
}
@@ -323,15 +328,6 @@ export function getThisMonthLastUnixTime(): number {
return moment.unix(getThisMonthFirstUnixTime()).add(1, 'months').subtract(1, 'seconds').unix();
}
export function getMonthFirstUnixTimeBySpecifiedUnixTime(unixTime: number): number {
const date = moment.unix(unixTime).set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
return date.subtract(date.date() - 1, 'days').unix();
}
export function getMonthLastUnixTimeBySpecifiedUnixTime(unixTime: number): number {
return moment.unix(getMonthFirstUnixTimeBySpecifiedUnixTime(unixTime)).add(1, 'months').subtract(1, 'seconds').unix();
}
export function getThisMonthSpecifiedDayFirstUnixTime(date: number): number {
return moment().set({ date: date, hour: 0, minute: 0, second: 0, millisecond: 0 }).unix();
}
@@ -349,10 +345,43 @@ export function getThisYearLastUnixTime(): number {
return moment.unix(getThisYearFirstUnixTime()).add(1, 'years').subtract(1, 'seconds').unix();
}
export function getSpecifiedDayFirstUnixTime(unixTime: number): number {
export function getYearFirstUnixTimeBySpecifiedUnixTime(unixTime: number): number {
const date = moment.unix(unixTime).set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
return date.subtract(date.dayOfYear() - 1, 'days').unix();
}
export function getYearLastUnixTimeBySpecifiedUnixTime(unixTime: number): number {
return moment.unix(getYearFirstUnixTimeBySpecifiedUnixTime(unixTime)).add(1, 'years').subtract(1, 'seconds').unix();
}
export function getQuarterFirstUnixTimeBySpecifiedUnixTime(unixTime: number): number {
const date = moment.unix(unixTime).set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
const month = date.month();
const quarterStartMonth = Math.floor(month / 3) * 3;
return date.set({ month: quarterStartMonth, date: 1 }).unix();
}
export function getQuarterLastUnixTimeBySpecifiedUnixTime(unixTime: number): number {
return moment.unix(getQuarterFirstUnixTimeBySpecifiedUnixTime(unixTime)).add(3, 'months').subtract(1, 'seconds').unix();
}
export function getMonthFirstUnixTimeBySpecifiedUnixTime(unixTime: number): number {
const date = moment.unix(unixTime).set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
return date.subtract(date.date() - 1, 'days').unix();
}
export function getMonthLastUnixTimeBySpecifiedUnixTime(unixTime: number): number {
return moment.unix(getMonthFirstUnixTimeBySpecifiedUnixTime(unixTime)).add(1, 'months').subtract(1, 'seconds').unix();
}
export function getDayFirstUnixTimeBySpecifiedUnixTime(unixTime: number): number {
return moment.unix(unixTime).set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).unix();
}
export function getDayLastUnixTimeBySpecifiedUnixTime(unixTime: number): number {
return moment.unix(unixTime).set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).add(1, 'days').subtract(1, 'seconds').unix();
}
export function getYearFirstUnixTime(year: number): number {
return moment().set({ year: year, month: 0, date: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }).unix();
}
@@ -581,6 +610,32 @@ export function getAllMonthsStartAndEndUnixTimes(startYearMonth: Year0BasedMonth
return allYearMonthTimes;
}
export function getAllDaysStartAndEndUnixTimes(startUnixTime: number, endUnixTime: number): YearMonthDayUnixTime[] {
const allYearMonthDayTimes: YearMonthDayUnixTime[] = [];
if (!startUnixTime || !endUnixTime) {
return allYearMonthDayTimes;
}
let unixTime: number = startUnixTime;
while (unixTime <= endUnixTime) {
const currentDay = parseDateFromUnixTime(unixTime);
const currentDayMinUnixTime = getDayFirstUnixTimeBySpecifiedUnixTime(unixTime);
const currentDayMaxUnixTime = getDayLastUnixTimeBySpecifiedUnixTime(unixTime);
allYearMonthDayTimes.push(YearMonthDayUnixTime.of({
year: currentDay.year(),
month: currentDay.month() + 1,
day: currentDay.date()
}, currentDayMinUnixTime, currentDayMaxUnixTime));
unixTime = currentDayMaxUnixTime + 1;
}
return allYearMonthDayTimes;
}
export function getDateTimeFormatType<T extends DateFormat | TimeFormat>(allFormatMap: Record<string, T>, allFormatArray: T[], formatTypeValue: number, languageDefaultTypeName: string, systemDefaultFormatType: T): T {
if (formatTypeValue > LANGUAGE_DEFAULT_DATE_TIME_FORMAT_VALUE && allFormatArray[formatTypeValue - 1] && allFormatArray[formatTypeValue - 1].key) {
return allFormatArray[formatTypeValue - 1];
+4
View File
@@ -74,6 +74,10 @@ export function getAllDateRanges<T extends Year1BasedMonth>(items: YearMonthItem
endYearMonth = `${maxYear}-${maxMonth}`;
}
return getAllDateRangesByYearMonthRange(startYearMonth, endYearMonth, fiscalYearStart, dateAggregationType);
}
export function getAllDateRangesByYearMonthRange(startYearMonth: Year1BasedMonth | string, endYearMonth: Year1BasedMonth | string, fiscalYearStart: number, dateAggregationType: number): YearUnixTime[] | FiscalYearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[] {
if (!startYearMonth || !endYearMonth) {
return [];
}
+10
View File
@@ -251,6 +251,13 @@
"31": "31."
}
},
"granularity": {
"FiscalYearly": "Fiscal Yearly",
"Yearly": "Yearly",
"Quarterly": "Quarterly",
"Monthly": "Monthly",
"Daily": "Daily"
},
"numeral": {
"Dot": "Punkt",
"Comma": "Komma",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "Konto kann nicht gelöscht werden",
"Unable to delete this sub-account": "Unable to delete this sub-account",
"Reconciliation Statement": "Reconciliation Statement",
"Show Account Balance Trends": "Show Account Balance Trends",
"Show Transaction List": "Show Transaction List",
"Update Closing Balance": "Update Closing Balance",
"Please enter the new closing balance for the account": "Please enter the new closing balance for the account",
"Transaction": "Transaktion",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "Nach Betrag sortieren",
"Sort by Display Order": "Nach Anzeigereihenfolge sortieren",
"Sort by Name": "Nach Name sortieren",
"Time Granularity": "Time Granularity",
"Aggregate by Month": "Nach Monat aggregieren",
"Aggregate by Quarter": "Nach Quartal aggregieren",
"Aggregate by Year": "Nach Jahr aggregieren",
+10
View File
@@ -251,6 +251,13 @@
"31": "31th"
}
},
"granularity": {
"FiscalYearly": "Fiscal Yearly",
"Yearly": "Yearly",
"Quarterly": "Quarterly",
"Monthly": "Monthly",
"Daily": "Daily"
},
"numeral": {
"Dot": "Dot",
"Comma": "Comma",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "Unable to delete this account",
"Unable to delete this sub-account": "Unable to delete this sub-account",
"Reconciliation Statement": "Reconciliation Statement",
"Show Account Balance Trends": "Show Account Balance Trends",
"Show Transaction List": "Show Transaction List",
"Update Closing Balance": "Update Closing Balance",
"Please enter the new closing balance for the account": "Please enter the new closing balance for the account",
"Transaction": "Transaction",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "Sort by Amount",
"Sort by Display Order": "Sort by Display Order",
"Sort by Name": "Sort by Name",
"Time Granularity": "Time Granularity",
"Aggregate by Month": "Aggregate by Month",
"Aggregate by Quarter": "Aggregate by Quarter",
"Aggregate by Year": "Aggregate by Year",
+10
View File
@@ -251,6 +251,13 @@
"31": "31"
}
},
"granularity": {
"FiscalYearly": "Fiscal Yearly",
"Yearly": "Yearly",
"Quarterly": "Quarterly",
"Monthly": "Monthly",
"Daily": "Daily"
},
"numeral": {
"Dot": "Punto",
"Comma": "Coma",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "No se puede eliminar esta cuenta",
"Unable to delete this sub-account": "Unable to delete this sub-account",
"Reconciliation Statement": "Reconciliation Statement",
"Show Account Balance Trends": "Show Account Balance Trends",
"Show Transaction List": "Show Transaction List",
"Update Closing Balance": "Update Closing Balance",
"Please enter the new closing balance for the account": "Please enter the new closing balance for the account",
"Transaction": "Transacción",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "Ordenar por Importe",
"Sort by Display Order": "Ordenar por orden de visualización",
"Sort by Name": "Ordenar por Nombre",
"Time Granularity": "Time Granularity",
"Aggregate by Month": "Agregado por mes",
"Aggregate by Quarter": "Agregado por trimestre",
"Aggregate by Year": "Agregado por año",
+27 -1
View File
@@ -147,6 +147,7 @@ import {
getTimezoneOffset,
getTimezoneOffsetMinutes,
getYear,
getQuarter,
isDateRangeMatchFullMonths,
isDateRangeMatchFullYears,
isPM,
@@ -496,6 +497,22 @@ export function useI18n() {
return ret;
}
function getLocalizedChartDateAggregationTypeAndDisplayName(fullName: boolean): TypeAndDisplayName[] {
const ret: TypeAndDisplayName[] = [];
const allTypes: ChartDateAggregationType[] = ChartDateAggregationType.values();
for (let i = 0; i < allTypes.length; i++) {
const type = allTypes[i];
ret.push({
type: type.type,
displayName: t(fullName ? type.fullName : `granularity.${type.shortName}`)
});
}
return ret;
}
function getAllMonthNames(type: string): string[] {
const ret = [];
const allMonths = Month.values();
@@ -1472,6 +1489,13 @@ export function useI18n() {
return formatMonthDay(monthDay, getLocalizedLongMonthDayFormat());
}
function formatUnixTimeToYearQuarter(unixTime: number): string {
const date = parseDateFromUnixTime(unixTime);
const year = getYear(date);
const quarter = getQuarter(date);
return formatYearQuarter(year, quarter);
}
function formatYearQuarter(year: number, quarter: number): string {
if (1 <= quarter && quarter <= 4) {
return t('format.yearQuarter.q' + quarter, {
@@ -1912,7 +1936,8 @@ export function useI18n() {
getAllTrendChartTypes: () => getLocalizedDisplayNameAndType(TrendChartType.values()),
getAllStatisticsChartDataTypes: (analysisType: StatisticsAnalysisType) => getLocalizedDisplayNameAndType(ChartDataType.values(analysisType)),
getAllStatisticsSortingTypes: () => getLocalizedDisplayNameAndType(ChartSortingType.values()),
getAllStatisticsDateAggregationTypes: () => getLocalizedDisplayNameAndType(ChartDateAggregationType.values()),
getAllStatisticsDateAggregationTypes: () => getLocalizedChartDateAggregationTypeAndDisplayName(true),
getAllStatisticsDateAggregationTypesWithShortName: () => getLocalizedChartDateAggregationTypeAndDisplayName(false),
getAllTransactionEditScopeTypes: () => getLocalizedDisplayNameAndType(TransactionEditScopeType.values()),
getAllTransactionTagFilterTypes: () => getLocalizedDisplayNameAndType(TransactionTagFilterType.values()),
getAllTransactionScheduledFrequencyTypes: () => getLocalizedDisplayNameAndType(ScheduledTemplateFrequencyType.values()),
@@ -1961,6 +1986,7 @@ export function useI18n() {
formatUnixTimeToShortTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortTimeFormat(), utcOffset, currentUtcOffset),
formatDateToLongDate,
formatMonthDayToLongDay,
formatUnixTimeToYearQuarter,
formatYearQuarter,
formatDateRange,
formatFiscalYearStartToLongDay,
+10
View File
@@ -251,6 +251,13 @@
"31": "31"
}
},
"granularity": {
"FiscalYearly": "Fiscal Yearly",
"Yearly": "Yearly",
"Quarterly": "Quarterly",
"Monthly": "Monthly",
"Daily": "Daily"
},
"numeral": {
"Dot": "Punto",
"Comma": "Virgola",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "Impossibile eliminare questo account",
"Unable to delete this sub-account": "Impossibile eliminare questo sotto-account",
"Reconciliation Statement": "Reconciliation Statement",
"Show Account Balance Trends": "Show Account Balance Trends",
"Show Transaction List": "Show Transaction List",
"Update Closing Balance": "Update Closing Balance",
"Please enter the new closing balance for the account": "Please enter the new closing balance for the account",
"Transaction": "Transazione",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "Ordina per importo",
"Sort by Display Order": "Ordina per ordine di visualizzazione",
"Sort by Name": "Ordina per nome",
"Time Granularity": "Time Granularity",
"Aggregate by Month": "Aggrega per mese",
"Aggregate by Quarter": "Aggrega per trimestre",
"Aggregate by Year": "Aggrega per anno",
+10
View File
@@ -251,6 +251,13 @@
"31": "31"
}
},
"granularity": {
"FiscalYearly": "Fiscal Yearly",
"Yearly": "Yearly",
"Quarterly": "Quarterly",
"Monthly": "Monthly",
"Daily": "Daily"
},
"numeral": {
"Dot": "ドット",
"Comma": "コンマ",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "この口座を削除できません",
"Unable to delete this sub-account": "Unable to delete this sub-account",
"Reconciliation Statement": "Reconciliation Statement",
"Show Account Balance Trends": "Show Account Balance Trends",
"Show Transaction List": "Show Transaction List",
"Update Closing Balance": "Update Closing Balance",
"Please enter the new closing balance for the account": "Please enter the new closing balance for the account",
"Transaction": "取引",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "金額で並べ替え",
"Sort by Display Order": "表示で並べ替え",
"Sort by Name": "名前で並べ替え",
"Time Granularity": "Time Granularity",
"Aggregate by Month": "月ごとに集計",
"Aggregate by Quarter": "四半期ごとに集計",
"Aggregate by Year": "年ごとに集計",
+10
View File
@@ -251,6 +251,13 @@
"31": "31º"
}
},
"granularity": {
"FiscalYearly": "Fiscal Yearly",
"Yearly": "Yearly",
"Quarterly": "Quarterly",
"Monthly": "Monthly",
"Daily": "Daily"
},
"numeral": {
"Dot": "Ponto",
"Comma": "Vírgula",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "Não foi possível deletar esta conta",
"Unable to delete this sub-account": "Não foi possível deletar esta subconta",
"Reconciliation Statement": "Reconciliation Statement",
"Show Account Balance Trends": "Show Account Balance Trends",
"Show Transaction List": "Show Transaction List",
"Update Closing Balance": "Update Closing Balance",
"Please enter the new closing balance for the account": "Please enter the new closing balance for the account",
"Transaction": "Transação",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "Classificar por Montante",
"Sort by Display Order": "Classificar por Ordem de Exibição",
"Sort by Name": "Classificar por Nome",
"Time Granularity": "Time Granularity",
"Aggregate by Month": "Agregado por Mês",
"Aggregate by Quarter": "Agregado por Trimestre",
"Aggregate by Year": "Agregado por Ano",
+10
View File
@@ -251,6 +251,13 @@
"31": "31-й"
}
},
"granularity": {
"FiscalYearly": "Fiscal Yearly",
"Yearly": "Yearly",
"Quarterly": "Quarterly",
"Monthly": "Monthly",
"Daily": "Daily"
},
"numeral": {
"Dot": "Точка",
"Comma": "Запятая",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "Не удалось удалить этот счет",
"Unable to delete this sub-account": "Unable to delete this sub-account",
"Reconciliation Statement": "Reconciliation Statement",
"Show Account Balance Trends": "Show Account Balance Trends",
"Show Transaction List": "Show Transaction List",
"Update Closing Balance": "Update Closing Balance",
"Please enter the new closing balance for the account": "Please enter the new closing balance for the account",
"Transaction": "Транзакция",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "Сортировать по сумме",
"Sort by Display Order": "Сортировать по порядку отображения",
"Sort by Name": "Сортировать по имени",
"Time Granularity": "Time Granularity",
"Aggregate by Month": "Агрегировать по месяцам",
"Aggregate by Quarter": "Агрегировать по кварталам",
"Aggregate by Year": "Агрегировать по годам",
+10
View File
@@ -251,6 +251,13 @@
"31": "31-й"
}
},
"granularity": {
"FiscalYearly": "Fiscal Yearly",
"Yearly": "Yearly",
"Quarterly": "Quarterly",
"Monthly": "Monthly",
"Daily": "Daily"
},
"numeral": {
"Dot": "Крапка",
"Comma": "Кома",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "Не вдалося видалити цей рахунок",
"Unable to delete this sub-account": "Не вдалося видалити цей субрахунок",
"Reconciliation Statement": "Reconciliation Statement",
"Show Account Balance Trends": "Show Account Balance Trends",
"Show Transaction List": "Show Transaction List",
"Update Closing Balance": "Update Closing Balance",
"Please enter the new closing balance for the account": "Please enter the new closing balance for the account",
"Transaction": "Транзакція",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "Сортувати за сумою",
"Sort by Display Order": "Сортувати за порядком відображення",
"Sort by Name": "Сортувати за назвою",
"Time Granularity": "Time Granularity",
"Aggregate by Month": "Агрегувати за місяцями",
"Aggregate by Quarter": "Агрегувати за кварталами",
"Aggregate by Year": "Агрегувати за роками",
+10
View File
@@ -251,6 +251,13 @@
"31": "Ngày 31"
}
},
"granularity": {
"FiscalYearly": "Fiscal Yearly",
"Yearly": "Yearly",
"Quarterly": "Quarterly",
"Monthly": "Monthly",
"Daily": "Daily"
},
"numeral": {
"Dot": "Dấu chấm",
"Comma": "Dấu phẩy",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "Không thể xóa tài khoản này",
"Unable to delete this sub-account": "Unable to delete this sub-account",
"Reconciliation Statement": "Reconciliation Statement",
"Show Account Balance Trends": "Show Account Balance Trends",
"Show Transaction List": "Show Transaction List",
"Update Closing Balance": "Update Closing Balance",
"Please enter the new closing balance for the account": "Please enter the new closing balance for the account",
"Transaction": "Giao dịch",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "Sắp xếp theo số tiền",
"Sort by Display Order": "Sắp xếp theo thứ tự hiển thị",
"Sort by Name": "Sắp xếp theo tên",
"Time Granularity": "Time Granularity",
"Aggregate by Month": "Tổng hợp theo tháng",
"Aggregate by Quarter": "Tổng hợp theo quý",
"Aggregate by Year": "Tổng hợp theo năm",
+10
View File
@@ -251,6 +251,13 @@
"31": "31"
}
},
"granularity": {
"FiscalYearly": "按财年",
"Yearly": "按年",
"Quarterly": "按季度",
"Monthly": "按月",
"Daily": "按天"
},
"numeral": {
"Dot": "句点",
"Comma": "逗号",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "无法删除该账户",
"Unable to delete this sub-account": "无法删除该子账户",
"Reconciliation Statement": "对账单",
"Show Account Balance Trends": "显示账户余额趋势",
"Show Transaction List": "显示交易列表",
"Update Closing Balance": "更新期末余额",
"Please enter the new closing balance for the account": "请输入账户的新期末余额",
"Transaction": "交易",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "按金额排序",
"Sort by Display Order": "按显示顺序排序",
"Sort by Name": "按名称排序",
"Time Granularity": "时间粒度",
"Aggregate by Month": "按月聚合",
"Aggregate by Quarter": "按季度聚合",
"Aggregate by Year": "按年聚合",
+10
View File
@@ -251,6 +251,13 @@
"31": "31"
}
},
"granularity": {
"FiscalYearly": "依財政年度",
"Yearly": "依年份",
"Quarterly": "依季度",
"Monthly": "依月份",
"Daily": "依日期"
},
"numeral": {
"Dot": "句點",
"Comma": "逗號",
@@ -1635,6 +1642,8 @@
"Unable to delete this account": "無法刪除此帳戶",
"Unable to delete this sub-account": "無法刪除此子帳戶",
"Reconciliation Statement": "對帳單",
"Show Account Balance Trends": "顯示帳戶餘額趨勢",
"Show Transaction List": "顯示交易清單",
"Update Closing Balance": "更新期末餘額",
"Please enter the new closing balance for the account": "請輸入帳戶的新期末餘額",
"Transaction": "交易",
@@ -1873,6 +1882,7 @@
"Sort by Amount": "依金額排序",
"Sort by Display Order": "依顯示順序排序",
"Sort by Name": "依名稱排序",
"Time Granularity": "時間粒度",
"Aggregate by Month": "依月份彙整",
"Aggregate by Quarter": "依季度彙整",
"Aggregate by Year": "依年份彙整",
+2
View File
@@ -109,6 +109,7 @@ import NumberPadSheet from '@/components/mobile/NumberPadSheet.vue';
import MapSheet from '@/components/mobile/MapSheet.vue';
import TransactionTagSelectionSheet from '@/components/mobile/TransactionTagSelectionSheet.vue';
import ScheduleFrequencySheet from '@/components/mobile/ScheduleFrequencySheet.vue';
import AccountBalanceTrendsBarChart from '@/components/mobile/AccountBalanceTrendsBarChart.vue';
import TextareaAutoSize from '@/directives/mobile/textareaAutoSize.ts';
@@ -197,6 +198,7 @@ app.component('InformationSheet', InformationSheet);
app.component('NumberPadSheet', NumberPadSheet);
app.component('MapSheet', MapSheet);
app.component('TransactionTagSelectionSheet', TransactionTagSelectionSheet);
app.component('AccountBalanceTrendsBarChart', AccountBalanceTrendsBarChart);
app.component('ScheduleFrequencySheet', ScheduleFrequencySheet);
app.directive('TextareaAutoSize', TextareaAutoSize);
@@ -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;
+2 -2
View File
@@ -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;
}
+2 -2
View File
@@ -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;