From 96761e3a1dd5bb9772fac38421d9d73588e439c7 Mon Sep 17 00:00:00 2001 From: isKonstantin Date: Sat, 10 Aug 2024 15:48:56 +0300 Subject: [PATCH] Renaming tags => transaction categories / account folders, added AI integration, added files, some fixes --- assets/css/multiselect.css | 6 +- .../{tagGroup.vue => folderGroup.vue} | 40 +- components/accounts/panel.vue | 76 +- components/ai/aiDialog.vue | 336 ++ ...sAnalytics.vue => categoriesAnalytics.vue} | 24 +- ...agsByMonths.vue => categoriesByMonths.vue} | 28 +- components/analytics/pie.vue | 70 +- components/analytics/totalByPeriod.vue | 4 +- ...newTagButton.vue => newCategoryButton.vue} | 2 +- ...ummary.vue => categoriesBudgetSummary.vue} | 38 +- components/modal/account/create.vue | 4 +- components/modal/account/edit.vue | 53 +- .../modal/account/{tag => folder}/create.vue | 18 +- .../modal/account/{tag => folder}/edit.vue | 36 +- components/modal/accumulation/set.vue | 22 +- components/modal/aiModal.vue | 31 + components/modal/files/view.vue | 154 + components/modal/recurring/create.vue | 36 +- components/modal/recurring/edit.vue | 38 +- components/modal/reports/create.vue | 2 +- .../modal/transaction/category/budget.vue | 184 ++ .../modal/transaction/category/create.vue | 114 + .../transaction/{tag => category}/edit.vue | 96 +- .../modal/transaction/create/create.vue | 2 +- .../modal/transaction/create/default.vue | 32 +- .../modal/transaction/create/internal.vue | 30 +- components/modal/transaction/csvImport.vue | 54 +- components/modal/transaction/edit/default.vue | 38 +- .../modal/transaction/edit/internal.vue | 24 +- components/modal/transaction/tag/create.vue | 114 - .../modal/transaction/tag/management.vue | 184 -- .../transaction/transactionAmountField.vue | 22 +- components/nav/menu/menu.vue | 4 +- components/nav/navigation.vue | 21 +- components/nav/user/user.vue | 176 +- components/reports/reportsPanel.vue | 45 +- components/select/account.vue | 12 +- components/select/currency.vue | 10 +- ...sactionTag.vue => transactionCategory.vue} | 24 +- .../transactions/{tag.vue => category.vue} | 6 +- components/transactions/filter.vue | 20 +- components/transactions/table/tableEntry.vue | 18 +- composables/useApi.ts | 11 +- composables/useApiLoader.ts | 172 +- lang/en-US.json | 172 +- lang/ro-RO.json | 90 +- lang/ru-RU.json | 167 +- libs/api/accounts/AccountsApi.ts | 14 +- .../accounts/folders/AccountsFoldersApi.ts | 98 + libs/api/accounts/tags/AccountsTagsApi.ts | 98 - libs/api/accumulations/AccumulationsApi.ts | 2 +- libs/api/ai/AiApi.ts | 150 + libs/api/analytics/AnalyticsApi.ts | 4 +- .../api/category/TransactionsCategoriesApi.ts | 206 ++ .../category/budget/CategoriesBudgetApi.ts | 128 + libs/api/files/FilesApi.ts | 117 + .../recurring/RecurringTransactionsApi.ts | 12 +- libs/api/reports/ReportsApi.ts | 8 +- libs/api/transactions/TransactionsApi.ts | 12 +- libs/api/transactions/TransactionsFilter.ts | 8 +- .../transactions/tag/TransactionsTagsApi.ts | 206 -- .../TransactionsTagsManagementApi.ts | 128 - libs/api/user/UserApi.ts | 6 +- nuxt.config.ts | 3 +- package-lock.json | 2872 +++++++++++++++-- package.json | 5 +- pages/analytics.vue | 18 +- pages/bulk.vue | 22 +- pages/categories.vue | 188 ++ pages/index.vue | 22 +- pages/notes.vue | 15 + pages/recurring.vue | 14 +- pages/register.vue | 16 +- pages/tags.vue | 187 -- pages/transactions.vue | 16 +- plugins/2.api.ts | 2 - tailwind.config.js | 3 + 77 files changed, 5436 insertions(+), 2004 deletions(-) rename components/accounts/{tagGroup.vue => folderGroup.vue} (76%) create mode 100644 components/ai/aiDialog.vue rename components/analytics/{tagsAnalytics.vue => categoriesAnalytics.vue} (78%) rename components/analytics/{tagsByMonths.vue => categoriesByMonths.vue} (80%) rename components/buttons/quickActions/{newTagButton.vue => newCategoryButton.vue} (93%) rename components/misc/{tagsManagementSummary.vue => categoriesBudgetSummary.vue} (69%) rename components/modal/account/{tag => folder}/create.vue (61%) rename components/modal/account/{tag => folder}/edit.vue (58%) create mode 100644 components/modal/aiModal.vue create mode 100644 components/modal/files/view.vue create mode 100644 components/modal/transaction/category/budget.vue create mode 100644 components/modal/transaction/category/create.vue rename components/modal/transaction/{tag => category}/edit.vue (51%) delete mode 100644 components/modal/transaction/tag/create.vue delete mode 100644 components/modal/transaction/tag/management.vue rename components/select/{transactionTag.vue => transactionCategory.vue} (75%) rename components/transactions/{tag.vue => category.vue} (80%) create mode 100644 libs/api/accounts/folders/AccountsFoldersApi.ts delete mode 100644 libs/api/accounts/tags/AccountsTagsApi.ts create mode 100644 libs/api/ai/AiApi.ts create mode 100644 libs/api/category/TransactionsCategoriesApi.ts create mode 100644 libs/api/category/budget/CategoriesBudgetApi.ts create mode 100644 libs/api/files/FilesApi.ts rename libs/api/{transactions => }/recurring/RecurringTransactionsApi.ts (87%) delete mode 100644 libs/api/transactions/tag/TransactionsTagsApi.ts delete mode 100644 libs/api/transactions/tag/management/TransactionsTagsManagementApi.ts create mode 100644 pages/categories.vue delete mode 100644 pages/tags.vue diff --git a/assets/css/multiselect.css b/assets/css/multiselect.css index d23ec3f..24ce117 100644 --- a/assets/css/multiselect.css +++ b/assets/css/multiselect.css @@ -42,7 +42,7 @@ } .multiselect-wrapper { - @apply relative h-12 font-bold text-sm mx-auto w-full flex items-center justify-end box-border cursor-pointer outline-none; + @apply relative h-12 text-sm mx-auto w-full flex items-center justify-end box-border cursor-pointer outline-none; } .multiselect-single-label { @@ -90,7 +90,7 @@ .multiselect-tags-search-wrapper { min-width: 3rem; - @apply inline-block relative mx-1 mb-1 flex-grow flex-shrink h-full; + @apply inline-block placeholder-red-700 relative mx-1 mb-1 flex-grow flex-shrink h-full; } .multiselect-tags-search { @@ -102,7 +102,7 @@ } .multiselect-placeholder { - @apply flex items-center opacity-90 font-normal h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 text-gray-400 rtl:left-auto rtl:right-0 rtl:pl-0 rtl:pr-3.5; + @apply flex items-center font-bold h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 text-gray-400 rtl:left-auto rtl:right-0 rtl:pl-0 rtl:pr-3.5; } .multiselect-caret { diff --git a/components/accounts/tagGroup.vue b/components/accounts/folderGroup.vue similarity index 76% rename from components/accounts/tagGroup.vue rename to components/accounts/folderGroup.vue index c44de9f..105153d 100644 --- a/components/accounts/tagGroup.vue +++ b/components/accounts/folderGroup.vue @@ -1,22 +1,22 @@ @@ -68,14 +68,14 @@ const props = defineProps({ } }) -const { $analyticsApi, $transactionsTagsApi, $tagsManagementApi, $currenciesApi, $serverConfigs, $transactionsApi } = useNuxtApp(); +const { $analyticsApi, $transactionsCategoriesApi, $categoriesBudgetApi, $currenciesApi, $serverConfigs, $transactionsApi } = useNuxtApp(); const data = ref([]); -const tagsTree = $transactionsTagsApi.getTagsTree(); -const tagsMap = $transactionsTagsApi.getTagsMap(); +const categoriesTree = $transactionsCategoriesApi.getCategoriesTree(); +const categoriesMap = $transactionsCategoriesApi.getCategoriesMap(); const currenciesMap = $currenciesApi.getCurrenciesMap(); -const managementMap = $tagsManagementApi.getManagementsMap(); +const budgetsMap = $categoriesBudgetApi.getBudgetsMap(); const formatAmount = (delta, currencyId) => { const currency = currenciesMap.value.get(currencyId); @@ -87,13 +87,13 @@ const formatAmount = (delta, currencyId) => { } const fetchData = async () => { - const fetched = await $analyticsApi.getTagAnalytics(props.date); + const fetched = await $analyticsApi.getCategoryAnalytics(props.date); const newData = []; for (const entry of fetched) { - const expected = managementMap.value.get(entry.managementId).amount + const expected = budgetsMap.value.get(entry.budgetId).amount - if (expected > 0 && props.onlyExpanses || props.onlyRoot && !tagsTree.value.find(t => t.tag.tagId === entry.tagId)) + if (expected > 0 && props.onlyExpanses || props.onlyRoot && !categoriesTree.value.find(t => t.category.categoryId === entry.categoryId)) continue; let percent = Math.max(Math.min(entry.delta / expected, 1), 0); @@ -113,8 +113,8 @@ const fetchData = async () => { continue; newData.push({ - id: entry.managementId, - tagId: entry.tagId, + id: entry.budgetId, + categoryId: entry.categoryId, currencyId: entry.currencyId, delta: entry.delta, expected: expected, @@ -130,7 +130,7 @@ $transactionsApi.registerUpdateListener(() => { fetchData(); }) -watch([() => props.date, () => props.onlyRoot], () => { +watch([() => props.date, () => props.onlyRoot, $categoriesBudgetApi.getBudgets()], () => { fetchData(); }) diff --git a/components/analytics/tagsByMonths.vue b/components/analytics/categoriesByMonths.vue similarity index 80% rename from components/analytics/tagsByMonths.vue rename to components/analytics/categoriesByMonths.vue index 125de2a..6a12aeb 100644 --- a/components/analytics/tagsByMonths.vue +++ b/components/analytics/categoriesByMonths.vue @@ -24,10 +24,10 @@ import ApexChart from "vue3-apexcharts"; import TransactionsFilterDiv from "~/components/transactions/filter.vue"; import {useColor} from "~/composables/useColor"; -const { $analyticsApi, $transactionsApi, $transactionsTagsApi, $currenciesApi, $serverConfigs } = useNuxtApp(); +const { $analyticsApi, $transactionsApi, $transactionsCategoriesApi, $currenciesApi, $serverConfigs } = useNuxtApp(); const { t, locale } = useI18n(); -const tagsMap = $transactionsTagsApi.getTagsMap(); +const categoriesMap = $transactionsCategoriesApi.getCategoriesMap(); const filter = ref(); const chartSeries = ref([]); @@ -51,7 +51,7 @@ $transactionsApi.registerUpdateListener(fetch) const buildChart = async () => { const xaxis = []; const series = []; - const tags = {}; + const categories = {}; const colors = []; let decimals = Number.MAX_VALUE; @@ -66,26 +66,26 @@ const buildChart = async () => { v.forEach((v) => { const currency = currenciesMap.value.get(v.currencyId) const currencySymbol = currency.symbol; - const tagName = tagsMap.value.get(v.tagId).name; + const categoryName = categoriesMap.value.get(v.categoryId).name; decimals = Math.min(decimals, currency.decimals); - let tagSeries = null; + let categorySeries = null; - if (tags[v.currencyId + "-" + v.tagId] === undefined) { - tagSeries = { - name: tagName + " (" + currencySymbol + ")", + if (categories[v.currencyId + "-" + v.categoriesId] === undefined) { + categorySeries = { + name: categoryName + " (" + currencySymbol + ")", data: [] } - tags[v.currencyId + "-" + v.tagId] = tagSeries; - series.push(tagSeries); - colors.push(useColor(v.tagId)); + categories[v.currencyId + "-" + v.categoryId] = categorySeries; + series.push(categorySeries); + colors.push(useColor(v.categoryId)); } else { - tagSeries = tags[v.currencyId + "-" + v.tagId]; + categorySeries = categories[v.currencyId + "-" + v.categoryiesId]; } - tagSeries.data[i] = v.delta; + categorySeries.data[i] = v.delta; }) i++; }) @@ -105,7 +105,7 @@ const buildChart = async () => { chartOptions.value = { chart: { - id: "tags-by-months-" + type.value, + id: "categories-by-months-" + type.value, type: type.value, stacked: type.value === 'bar', foreColor: undefined, diff --git a/components/analytics/pie.vue b/components/analytics/pie.vue index 68531f3..ff6e9ab 100644 --- a/components/analytics/pie.vue +++ b/components/analytics/pie.vue @@ -12,8 +12,8 @@ @@ -25,7 +25,7 @@ type="pie" :options="chartOptions" :series="chartSeries" - @dataPointSelection="tagSelected" + @dataPointSelection="categorySelected" >

@@ -63,7 +63,7 @@ const props = defineProps({ } }) -const { $transactionsTagsApi, $analyticsApi, $transactionsApi, $currenciesApi } = useNuxtApp(); +const { $transactionsCategoriesApi, $analyticsApi, $transactionsApi, $currenciesApi } = useNuxtApp(); const { t, locale } = useI18n(); const mode = computed(() => props.mode === "month") @@ -76,32 +76,32 @@ const date = ref(defaultDate()); const noData = ref(true); const currenciesMap = $currenciesApi.getCurrenciesMap(); -const tagsTree = $transactionsTagsApi.getTagsTree(); -const tagsMap = $transactionsTagsApi.getTagsTreeMap(); +const categoriesTree = $transactionsCategoriesApi.getCategoriesTree(); +const categoriesMap = $transactionsCategoriesApi.getCategoriesTreeMap(); const view = ref([]); -const tags = $transactionsTagsApi.getTags(); +const categories = $transactionsCategoriesApi.getCategories(); const analytics = ref([]); -const pushToView = (tagId) => { - view.value.push(tagId); +const pushToView = (categoryId) => { + view.value.push(categoryId); } -const cutUnder = (tagId) => { - view.value.length = view.value.findIndex(id => id === tagId) + 1; +const cutUnder = (categoryId) => { + view.value.length = view.value.findIndex(id => id === categoryId) + 1; } const resetView = () => { view.value = []; } -const tagSelected = (event, chartContext, config) => { - const nextTagId = chartMap.value[config.dataPointIndex]; +const categorySelected = (event, chartContext, config) => { + const nextCategoryId = chartMap.value[config.dataPointIndex]; - if (nextTagId === -1 || tagsMap.value.get(nextTagId).childs.length === 0) + if (nextCategoryId === -1 || categoriesMap.value.get(nextCategoryId).childs.length === 0) return; - pushToView(nextTagId); + pushToView(nextCategoryId); setTimeout(() => { buildChart(); }, 0); @@ -159,11 +159,11 @@ const formatter = (value) => { return (props.sign > 0 ? "" : "-") + formatAmount(value); } -const childsSum = (allData, tagObject, sign) => { +const childsSum = (allData, categoryObject, sign) => { let sum = 0; - tagObject.childs.forEach(t => { - const childDelta = allData[t.tag.tagId]; + categoryObject.childs.forEach(t => { + const childDelta = allData[t.category.categoryId]; sum += childDelta && childDelta * sign > 0 ? childDelta : 0; if (t.childs && t.childs.length > 0) { @@ -184,7 +184,7 @@ const buildChart = () => { id: props.id, foreColor: undefined, events: { - dataPointSelection: tagSelected + dataPointSelection: categorySelected }, }, @@ -220,42 +220,42 @@ const buildChart = () => { if (mv.currencyId !== currency.value) return; - allData[mv.tagId] = (allData[mv.tagId] ? allData[mv.tagId] : 0) + mv.delta; + allData[mv.categoryId] = (allData[mv.categoryId] ? allData[mv.categoryId] : 0) + mv.delta; }); }); - let needsToDisplay = (view.value.length > 0 ? tagsMap.value.get(view.value[view.value.length - 1]).childs : tagsTree.value).map((t) => t.tag.tagId); + let needsToDisplay = (view.value.length > 0 ? categoriesMap.value.get(view.value[view.value.length - 1]).childs : categoriesTree.value).map((t) => t.category.categoryId); if (view.value.length > 0) { - const parentTag = view.value[view.value.length - 1]; + const parentCategory = view.value[view.value.length - 1]; - needsToDisplay.push(parentTag); + needsToDisplay.push(parentCategory); } let totalAmount = 0; - needsToDisplay.forEach((tagId) => { - const tag = tagsMap.value.get(tagId); - const tagName = tag.tag.name; - let tagDelta = allData[tagId] ? allData[tagId] : 0; - const isParent = view.value[view.value.length - 1] === tagId; + needsToDisplay.forEach((categoryId) => { + const category = categoriesMap.value.get(categoryId); + const categoryName = category.category.name; + let categoryDelta = allData[categoryId] ? allData[categoryId] : 0; + const isParent = view.value[view.value.length - 1] === categoryId; - if (tagDelta * sign < 0) - tagDelta = 0; + if (categoryDelta * sign < 0) + categoryDelta = 0; - const sum = tagDelta + (isParent ? 0 : childsSum(allData, tag, sign)); + const sum = categoryDelta + (isParent ? 0 : childsSum(allData, category, sign)); if (sum === 0) return; - const tagChartIndex = newSeries.push(Math.abs(sum)) - 1; + const categoryChartIndex = newSeries.push(Math.abs(sum)) - 1; totalAmount += Math.abs(sum); - chartMap.value[tagChartIndex] = isParent ? -1 : tagId; - newOptions.labels.push(tagName); - newOptions.colors.push(useColor(tagId)); + chartMap.value[categoryChartIndex] = isParent ? -1 : categoryId; + newOptions.labels.push(categoryName); + newOptions.colors.push(useColor(categoryId)); }) noData.value = newSeries.length === 0; diff --git a/components/analytics/totalByPeriod.vue b/components/analytics/totalByPeriod.vue index 078a487..d87ad7e 100644 --- a/components/analytics/totalByPeriod.vue +++ b/components/analytics/totalByPeriod.vue @@ -12,7 +12,7 @@ import ApexChart from "vue3-apexcharts"; import {TransactionsFilter} from "~/libs/api/transactions/TransactionsFilter"; -const { $analyticsApi, $transactionsApi, $transactionsTagsApi, $currenciesApi, $serverConfigs } = useNuxtApp(); +const { $analyticsApi, $transactionsApi, $transactionsCategoriesApi, $currenciesApi, $serverConfigs } = useNuxtApp(); const { t, locale } = useI18n(); const props = defineProps({ @@ -32,7 +32,7 @@ const props = defineProps({ } }) -const tagsMap = $transactionsTagsApi.getTagsMap(); +const categoriesMap = $transactionsCategoriesApi.getCategoriesMap(); const currenciesMap = $currenciesApi.getCurrenciesMap(); const currency = computed(() => currenciesMap.value.get(props.currencyId)); diff --git a/components/buttons/quickActions/newTagButton.vue b/components/buttons/quickActions/newCategoryButton.vue similarity index 93% rename from components/buttons/quickActions/newTagButton.vue rename to components/buttons/quickActions/newCategoryButton.vue index e372da4..c272a3e 100644 --- a/components/buttons/quickActions/newTagButton.vue +++ b/components/buttons/quickActions/newCategoryButton.vue @@ -5,7 +5,7 @@ - {{ $t("mainPage.buttons.newTag") }} + {{ $t("mainPage.buttons.newCategory") }}

diff --git a/components/misc/tagsManagementSummary.vue b/components/misc/categoriesBudgetSummary.vue similarity index 69% rename from components/misc/tagsManagementSummary.vue rename to components/misc/categoriesBudgetSummary.vue index d1fce56..0c6bde2 100644 --- a/components/misc/tagsManagementSummary.vue +++ b/components/misc/categoriesBudgetSummary.vue @@ -5,11 +5,11 @@
{{ currenciesMap.get(m.currencyId).code }}
+{{ formatDelta(m.amount, m.currencyId) }}
- {{ capitalizeFirstLetter($t("tagsPage.table.management.dateTypes.perMonth")) }} + {{ capitalizeFirstLetter($t("categoriesPage.table.budget.dateTypes.perMonth")) }}
@@ -18,17 +18,17 @@
{{ currenciesMap.get(m.currencyId).code }}
+{{ formatDelta(m.amount, m.currencyId) }}
- {{ capitalizeFirstLetter($t("tagsPage.table.management.dateTypes.perQuartal")) }} + {{ capitalizeFirstLetter($t("categoriesPage.table.budget.dateTypes.perQuartal")) }}
-

{{ $t("tagsPage.tagsSummary.noData.income") }}

+

{{ $t("categoriesPage.categoriesSummary.noData.income") }}

@@ -36,11 +36,11 @@
{{ currenciesMap.get(m.currencyId).code }}
{{ formatDelta(m.amount, m.currencyId) }}
- {{ capitalizeFirstLetter($t("tagsPage.table.management.dateTypes.perMonth")) }} + {{ capitalizeFirstLetter($t("categoriesPage.table.budget.dateTypes.perMonth")) }}
@@ -49,17 +49,17 @@
{{ currenciesMap.get(m.currencyId).code }}
{{ formatDelta(m.amount, m.currencyId) }}
- {{ capitalizeFirstLetter($t("tagsPage.table.management.dateTypes.perQuartal")) }} + {{ capitalizeFirstLetter($t("categoriesPage.table.budget.dateTypes.perQuartal")) }}
-

{{ $t("tagsPage.tagsSummary.noData.expanse") }}

+

{{ $t("categoriesPage.categoriesSummary.noData.expanse") }}

@@ -67,12 +67,12 @@ + + \ No newline at end of file diff --git a/components/modal/files/view.vue b/components/modal/files/view.vue new file mode 100644 index 0000000..fa306da --- /dev/null +++ b/components/modal/files/view.vue @@ -0,0 +1,154 @@ + + + + + \ No newline at end of file diff --git a/components/modal/recurring/create.vue b/components/modal/recurring/create.vue index f896d67..6b168c4 100644 --- a/components/modal/recurring/create.vue +++ b/components/modal/recurring/create.vue @@ -14,15 +14,15 @@
-
@@ -147,7 +147,7 @@ const props = defineProps({ const emit = defineEmits(['close']); -const {$serverConfigs, $transactionsTagsApi, $recurringTransactionsApi, $currenciesApi, $accountsApi, $toastsManager} = useNuxtApp(); +const {$serverConfigs, $transactionsCategoriesApi, $recurringTransactionsApi, $currenciesApi, $accountsApi, $toastsManager} = useNuxtApp(); const { t, locale } = useI18n(); const configs = $serverConfigs.configs.transactions; @@ -155,8 +155,8 @@ const configs = $serverConfigs.configs.transactions; const accounts = $accountsApi.getAccounts(); const accountsMap = $accountsApi.getAccountsMap(); const currenciesMap = $currenciesApi.getCurrenciesMap(); -const tagsTree = $transactionsTagsApi.getTagsTree(); -const tagsMap = $transactionsTagsApi.getTagsTreeMap(); +const categoriesTree = $transactionsCategoriesApi.getCategoriesTree(); +const categoriesMap = $transactionsCategoriesApi.getCategoriesTreeMap(); const repeatModeText = (func) => { const type = ["days", "weeks", "months"][func]; @@ -167,7 +167,7 @@ const repeatModeText = (func) => { const account = ref(); const amount = ref(); const description = ref(""); -const parentTag = ref(); +const parentCategory = ref(); const sign = ref(1); const signChoice = ref(true); @@ -183,7 +183,7 @@ const highlightErrors = ref(false); const allValid = computed(() => account.value !== undefined && amount.value !== undefined && amount.value !== 0 && - parentTag.value !== undefined && + parentCategory.value !== undefined && nextRepeat.value && repeatFunc.value !== undefined && repeatArg.value !== undefined && @@ -191,23 +191,23 @@ const allValid = computed(() => account.value !== undefined && notificationMode !== undefined ); -watch(parentTag, () => { - if (parentTag.value === undefined) { +watch(parentCategory, () => { + if (parentCategory.value === undefined) { signChoice.value = true; return; } - const parentTagObject = tagsMap.value.get(parentTag.value); + const parentCategoryObject = categoriesMap.value.get(parentCategory.value); - if (parentTagObject === undefined || parentTagObject.tag === undefined) { + if (parentCategoryObject === undefined || parentCategoryObject.category === undefined) { signChoice.value = true; return; } - if (parentTagObject.tag.type !== 0) - sign.value = parentTagObject.tag.type + if (parentCategoryObject.category.type !== 0) + sign.value = parentCategoryObject.category.type - signChoice.value = parentTagObject.tag.type === 0; + signChoice.value = parentCategoryObject.category.type === 0; }); const currency = computed(() => { @@ -245,7 +245,7 @@ const create = () => { highlightErrors.value = false; close(); - $recurringTransactionsApi.newRecurring(parentTag.value, account.value, nextRepeat.value, repeatFunc.value, repeatArg.value, notificationMode.value, amount.value * sign.value, description.value.length > 0 ? description.value : null).then((s) => { + $recurringTransactionsApi.newRecurring(parentCategory.value, account.value, nextRepeat.value, repeatFunc.value, repeatArg.value, notificationMode.value, amount.value * sign.value, description.value.length > 0 ? description.value : null).then((s) => { if (s) { $toastsManager.pushToast(t("modals.newRecurring.messages.success"), 2500, "success"); }else { diff --git a/components/modal/recurring/edit.vue b/components/modal/recurring/edit.vue index 938476d..e59531f 100644 --- a/components/modal/recurring/edit.vue +++ b/components/modal/recurring/edit.vue @@ -14,15 +14,15 @@
-
@@ -151,7 +151,7 @@ const props = defineProps({ const emit = defineEmits(['close']); -const {$serverConfigs, $transactionsTagsApi, $recurringTransactionsApi, $currenciesApi, $accountsApi, $toastsManager} = useNuxtApp(); +const {$serverConfigs, $transactionsCategoriesApi, $recurringTransactionsApi, $currenciesApi, $accountsApi, $toastsManager} = useNuxtApp(); const { t, locale } = useI18n(); const configs = $serverConfigs.configs.transactions; @@ -159,8 +159,8 @@ const configs = $serverConfigs.configs.transactions; const accounts = $accountsApi.getAccounts(); const accountsMap = $accountsApi.getAccountsMap(); const currenciesMap = $currenciesApi.getCurrenciesMap(); -const tagsTree = $transactionsTagsApi.getTagsTree(); -const tagsMap = $transactionsTagsApi.getTagsTreeMap(); +const categoriesTree = $transactionsCategoriesApi.getCategoriesTree(); +const categoriesMap = $transactionsCategoriesApi.getCategoriesTreeMap(); const repeatModeText = (func) => { const type = ["days", "weeks", "months"][func]; @@ -171,7 +171,7 @@ const repeatModeText = (func) => { const account = ref(); const amount = ref(); const description = ref(""); -const parentTag = ref(); +const parentCategory = ref(); const sign = ref(1); const signChoice = ref(true); const nextRepeat = ref(new Date()); @@ -183,7 +183,7 @@ const highlightErrors = ref(false); const allValid = computed(() => account.value !== undefined && amount.value !== undefined && amount.value !== 0 && - parentTag.value !== undefined && + parentCategory.value !== undefined && nextRepeat.value && repeatFunc.value !== undefined && repeatArg.value !== undefined && @@ -197,7 +197,7 @@ watch(() => props.recurring, (newV, oldV) => { account.value = newV.accountId; amount.value = newV.delta; description.value = newV.description || ""; - parentTag.value = newV.tagId; + parentCategory.value = newV.categoryId; nextRepeat.value = new Date(newV.nextRepeat) repeatFunc.value = newV.repeatType; repeatArg.value = newV.repeatArg; @@ -206,23 +206,23 @@ watch(() => props.recurring, (newV, oldV) => { amountChanged(); }); -watch(parentTag, () => { - if (parentTag.value === undefined) { +watch(parentCategory, () => { + if (parentCategory.value === undefined) { signChoice.value = true; return; } - const parentTagObject = tagsMap.value.get(parentTag.value); + const parentCategoryObject = categoriesMap.value.get(parentCategory.value); - if (parentTagObject === undefined || parentTagObject.tag === undefined) { + if (parentCategoryObject === undefined || parentCategoryObject.category === undefined) { signChoice.value = true; return; } - if (parentTagObject.tag.type !== 0) - sign.value = parentTagObject.tag.type + if (parentCategoryObject.category.type !== 0) + sign.value = parentCategoryObject.category.type - signChoice.value = parentTagObject.tag.type === 0; + signChoice.value = parentCategoryObject.category.type === 0; }); const currency = computed(() => { @@ -260,7 +260,7 @@ const apply = () => { highlightErrors.value = false; close(); - $recurringTransactionsApi.editRecurring(props.recurring.recurringTransactionId, parentTag.value, account.value, nextRepeat.value, repeatFunc.value, repeatArg.value, notificationMode.value, amount.value * sign.value, description.value.length > 0 ? description.value : null).then((s) => { + $recurringTransactionsApi.editRecurring(props.recurring.recurringTransactionId, parentCategory.value, account.value, nextRepeat.value, repeatFunc.value, repeatArg.value, notificationMode.value, amount.value * sign.value, description.value.length > 0 ? description.value : null).then((s) => { if (s) { $toastsManager.pushToast(t("modals.editRecurring.messages.success"), 2500, "success"); }else { diff --git a/components/modal/reports/create.vue b/components/modal/reports/create.vue index aff63ca..47c0821 100644 --- a/components/modal/reports/create.vue +++ b/components/modal/reports/create.vue @@ -91,7 +91,7 @@ const create = () => { dateLocale: t('reportsPanel.backendLang.dateLocale'), account: t('reportsPanel.backendLang.account'), delta: t('reportsPanel.backendLang.delta'), - tag: t('reportsPanel.backendLang.tag'), + category: t('reportsPanel.backendLang.category'), currency: t('reportsPanel.backendLang.currency'), created: t('reportsPanel.backendLang.created'), description: t('reportsPanel.backendLang.description'), diff --git a/components/modal/transaction/category/budget.vue b/components/modal/transaction/category/budget.vue new file mode 100644 index 0000000..56e098c --- /dev/null +++ b/components/modal/transaction/category/budget.vue @@ -0,0 +1,184 @@ + + + + + \ No newline at end of file diff --git a/components/modal/transaction/category/create.vue b/components/modal/transaction/category/create.vue new file mode 100644 index 0000000..cea8124 --- /dev/null +++ b/components/modal/transaction/category/create.vue @@ -0,0 +1,114 @@ + + + + + \ No newline at end of file diff --git a/components/modal/transaction/tag/edit.vue b/components/modal/transaction/category/edit.vue similarity index 51% rename from components/modal/transaction/tag/edit.vue rename to components/modal/transaction/category/edit.vue index 8add29b..cb2682d 100644 --- a/components/modal/transaction/tag/edit.vue +++ b/components/modal/transaction/category/edit.vue @@ -1,14 +1,14 @@ - - - - \ No newline at end of file diff --git a/components/modal/transaction/tag/management.vue b/components/modal/transaction/tag/management.vue deleted file mode 100644 index 696ed27..0000000 --- a/components/modal/transaction/tag/management.vue +++ /dev/null @@ -1,184 +0,0 @@ - - - - - \ No newline at end of file diff --git a/components/modal/transaction/transactionAmountField.vue b/components/modal/transaction/transactionAmountField.vue index b404570..024d048 100644 --- a/components/modal/transaction/transactionAmountField.vue +++ b/components/modal/transaction/transactionAmountField.vue @@ -34,7 +34,7 @@ const props = defineProps({ }, - tagId: { + categoryId: { required: true }, @@ -53,7 +53,7 @@ const props = defineProps({ const emit = defineEmits(['update:modelValue']) -const { $transactionsTagsApi, $currenciesApi } = useNuxtApp(); +const { $transactionsCategoriesApi, $currenciesApi } = useNuxtApp(); const { t, locale } = useI18n(); const sign = ref(props.signOverride ? props.signOverride : 1); @@ -61,7 +61,7 @@ const signChoice = ref(!props.signOverride); const signManuallyChoice = ref(false); const currenciesMap = $currenciesApi.getCurrenciesMap(); -const tagsMap = $transactionsTagsApi.getTagsTreeMap(); +const categoriesMap = $transactionsCategoriesApi.getCategoriesTreeMap(); const currency = ref(); const rawAmount = ref(""); @@ -114,25 +114,25 @@ const updateSign = () => { if (props.signOverride) return; - if (props.tagId === undefined) { + if (props.categoryId === undefined) { signChoice.value = true; return; } - const parentTagObject = tagsMap.value.get(props.tagId); + const parentCategoryObject = categoriesMap.value.get(props.categoryId); - if (parentTagObject === undefined || parentTagObject.tag === undefined) { + if (parentCategoryObject === undefined || parentCategoryObject.category === undefined) { signChoice.value = true; return; } - if (parentTagObject.tag.type !== 0) - sign.value = parentTagObject.tag.type + if (parentCategoryObject.category.type !== 0) + sign.value = parentCategoryObject.category.type - signChoice.value = parentTagObject.tag.type === 0; + signChoice.value = parentCategoryObject.category.type === 0; } -watch(() => props.tagId, (value, oldValue, onCleanup) => { +watch(() => props.categoryId, (value, oldValue, onCleanup) => { updateSign(); }); @@ -152,7 +152,7 @@ if (props.modelValue) { amountChanged(); } -if (props.tagId) { +if (props.categoryId) { updateSign(); } diff --git a/components/nav/menu/menu.vue b/components/nav/menu/menu.vue index b87d8c7..81fe558 100644 --- a/components/nav/menu/menu.vue +++ b/components/nav/menu/menu.vue @@ -21,13 +21,13 @@
  • - + - {{$t("navigation.tags")}} + {{$t("navigation.categories")}}
  • diff --git a/components/nav/navigation.vue b/components/nav/navigation.vue index 9571ab1..7ef00b9 100644 --- a/components/nav/navigation.vue +++ b/components/nav/navigation.vue @@ -10,6 +10,8 @@ + +
    @@ -29,8 +31,18 @@
    @@ -41,8 +53,11 @@ import Theme from "~/components/nav/user/theme.vue"; import User from "~/components/nav/user/user.vue"; const { $serverConfigs } = useNuxtApp(); -const configs = $serverConfigs.configs.users; -const demoMode = configs.demoMode; +const configs = $serverConfigs.configs; +const demoMode = configs.users.demoMode; + +const aiModal = ref(false); +const aiAvailable = configs.ai.enabled; diff --git a/components/nav/user/user.vue b/components/nav/user/user.vue index 26c58f4..3968dc5 100644 --- a/components/nav/user/user.vue +++ b/components/nav/user/user.vue @@ -4,77 +4,103 @@
    - + + @@ -82,18 +108,34 @@ import Avatar from "~/components/nav/user/avatar.vue"; import ModalLanguageEdit from "~/components/modal/language/edit.vue"; -const { $userApi, $adminApi, $toastsManager } = useNuxtApp(); +const props = defineProps({ + aiAvailable: { + type: Boolean, + required: true + } +}) + +const { $userApi, $adminApi, $filesApi, $toastsManager } = useNuxtApp(); const { t, locale } = useI18n(); const adminAllowed = $adminApi.getAllowed(); const username = $userApi.getUsername(); -const avatarLetter = username.value.charAt(0).toUpperCase(); +const avatarLetter = username.value?.charAt(0).toUpperCase(); const changePasswordOpened = ref(false); const sessionsOpened = ref(false); const notificationsOpened = ref(false); const languageOpened = ref(false); +const filesOpened = ref(false); + +const emit = defineEmits(['callAi']) + +const storage = $filesApi.getAvailableSpace(); + +const callAi = () => { + emit('callAi'); +} const logout = () => { $userApi.logout(); diff --git a/components/reports/reportsPanel.vue b/components/reports/reportsPanel.vue index 6608a54..2324744 100644 --- a/components/reports/reportsPanel.vue +++ b/components/reports/reportsPanel.vue @@ -11,6 +11,7 @@ {{ $t("reportsPanel.table.created") }} {{ $t("reportsPanel.table.type") }} {{ $t("reportsPanel.table.status") }} + {{ $t("reportsPanel.table.size") }} {{ $t("reportsPanel.table.expires") }} {{ $t("reportsPanel.table.description") }} {{ $t("reportsPanel.table.action")}} @@ -21,16 +22,17 @@ - {{ new Date(report.created).toLocaleString(locale, {year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit'}) }} + {{ getFileDate(report.fileId, false) }} {{ $t("reportsPanel.types." + types[report.type]) }} {{ $t("reportsPanel.status." + statuses[report.status + 1]) }} - {{ new Date(report.expires).toLocaleString(locale, {year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit'}) }} + {{ getFileSize(report.fileId) }} + {{ getFileDate(report.fileId, true) }} {{ report.description }} - + - + @@ -50,11 +52,15 @@ import EditButton from "~/components/buttons/editButton.vue"; import DeleteButton from "~/components/buttons/deleteButton.vue"; import DownloadButton from "~/components/buttons/downloadButton.vue"; import ShareButton from "~/components/buttons/shareButton.vue"; +import prettyBytes from "pretty-bytes"; const { t, locale, getLocaleMessage } = useI18n(); const i18n = useI18n(); -const { $serverConfigs, $toastsManager, $reportsApi } = useNuxtApp(); +const { $serverConfigs, $toastsManager, $filesApi, $reportsApi } = useNuxtApp(); + +const files = $filesApi.getFiles(); +const filesMap = $filesApi.getFilesMap(); const configs = $serverConfigs.configs.reports; const reportsList = ref(); @@ -73,18 +79,25 @@ const statuses = [ "available" ] -let reportsTimer; +const getFileSize = (fileId) => { + const file = filesMap.value.get(fileId); + if (!file) + return t("reportsPanel.status.inProgress"); -const fetchReports = async () => { - reportsList.value = await $reportsApi.getReports(); + return prettyBytes(file.size, {locale: locale.value}); +} + +const getFileDate = (fileId, expiresNeed) => { + const file = filesMap.value.get(fileId); - const userWait = reportsList.value.find((e) => e.status === 0) !== undefined; + if (!file) + return t("reportsPanel.status.inProgress"); - if (userWait && !reportsTimer) { - reportsTimer = setTimeout(fetchReports, 1000); - }else if (!userWait) { - reportsTimer = null; - } + return new Date(expiresNeed ? file.expiresAt : file.createdAt).toLocaleString(locale, {year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit'}); +} + +const fetchReports = async () => { + reportsList.value = await $reportsApi.getReports(); } const copyToClipboard = (text) => { @@ -103,6 +116,10 @@ const download = (url) => { link.click() } +$reportsApi.registerUpdateListener(() => { + fetchReports(); +}); + fetchReports() diff --git a/components/select/account.vue b/components/select/account.vue index d27f6d6..eb863c1 100644 --- a/components/select/account.vue +++ b/components/select/account.vue @@ -30,22 +30,22 @@ const props = defineProps({ const emit = defineEmits(['update:modelValue', 'selected']) const { t } = useI18n(); -const {$accountsApi, $accountsTagsApi} = useNuxtApp(); +const {$accountsApi, $accountsFoldersApi} = useNuxtApp(); -const tags = $accountsTagsApi.getTags(); +const folders = $accountsFoldersApi.getFolders(); const accounts = $accountsApi.getAccounts(); const value = ref(props.modelValue || null); -const getTagAccounts = (t) => { - return useArrayFilter(accounts, a => a.tagId === t.tagId && (!props.currencyFilter || a.currencyId === props.currencyFilter)).value +const getFolderAccounts = (t) => { + return useArrayFilter(accounts, a => a.folderId === t.folderId && (!props.currencyFilter || a.currencyId === props.currencyFilter)).value } const options = computed(() => { const resultArray = []; - tags.value.forEach((t) => { - const options = getTagAccounts(t).map((a) => { + folders.value.forEach((t) => { + const options = getFolderAccounts(t).map((a) => { if (props.excludeAccount && props.excludeAccount === a.accountId) return null; diff --git a/components/select/currency.vue b/components/select/currency.vue index 19aad6a..5bc410c 100644 --- a/components/select/currency.vue +++ b/components/select/currency.vue @@ -7,9 +7,9 @@ :no-results-text="$t('selects.noResults')" > - @@ -25,7 +25,7 @@ import Multiselect from '@vueform/multiselect' const props = defineProps({ - tagsTree: { + categoriesTree: { required: true }, @@ -34,7 +34,7 @@ const props = defineProps({ required: true }, - excludeTagId: { + excludeCategoryId: { required: false }, @@ -58,7 +58,7 @@ const rootParent = { value: -1, disabled: false, deep: 0, - label: t('selects.transactionTagSelect.withoutParent') + label: t('selects.transactionCategorySelect.withoutParent') }; const parentSelectRender = ref([]); @@ -83,7 +83,7 @@ const search = (e, ms) => { disabled: false, deep: 0, label: lastSearch.text, - newTag: true + newCategory: true } ms.select(result); @@ -99,10 +99,10 @@ const selectRender = (deep, elements, resultArray) => { elements.forEach((e) => { resultArray.push( { - value: e.tag.tagId, - disabled: e.tag.tagId == props.excludeTagId, + value: e.category.categoryId, + disabled: e.category.categoryId == props.excludeCategoryId, deep: deep, - label: e.tag.name + label: e.category.name } ) @@ -123,16 +123,16 @@ watch(() => props.modelValue, () => { value.value = props.modelValue; }); -watch(() => [props.tagsTree, props.canBeWithoutParent, props.excludeTagId], (l, n) => { +watch(() => [props.categoriesTree, props.canBeWithoutParent, props.excludeCategoryId], (l, n) => { parentSelectRender.value = []; if (props.canBeWithoutParent) parentSelectRender.value.push(rootParent); - selectRender(0, props.tagsTree, parentSelectRender.value) + selectRender(0, props.categoriesTree, parentSelectRender.value) }, {deep: true}) -selectRender(0, props.tagsTree, parentSelectRender.value) +selectRender(0, props.categoriesTree, parentSelectRender.value) diff --git a/components/transactions/tag.vue b/components/transactions/category.vue similarity index 80% rename from components/transactions/tag.vue rename to components/transactions/category.vue index 70bca02..f08dd88 100644 --- a/components/transactions/tag.vue +++ b/components/transactions/category.vue @@ -4,15 +4,15 @@

    - {{treeEntry.tag.name}} + {{treeEntry.category.name}}

    - {{treeEntry.tag.tagId}} + {{treeEntry.category.categoryId}}

    - {{treeEntry.tag}} + {{treeEntry.category}}
    diff --git a/components/transactions/filter.vue b/components/transactions/filter.vue index 4449c63..2b21bd1 100644 --- a/components/transactions/filter.vue +++ b/components/transactions/filter.vue @@ -1,16 +1,16 @@