From 415f22c015d4d81901c16d0dd534285582f9c441 Mon Sep 17 00:00:00 2001 From: isKonstantin Date: Fri, 2 Aug 2024 11:05:31 +0300 Subject: [PATCH] Added tag management, some fixes --- components/analytics/pie.vue | 11 +- components/analytics/tagsAnalytics.vue | 143 ++++++++++++++ components/analytics/tagsByMonths.vue | 2 +- components/misc/tagsManagementSummary.vue | 159 +++++++++++++++ .../modal/transaction/tag/management.vue | 184 ++++++++++++++++++ components/select/currency.vue | 16 +- composables/useApiLoader.ts | 9 + lang/en-US.json | 54 ++++- lang/ru-RU.json | 52 +++++ libs/api/analytics/AnalyticsApi.ts | 18 ++ .../TransactionsTagsManagementApi.ts | 128 ++++++++++++ package-lock.json | 4 +- package.json | 2 +- pages/analytics.vue | 30 ++- pages/index.vue | 19 +- pages/login.vue | 6 +- pages/tags.vue | 172 +++++++++++----- tailwind.config.js | 8 +- 18 files changed, 946 insertions(+), 71 deletions(-) create mode 100644 components/analytics/tagsAnalytics.vue create mode 100644 components/misc/tagsManagementSummary.vue create mode 100644 components/modal/transaction/tag/management.vue create mode 100644 libs/api/transactions/tag/management/TransactionsTagsManagementApi.ts diff --git a/components/analytics/pie.vue b/components/analytics/pie.vue index 6806b59..68531f3 100644 --- a/components/analytics/pie.vue +++ b/components/analytics/pie.vue @@ -12,7 +12,7 @@ @@ -81,7 +81,7 @@ const tagsMap = $transactionsTagsApi.getTagsTreeMap(); const view = ref([]); const tags = $transactionsTagsApi.getTags(); -const analytics = ref(); +const analytics = ref([]); const pushToView = (tagId) => { view.value.push(tagId); @@ -121,7 +121,7 @@ const chartMap = ref({}); const getDays = () => { return { first: mode.value ? new Date(date.value.year, date.value.month, 1) : new Date(date.value, 0, 1), - last: mode.value ? new Date(date.value.year, date.value.month + 1, 1) : new Date(date.value + 1, 0, 1) + last: mode.value ? new Date(date.value.year, date.value.month + 1, 0, 23,59,59, 999) : new Date(date.value + 1, 0, 0, 23,59,59, 999) } } @@ -159,7 +159,7 @@ const formatter = (value) => { return (props.sign > 0 ? "" : "-") + formatAmount(value); } -const childsSum = (allData, tagObject, currencyId, sign) => { +const childsSum = (allData, tagObject, sign) => { let sum = 0; tagObject.childs.forEach(t => { @@ -169,6 +169,7 @@ const childsSum = (allData, tagObject, currencyId, sign) => { if (t.childs && t.childs.length > 0) { sum += childsSum(allData, t, sign); } + }) return sum; @@ -243,7 +244,7 @@ const buildChart = () => { if (tagDelta * sign < 0) tagDelta = 0; - const sum = tagDelta + (isParent ? 0 : childsSum(allData, tag, currency.value, sign)); + const sum = tagDelta + (isParent ? 0 : childsSum(allData, tag, sign)); if (sum === 0) return; diff --git a/components/analytics/tagsAnalytics.vue b/components/analytics/tagsAnalytics.vue new file mode 100644 index 0000000..3c7dbc0 --- /dev/null +++ b/components/analytics/tagsAnalytics.vue @@ -0,0 +1,143 @@ + + + + + \ No newline at end of file diff --git a/components/analytics/tagsByMonths.vue b/components/analytics/tagsByMonths.vue index f858a91..125de2a 100644 --- a/components/analytics/tagsByMonths.vue +++ b/components/analytics/tagsByMonths.vue @@ -32,7 +32,7 @@ const tagsMap = $transactionsTagsApi.getTagsMap(); const filter = ref(); const chartSeries = ref([]); -const type = ref('area'); +const type = ref('bar'); const chartOptions = ref({}); diff --git a/components/misc/tagsManagementSummary.vue b/components/misc/tagsManagementSummary.vue new file mode 100644 index 0000000..d1fce56 --- /dev/null +++ b/components/misc/tagsManagementSummary.vue @@ -0,0 +1,159 @@ + + + + + \ No newline at end of file diff --git a/components/modal/transaction/tag/management.vue b/components/modal/transaction/tag/management.vue new file mode 100644 index 0000000..696ed27 --- /dev/null +++ b/components/modal/transaction/tag/management.vue @@ -0,0 +1,184 @@ + + + + + \ No newline at end of file diff --git a/components/select/currency.vue b/components/select/currency.vue index efbb3d5..19aad6a 100644 --- a/components/select/currency.vue +++ b/components/select/currency.vue @@ -35,6 +35,11 @@ import Multiselect from "@vueform/multiselect"; const props = defineProps({ modelValue: { + }, + + excludeCurrencies: { + type: Array, + required: false } }) @@ -49,11 +54,12 @@ const options = computed(() => { const resultArray = []; currencies.value.forEach((c) => { - resultArray.push({ - label: c.code + " - " + c.description + " (" + c.symbol + ")", - code: c.code, - value: c.currencyId - }); + if (!props.excludeCurrencies || !props.excludeCurrencies.find(exc => exc === c.currencyId)) + resultArray.push({ + label: c.code + " - " + c.description + " (" + c.symbol + ")", + code: c.code, + value: c.currencyId + }); }); return resultArray; diff --git a/composables/useApiLoader.ts b/composables/useApiLoader.ts index 8bf75f3..82983b3 100644 --- a/composables/useApiLoader.ts +++ b/composables/useApiLoader.ts @@ -19,6 +19,7 @@ import {ReportsApi} from "~/libs/api/reports/ReportsApi"; import {ca} from "date-fns/locale"; import {useStorage} from "@vueuse/core"; import {useServer} from "~/composables/useServer"; +import {TransactionsTagsManagementApi} from "~/libs/api/transactions/tag/management/TransactionsTagsManagementApi"; export const useApiLoader = new class ApiLoader { private readonly serverConfigs: ConfigManager; @@ -39,6 +40,9 @@ export const useApiLoader = new class ApiLoader { private readonly adminApi : AdminApi; private readonly accumulationsApi: AccumulationsApi; + + private readonly transactionTagsManagementApi : TransactionsTagsManagementApi; + private readonly reportsApi: ReportsApi; private websocketClient : WebSocket | null = null; @@ -60,6 +64,9 @@ export const useApiLoader = new class ApiLoader { this.notesApi = new NotesApi(); this.notificationsApi = new NotificationApi(); this.accumulationsApi = new AccumulationsApi(); + + this.transactionTagsManagementApi = new TransactionsTagsManagementApi(); + this.reportsApi = new ReportsApi(); this.adminApi = new AdminApi(); @@ -81,6 +88,7 @@ export const useApiLoader = new class ApiLoader { this.notesApi.init(), this.notificationsApi.init(), this.accumulationsApi.init(), + this.transactionTagsManagementApi.init(), this.reportsApi.init(), this.adminApi.init() ]).then(results => { @@ -180,6 +188,7 @@ export const useApiLoader = new class ApiLoader { sessionsApi: this.sessionsApi, notificationsApi: this.notificationsApi, accumulationsApi: this.accumulationsApi, + tagsManagementApi: this.transactionTagsManagementApi, reportsApi: this.reportsApi, adminApi: this.adminApi } diff --git a/lang/en-US.json b/lang/en-US.json index 1d5fbf5..b96c455 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -45,7 +45,10 @@ "allNotesButton": "All notes" }, "summary": { - "emptyMessage": "There will be a general summary of the accounts" + "emptyMessage": "A general summary of the accounts will be provided here" + }, + "tags": { + "emptyMessage": "Here you will see a general summary of expenses by category for the current month" } }, @@ -118,6 +121,14 @@ "path": "Path:", "root": "Root", + "tagsSummary": { + "maxContribution": "Maximum contribution", + "noData": { + "income": "No income data in the budget", + "expanse": "No expanse data in the budget" + } + }, + "table": { "name": "Name", "tagType": { @@ -126,6 +137,15 @@ "income": "Income", "mixed": "Mixed" }, + + "management": { + "title": "Budget", + "dateTypes": { + "perMonth": "per month", + "perQuartal": "per quartal" + } + }, + "description": "Description", "action": "Action", "toChilds": "To Childs" @@ -157,6 +177,11 @@ "expensesTitle": "Expenses for year", "incomesTitle": "Incomes for year" } + }, + + "tags": { + "title": "Budget analysis by category", + "noData": "No data" } }, @@ -378,6 +403,33 @@ "deleteAllBulk": "Delete all bulk transactions? This action cannot be undone" }, + "tagManagement": { + "title": "Expected expenses and income", + + "messages": { + "new": { + "success": "Management created", + "error": "Oops, failed to create management" + }, + "edit": { + "success": "Management edited", + "error": "Oops, failed to edit management" + }, + "remove": { + "success": "Management removed", + "error": "Oops, failed to remove management" + } + }, + + "placeholders": { + "dateType": { + "label": "Frequency", + "perMonth": "Per month", + "perQuartal": "Per quartal" + } + } + }, + "csvImport": { "title": "CSV Import", "importMessage": "Imported {n} transactions", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index f44736c..0141de0 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -46,6 +46,9 @@ }, "summary": { "emptyMessage": "Здесь будет общая сводка счетов" + }, + "tags": { + "emptyMessage": "Здесь будет общая сводка трат по категориям за текущий месяц" } }, @@ -120,6 +123,14 @@ "path": "Путь:", "root": "Корень", + "tagsSummary": { + "maxContribution": "Максимальный вклад", + "noData": { + "income": "В бюджете отсутствуют данные о доходах", + "expanse": "В бюджете отсутствуют данные о расходах" + } + }, + "table": { "name": "Имя", "tagType": { @@ -128,6 +139,15 @@ "income": "Доход", "mixed": "Разное" }, + + "management": { + "title": "Бюджет", + "dateTypes": { + "perMonth": "в месяц", + "perQuartal": "в квартал" + } + }, + "description": "Описание", "action": "Действие", "toChilds": "To Childs" @@ -159,6 +179,11 @@ "expensesTitle": "Расходы за год", "incomesTitle": "Доходы за год" } + }, + + "tags": { + "title": "Аналитика бюджета по категориям", + "noData": "Нет данных" } }, @@ -380,6 +405,33 @@ "deleteAllBulk": "Удалить все транзакции? Это действие невозможно отменить" }, + "tagManagement": { + "title": "Ожидаемые расходы и доходы", + + "messages": { + "new": { + "success": "Бюджет создан", + "error": "Ошибка при создании бюджета" + }, + "edit": { + "success": "Бюджет изменен", + "error": "Ошибка при изменении бюджета" + }, + "remove": { + "success": "Бюджет удален", + "error": "Ошибка при изменении бюджета" + } + }, + + "placeholders": { + "dateType": { + "label": "Переодичность", + "perMonth": "В месяц", + "perQuartal": "В квартал" + } + } + }, + "csvImport": { "title": "Импорт из CSV файла", "importMessage": "Импортировано {n} транзакций | Импортирована {n} транзакция | Импортировано {n} транзакций | Импортировано {n} транзакций", diff --git a/libs/api/analytics/AnalyticsApi.ts b/libs/api/analytics/AnalyticsApi.ts index 5791c36..11ec7a1 100644 --- a/libs/api/analytics/AnalyticsApi.ts +++ b/libs/api/analytics/AnalyticsApi.ts @@ -1,5 +1,6 @@ import {TransactionsFilter} from "~/libs/api/transactions/TransactionsFilter"; import {AbstractApi} from "~/libs/api/AbstractApi"; +import {da} from "date-fns/locale"; export class AnalyticsApi extends AbstractApi { async init(): Promise { @@ -35,4 +36,21 @@ export class AnalyticsApi extends AbstractApi { return new Map(Object.entries(data.value.total)); } + + public async getTagAnalytics(time: Date) : Promise { + const opts = { + method: "GET", + params: { + time: time.toISOString() + } + }; + + const {data, error} = await useApi("/user/analytics/getTagsAnalytics", opts); + + if (error.value !== null) { + return null; + } + + return data.value.result; + } } \ No newline at end of file diff --git a/libs/api/transactions/tag/management/TransactionsTagsManagementApi.ts b/libs/api/transactions/tag/management/TransactionsTagsManagementApi.ts new file mode 100644 index 0000000..2dc3fd0 --- /dev/null +++ b/libs/api/transactions/tag/management/TransactionsTagsManagementApi.ts @@ -0,0 +1,128 @@ +import {AbstractApi} from "~/libs/api/AbstractApi"; +import {Ref} from "vue"; + +export class TransactionsTagsManagementApi extends AbstractApi { + private managements : Ref> = ref([]); + private tagToManagementsMap : Ref>> = ref(new Map>); + private managementsMap : Ref> = ref(new Map()); + + async init(): Promise { + await this.fetch(); + } + + public async fetch() : Promise { + const {data} = await useApi("/user/transactions/tags/management/getList"); + + if (data.value === null) + return; + + this.managements.value = data.value.managements || []; + + this.rebuildMap(); + } + + private rebuildMap() { + const newMap = new Map>(); + const map2 = new Map(); + + this.managements.value.forEach((management) => { + if (!newMap.has(management.tagId)) + newMap.set(management.tagId, new Array()); + + newMap.get(management.tagId)?.push(management); + map2.set(management.managementId, management); + }) + + this.tagToManagementsMap.value = newMap; + this.managementsMap.value = map2; + } + + public getManagements(): Ref> { + return this.managements; + } + + public getManagementsMap(): Ref> { + return this.managementsMap; + } + + public getTagToManagementsMap(): Ref>> { + return this.tagToManagementsMap; + } + + public async addManagement(tagId : number, currencyId: number, dateType: number, amount: number) : Promise { + const opts = { + method: "POST", + params: { + tagId: tagId, + currencyId: currencyId, + dateType: dateType, + amount: amount + } + } + + const {data: newManagement, error} = await useApi("/user/transactions/tags/management/add", opts); + + if (error.value !== null) { + return -1; + } + + this.managements.value?.push({ + managementId: newManagement.value.managementId, + tagId: tagId, + currencyId: currencyId, + dateType: dateType, + amount: amount + }); + + this.rebuildMap(); + + return newManagement.value.managementId; + } + + public async editManagement(managementId: number, tagId : number, currencyId: number, dateType: number, amount: number) : Promise { + const opts = { + method: "POST", + params: { + managementId: managementId, + tagId: tagId, + currencyId: currencyId, + dateType: dateType, + amount: amount + } + } + + const {error} = await useApi("/user/transactions/tags/management/edit", opts); + + if (error.value !== null) + return false; + + const saved = this.managements.value?.find(m => m.managementId === managementId); + + saved.tagId = tagId; + saved.currencyId = currencyId; + saved.dateType = dateType; + saved.amount = amount; + + return true; + } + + public async removeManagement(managementId: number) : Promise { + const opts = { + method: "POST", + params: { + managementId: managementId + } + }; + + const {error} = await useApi("/user/transactions/tags/management/remove", opts); + + if (error.value !== null) { + return false; + } + + this.managements.value = this.managements.value.filter((m) => m.managementId != managementId); + this.rebuildMap(); + + return true; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bc54214..2db283d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "finwave", - "version": "0.16.1", + "version": "0.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "finwave", - "version": "0.16.1", + "version": "0.17.0", "hasInstallScript": true, "dependencies": { "@coingecko/cryptoformat": "^0.5.4", diff --git a/package.json b/package.json index 1bb43bc..e8e6ae8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "finwave", - "version": "0.16.1", + "version": "0.17.0", "private": true, "scripts": { "build": "nuxt build", diff --git a/pages/analytics.vue b/pages/analytics.vue index 1b85e9c..341dff8 100644 --- a/pages/analytics.vue +++ b/pages/analytics.vue @@ -5,6 +5,19 @@ +
+

+ {{ $t("analyticsPage.tags.title") }} +

+ +
+ +
+ + +
+

@@ -37,8 +50,7 @@

- - + @@ -48,6 +60,8 @@ import ApexChart from "vue3-apexcharts"; import TransactionsFilterDiv from "~/components/transactions/filter.vue"; import TagsByMonths from "~/components/analytics/tagsByMonths.vue"; import Pie from "~/components/analytics/pie.vue"; +import TagsAnalytics from "~/components/analytics/tagsAnalytics.vue"; +import Datepicker from "@vuepic/vue-datepicker"; definePageMeta({ middleware: [ @@ -58,6 +72,18 @@ definePageMeta({ const { $analyticsApi, $transactionsTagsApi, $currenciesApi, $serverConfigs } = useNuxtApp(); const { t, locale } = useI18n(); +const today = new Date(); + +const dateForTags = ref(); +const monthPicker = ref(); + +watch(monthPicker, (value, oldValue, onCleanup) => { + if (value) + dateForTags.value = new Date(value.year, value.month + 1, 1); +}) + +monthPicker.value = { year: today.getFullYear(), month: today.getMonth() }; + diff --git a/pages/index.vue b/pages/index.vue index 0559fe4..c5675f4 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -14,9 +14,21 @@ - - - +
+ + + + + + +
@@ -30,6 +42,7 @@ import NewCurrencyButton from "~/components/buttons/quickActions/newCurrencyButt import NewNoteButton from "~/components/buttons/quickActions/newNoteButton.vue"; import {useApiLoader} from "~/composables/useApiLoader"; import {reloadNuxtApp} from "#app"; +import TagsAnalytics from "~/components/analytics/tagsAnalytics.vue"; definePageMeta({ middleware: [ diff --git a/pages/login.vue b/pages/login.vue index 3c8c769..8bdcdbe 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -141,10 +141,10 @@ if (demoMode) { loading.value = false; errorMessage.value = "loginPage.errors.demoError"; + }else { + login.value = data.value.username; + password.value = data.value.password; } - - login.value = data.value.username; - password.value = data.value.password; } diff --git a/pages/tags.vue b/pages/tags.vue index ccace4b..5abf448 100644 --- a/pages/tags.vue +++ b/pages/tags.vue @@ -1,56 +1,79 @@