From cc920cff9ad886f773c89af38cc4452caff10b71 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Tue, 4 Feb 2025 12:45:25 +0800 Subject: [PATCH] migrate transaction template store to composition API and typescript --- src/lib/common.ts | 2 +- src/lib/{template.js => template.ts} | 10 +- src/models/transaction.ts | 2 +- src/models/transaction_template.ts | 123 ++++- src/stores/index.ts | 3 +- src/stores/transactionTemplate.js | 459 ----------------- src/stores/transactionTemplate.ts | 469 ++++++++++++++++++ src/views/desktop/templates/ListPage.vue | 4 +- src/views/desktop/transactions/ListPage.vue | 2 +- .../transactions/list/dialogs/EditDialog.vue | 4 +- src/views/mobile/HomePage.vue | 2 +- src/views/mobile/templates/ListPage.vue | 4 +- src/views/mobile/transactions/EditPage.vue | 4 +- 13 files changed, 612 insertions(+), 476 deletions(-) rename src/lib/{template.js => template.ts} (58%) delete mode 100644 src/stores/transactionTemplate.js create mode 100644 src/stores/transactionTemplate.ts diff --git a/src/lib/common.ts b/src/lib/common.ts index dfc72cbb..3b2f569c 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -13,7 +13,7 @@ export function isObject(val: unknown): val is object { return val != null && typeof(val) === 'object' && !isArray(val); } -export function isArray(val: unknown): val is [] { +export function isArray(val: unknown): val is T[] { if (isFunction(Array.isArray)) { return Array.isArray(val); } diff --git a/src/lib/template.js b/src/lib/template.ts similarity index 58% rename from src/lib/template.js rename to src/lib/template.ts index b895c517..40d9e313 100644 --- a/src/lib/template.js +++ b/src/lib/template.ts @@ -1,4 +1,6 @@ -export function isNoAvailableTemplate(templates, showHidden) { +import { TransactionTemplate } from '@/models/transaction_template.ts'; + +export function isNoAvailableTemplate(templates: TransactionTemplate[], showHidden: boolean): boolean { for (let i = 0; i < templates.length; i++) { if (showHidden || !templates[i].hidden) { return false; @@ -8,7 +10,7 @@ export function isNoAvailableTemplate(templates, showHidden) { return true; } -export function getAvailableTemplateCount(templates, showHidden) { +export function getAvailableTemplateCount(templates: TransactionTemplate[], showHidden: boolean): number { let count = 0; for (let i = 0; i < templates.length; i++) { @@ -20,7 +22,7 @@ export function getAvailableTemplateCount(templates, showHidden) { return count; } -export function getFirstShowingId(templates, showHidden) { +export function getFirstShowingId(templates: TransactionTemplate[], showHidden: boolean): string | null { for (let i = 0; i < templates.length; i++) { if (showHidden || !templates[i].hidden) { return templates[i].id; @@ -30,7 +32,7 @@ export function getFirstShowingId(templates, showHidden) { return null; } -export function getLastShowingId(templates, showHidden) { +export function getLastShowingId(templates: TransactionTemplate[], showHidden: boolean): string | null { for (let i = templates.length - 1; i >= 0; i--) { if (showHidden || !templates[i].hidden) { return templates[i].id; diff --git a/src/models/transaction.ts b/src/models/transaction.ts index 33f4ada5..447dd3a6 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -38,7 +38,7 @@ export class Transaction implements TransactionInfoResponse { private _day?: number = undefined; // only for displaying transaction in transaction list private _dayOfWeek?: string = undefined; // only for displaying transaction in transaction list - private constructor(id: string, timeSequenceId: string, type: number, categoryId: string, time: number, timeZone: string | undefined, utcOffset: number, sourceAccountId: string, destinationAccountId: string, sourceAmount: number, destinationAmount: number, hideAmount: boolean, tagIds: string[], comment: string, editable: boolean) { + protected constructor(id: string, timeSequenceId: string, type: number, categoryId: string, time: number, timeZone: string | undefined, utcOffset: number, sourceAccountId: string, destinationAccountId: string, sourceAmount: number, destinationAmount: number, hideAmount: boolean, tagIds: string[], comment: string, editable: boolean) { this.id = id; this.timeSequenceId = timeSequenceId; this.type = type; diff --git a/src/models/transaction_template.ts b/src/models/transaction_template.ts index 576ab348..2b322391 100644 --- a/src/models/transaction_template.ts +++ b/src/models/transaction_template.ts @@ -1,4 +1,125 @@ -import type { TransactionInfoResponse } from './transaction.ts'; +import { TransactionType } from '@/core/transaction.ts'; +import { TemplateType } from '@/core/template.ts'; + +import { Transaction, type TransactionInfoResponse } from './transaction.ts'; + +export class TransactionTemplate extends Transaction implements TransactionTemplateInfoResponse { + public templateType: number; + public name: string; + public scheduledFrequencyType?: number; + public scheduledFrequency?: string; + public scheduledAt?: number; + public displayOrder: number; + public hidden: boolean; + + private constructor(id: string, templateType: number, name: string, type: number, categoryId: string, utcOffset: number, sourceAccountId: string, destinationAccountId: string, sourceAmount: number, destinationAmount: number, hideAmount: boolean, scheduledFrequencyType: number | undefined, scheduledFrequency: string | undefined, scheduledAt: number | undefined, tagIds: string[], comment: string, editable: boolean, displayOrder: number, hidden: boolean) { + super(id, '', type, categoryId, 0, undefined, utcOffset, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, editable); + this.templateType = templateType; + this.name = name; + this.scheduledFrequencyType = scheduledFrequencyType; + this.scheduledFrequency = scheduledFrequency; + this.scheduledAt = scheduledAt; + this.displayOrder = displayOrder; + this.hidden = hidden; + } + + public toTemplateCreateRequest(clientSessionId: string): TransactionTemplateCreateRequest { + return { + templateType: this.templateType, + name: this.name, + type: this.type, + categoryId: this.getCategoryId(), + sourceAccountId: this.sourceAccountId, + destinationAccountId: this.type === TransactionType.Transfer ? this.destinationAccountId : '0', + sourceAmount: this.sourceAmount, + destinationAmount: this.type === TransactionType.Transfer ? this.destinationAmount : 0, + hideAmount: this.hideAmount, + tagIds: this.tagIds, + comment: this.comment, + scheduledFrequencyType: this.templateType === TemplateType.Schedule.type ? this.scheduledFrequencyType : undefined, + scheduledFrequency: this.templateType === TemplateType.Schedule.type ? this.scheduledFrequency : undefined, + utcOffset: this.templateType === TemplateType.Schedule.type ? this.utcOffset : undefined, + clientSessionId: clientSessionId + }; + } + + public toTemplateModifyRequest(): TransactionTemplateModifyRequest { + return { + id: this.id, + name: this.name, + type: this.type, + categoryId: this.getCategoryId(), + sourceAccountId: this.sourceAccountId, + destinationAccountId: this.type === TransactionType.Transfer ? this.destinationAccountId : '0', + sourceAmount: this.sourceAmount, + destinationAmount: this.type === TransactionType.Transfer ? this.destinationAmount : 0, + hideAmount: this.hideAmount, + tagIds: this.tagIds, + comment: this.comment, + scheduledFrequencyType: this.templateType === TemplateType.Schedule.type ? this.scheduledFrequencyType : undefined, + scheduledFrequency: this.templateType === TemplateType.Schedule.type ? this.scheduledFrequency : undefined, + utcOffset: this.templateType === TemplateType.Schedule.type ? this.utcOffset : undefined + }; + } + + public static createNewTransactionTemplate(transaction: Transaction): TransactionTemplate { + return new TransactionTemplate( + transaction.id, + 0, // templateType + '', // name + transaction.type, + transaction.categoryId, + transaction.utcOffset, + transaction.sourceAccountId, + transaction.destinationAccountId, + transaction.sourceAmount, + transaction.destinationAmount, + transaction.hideAmount, + undefined, // scheduledFrequencyType + undefined, // scheduledFrequency + undefined, // scheduledAt + transaction.tagIds, + transaction.comment, + true, + 0, + false + ); + } + + public static ofTemplate(templateResponse: TransactionTemplateInfoResponse): TransactionTemplate { + return new TransactionTemplate( + templateResponse.id, + templateResponse.templateType, + templateResponse.name, + templateResponse.type, + templateResponse.categoryId, + templateResponse.utcOffset ?? 0, + templateResponse.sourceAccountId, + templateResponse.destinationAccountId, + templateResponse.sourceAmount, + templateResponse.destinationAmount, + templateResponse.hideAmount, + templateResponse.scheduledFrequencyType, + templateResponse.scheduledFrequency, + templateResponse.scheduledAt, + templateResponse.tagIds, + templateResponse.comment, + true, // editable + templateResponse.displayOrder, + templateResponse.hidden + ); + } + + public static ofManyTemplates(templateResponses: TransactionTemplateInfoResponse[]): TransactionTemplate[] { + const templates: TransactionTemplate[] = []; + + for (const templateResponse of templateResponses) { + templates.push(TransactionTemplate.ofTemplate(templateResponse)); + } + + return templates; + } +} export interface TransactionTemplateCreateRequest { readonly templateType: number; diff --git a/src/stores/index.ts b/src/stores/index.ts index 9a33fe11..a71c5064 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -6,8 +6,7 @@ import { useUserStore } from './user.ts'; import { useAccountsStore } from './account.ts'; import { useTransactionCategoriesStore } from './transactionCategory.ts'; import { useTransactionTagsStore } from './transactionTag.ts'; -// @ts-expect-error the above file is migrating to ts -import { useTransactionTemplatesStore } from './transactionTemplate.js'; +import { useTransactionTemplatesStore } from './transactionTemplate.ts'; import { useTransactionsStore } from './transaction.ts'; import { useOverviewStore } from './overview.ts'; import { useStatisticsStore } from './statistics.ts'; diff --git a/src/stores/transactionTemplate.js b/src/stores/transactionTemplate.js deleted file mode 100644 index afc20998..00000000 --- a/src/stores/transactionTemplate.js +++ /dev/null @@ -1,459 +0,0 @@ -import { defineStore } from 'pinia'; - -import { TransactionType } from '@/core/transaction.ts'; -import { TemplateType } from '@/core/template.ts'; -import { isDefined, isObject, isArray, isEquals } from '@/lib/common.ts'; -import services from '@/lib/services.ts'; -import logger from '@/lib/logger.ts'; - -function loadTransactionTemplateList(state, templateType, templates) { - state.allTransactionTemplates[templateType] = templates; - state.allTransactionTemplatesMap[templateType] = {}; - - for (let i = 0; i < templates.length; i++) { - const template = templates[i]; - state.allTransactionTemplatesMap[templateType][template.id] = template; - } -} - -function addTemplateToTransactionTemplateList(state, templateType, template) { - if (isArray(state.allTransactionTemplates[templateType])) { - state.allTransactionTemplates[templateType].push(template); - } - - if (isObject(state.allTransactionTemplatesMap[templateType])) { - state.allTransactionTemplatesMap[templateType][template.id] = template; - } -} - -function updateTemplateInTransactionTemplateList(state, templateType, template) { - if (isArray(state.allTransactionTemplates[templateType])) { - for (let i = 0; i < state.allTransactionTemplates[templateType].length; i++) { - if (state.allTransactionTemplates[templateType][i].id === template.id) { - state.allTransactionTemplates[templateType].splice(i, 1, template); - break; - } - } - } - - if (isObject(state.allTransactionTemplatesMap[templateType])) { - state.allTransactionTemplatesMap[templateType][template.id] = template; - } -} - -function updateTemplateDisplayOrderInTransactionTemplateList(state, templateType, { from, to }) { - if (isArray(state.allTransactionTemplates[templateType])) { - state.allTransactionTemplates[templateType].splice(to, 0, state.allTransactionTemplates[templateType].splice(from, 1)[0]); - } -} - -function updateTemplateVisibilityInTransactionTemplateList(state, templateType, { template, hidden }) { - if (isObject(state.allTransactionTemplatesMap[templateType])) { - if (state.allTransactionTemplatesMap[templateType][template.id]) { - state.allTransactionTemplatesMap[templateType][template.id].hidden = hidden; - } - } -} - -function removeTemplateFromTransactionTemplateList(state, templateType, template) { - if (isArray(state.allTransactionTemplates[templateType])) { - for (let i = 0; i < state.allTransactionTemplates[templateType].length; i++) { - if (state.allTransactionTemplates[templateType][i].id === template.id) { - state.allTransactionTemplates[templateType].splice(i, 1); - break; - } - } - } - - if (isObject(state.allTransactionTemplatesMap[templateType])) { - if (state.allTransactionTemplatesMap[templateType][template.id]) { - delete state.allTransactionTemplatesMap[templateType][template.id]; - } - } -} - -export const useTransactionTemplatesStore = defineStore('transactionTemplates', { - state: () => ({ - allTransactionTemplates: {}, - allTransactionTemplatesMap: {}, - transactionTemplateListStatesInvalid: {}, - }), - getters: { - allVisibleTemplates(state) { - const allVisibleTemplates = {}; - - for (const templateType in state.allTransactionTemplates) { - if (!Object.prototype.hasOwnProperty.call(state.allTransactionTemplates, templateType)) { - continue; - } - - const visibleTemplates = []; - - for (let i = 0; i < state.allTransactionTemplates[templateType].length; i++) { - const template = state.allTransactionTemplates[templateType][i]; - - if (!template.hidden) { - visibleTemplates.push(template); - } - } - - allVisibleTemplates[templateType] = visibleTemplates; - } - - return allVisibleTemplates; - }, - allAvailableTemplatesCount(state) { - const allAvailableTemplateCounts = {}; - - for (const templateType in state.allTransactionTemplates) { - if (!Object.prototype.hasOwnProperty.call(state.allTransactionTemplates, templateType)) { - continue; - } - - allAvailableTemplateCounts[templateType] = state.allTransactionTemplates[templateType].length; - } - - return allAvailableTemplateCounts; - }, - allVisibleTemplatesCount(state) { - const allVisibleTemplateCounts = {}; - - for (const templateType in state.allVisibleTemplates) { - if (!Object.prototype.hasOwnProperty.call(state.allVisibleTemplates, templateType)) { - continue; - } - - allVisibleTemplateCounts[templateType] = state.allVisibleTemplates[templateType].length; - } - - return allVisibleTemplateCounts; - } - }, - actions: { - updateTransactionTemplateListInvalidState(templateType, invalidState) { - this.transactionTemplateListStatesInvalid[templateType] = invalidState; - }, - resetTransactionTemplates() { - this.allTransactionTemplates = {}; - this.allTransactionTemplatesMap = {}; - this.transactionTemplateListStatesInvalid = {}; - }, - loadAllTemplates({ templateType, force }) { - const self = this; - - if (!force && isDefined(self.transactionTemplateListStatesInvalid[templateType]) && !self.transactionTemplateListStatesInvalid[templateType]) { - return new Promise((resolve) => { - resolve(self.allTransactionTemplates[templateType] || []); - }); - } - - return new Promise((resolve, reject) => { - services.getAllTransactionTemplates({ templateType }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to retrieve template list' }); - return; - } - - if (!isDefined(self.transactionTemplateListStatesInvalid[templateType]) || self.transactionTemplateListStatesInvalid[templateType]) { - self.updateTransactionTemplateListInvalidState(templateType, false); - } - - if (force && data.result && isEquals(self.allTransactionTemplates[templateType], data.result)) { - reject({ message: 'Template list is up to date', isUpToDate: true }); - return; - } - - loadTransactionTemplateList(self, templateType, data.result); - - resolve(data.result); - }).catch(error => { - if (force) { - logger.error('failed to force load template list', error); - } else { - logger.error('failed to load template list', error); - } - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to retrieve template list' }); - } else { - reject(error); - } - }); - }); - }, - getTemplate({ templateId }) { - return new Promise((resolve, reject) => { - services.getTransactionTemplate({ - id: templateId - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to retrieve template' }); - return; - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to load template info', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to retrieve template' }); - } else { - reject(error); - } - }); - }); - }, - saveTemplateContent({ template, isEdit, clientSessionId }) { - const self = this; - - const submitTemplate = { - templateType: template.templateType, - name: template.name, - type: template.type, - sourceAccountId: template.sourceAccountId, - sourceAmount: template.sourceAmount, - destinationAccountId: '0', - destinationAmount: 0, - hideAmount: template.hideAmount, - tagIds: template.tagIds, - comment: template.comment - }; - - if (clientSessionId) { - submitTemplate.clientSessionId = clientSessionId; - } - - if (template.templateType === TemplateType.Schedule.type) { - submitTemplate.scheduledFrequencyType = template.scheduledFrequencyType; - submitTemplate.scheduledFrequency = template.scheduledFrequency; - submitTemplate.utcOffset = template.utcOffset; - } - - if (template.type === TransactionType.Expense) { - submitTemplate.categoryId = template.expenseCategoryId; - } else if (template.type === TransactionType.Income) { - submitTemplate.categoryId = template.incomeCategoryId; - } else if (template.type === TransactionType.Transfer) { - submitTemplate.categoryId = template.transferCategoryId; - submitTemplate.destinationAccountId = template.destinationAccountId; - submitTemplate.destinationAmount = template.destinationAmount; - } else { - return Promise.reject('An error occurred'); - } - - if (isEdit) { - submitTemplate.id = template.id; - } - - return new Promise((resolve, reject) => { - let promise = null; - - if (!submitTemplate.id) { - promise = services.addTransactionTemplate(submitTemplate); - } else { - promise = services.modifyTransactionTemplate(submitTemplate); - } - - promise.then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - if (!submitTemplate.id) { - reject({ message: 'Unable to add template' }); - } else { - reject({ message: 'Unable to save template' }); - } - return; - } - - if (!submitTemplate.id) { - addTemplateToTransactionTemplateList(self, template.templateType, data.result); - } else { - updateTemplateInTransactionTemplateList(self, template.templateType, data.result); - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to save template', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - if (!submitTemplate.id) { - reject({ message: 'Unable to add template' }); - } else { - reject({ message: 'Unable to save template' }); - } - } else { - reject(error); - } - }); - }); - }, - changeTemplateDisplayOrder({ templateType, templateId, from, to }) { - const self = this; - - return new Promise((resolve, reject) => { - let template = null; - - if (!isArray(self.allTransactionTemplates[templateType])) { - reject({ message: 'Unable to move template' }); - return; - } - - for (let i = 0; i < self.allTransactionTemplates[templateType].length; i++) { - if (self.allTransactionTemplates[templateType][i].id === templateId) { - template = self.allTransactionTemplates[templateType][i]; - break; - } - } - - if (!template || !self.allTransactionTemplates[templateType][to]) { - reject({ message: 'Unable to move template' }); - return; - } - - if (isDefined(self.transactionTemplateListStatesInvalid[templateType]) && !self.transactionTemplateListStatesInvalid[templateType]) { - self.updateTransactionTemplateListInvalidState(templateType, true); - } - - updateTemplateDisplayOrderInTransactionTemplateList(self, templateType, { - template: template, - from: from, - to: to - }); - - resolve(); - }); - }, - updateTemplateDisplayOrders({ templateType }) { - const self = this; - const newDisplayOrders = []; - - if (isArray(self.allTransactionTemplates[templateType])) { - for (let i = 0; i < self.allTransactionTemplates[templateType].length; i++) { - newDisplayOrders.push({ - id: self.allTransactionTemplates[templateType][i].id, - displayOrder: i + 1 - }); - } - } - - return new Promise((resolve, reject) => { - services.moveTransactionTemplate({ - newDisplayOrders: newDisplayOrders - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to move template' }); - return; - } - - if (!isDefined(self.transactionTemplateListStatesInvalid[templateType]) || self.transactionTemplateListStatesInvalid[templateType]) { - self.updateTransactionTemplateListInvalidState(templateType, false); - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to save templates display order', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to move template' }); - } else { - reject(error); - } - }); - }); - }, - hideTemplate({ template, hidden }) { - const self = this; - - return new Promise((resolve, reject) => { - services.hideTransactionTemplate({ - id: template.id, - hidden: hidden - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - if (hidden) { - reject({ message: 'Unable to hide this template' }); - } else { - reject({ message: 'Unable to unhide this template' }); - } - - return; - } - - updateTemplateVisibilityInTransactionTemplateList(self, template.templateType, { - template: template, - hidden: hidden - }); - - resolve(data.result); - }).catch(error => { - logger.error('failed to change template visibility', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - if (hidden) { - reject({ message: 'Unable to hide this template' }); - } else { - reject({ message: 'Unable to unhide this template' }); - } - } else { - reject(error); - } - }); - }); - }, - deleteTemplate({ template, beforeResolve }) { - const self = this; - - return new Promise((resolve, reject) => { - services.deleteTransactionTemplate({ - id: template.id - }).then(response => { - const data = response.data; - - if (!data || !data.success || !data.result) { - reject({ message: 'Unable to delete this template' }); - return; - } - - if (beforeResolve) { - beforeResolve(() => { - removeTemplateFromTransactionTemplateList(self, template.templateType, template); - }); - } else { - removeTemplateFromTransactionTemplateList(self, template.templateType, template); - } - - resolve(data.result); - }).catch(error => { - logger.error('failed to delete template', error); - - if (error.response && error.response.data && error.response.data.errorMessage) { - reject({ error: error.response.data }); - } else if (!error.processed) { - reject({ message: 'Unable to delete this template' }); - } else { - reject(error); - } - }); - }); - } - } -}); diff --git a/src/stores/transactionTemplate.ts b/src/stores/transactionTemplate.ts new file mode 100644 index 00000000..9a2210c3 --- /dev/null +++ b/src/stores/transactionTemplate.ts @@ -0,0 +1,469 @@ +import { ref, computed } from 'vue'; +import { defineStore } from 'pinia'; + +import type { BeforeResolveFunction } from '@/core/base.ts'; + +import { TransactionType } from '@/core/transaction.ts'; + +import { + type TransactionTemplateInfoResponse, + type TransactionTemplateNewDisplayOrderRequest, + TransactionTemplate +} from '@/models/transaction_template.ts'; + +import { isDefined, isObject, isArray, isEquals } from '@/lib/common.ts'; + +import logger from '@/lib/logger.ts'; +import services, { type ApiResponsePromise } from '@/lib/services.ts'; + +export const useTransactionTemplatesStore = defineStore('transactionTemplates', () =>{ + const allTransactionTemplates = ref>({}); + const allTransactionTemplatesMap = ref>>({}); + const transactionTemplateListStatesInvalid = ref>({}); + + const allVisibleTemplates = computed>(() => { + const allVisibleTemplates: Record = {}; + + for (const templateType in allTransactionTemplates.value) { + if (!Object.prototype.hasOwnProperty.call(allTransactionTemplates.value, templateType)) { + continue; + } + + const allTemplates = allTransactionTemplates.value[templateType]; + const visibleTemplates: TransactionTemplate[] = []; + + for (let i = 0; i < allTemplates.length; i++) { + const template = allTemplates[i]; + + if (!template.hidden) { + visibleTemplates.push(template); + } + } + + allVisibleTemplates[templateType] = visibleTemplates; + } + + return allVisibleTemplates; + }); + + const allAvailableTemplatesCount = computed>(() => { + const allAvailableTemplateCounts: Record = {}; + + for (const templateType in allTransactionTemplates.value) { + if (!Object.prototype.hasOwnProperty.call(allTransactionTemplates.value, templateType)) { + continue; + } + + allAvailableTemplateCounts[templateType] = allTransactionTemplates.value[templateType].length; + } + + return allAvailableTemplateCounts; + }); + + const allVisibleTemplatesCount = computed>(() => { + const allVisibleTemplateCounts: Record = {}; + + for (const templateType in allVisibleTemplates.value) { + if (!Object.prototype.hasOwnProperty.call(allVisibleTemplates.value, templateType)) { + continue; + } + + allVisibleTemplateCounts[templateType] = allVisibleTemplates.value[templateType].length; + } + + return allVisibleTemplateCounts; + }); + + function loadTransactionTemplateList(templateType: number, templates: TransactionTemplate[]): void { + allTransactionTemplates.value[templateType] = templates; + allTransactionTemplatesMap.value[templateType] = {}; + + for (let i = 0; i < templates.length; i++) { + const template = templates[i]; + allTransactionTemplatesMap.value[templateType][template.id] = template; + } + } + + function addTemplateToTransactionTemplateList(templateType: number, template: TransactionTemplate): void { + const templates = allTransactionTemplates.value[templateType]; + const templateMap = allTransactionTemplatesMap.value[templateType]; + + if (isArray(templates)) { + templates.push(template); + } + + if (isObject(templateMap)) { + templateMap[template.id] = template; + } + } + + function updateTemplateInTransactionTemplateList(templateType: number, template: TransactionTemplate): void { + const templates = allTransactionTemplates.value[templateType]; + const templateMap = allTransactionTemplatesMap.value[templateType]; + + if (isArray(templates)) { + for (let i = 0; i < templates.length; i++) { + if (templates[i].id === template.id) { + templates.splice(i, 1, template); + break; + } + } + } + + if (isObject(templateMap)) { + templateMap[template.id] = template; + } + } + + function updateTemplateDisplayOrderInTransactionTemplateList(templateType: number, { from, to }: { from: number, to: number }): void { + const templates = allTransactionTemplates.value[templateType]; + + if (isArray(templates)) { + templates.splice(to, 0, templates.splice(from, 1)[0]); + } + } + + function updateTemplateVisibilityInTransactionTemplateList(templateType: number, { template, hidden }: { template: TransactionTemplate, hidden: boolean }): void { + const templateMap = allTransactionTemplatesMap.value[templateType]; + + if (isObject(templateMap)) { + if (templateMap[template.id]) { + templateMap[template.id].hidden = hidden; + } + } + } + + function removeTemplateFromTransactionTemplateList(templateType: number, template: TransactionTemplate): void { + const templates = allTransactionTemplates.value[templateType]; + const templateMap = allTransactionTemplatesMap.value[templateType]; + + if (isArray(templates)) { + for (let i = 0; i < templates.length; i++) { + if (templates[i].id === template.id) { + templates.splice(i, 1); + break; + } + } + } + + if (isObject(templateMap)) { + if (templateMap[template.id]) { + delete templateMap[template.id]; + } + } + } + + function updateTransactionTemplateListInvalidState(templateType: number, invalidState: boolean): void { + transactionTemplateListStatesInvalid.value[templateType] = invalidState; + } + + function resetTransactionTemplates(): void { + allTransactionTemplates.value = {}; + allTransactionTemplatesMap.value = {}; + transactionTemplateListStatesInvalid.value = {}; + } + + function loadAllTemplates({ templateType, force }: { templateType: number, force?: boolean }): Promise { + if (!force && isDefined(transactionTemplateListStatesInvalid.value[templateType]) && !transactionTemplateListStatesInvalid.value[templateType]) { + return new Promise((resolve) => { + resolve(allTransactionTemplates.value[templateType] || []); + }); + } + + return new Promise((resolve, reject) => { + services.getAllTransactionTemplates({ templateType }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve template list' }); + return; + } + + if (!isDefined(transactionTemplateListStatesInvalid.value[templateType]) || transactionTemplateListStatesInvalid.value[templateType]) { + updateTransactionTemplateListInvalidState(templateType, false); + } + + const templates = TransactionTemplate.ofManyTemplates(data.result); + + if (force && data.result && isEquals(allTransactionTemplates.value[templateType], templates)) { + reject({ message: 'Template list is up to date', isUpToDate: true }); + return; + } + + loadTransactionTemplateList(templateType, templates); + + resolve(templates); + }).catch(error => { + if (force) { + logger.error('failed to force load template list', error); + } else { + logger.error('failed to load template list', error); + } + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve template list' }); + } else { + reject(error); + } + }); + }); + } + + function getTemplate({ templateId }: { templateId: string }): Promise { + return new Promise((resolve, reject) => { + services.getTransactionTemplate({ + id: templateId + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve template' }); + return; + } + + const template = TransactionTemplate.ofTemplate(data.result); + + resolve(template); + }).catch(error => { + logger.error('failed to load template info', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve template' }); + } else { + reject(error); + } + }); + }); + } + + function saveTemplateContent({ template, isEdit, clientSessionId }: { template: TransactionTemplate, isEdit: boolean, clientSessionId: string }): Promise { + return new Promise((resolve, reject) => { + let promise: ApiResponsePromise; + + if (template.type !== TransactionType.Expense && + template.type !== TransactionType.Income && + template.type !== TransactionType.Transfer) { + reject({ message: 'An error occurred' }); + return; + } + + if (!isEdit) { + promise = services.addTransactionTemplate(template.toTemplateCreateRequest(clientSessionId)); + } else { + promise = services.modifyTransactionTemplate(template.toTemplateModifyRequest()); + } + + promise.then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (!isEdit) { + reject({ message: 'Unable to add template' }); + } else { + reject({ message: 'Unable to save template' }); + } + return; + } + + const template = TransactionTemplate.ofTemplate(data.result); + + if (!isEdit) { + addTemplateToTransactionTemplateList(template.templateType, template); + } else { + updateTemplateInTransactionTemplateList(template.templateType, template); + } + + resolve(template); + }).catch(error => { + logger.error('failed to save template', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + if (!isEdit) { + reject({ message: 'Unable to add template' }); + } else { + reject({ message: 'Unable to save template' }); + } + } else { + reject(error); + } + }); + }); + } + + function changeTemplateDisplayOrder({ templateType, templateId, from, to }: { templateType: number, templateId: string, from: number, to: number }): Promise { + return new Promise((resolve, reject) => { + let template: TransactionTemplate | null = null; + + if (!isArray(allTransactionTemplates.value[templateType])) { + reject({ message: 'Unable to move template' }); + return; + } + + for (let i = 0; i < allTransactionTemplates.value[templateType].length; i++) { + if (allTransactionTemplates.value[templateType][i].id === templateId) { + template = allTransactionTemplates.value[templateType][i]; + break; + } + } + + if (!template || !allTransactionTemplates.value[templateType][to]) { + reject({ message: 'Unable to move template' }); + return; + } + + if (isDefined(transactionTemplateListStatesInvalid.value[templateType]) && !transactionTemplateListStatesInvalid.value[templateType]) { + updateTransactionTemplateListInvalidState(templateType, true); + } + + updateTemplateDisplayOrderInTransactionTemplateList(templateType, { from, to }); + + resolve(); + }); + } + + function updateTemplateDisplayOrders({ templateType }: { templateType: number }): Promise { + const newDisplayOrders: TransactionTemplateNewDisplayOrderRequest[] = []; + + if (isArray(allTransactionTemplates.value[templateType])) { + for (let i = 0; i < allTransactionTemplates.value[templateType].length; i++) { + newDisplayOrders.push({ + id: allTransactionTemplates.value[templateType][i].id, + displayOrder: i + 1 + }); + } + } + + return new Promise((resolve, reject) => { + services.moveTransactionTemplate({ + newDisplayOrders: newDisplayOrders + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to move template' }); + return; + } + + if (!isDefined(transactionTemplateListStatesInvalid.value[templateType]) || transactionTemplateListStatesInvalid.value[templateType]) { + updateTransactionTemplateListInvalidState(templateType, false); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to save templates display order', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to move template' }); + } else { + reject(error); + } + }); + }); + } + + function hideTemplate({ template, hidden }: { template: TransactionTemplate, hidden: boolean }): Promise { + return new Promise((resolve, reject) => { + services.hideTransactionTemplate({ + id: template.id, + hidden: hidden + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (hidden) { + reject({ message: 'Unable to hide this template' }); + } else { + reject({ message: 'Unable to unhide this template' }); + } + + return; + } + + updateTemplateVisibilityInTransactionTemplateList(template.templateType, { + template: template, + hidden: hidden + }); + + resolve(data.result); + }).catch(error => { + logger.error('failed to change template visibility', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + if (hidden) { + reject({ message: 'Unable to hide this template' }); + } else { + reject({ message: 'Unable to unhide this template' }); + } + } else { + reject(error); + } + }); + }); + } + + function deleteTemplate({ template, beforeResolve }: { template: TransactionTemplate, beforeResolve?: BeforeResolveFunction }): Promise { + return new Promise((resolve, reject) => { + services.deleteTransactionTemplate({ + id: template.id + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to delete this template' }); + return; + } + + if (beforeResolve) { + beforeResolve(() => { + removeTemplateFromTransactionTemplateList(template.templateType, template); + }); + } else { + removeTemplateFromTransactionTemplateList(template.templateType, template); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to delete template', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to delete this template' }); + } else { + reject(error); + } + }); + }); + } + + return { + // states + allTransactionTemplates, + allTransactionTemplatesMap, + transactionTemplateListStatesInvalid, + // computed states + allVisibleTemplates, + allAvailableTemplatesCount, + allVisibleTemplatesCount, + // functions + updateTransactionTemplateListInvalidState, + resetTransactionTemplates, + loadAllTemplates, + getTemplate, + saveTemplateContent, + changeTemplateDisplayOrder, + updateTemplateDisplayOrders, + hideTemplate, + deleteTemplate + }; +}); diff --git a/src/views/desktop/templates/ListPage.vue b/src/views/desktop/templates/ListPage.vue index eeade772..3a9b71ef 100644 --- a/src/views/desktop/templates/ListPage.vue +++ b/src/views/desktop/templates/ListPage.vue @@ -149,13 +149,13 @@ import EditDialog from '@/views/desktop/transactions/list/dialogs/EditDialog.vue'; import { mapStores } from 'pinia'; -import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.js'; +import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.ts'; import { TemplateType } from '@/core/template.ts'; import { isNoAvailableTemplate, getAvailableTemplateCount -} from '@/lib/template.js'; +} from '@/lib/template.ts'; import { mdiRefresh, diff --git a/src/views/desktop/transactions/ListPage.vue b/src/views/desktop/transactions/ListPage.vue index b3060e5a..ca44a890 100644 --- a/src/views/desktop/transactions/ListPage.vue +++ b/src/views/desktop/transactions/ListPage.vue @@ -594,7 +594,7 @@ import { useAccountsStore } from '@/stores/account.ts'; import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import { useTransactionTagsStore } from '@/stores/transactionTag.ts'; import { useTransactionsStore } from '@/stores/transaction.ts'; -import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.js'; +import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.ts'; import { DateRangeScene, DateRange } from '@/core/datetime.ts'; import { AmountFilterType } from '@/core/numeral.ts'; diff --git a/src/views/desktop/transactions/list/dialogs/EditDialog.vue b/src/views/desktop/transactions/list/dialogs/EditDialog.vue index 76ba9d7c..59fe1be3 100644 --- a/src/views/desktop/transactions/list/dialogs/EditDialog.vue +++ b/src/views/desktop/transactions/list/dialogs/EditDialog.vue @@ -397,7 +397,7 @@ import { useAccountsStore } from '@/stores/account.ts'; import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts'; import { useTransactionTagsStore } from '@/stores/transactionTag.ts'; import { useTransactionsStore } from '@/stores/transaction.ts'; -import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.js'; +import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.ts'; import { useExchangeRatesStore } from '@/stores/exchangeRates.ts'; import { CategoryType } from '@/core/category.ts'; @@ -407,6 +407,7 @@ import { TRANSACTION_MAX_PICTURE_COUNT } from '@/consts/transaction.ts'; import { KnownErrorCode } from '@/consts/api.ts'; import { SUPPORTED_IMAGE_EXTENSIONS } from '@/consts/file.ts'; import { TransactionTag } from '@/models/transaction_tag.ts'; +import { TransactionTemplate } from '@/models/transaction_template.ts'; import { isArray, @@ -836,6 +837,7 @@ export default { } } } else if (self.type === 'template') { + self.transaction = TransactionTemplate.createNewTransactionTemplate(self.transaction); self.transaction.name = ''; if (options && options.templateType) { diff --git a/src/views/mobile/HomePage.vue b/src/views/mobile/HomePage.vue index d7867b00..2693383d 100644 --- a/src/views/mobile/HomePage.vue +++ b/src/views/mobile/HomePage.vue @@ -204,7 +204,7 @@ import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/setting.ts'; import { useUserStore } from '@/stores/user.ts'; -import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.js'; +import { useTransactionTemplatesStore } from '@/stores/transactionTemplate.ts'; import { useOverviewStore } from '@/stores/overview.ts'; import { DateRange } from '@/core/datetime.ts'; diff --git a/src/views/mobile/templates/ListPage.vue b/src/views/mobile/templates/ListPage.vue index 4f2d8dbb..1a366bf7 100644 --- a/src/views/mobile/templates/ListPage.vue +++ b/src/views/mobile/templates/ListPage.vue @@ -86,7 +86,7 @@