From 14b4e40039868acd5802f348cf697f41300490aa Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 4 Aug 2025 01:22:36 +0800 Subject: [PATCH] reconciliation statement page / dialog supports account balance trends chart (#184) --- .../base/AccountBalanceTrendsChartBase.ts | 183 +++++++++++++++++ .../desktop/AccountBalanceTrendsChart.vue | 184 ++++++++++++++++++ .../mobile/AccountBalanceTrendsBarChart.vue | 133 +++++++++++++ src/core/datetime.ts | 26 +++ src/core/statistics.ts | 18 +- src/desktop-main.ts | 2 + src/lib/datetime.ts | 75 ++++++- src/lib/statistics.ts | 4 + src/locales/de.json | 10 + src/locales/en.json | 10 + src/locales/es.json | 10 + src/locales/helpers.ts | 28 ++- src/locales/it.json | 10 + src/locales/ja.json | 10 + src/locales/pt_BR.json | 10 + src/locales/ru.json | 10 + src/locales/uk.json | 10 + src/locales/vi.json | 10 + src/locales/zh_Hans.json | 10 + src/locales/zh_Hant.json | 10 + src/mobile-main.ts | 2 + .../ReconciliationStatementPageBase.ts | 8 + .../dialogs/ReconciliationStatementDialog.vue | 94 ++++++++- src/views/desktop/transactions/ListPage.vue | 4 +- .../accounts/ReconciliationStatementPage.vue | 71 ++++++- src/views/mobile/transactions/ListPage.vue | 4 +- 26 files changed, 917 insertions(+), 29 deletions(-) create mode 100644 src/components/base/AccountBalanceTrendsChartBase.ts create mode 100644 src/components/desktop/AccountBalanceTrendsChart.vue create mode 100644 src/components/mobile/AccountBalanceTrendsBarChart.vue diff --git a/src/components/base/AccountBalanceTrendsChartBase.ts b/src/components/base/AccountBalanceTrendsChartBase.ts new file mode 100644 index 00000000..bdc87894 --- /dev/null +++ b/src/components/base/AccountBalanceTrendsChartBase.ts @@ -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(() => { + 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(() => { + 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(() => { + const ret: AccountBalanceTrendsChartItem[] = []; + + if (!dataDateRange.value || !allDateRanges.value || allDateRanges.value.length < 1 || !props.items || props.items.length < 1) { + return ret; + } + + const dayDataItemsMap: Record = {}; + + 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(() => { + if (!allDataItems.value || allDataItems.value.length < 1) { + return []; + } + + return allDataItems.value.map(item => item.displayDate); + }); + + return { + // computed states + allDateRanges, + allDataItems, + allDisplayDateRanges + }; +} diff --git a/src/components/desktop/AccountBalanceTrendsChart.vue b/src/components/desktop/AccountBalanceTrendsChart.vue new file mode 100644 index 00000000..44752f2a --- /dev/null +++ b/src/components/desktop/AccountBalanceTrendsChart.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/src/components/mobile/AccountBalanceTrendsBarChart.vue b/src/components/mobile/AccountBalanceTrendsBarChart.vue new file mode 100644 index 00000000..7184bf63 --- /dev/null +++ b/src/components/mobile/AccountBalanceTrendsBarChart.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/core/datetime.ts b/src/core/datetime.ts index 38ce0d57..3ce2652a 100644 --- a/src/core/datetime.ts +++ b/src/core/datetime.ts @@ -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 { diff --git a/src/core/statistics.ts b/src/core/statistics.ts index 7aa68e61..bb8a6bfa 100644 --- a/src/core/statistics.ts +++ b/src/core/statistics.ts @@ -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 = {}; - 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; diff --git a/src/desktop-main.ts b/src/desktop-main.ts index 454b1cbb..ee6084dc 100644 --- a/src/desktop-main.ts +++ b/src/desktop-main.ts @@ -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'); diff --git a/src/lib/datetime.ts b/src/lib/datetime.ts index 5a724644..d07da70f 100644 --- a/src/lib/datetime.ts +++ b/src/lib/datetime.ts @@ -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(allFormatMap: Record, 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]; diff --git a/src/lib/statistics.ts b/src/lib/statistics.ts index 5e2be9d1..291ee128 100644 --- a/src/lib/statistics.ts +++ b/src/lib/statistics.ts @@ -74,6 +74,10 @@ export function getAllDateRanges(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 []; } diff --git a/src/locales/de.json b/src/locales/de.json index 4680674f..919e1f82 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -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", diff --git a/src/locales/en.json b/src/locales/en.json index d7326ba3..c20b7c9e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/locales/es.json b/src/locales/es.json index 5f59fa51..8962ec7a 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -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", diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index 60a92ab8..6cf913da 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -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, diff --git a/src/locales/it.json b/src/locales/it.json index 3ea1e340..3e382c5b 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -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", diff --git a/src/locales/ja.json b/src/locales/ja.json index 6dec8117..8f1c120c 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -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": "年ごとに集計", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index ab81c173..557646fa 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -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", diff --git a/src/locales/ru.json b/src/locales/ru.json index dc69ac59..950157d5 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -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": "Агрегировать по годам", diff --git a/src/locales/uk.json b/src/locales/uk.json index 4ae81817..59309cf0 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -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": "Агрегувати за роками", diff --git a/src/locales/vi.json b/src/locales/vi.json index eda9e8fb..811a2f44 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -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", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index c7a096ea..c0bca157 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -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": "按年聚合", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index a9397895..fe72b9b1 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -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": "依年份彙整", diff --git a/src/mobile-main.ts b/src/mobile-main.ts index b57c0d47..e1cde0cc 100644 --- a/src/mobile-main.ts +++ b/src/mobile-main.ts @@ -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); diff --git a/src/views/base/accounts/ReconciliationStatementPageBase.ts b/src/views/base/accounts/ReconciliationStatementPageBase.ts index a8282b4c..8345e4be 100644 --- a/src/views/base/accounts/ReconciliationStatementPageBase.ts +++ b/src/views/base/accounts/ReconciliationStatementPageBase.ts @@ -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(() => getTimezoneOffsetMinutes(settingsStore.appSettings.timeZone)); const defaultCurrency = computed(() => userStore.currentUserDefaultCurrency); + const allChartTypes = computed(() => getAllTrendChartTypes()); + const allDateAggregationTypes = computed(() => getAllStatisticsDateAggregationTypesWithShortName()); + const currentAccount = computed(() => allAccountsMap.value[accountId.value]); const currentAccountCurrency = computed(() => currentAccount.value?.currency ?? defaultCurrency.value); const isCurrentLiabilityAccount = computed(() => currentAccount.value?.isLiability ?? false); @@ -266,6 +272,8 @@ export function useReconciliationStatementPageBase() { fiscalYearStart, currentTimezoneOffsetMinutes, defaultCurrency, + allChartTypes, + allDateAggregationTypes, currentAccount, currentAccountCurrency, isCurrentLiabilityAccount, diff --git a/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue index 24a802f9..453086d9 100644 --- a/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue +++ b/src/views/desktop/accounts/list/dialogs/ReconciliationStatementDialog.vue @@ -14,6 +14,34 @@ {{ tt('Refresh') }} + + + + + + + + + + + + + @@ -27,6 +55,15 @@ :title="tt('Update Closing Balance')" @click="updateClosingBalance()"> + + + @@ -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" > + + + +
('amountInputDialog'); const snackbar = useTemplateRef('snackbar'); const editDialog = useTemplateRef('editDialog'); @@ -291,6 +375,9 @@ const showState = ref(false); const loading = ref(false); const currentPage = ref(1); const countPerPage = ref(10); +const showAccountBalanceTrendsCharts = ref(false); +const chartType = ref(TrendChartType.Default.type); +const chartDataDateAggregationType = ref(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; diff --git a/src/views/desktop/transactions/ListPage.vue b/src/views/desktop/transactions/ListPage.vue index 865a968a..656151e7 100644 --- a/src/views/desktop/transactions/ListPage.vue +++ b/src/views/desktop/transactions/ListPage.vue @@ -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; diff --git a/src/views/mobile/accounts/ReconciliationStatementPage.vue b/src/views/mobile/accounts/ReconciliationStatementPage.vue index 37c3fd6f..b550c372 100644 --- a/src/views/mobile/accounts/ReconciliationStatementPage.vue +++ b/src/views/mobile/accounts/ReconciliationStatementPage.vue @@ -71,7 +71,7 @@ + v-if="finishQuery && !showAccountBalanceTrendsCharts && loading">
    + v-if="finishQuery && !showAccountBalanceTrendsCharts && !loading && (!allReconciliationStatementVirtualListItems || !allReconciliationStatementVirtualListItems.length)"> + v-if="finishQuery && !showAccountBalanceTrendsCharts && !loading && allReconciliationStatementVirtualListItems && allReconciliationStatementVirtualListItems.length">
      + + +
      +
      +
      + {{ tt('Time Granularity') }} + {{ chartDataDateAggregationTypeDisplayName }} +
      +
      +
      + + + +
      + + + + + + + + + + + + {{ tt('Refresh') }} + {{ tt('Show Account Balance Trends') }} + {{ tt('Show Transaction List') }} {{ tt('Cancel') }} @@ -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(false); const loading = ref(false); const loadingError = ref(null); const queryDateRangeType = ref(DateRange.ThisMonth.type); +const showAccountBalanceTrendsCharts = ref(false); +const chartDataDateAggregationType = ref(undefined); const transactionToDelete = ref(null); const newClosingBalance = ref(0); const showCustomDateRangeSheet = ref(false); const showNewClosingBalanceSheet = ref(false); const showMoreActionSheet = ref(false); const showDeleteActionSheet = ref(false); +const showChartDataDateAggregationTypePopover = ref(false); const virtualDataItems = ref({ items: [], topPosition: 0 @@ -407,6 +457,14 @@ const allReconciliationStatementVirtualListItems = computed(() => { + 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; } diff --git a/src/views/mobile/transactions/ListPage.vue b/src/views/mobile/transactions/ListPage.vue index cb35d383..22210c4c 100644 --- a/src/views/mobile/transactions/ListPage.vue +++ b/src/views/mobile/transactions/ListPage.vue @@ -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;