migrate transaction category preset page to composition API and typescript

This commit is contained in:
MaysWind
2025-01-16 00:43:50 +08:00
parent 6ef42a9303
commit b09b66adc3
5 changed files with 190 additions and 116 deletions
+1 -14
View File
@@ -1,17 +1,4 @@
import type { ColorValue } from '@/core/color.ts'; import type { PresetCategory } from '@/core/category.ts';
export interface PresetCategory {
readonly name: string;
readonly categoryIconId: string;
readonly color: ColorValue;
readonly subCategories: PresetSubCategory[];
}
export interface PresetSubCategory {
readonly name: string;
readonly categoryIconId: string;
readonly color: ColorValue;
}
export const DEFAULT_EXPENSE_CATEGORIES: PresetCategory[] = [ export const DEFAULT_EXPENSE_CATEGORIES: PresetCategory[] = [
{ {
+36
View File
@@ -1,5 +1,41 @@
import type { ColorValue } from '@/core/color.ts';
export enum CategoryType { export enum CategoryType {
Income = 1, Income = 1,
Expense = 2, Expense = 2,
Transfer = 3 Transfer = 3
} }
export const ALL_CATEGORY_TYPES: CategoryType[] = [
CategoryType.Income,
CategoryType.Expense,
CategoryType.Transfer
];
export interface PresetCategory {
readonly name: string;
readonly categoryIconId: string;
readonly color: ColorValue;
readonly subCategories: PresetSubCategory[];
}
export interface PresetSubCategory {
readonly name: string;
readonly categoryIconId: string;
readonly color: ColorValue;
}
export interface LocalizedPresetCategory {
readonly name: string;
readonly type: CategoryType;
readonly icon: string;
readonly color: ColorValue;
readonly subCategories: LocalizedPresetSubCategory[];
}
export interface LocalizedPresetSubCategory {
readonly name: string;
readonly type: CategoryType;
readonly icon: string;
readonly color: ColorValue;
}
+68 -2
View File
@@ -1,7 +1,7 @@
import { useI18n as useVueI18n } from 'vue-i18n'; import { useI18n as useVueI18n } from 'vue-i18n';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import type { TypeAndName, TypeAndDisplayName, LocalizedSwitchOption } from '@/core/base.ts'; import type { PartialRecord, TypeAndName, TypeAndDisplayName, LocalizedSwitchOption } from '@/core/base.ts';
import { type LanguageInfo, type LanguageOption, ALL_LANGUAGES, DEFAULT_LANGUAGE } from '@/locales/index.ts'; import { type LanguageInfo, type LanguageOption, ALL_LANGUAGES, DEFAULT_LANGUAGE } from '@/locales/index.ts';
@@ -49,6 +49,14 @@ import {
AccountCategory AccountCategory
} from '@/core/account.ts'; } from '@/core/account.ts';
import {
type PresetCategory,
type LocalizedPresetCategory,
type LocalizedPresetSubCategory,
CategoryType,
ALL_CATEGORY_TYPES
} from '@/core/category.ts';
import { import {
TransactionEditScopeType, TransactionEditScopeType,
TransactionTagFilterType TransactionTagFilterType
@@ -72,6 +80,7 @@ import type { ErrorResponse } from '@/core/api.ts';
import { UTC_TIMEZONE, ALL_TIMEZONES } from '@/consts/timezone.ts'; import { UTC_TIMEZONE, ALL_TIMEZONES } from '@/consts/timezone.ts';
import { ALL_CURRENCIES } from '@/consts/currency.ts'; import { ALL_CURRENCIES } from '@/consts/currency.ts';
import { DEFAULT_EXPENSE_CATEGORIES, DEFAULT_INCOME_CATEGORIES, DEFAULT_TRANSFER_CATEGORIES } from '@/consts/category.ts';
import { KnownErrorCode, SPECIFIED_API_NOT_FOUND_ERRORS, PARAMETERIZED_ERRORS } from '@/consts/api.ts'; import { KnownErrorCode, SPECIFIED_API_NOT_FOUND_ERRORS, PARAMETERIZED_ERRORS } from '@/consts/api.ts';
import type { LatestExchangeRateResponse, LocalizedLatestExchangeRate } from '@/models/exchange_rate.ts'; import type { LatestExchangeRateResponse, LocalizedLatestExchangeRate } from '@/models/exchange_rate.ts';
@@ -81,7 +90,8 @@ import {
isObject, isObject,
isString, isString,
isNumber, isNumber,
isBoolean isBoolean,
copyArrayTo
} from '@/lib/common.ts'; } from '@/lib/common.ts';
import { import {
@@ -842,6 +852,61 @@ export function useI18n() {
return ret; return ret;
} }
function getAllTransactionDefaultCategories(categoryType: 0 | CategoryType, locale: string): PartialRecord<CategoryType, LocalizedPresetCategory[]> {
const allCategories: PartialRecord<CategoryType, LocalizedPresetCategory[]> = {};
const categoryTypes: CategoryType[] = [];
if (categoryType === 0) {
categoryTypes.push(...ALL_CATEGORY_TYPES);
} else {
categoryTypes.push(categoryType);
}
for (let i = 0; i < categoryTypes.length; i++) {
const categories: LocalizedPresetCategory[] = [];
const categoryType = categoryTypes[i];
let defaultCategories: PresetCategory[] = [];
if (categoryType === CategoryType.Income) {
defaultCategories = copyArrayTo(DEFAULT_INCOME_CATEGORIES, []);
} else if (categoryType === CategoryType.Expense) {
defaultCategories = copyArrayTo(DEFAULT_EXPENSE_CATEGORIES, []);
} else if (categoryType === CategoryType.Transfer) {
defaultCategories = copyArrayTo(DEFAULT_TRANSFER_CATEGORIES, []);
}
for (let j = 0; j < defaultCategories.length; j++) {
const category = defaultCategories[j];
const submitCategory: LocalizedPresetCategory = {
name: t('category.' + category.name, {}, { locale: locale }),
type: categoryType,
icon: category.categoryIconId,
color: category.color,
subCategories: []
};
for (let k = 0; k < category.subCategories.length; k++) {
const subCategory = category.subCategories[k];
const submitSubCategory: LocalizedPresetSubCategory = {
name: t('category.' + subCategory.name, {}, { locale: locale }),
type: categoryType,
icon: subCategory.categoryIconId,
color: subCategory.color
};
submitCategory.subCategories.push(submitSubCategory);
}
categories.push(submitCategory);
}
allCategories[categoryType] = categories;
}
return allCategories;
}
function getAllDisplayExchangeRates(exchangeRatesData?: LatestExchangeRateResponse): LocalizedLatestExchangeRate[] { function getAllDisplayExchangeRates(exchangeRatesData?: LatestExchangeRateResponse): LocalizedLatestExchangeRate[] {
const availableExchangeRates: LocalizedLatestExchangeRate[] = []; const availableExchangeRates: LocalizedLatestExchangeRate[] = [];
@@ -1269,6 +1334,7 @@ export function useI18n() {
getAllTransactionEditScopeTypes: () => getLocalizedDisplayNameAndType(TransactionEditScopeType.values()), getAllTransactionEditScopeTypes: () => getLocalizedDisplayNameAndType(TransactionEditScopeType.values()),
getAllTransactionTagFilterTypes: () => getLocalizedDisplayNameAndType(TransactionTagFilterType.values()), getAllTransactionTagFilterTypes: () => getLocalizedDisplayNameAndType(TransactionTagFilterType.values()),
getAllTransactionScheduledFrequencyTypes: () => getLocalizedDisplayNameAndType(ScheduledTemplateFrequencyType.values()), getAllTransactionScheduledFrequencyTypes: () => getLocalizedDisplayNameAndType(ScheduledTemplateFrequencyType.values()),
getAllTransactionDefaultCategories,
getAllDisplayExchangeRates, getAllDisplayExchangeRates,
// get localized info // get localized info
getMonthShortName, getMonthShortName,
+2 -11
View File
@@ -1,4 +1,4 @@
import { CategoryType } from '@/core/category.ts'; import { type LocalizedPresetCategory, CategoryType } from '@/core/category.ts';
import { DEFAULT_CATEGORY_ICON_ID } from '@/consts/icon.ts'; import { DEFAULT_CATEGORY_ICON_ID } from '@/consts/icon.ts';
import { DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts'; import { DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts';
@@ -131,16 +131,7 @@ export interface TransactionCategoryCreateRequest {
} }
export interface TransactionCategoryCreateBatchRequest { export interface TransactionCategoryCreateBatchRequest {
readonly categories: TransactionCategoryCreateWithSubCategories[]; readonly categories: LocalizedPresetCategory[];
}
export interface TransactionCategoryCreateWithSubCategories {
readonly name: string;
readonly type: number;
readonly icon: string;
readonly color: string;
readonly comment: string;
readonly subCategories: TransactionCategoryCreateRequest[];
} }
export interface TransactionCategoryModifyRequest { export interface TransactionCategoryModifyRequest {
+83 -89
View File
@@ -1,11 +1,11 @@
<template> <template>
<f7-page @page:afterin="onPageAfterIn"> <f7-page @page:afterin="onPageAfterIn">
<f7-navbar> <f7-navbar>
<f7-nav-left :back-link="$t('Back')"></f7-nav-left> <f7-nav-left :back-link="tt('Back')"></f7-nav-left>
<f7-nav-title :title="$t('Default Categories')"></f7-nav-title> <f7-nav-title :title="tt('Default Categories')"></f7-nav-title>
<f7-nav-right> <f7-nav-right>
<f7-link icon-f7="ellipsis" v-if="isPresetHasCategories" @click="showMoreActionSheet = true"></f7-link> <f7-link icon-f7="ellipsis" v-if="isPresetHasCategories" @click="showMoreActionSheet = true"></f7-link>
<f7-link :text="$t('Save')" :class="{ 'disabled': submitting }" v-if="isPresetHasCategories" @click="save"></f7-link> <f7-link :text="tt('Save')" :class="{ 'disabled': submitting }" v-if="isPresetHasCategories" @click="save"></f7-link>
</f7-nav-right> </f7-nav-right>
</f7-navbar> </f7-navbar>
@@ -38,10 +38,10 @@
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false"> <f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group> <f7-actions-group>
<f7-actions-button @click="showChangeLocaleSheet = true">{{ $t('Change Language') }}</f7-actions-button> <f7-actions-button @click="showChangeLocaleSheet = true">{{ tt('Change Language') }}</f7-actions-button>
</f7-actions-group> </f7-actions-group>
<f7-actions-group> <f7-actions-group>
<f7-actions-button bold close>{{ $t('Cancel') }}</f7-actions-button> <f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
</f7-actions-group> </f7-actions-group>
</f7-actions> </f7-actions>
@@ -55,98 +55,92 @@
</f7-page> </f7-page>
</template> </template>
<script> <script setup lang="ts">
import { mapStores } from 'pinia';
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { CategoryType } from '@/core/category.ts'; import { ref, computed } from 'vue';
import type { Router } from 'framework7/types';
import { useI18n } from '@/locales/helpers.ts';
import { useI18nUIComponents, showLoading, hideLoading } from '@/lib/ui/mobile.ts';
import type { PartialRecord } from '@/core/base.ts';
import type { LanguageOption } from '@/locales/index.ts';
import { type LocalizedPresetCategory, CategoryType } from '@/core/category.ts';
import { getObjectOwnFieldCount, categorizedArrayToPlainArray } from '@/lib/common.ts'; import { getObjectOwnFieldCount, categorizedArrayToPlainArray } from '@/lib/common.ts';
export default { const props = defineProps<{
props: [ f7route: Router.Route;
'f7route', f7router: Router.Router;
'f7router' }>();
],
data() {
const self = this;
return { const { tt, getCurrentLanguageTag, getAllLanguageOptions, getAllTransactionDefaultCategories } = useI18n();
loadingError: null, const { showToast, routeBackOnError } = useI18nUIComponents();
currentLocale: self.$locale.getCurrentLanguageTag(),
categoryType: 0,
submitting: false,
showMoreActionSheet: false,
showChangeLocaleSheet: false
};
},
computed: {
...mapStores(useTransactionCategoriesStore),
allLanguages() {
return this.$locale.getAllLanguageInfoArray(false);
},
allPresetCategories() {
return this.$locale.getAllTransactionDefaultCategories(this.categoryType, this.currentLocale);
},
isPresetHasCategories() {
return getObjectOwnFieldCount(this.allPresetCategories);
}
},
created() {
const self = this;
const query = self.f7route.query;
self.categoryType = parseInt(query.type); const transactionCategoriesStore = useTransactionCategoriesStore();
if (self.categoryType !== 0 && const loadingError = ref<unknown | null>(null);
self.categoryType !== CategoryType.Income && const currentLocale = ref<string>(getCurrentLanguageTag());
self.categoryType !== CategoryType.Expense && const categoryType = ref<number>(0);
self.categoryType !== CategoryType.Transfer) { const submitting = ref<boolean>(false);
self.$toast('Parameter Invalid'); const showMoreActionSheet = ref<boolean>(false);
self.loadingError = 'Parameter Invalid'; const showChangeLocaleSheet = ref<boolean>(false);
}
},
methods: {
onPageAfterIn() {
this.$routeBackOnError(this.f7router, 'loadingError');
},
save() {
const self = this;
const router = self.f7router;
self.submitting = true; const allLanguages = computed<LanguageOption[]>(() => getAllLanguageOptions(false));
self.$showLoading(() => self.submitting); const allPresetCategories = computed<PartialRecord<CategoryType, LocalizedPresetCategory[]>>(() => getAllTransactionDefaultCategories(categoryType.value, currentLocale.value));
const isPresetHasCategories = computed<boolean>(() => getObjectOwnFieldCount(allPresetCategories.value) > 0);
const submitCategories = categorizedArrayToPlainArray(self.allPresetCategories); function getCategoryTypeName(categoryType: CategoryType): string {
switch (categoryType) {
self.transactionCategoriesStore.addCategories({ case CategoryType.Income:
categories: submitCategories return tt('Income Categories');
}).then(() => { case CategoryType.Expense:
self.submitting = false; return tt('Expense Categories');
self.$hideLoading(); case CategoryType.Transfer:
return tt('Transfer Categories');
self.$toast('You have added preset categories'); default:
router.back(); return tt('Transaction Categories');
}).catch(error => {
self.submitting = false;
self.$hideLoading();
if (!error.processed) {
self.$toast(error.message || error);
}
});
},
getCategoryTypeName(categoryType) {
switch (categoryType) {
case CategoryType.Income.toString():
return this.$t('Income Categories');
case CategoryType.Expense.toString():
return this.$t('Expense Categories');
case CategoryType.Transfer.toString():
return this.$t('Transfer Categories');
default:
return this.$t('Transaction Categories');
}
}
} }
}; }
function save(): void {
const router = props.f7router;
submitting.value = true;
showLoading(() => submitting.value);
const submitCategories = categorizedArrayToPlainArray(allPresetCategories.value);
transactionCategoriesStore.addCategories({
categories: submitCategories
}).then(() => {
submitting.value = false;
hideLoading();
showToast('You have added preset categories');
router.back();
}).catch(error => {
submitting.value = false;
hideLoading();
if (!error.processed) {
showToast(error.message || error);
}
});
}
function onPageAfterIn(): void {
routeBackOnError(props.f7router, loadingError);
}
const query = props.f7route.query;
categoryType.value = parseInt(query['type'] || '0');
if (categoryType.value !== 0 &&
categoryType.value !== CategoryType.Income &&
categoryType.value !== CategoryType.Expense &&
categoryType.value !== CategoryType.Transfer) {
showToast('Parameter Invalid');
loadingError.value = 'Parameter Invalid';
}
</script> </script>