From 1d2002e92f3c14f91b6e25df0ae605c07c16e1de Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sat, 10 May 2025 01:05:11 +0800 Subject: [PATCH] only months can be selected in transaction calendar mode --- src/components/base/MonthSelectionBase.ts | 69 ++++++++++ .../desktop/MonthSelectionDialog.vue | 127 ++++++++++++++++++ src/components/mobile/MonthSelectionSheet.vue | 112 +++++++++++++++ src/desktop-main.ts | 2 + src/lib/datetime.ts | 4 + src/mobile-main.ts | 2 + .../transactions/TransactionListPageBase.ts | 15 ++- src/views/desktop/transactions/ListPage.vue | 48 ++++++- src/views/mobile/transactions/ListPage.vue | 49 ++++++- 9 files changed, 421 insertions(+), 7 deletions(-) create mode 100644 src/components/base/MonthSelectionBase.ts create mode 100644 src/components/desktop/MonthSelectionDialog.vue create mode 100644 src/components/mobile/MonthSelectionSheet.vue diff --git a/src/components/base/MonthSelectionBase.ts b/src/components/base/MonthSelectionBase.ts new file mode 100644 index 00000000..81a65c36 --- /dev/null +++ b/src/components/base/MonthSelectionBase.ts @@ -0,0 +1,69 @@ +import { ref, computed } from 'vue'; + +import type { YearMonth } from '@/core/datetime.ts'; + +import { + getYearMonthObjectFromUnixTime, + getYearMonthObjectFromString, + getYearMonthStringFromObject, + getCurrentYear, + getThisMonthFirstUnixTime +} from '@/lib/datetime.ts'; + +import { useI18n } from '@/locales/helpers.ts'; + +export interface CommonMonthSelectionProps { + modelValue?: string; + title?: string; + hint?: string; + show: boolean; +} + +function getYearMonthValueFromProps(props: CommonMonthSelectionProps): YearMonth { + let value: YearMonth = getYearMonthObjectFromUnixTime(getThisMonthFirstUnixTime()); + + if (props.modelValue) { + const yearMonth = getYearMonthObjectFromString(props.modelValue); + + if (yearMonth) { + value = yearMonth; + } + } + + return value; +} + +export function useMonthSelectionBase(props: CommonMonthSelectionProps) { + const { isLongDateMonthAfterYear } = useI18n(); + + const yearRange = ref([ + 2000, + getCurrentYear() + 1 + ]); + + const monthValue = ref(getYearMonthValueFromProps(props)); + + const isYearFirst = computed(() => isLongDateMonthAfterYear()); + + function getTextualYearMonth(): string | null { + if (!monthValue.value) { + return null; + } + + if (monthValue.value.year <= 0 || monthValue.value.month < 0) { + throw new Error('Date is too early'); + } + + return getYearMonthStringFromObject(monthValue.value); + } + + return { + // states + yearRange, + monthValue, + // computed states + isYearFirst, + // functions + getTextualYearMonth + }; +} diff --git a/src/components/desktop/MonthSelectionDialog.vue b/src/components/desktop/MonthSelectionDialog.vue new file mode 100644 index 00000000..96495bc4 --- /dev/null +++ b/src/components/desktop/MonthSelectionDialog.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/src/components/mobile/MonthSelectionSheet.vue b/src/components/mobile/MonthSelectionSheet.vue new file mode 100644 index 00000000..63d6b527 --- /dev/null +++ b/src/components/mobile/MonthSelectionSheet.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/src/desktop-main.ts b/src/desktop-main.ts index 08a9fc96..c511b461 100644 --- a/src/desktop-main.ts +++ b/src/desktop-main.ts @@ -93,6 +93,7 @@ import SnackBar from '@/components/desktop/SnackBar.vue'; import PieChartComponent from '@/components/desktop/PieChart.vue'; import TrendsChartComponent from '@/components/desktop/TrendsChart.vue'; import DateRangeSelectionDialog from '@/components/desktop/DateRangeSelectionDialog.vue'; +import MonthSelectionDialog from '@/components/desktop/MonthSelectionDialog.vue'; import MonthRangeSelectionDialog from '@/components/desktop/MonthRangeSelectionDialog.vue'; import SwitchToMobileDialog from '@/components/desktop/SwitchToMobileDialog.vue'; @@ -468,6 +469,7 @@ app.component('SnackBar', SnackBar); app.component('PieChart', PieChartComponent); app.component('TrendsChart', TrendsChartComponent); app.component('DateRangeSelectionDialog', DateRangeSelectionDialog); +app.component('MonthSelectionDialog', MonthSelectionDialog); app.component('MonthRangeSelectionDialog', MonthRangeSelectionDialog); app.component('SwitchToMobileDialog', SwitchToMobileDialog); diff --git a/src/lib/datetime.ts b/src/lib/datetime.ts index 1d4e598b..73cfaaba 100644 --- a/src/lib/datetime.ts +++ b/src/lib/datetime.ts @@ -175,6 +175,10 @@ export function getCurrentYear(): number { return moment().year(); } +export function getCurrentYearAndMonth(): string { + return getYearAndMonth(moment()); +} + export function getCurrentDay(): number { return moment().date(); } diff --git a/src/mobile-main.ts b/src/mobile-main.ts index 7af66488..a1e0588e 100644 --- a/src/mobile-main.ts +++ b/src/mobile-main.ts @@ -93,6 +93,7 @@ import PasscodeInputSheet from '@/components/mobile/PasscodeInputSheet.vue'; import DateTimeSelectionSheet from '@/components/mobile/DateTimeSelectionSheet.vue'; import DateSelectionSheet from '@/components/mobile/DateSelectionSheet.vue'; import DateRangeSelectionSheet from '@/components/mobile/DateRangeSelectionSheet.vue'; +import MonthSelectionSheet from '@/components/mobile/MonthSelectionSheet.vue'; import MonthRangeSelectionSheet from '@/components/mobile/MonthRangeSelectionSheet.vue'; import ListItemSelectionSheet from '@/components/mobile/ListItemSelectionSheet.vue'; import ListItemSelectionPopup from '@/components/mobile/ListItemSelectionPopup.vue'; @@ -178,6 +179,7 @@ app.component('PasscodeInputSheet', PasscodeInputSheet); app.component('DateTimeSelectionSheet', DateTimeSelectionSheet); app.component('DateSelectionSheet', DateSelectionSheet); app.component('DateRangeSelectionSheet', DateRangeSelectionSheet); +app.component('MonthSelectionSheet', MonthSelectionSheet); app.component('MonthRangeSelectionSheet', MonthRangeSelectionSheet); app.component('ListItemSelectionSheet', ListItemSelectionSheet); app.component('ListItemSelectionPopup', ListItemSelectionPopup); diff --git a/src/views/base/transactions/TransactionListPageBase.ts b/src/views/base/transactions/TransactionListPageBase.ts index fe81d330..549646df 100644 --- a/src/views/base/transactions/TransactionListPageBase.ts +++ b/src/views/base/transactions/TransactionListPageBase.ts @@ -27,12 +27,14 @@ import { getLocalDatetimeFromUnixTime, getActualUnixTimeForStore, getDummyUnixTimeForLocalUsage, + getCurrentYearAndMonth, parseDateFromUnixTime, - getUnixTime, + getYearAndMonth, getYear, getMonth, - getDay, getYearMonthFirstUnixTime, + getDay, + getUnixTime, isDateRangeMatchOneMonth } from '@/lib/datetime.ts'; @@ -171,6 +173,14 @@ export function useTransactionListPageBase() { const queryMinTime = computed(() => formatUnixTimeToLongDateTime(query.value.minTime)); const queryMaxTime = computed(() => formatUnixTimeToLongDateTime(query.value.maxTime)); const queryMonthlyData = computed(() => isDateRangeMatchOneMonth(query.value.minTime, query.value.maxTime)); + const queryMonth = computed(() => { + if (!query.value.minTime || !query.value.maxTime) { + return getCurrentYearAndMonth(); + } + + return getYearAndMonth(parseDateFromUnixTime(query.value.minTime)); + }); + const queryAllFilterCategoryIds = computed>(() => transactionsStore.allFilterCategoryIds); const queryAllFilterAccountIds = computed>(() => transactionsStore.allFilterAccountIds); const queryAllFilterTagIds = computed>(() => transactionsStore.allFilterTagIds); @@ -373,6 +383,7 @@ export function useTransactionListPageBase() { queryMinTime, queryMaxTime, queryMonthlyData, + queryMonth, queryAllFilterCategoryIds, queryAllFilterAccountIds, queryAllFilterTagIds, diff --git a/src/views/desktop/transactions/ListPage.vue b/src/views/desktop/transactions/ListPage.vue index 4d139b90..250e0ea4 100644 --- a/src/views/desktop/transactions/ListPage.vue +++ b/src/views/desktop/transactions/ListPage.vue @@ -603,6 +603,13 @@ v-model:show="showCustomDateRangeDialog" @dateRange:change="changeCustomDateFilter" @error="onShowDateRangeError" /> + + + @@ -678,9 +685,11 @@ import { parseDateFromUnixTime, getYear, getMonth, - getSpecifiedDayFirstUnixTime, getBrowserTimezoneOffsetMinutes, getActualUnixTimeForStore, + getSpecifiedDayFirstUnixTime, + getYearMonthFirstUnixTime, + getYearMonthLastUnixTime, getShiftedDateRangeAndDateType, getShiftedDateRangeAndDateTypeForBillingCycle, getDateTypeByDateRange, @@ -787,6 +796,7 @@ const { queryMinTime, queryMaxTime, queryMonthlyData, + queryMonth, queryAllFilterCategoryIds, queryAllFilterAccountIds, queryAllFilterTagIds, @@ -851,6 +861,7 @@ const amountMenuState = ref(false); const alwaysShowNav = ref(display.mdAndUp.value); const showNav = ref(display.mdAndUp.value); const showCustomDateRangeDialog = ref(false); +const showCustomMonthDialog = ref(false); const showFilterAccountDialog = ref(false); const showFilterCategoryDialog = ref(false); const showFilterTagDialog = ref(false); @@ -1236,7 +1247,12 @@ function changeDateFilter(dateRange: TimeRangeAndDateType | number | null): void customMinDatetime.value = query.value.minTime; } - showCustomDateRangeDialog.value = true; + if (pageType.value === TransactionListPageType.Calendar.type) { + showCustomMonthDialog.value = true; + } else { + showCustomDateRangeDialog.value = true; + } + return; } @@ -1313,6 +1329,34 @@ function changeCustomDateFilter(minTime: number, maxTime: number): void { updateUrlWhenChanged(changed); } +function changeCustomMonthDateFilter(yearMonth: string): void { + if (!yearMonth) { + return; + } + + const minTime = getYearMonthFirstUnixTime(yearMonth); + const maxTime = getYearMonthLastUnixTime(yearMonth); + const dateType = getDateTypeByDateRange(minTime, maxTime, firstDayOfWeek.value, DateRangeScene.Normal); + + if (pageType.value === TransactionListPageType.Calendar.type) { + currentCalendarDate.value = getMonthFirstDayOrCurrentDayShortDate(minTime); + } + + if (query.value.dateType === dateType && query.value.maxTime === maxTime && query.value.minTime === minTime) { + showCustomMonthDialog.value = false; + return; + } + + const changed = transactionsStore.updateTransactionListFilter({ + dateType: dateType, + maxTime: maxTime, + minTime: minTime + }); + + showCustomMonthDialog.value = false; + updateUrlWhenChanged(changed); +} + function shiftDateRange(startTime: number, endTime: number, scale: number): void { if (pageType.value === TransactionListPageType.List.type && recentDateRangeIndex.value === 0) { // first item is "All" return; diff --git a/src/views/mobile/transactions/ListPage.vue b/src/views/mobile/transactions/ListPage.vue index de585ce9..52e5715c 100644 --- a/src/views/mobile/transactions/ListPage.vue +++ b/src/views/mobile/transactions/ListPage.vue @@ -342,6 +342,12 @@ @dateRange:change="changeCustomDateFilter"> + + + @@ -603,9 +609,13 @@ import { import { getCurrentUnixTime, parseDateFromUnixTime, - getSpecifiedDayFirstUnixTime, getBrowserTimezoneOffsetMinutes, getActualUnixTimeForStore, + getYear, + getMonth, + getSpecifiedDayFirstUnixTime, + getYearMonthFirstUnixTime, + getYearMonthLastUnixTime, getShiftedDateRangeAndDateType, getShiftedDateRangeAndDateTypeForBillingCycle, getDateTypeByDateRange, @@ -613,7 +623,7 @@ import { getDateRangeByDateType, getDateRangeByBillingCycleDateType, getFullMonthDateRange, - getMonthFirstDayOrCurrentDayShortDate, getYear, getMonth + getMonthFirstDayOrCurrentDayShortDate } from '@/lib/datetime.ts'; import { categoryTypeToTransactionType, @@ -660,6 +670,7 @@ const { queryMinTime, queryMaxTime, queryMonthlyData, + queryMonth, queryAllFilterCategoryIds, queryAllFilterAccountIds, queryAllFilterTagIds, @@ -700,6 +711,7 @@ const showCategoryPopover = ref(false); const showAccountPopover = ref(false); const showMorePopover = ref(false); const showCustomDateRangeSheet = ref(false); +const showCustomMonthSheet = ref(false); const showDeleteActionSheet = ref(false); const allTransactionTagFilterTypes = computed(() => getAllTransactionTagFilterTypes()); @@ -941,7 +953,12 @@ function changeDateFilter(dateType: number): void { customMinDatetime.value = query.value.minTime; } - showCustomDateRangeSheet.value = true; + if (pageType.value === TransactionListPageType.Calendar.type) { + showCustomMonthSheet.value = true; + } else { + showCustomDateRangeSheet.value = true; + } + showDatePopover.value = false; return; } else if (query.value.dateType === dateType) { @@ -1019,6 +1036,32 @@ function changeCustomDateFilter(minTime: number, maxTime: number): void { } } +function changeCustomMonthDateFilter(yearMonth: string): void { + if (!yearMonth) { + return; + } + + const minTime = getYearMonthFirstUnixTime(yearMonth); + const maxTime = getYearMonthLastUnixTime(yearMonth); + const dateType = getDateTypeByDateRange(minTime, maxTime, firstDayOfWeek.value, DateRangeScene.Normal); + + if (pageType.value === TransactionListPageType.Calendar.type) { + currentCalendarDate.value = getMonthFirstDayOrCurrentDayShortDate(minTime); + } + + const changed = transactionsStore.updateTransactionListFilter({ + dateType: dateType, + maxTime: maxTime, + minTime: minTime + }); + + showCustomMonthSheet.value = false; + + if (changed) { + reload(); + } +} + function shiftDateRange(minTime: number, maxTime: number, scale: number): void { if (query.value.dateType === DateRange.All.type) { return;