tags and new analytics new in budget
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"chartjs-plugin-datalabels": "^2.2.0",
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
|
"circle-progress.vue": "^3.3.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
@@ -2195,6 +2196,12 @@
|
|||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/circle-progress.vue": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/circle-progress.vue/-/circle-progress.vue-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-Ynj/ZaB6LvbkWiT42OURlfE3gZN0fbCOgag1DMPJ7Dj87qUoDZocCPF0Os1xhXwdo6dqU9BchYVkt6QOXl19NQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/clean-css": {
|
"node_modules/clean-css": {
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
||||||
@@ -11333,6 +11340,11 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
"resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
||||||
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="
|
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="
|
||||||
},
|
},
|
||||||
|
"circle-progress.vue": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/circle-progress.vue/-/circle-progress.vue-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-Ynj/ZaB6LvbkWiT42OURlfE3gZN0fbCOgag1DMPJ7Dj87qUoDZocCPF0Os1xhXwdo6dqU9BchYVkt6QOXl19NQ=="
|
||||||
|
},
|
||||||
"clean-css": {
|
"clean-css": {
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"chartjs-plugin-datalabels": "^2.2.0",
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
|
"circle-progress.vue": "^3.3.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
|
|||||||
@@ -137,9 +137,12 @@ const chartOptions = {
|
|||||||
// Преобразование данных для таблицы
|
// Преобразование данных для таблицы
|
||||||
const prepareTableData = (categories) => {
|
const prepareTableData = (categories) => {
|
||||||
// 1. Собираем все уникальные значения date из monthlySums
|
// 1. Собираем все уникальные значения date из monthlySums
|
||||||
|
|
||||||
|
const onlyExpenses = categories.filter((it) => it.categoryType == "EXPENSE")
|
||||||
|
|
||||||
const allDates = [
|
const allDates = [
|
||||||
...new Set(
|
...new Set(
|
||||||
categories.flatMap((category) =>
|
onlyExpenses.flatMap((category) =>
|
||||||
category.monthlySums.map((sumItem) => sumItem.date)
|
category.monthlySums.map((sumItem) => sumItem.date)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -164,7 +167,7 @@ const prepareTableData = (categories) => {
|
|||||||
// console.log(tableColumns.value[0].field);
|
// console.log(tableColumns.value[0].field);
|
||||||
const sums = {}
|
const sums = {}
|
||||||
// 4. Формируем строки (для каждой категории)
|
// 4. Формируем строки (для каждой категории)
|
||||||
const rows = categories.map((category) => {
|
const rows = onlyExpenses.map((category) => {
|
||||||
// Начинаем со строки, где есть поле с именем категории
|
// Начинаем со строки, где есть поле с именем категории
|
||||||
const row = {category: category.categoryIcon + " " + category.categoryName};
|
const row = {category: category.categoryIcon + " " + category.categoryName};
|
||||||
let categorySum = 0
|
let categorySum = 0
|
||||||
@@ -218,6 +221,7 @@ const selectedSpace = computed(() => spaceStore.space)
|
|||||||
|
|
||||||
watch( selectedSpace, async (newValue, oldValue) => {
|
watch( selectedSpace, async (newValue, oldValue) => {
|
||||||
if (newValue != oldValue) {
|
if (newValue != oldValue) {
|
||||||
|
await fetchCategoriesCatalog()
|
||||||
await fetchCategoriesSums()
|
await fetchCategoriesSums()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -249,26 +253,30 @@ const fetchCategoriesCatalog = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([ fetchCategoriesCatalog()])
|
|
||||||
if (selectedSpace.value) {
|
if (selectedSpace.value) {
|
||||||
|
await fetchCategoriesCatalog()
|
||||||
await fetchCategoriesSums();
|
await fetchCategoriesSums();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<LoadingView v-if="loading"/>
|
<LoadingView v-if="loading"/>
|
||||||
|
|
||||||
<div v-else class="p-4 pb-20 lg:pb-4 bg-gray-100 flex flex-col gap-4 h-full items-center justify-items-center ">
|
<div v-else class="p-4 pb-20 lg:pb-4 bg-gray-100 flex flex-col gap-4 h-full items-center justify-items-center ">
|
||||||
<div v-if="!selectedSpace" class="flex w-full h-full items-center justify-center">
|
<div v-if="!selectedSpace" class="flex w-full h-full items-center justify-center">
|
||||||
<p>Сперва нужно выбрать Пространство.
|
<p>Сперва нужно выбрать Пространство.
|
||||||
<button class="text-blue-500 hover:underline" @click="router.push('/spaces').then((res) => router.go(0))">Перейти</button>
|
<button class="text-blue-500 hover:underline" @click="router.push('/spaces').then((res) => router.go(0))">Перейти</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="dataTableCategories.length==0">
|
|
||||||
|
<div v-else-if="dataTableCategories.length <= 1 && dataTableCategories.keys().next()">
|
||||||
Начните записывать траты и здесь появится информация.
|
Начните записывать траты и здесь появится информация.
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="!items-center w-5/6 bg-white">
|
<div v-else class="!flex !flex-col !items-center w-5/6 !gap-4 ">
|
||||||
<Accordion value="1" class=" " @tab-open="isChartOpen=true"
|
<Accordion value="1" class="!w-full " @tab-open="isChartOpen=true"
|
||||||
@tab-close="closeChart">
|
@tab-close="closeChart">
|
||||||
<AccordionPanel value="0">
|
<AccordionPanel value="0">
|
||||||
<AccordionHeader>График</AccordionHeader>
|
<AccordionHeader>График</AccordionHeader>
|
||||||
@@ -291,7 +299,7 @@ onMounted(async () => {
|
|||||||
<span v-html="catType"/>
|
<span v-html="catType"/>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="cat in category" class="px-2 py-2 hover:bg-blue-50 rounded-md line-clamp- w-full"
|
<li v-for="cat in category" class="px-2 py-2 hover:bg-blue-50 rounded-md line-clamp- w-full"
|
||||||
:class="selectedCategory.id == cat.id? '!bg-emerald-50 text-emerald-700': '' " @click="selectedCategory=cat"> {{cat.name}}</li>
|
:class="selectedCategory.id == cat.id? '!bg-emerald-50 text-emerald-700': '' " @click="selectedCategory=cat"> {{cat.icon}} {{cat.name}}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -314,9 +322,8 @@ onMounted(async () => {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
<div class="items-center !w-full bg-white">
|
||||||
|
<DataTable :value="dataTableCategories" responsiveLayout="scroll" filter stripedRows class="">
|
||||||
<DataTable :value="dataTableCategories" responsiveLayout="scroll" filter stripedRows class="w-5/6 items-center">
|
|
||||||
<Column
|
<Column
|
||||||
:field="tableColumns[0].field"
|
:field="tableColumns[0].field"
|
||||||
:header="tableColumns[0].header"
|
:header="tableColumns[0].header"
|
||||||
@@ -337,6 +344,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -59,28 +59,26 @@ const errors = ref({ username: '', password: '' })
|
|||||||
|
|
||||||
// Получение tg_id (Telegram ID)
|
// Получение tg_id (Telegram ID)
|
||||||
const tg_id = computed(() => {
|
const tg_id = computed(() => {
|
||||||
|
|
||||||
if (window.Telegram?.WebApp) {
|
if (window.Telegram?.WebApp) {
|
||||||
const tg = window.Telegram.WebApp
|
const tg = window.Telegram.WebApp
|
||||||
tg.expand() // Разворачиваем WebApp
|
tg.expand() // Разворачиваем WebApp
|
||||||
return tg.initDataUnsafe.user?.id ?? null
|
return tg.initDataUnsafe.user?.id ?? null
|
||||||
}
|
}
|
||||||
return null
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// Авто-вход по Telegram ID
|
// Авто-вход по Telegram ID
|
||||||
const autoLoginWithTgId = async () => {
|
const autoLoginWithTgId = async () => {
|
||||||
if (tg_id.value) {
|
if (tg_id.value) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post('/auth/token/tg', { tg_id: tg_id.value })
|
await userStore.login(null, null, tg_id.value)
|
||||||
const token = response.data.access_token
|
|
||||||
localStorage.setItem('token', token)
|
|
||||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
|
||||||
|
|
||||||
toast.add({ severity: 'success', summary: 'Вход выполнен', detail: 'Добро пожаловать!', life: 3000 })
|
// toast.add({ severity: 'success', summary: 'Вход выполнен', detail: 'Добро пожаловать!', life: 3000 })
|
||||||
await router.replace(route.query['back'] ? route.query['back'].toString() : '/')
|
await router.replace(route.query['back'] ? route.query['back'].toString() : '/')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
toast.add({ severity: 'error', summary: 'Ошибка входа', detail: 'Ошибка Telegram авторизации', life: 3000 })
|
// toast.add({ severity: 'error', summary: 'Ошибка входа', detail: 'Ошибка Telegram авторизации', life: 3000 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import Button from "primevue/button";
|
|||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
import FloatLabel from "primevue/floatlabel";
|
import FloatLabel from "primevue/floatlabel";
|
||||||
import DatePicker from "primevue/datepicker";
|
import DatePicker from "primevue/datepicker";
|
||||||
import {onMounted, ref} from "vue";
|
import {computed, onMounted, ref, watch} from "vue";
|
||||||
import {getMonthName} from "@/utils/utils";
|
import {getMonthName} from "@/utils/utils";
|
||||||
import {Budget} from "@/models/Budget";
|
import {Budget} from "@/models/Budget";
|
||||||
import {getCategories} from "@/services/categoryService";
|
import {getCategories} from "@/services/categoryService";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
opened: {
|
opened: {
|
||||||
@@ -63,9 +64,31 @@ const resetForm = () => {
|
|||||||
}
|
}
|
||||||
budget.value.name = getMonthName(budget.value.dateFrom.getMonth()) + ' ' + budget.value.dateFrom.getFullYear();
|
budget.value.name = getMonthName(budget.value.dateFrom.getMonth()) + ' ' + budget.value.dateFrom.getFullYear();
|
||||||
}
|
}
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedSpace.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
// loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
await fetchCategories()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching budget infos:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
resetForm()
|
resetForm()
|
||||||
|
if (selectedSpace.value) {
|
||||||
|
fetchCategories()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -91,6 +114,7 @@ onMounted(() => {
|
|||||||
<Checkbox v-model="createRecurrentPayments" binary/>
|
<Checkbox v-model="createRecurrentPayments" binary/>
|
||||||
Создать ежемесячные платежи?
|
Создать ежемесячные платежи?
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="categories.length ==0" class="text-red-500 font-bold">Сперва лучше создать категории</div>
|
||||||
<div class="flex flex-row gap-2 justify-end items-center">
|
<div class="flex flex-row gap-2 justify-end items-center">
|
||||||
<Button label="Создать" severity="success" icon="pi pi-save" @click="create"/>
|
<Button label="Создать" severity="success" icon="pi pi-save" @click="create"/>
|
||||||
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
|
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
|
||||||
|
|||||||
@@ -39,10 +39,10 @@
|
|||||||
|
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<div class="text-2xl">{{ warn.message.icon }}</div>
|
<div class="text-2xl">{{ warn.message.icon }}</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="font-bold">{{ warn.message.title }}</span>
|
<span class="font-bold">{{ warn.message.title }}</span>
|
||||||
<span v-html="warn.message.body"></span>
|
<span v-html="warn.message.body"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button @click="hideWarn(warn.id)"><i class="pi pi-times" style="font-size: 0.7rem"></i></button>
|
<button @click="hideWarn(warn.id)"><i class="pi pi-times" style="font-size: 0.7rem"></i></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,7 +50,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex items-center justify-center p-16">
|
<div v-else class="flex items-center justify-center p-16">
|
||||||
<button @click="fetchWarns(true)">Показать скрытые</button></div>
|
<button @click="fetchWarns(true)">Показать скрытые</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -352,7 +353,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted, ref} from 'vue';
|
import {computed, onMounted, ref, watch} from 'vue';
|
||||||
import Chart from 'primevue/chart';
|
import Chart from 'primevue/chart';
|
||||||
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
|
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
|
||||||
import {
|
import {
|
||||||
@@ -361,7 +362,7 @@ import {
|
|||||||
getBudgetTransactions,
|
getBudgetTransactions,
|
||||||
updateBudgetCategoryRequest,
|
updateBudgetCategoryRequest,
|
||||||
getWarns,
|
getWarns,
|
||||||
hideWarnRequest
|
hideWarnRequest
|
||||||
} from "@/services/budgetsService";
|
} from "@/services/budgetsService";
|
||||||
import {Budget, BudgetCategory, Warn} from "@/models/Budget";
|
import {Budget, BudgetCategory, Warn} from "@/models/Budget";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
@@ -378,6 +379,7 @@ import {Chart as ChartJS} from 'chart.js/auto';
|
|||||||
import SelectButton from "primevue/selectbutton";
|
import SelectButton from "primevue/selectbutton";
|
||||||
import Divider from "primevue/divider";
|
import Divider from "primevue/divider";
|
||||||
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
|
||||||
// Зарегистрируем плагин
|
// Зарегистрируем плагин
|
||||||
ChartJS.register(ChartDataLabels);
|
ChartJS.register(ChartDataLabels);
|
||||||
@@ -563,9 +565,9 @@ const fetchBudgetTransactions = async () => {
|
|||||||
|
|
||||||
const updateTransactions = async () => {
|
const updateTransactions = async () => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await Promise.all([fetchBudgetInfo(),fetchWarns()])
|
await Promise.all([fetchBudgetInfo(), fetchWarns()])
|
||||||
|
|
||||||
}, )
|
},)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,14 +609,14 @@ const transactionCategoriesSums = computed(() => {
|
|||||||
|
|
||||||
const budgetInfo = ref<Budget>();
|
const budgetInfo = ref<Budget>();
|
||||||
const fetchBudgetInfo = async () => {
|
const fetchBudgetInfo = async () => {
|
||||||
await getBudgetInfo(route.params.id).then((data) => {
|
await getBudgetInfo(route.params.id).then((data) => {
|
||||||
budget.value = data
|
budget.value = data
|
||||||
plannedExpenses.value = budget.value?.plannedExpenses
|
plannedExpenses.value = budget.value?.plannedExpenses
|
||||||
plannedIncomes.value = budget.value?.plannedIncomes
|
plannedIncomes.value = budget.value?.plannedIncomes
|
||||||
transactions.value = budget.value?.transactions
|
transactions.value = budget.value?.transactions
|
||||||
categories.value = budget.value?.categories
|
categories.value = budget.value?.categories
|
||||||
updateLoading.value = false
|
updateLoading.value = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -623,7 +625,7 @@ const fetchBudgetInfo = async () => {
|
|||||||
const updateBudgetCategory = async (category) => {
|
const updateBudgetCategory = async (category) => {
|
||||||
|
|
||||||
// loading.value = true
|
// loading.value = true
|
||||||
await updateBudgetCategoryRequest(budget.value.id, category)
|
await updateBudgetCategoryRequest(budget.value.id, category)
|
||||||
await fetchBudgetInfo()
|
await fetchBudgetInfo()
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await fetchWarns()
|
await fetchWarns()
|
||||||
@@ -844,19 +846,42 @@ const fetchWarns = async (hidden: Boolean = null) => {
|
|||||||
warns.value = await getWarns(route.params.id, hidden)
|
warns.value = await getWarns(route.params.id, hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedSpace.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
fetchBudgetInfo()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching budget infos:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
if (selectedSpace.value) {
|
||||||
fetchBudgetInfo(),
|
fetchBudgetInfo()
|
||||||
fetchWarns()
|
fetchWarns()
|
||||||
// budget.value = await getBudgetInfo(route.params.id),
|
|
||||||
// fetchPlannedIncomes(),
|
await Promise.all([
|
||||||
// fetchPlannedExpenses(),
|
fetchBudgetInfo(),
|
||||||
// fetchBudgetCategories(),
|
fetchWarns()
|
||||||
// fetchBudgetTransactions(),
|
// budget.value = await getBudgetInfo(route.params.id),
|
||||||
]);
|
// fetchPlannedIncomes(),
|
||||||
|
// fetchPlannedExpenses(),
|
||||||
|
// fetchBudgetCategories(),
|
||||||
|
// fetchBudgetTransactions(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during fetching data:', error);
|
console.error('Error during fetching data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {useDrawerStore} from "@/stores/drawerStore";
|
|||||||
import {EventBus} from '@/utils/EventBus.ts';
|
import {EventBus} from '@/utils/EventBus.ts';
|
||||||
import {useToast} from "primevue/usetoast";
|
import {useToast} from "primevue/usetoast";
|
||||||
import {useSpaceStore} from "@/stores/spaceStore";
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {CircleProgressBar} from 'circle-progress.vue';
|
||||||
|
|
||||||
// Зарегистрируем плагин
|
// Зарегистрируем плагин
|
||||||
ChartJS.register(ChartDataLabels);
|
ChartJS.register(ChartDataLabels);
|
||||||
@@ -108,9 +109,17 @@ const savingRatio = computed(() => {
|
|||||||
return totalExpenses.value == 0 ? 0 : totalSaving.value / totalExpenses.value * 100
|
return totalExpenses.value == 0 ? 0 : totalSaving.value / totalExpenses.value * 100
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const totalSaving = computed(() => {
|
const totalSaving = computed(() => {
|
||||||
let value = 0
|
let value = 0
|
||||||
categories.value.filter((cat) => cat.category.id == "677bc767c7857460a491bd4f").forEach(cat => {
|
|
||||||
|
categories.value.filter((cat) =>
|
||||||
|
cat.category.tags.some(tag => {
|
||||||
|
|
||||||
|
return tag.code == "savings";
|
||||||
|
})
|
||||||
|
).forEach(cat => {
|
||||||
|
// console.log(cat)
|
||||||
value += cat.currentLimit
|
value += cat.currentLimit
|
||||||
})
|
})
|
||||||
return value
|
return value
|
||||||
@@ -763,14 +772,14 @@ onUnmounted(async () => {
|
|||||||
<div class=" flex flex-col gap-3">
|
<div class=" flex flex-col gap-3">
|
||||||
<div class="flex flex-row justify-between ">
|
<div class="flex flex-row justify-between ">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h2 class="text-4xl font-bold">Бюджет {{ budget.name }} </h2>
|
<h2 class="text-4xl font-bold text-gray-700">Бюджет {{ budget.name }} </h2>
|
||||||
<div class="flex flex-row gap-2 text-xl">{{ formatDate(budget.dateFrom) }} -
|
<div class="flex flex-row gap-2 text-xl text-gray-700">{{ formatDate(budget.dateFrom) }} -
|
||||||
{{ formatDate(budget.dateTo) }}
|
{{ formatDate(budget.dateTo) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 justify-center">
|
<div class="flex flex-col gap-2 justify-center">
|
||||||
<button class="flex flex-row bg-white py-6 px-4 shadow-lg rounded-lg items-center h-6 justify-center gap-2"
|
<button class="flex flex-row bg-white py-6 px-4 shadow-md rounded-lg items-center h-6 justify-center gap-2"
|
||||||
@click="warnsOpened = !warnsOpened">
|
@click="warnsOpened = !warnsOpened">
|
||||||
<span class="bg-gray-300 p-1 rounded font-bold">{{
|
<span class="bg-gray-300 p-1 rounded font-bold">{{
|
||||||
warns ? warns.length : 0
|
warns ? warns.length : 0
|
||||||
@@ -778,7 +787,7 @@ onUnmounted(async () => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="warnsOpened"
|
<div v-if="warnsOpened"
|
||||||
class="absolute h-fit max-h-128 w-128 overflow-auto bg-white shadow-lg rounded-lg top-32 right-4 z-50">
|
class="absolute h-fit max-h-128 max-w-128 overflow-auto bg-white shadow-md rounded-lg top-32 right-4 z-50">
|
||||||
<div v-if="checkWarnsExists" class="flex flex-col p-4">
|
<div v-if="checkWarnsExists" class="flex flex-col p-4">
|
||||||
<div v-for="warn in warns">
|
<div v-for="warn in warns">
|
||||||
<div class="flex flex-row items-center gap-2 justify-between">
|
<div class="flex flex-row items-center gap-2 justify-between">
|
||||||
@@ -807,143 +816,214 @@ onUnmounted(async () => {
|
|||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<!-- Аналитика и плановые доходы/расходы -->
|
<!-- Аналитика и плановые доходы/расходы -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-start ">
|
<div class="grid grid-cols-1 lg:grid-cols-3 lg:gap-4 items-start ">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<!-- Блок Аналитики (25%) -->
|
<!-- Блок Аналитики (25%) -->
|
||||||
<div
|
<h3 class="text-2xl font-bold text-gray-700">Аналитика</h3>
|
||||||
class="bg-white p-4 shadow-lg rounded-lg flex flex-col gap-4 items-start ">
|
<div class=" flex flex-col gap-3 items-start ">
|
||||||
<h3 class="text-xl font-bold">Аналитика</h3>
|
<div class=" bg-white flex p-4 flex-col shadow-md rounded-lg w-full h-full">
|
||||||
|
<SelectButton v-model="selectedChart" :options="modes" optionLabel="label" optionIcon="icon">
|
||||||
|
<template #option="slotProps">
|
||||||
|
<i :class="slotProps.option.icon"></i>
|
||||||
|
</template>
|
||||||
|
</SelectButton>
|
||||||
|
<Chart v-if="selectedChart.value=='bar'" type="bar" :data="incomeExpenseChartData"
|
||||||
|
:options="incomeExpenseChartOptions" class="!w-full"
|
||||||
|
style="width: 100%"/>
|
||||||
|
|
||||||
<SelectButton v-model="selectedChart" :options="modes" optionLabel="label" optionIcon="icon">
|
<Chart v-if="selectedChart.value=='pie'" type="pie" :data="pieChartData" :options="pieChartOptions"
|
||||||
<template #option="slotProps">
|
class="chart "/>
|
||||||
<i :class="slotProps.option.icon"></i>
|
</div>
|
||||||
</template>
|
<div></div>
|
||||||
</SelectButton>
|
<div class=" flex flex-col gap-2 relative border-[0.1rem] rounded-xl p-2 w-full h-full pt-4">
|
||||||
<Chart v-if="selectedChart.value=='bar'" type="bar" :data="incomeExpenseChartData"
|
<p class="absolute -top-3 left-1/2 -translate-x-1/2 bg-gray-100 px-2 text-gray-700 font-semibold">
|
||||||
:options="incomeExpenseChartOptions" class="!w-full"
|
Запланировано</p>
|
||||||
style="width: 100%"/>
|
<div class="flex gap-5 items-center justify-items-center w-full ">
|
||||||
|
<button class="flex flex-row gap-1 items-center w-full" @click="detailedShowed = !detailedShowed">
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full ">
|
||||||
|
<div class="group justify-end flex w-full">
|
||||||
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
|
v-tooltip="'Рассчитывается по принципу сумма плановых поступлений'"/>
|
||||||
|
<!-- <div class="absolute w-fit bg-gray-800 text-white text-xs px-2 py-1 rounded hidden group-hover:block transition-opacity cursor-pointer">-->
|
||||||
|
<!-- Рассчитывается по принципу сумма плановых поступлений-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</div>
|
||||||
|
<h4 class="text-lg font-bold">Приходы</h4>
|
||||||
|
|
||||||
<Chart v-if="selectedChart.value=='pie'" type="pie" :data="pieChartData" :options="pieChartOptions"
|
<div class="font-light text-center ">
|
||||||
class="chart "/>
|
{{ formatAmount(totalIncomes) }}
|
||||||
|
|
||||||
<div class="flex gap-5 items-center justify-items-center w-full ">
|
|
||||||
<div class="w-full">
|
|
||||||
<button class="grid grid-cols-2 gap-5 items-center w-full" @click="detailedShowed = !detailedShowed">
|
|
||||||
<div class="flex flex-col items-center font-bold ">
|
|
||||||
<h4 class="text-lg font-bold">Поступления</h4>
|
|
||||||
|
|
||||||
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
|
||||||
+{{ formatAmount(totalIncomes) }}
|
|
||||||
₽
|
₽
|
||||||
</div>
|
</div>
|
||||||
<!-- <p>Total Incomes</p>-->
|
<!-- <p>Total Incomes</p>-->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center ">
|
-
|
||||||
<h4 class="text-lg font-bold ">Расходы</h4>
|
<div class="flex flex-col items-center bg-white p-2 shadow-md rounded-lg w-full h-full ">
|
||||||
|
<div class="group justify-end flex w-full">
|
||||||
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
|
v-tooltip="'Рассчитывается по принципу сумма плановых трат + разница между суммой плана категории и лимитом категории'"/>
|
||||||
|
|
||||||
<div class="font-bold bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center"
|
</div>
|
||||||
:class="totalExpenses > totalIncomes ? ' text-red-700' : ''">
|
<div class="flex flex-col items-center">
|
||||||
-{{ formatAmount(totalExpenses) }} ({{ formatAmount(totalExpenses - totalIncomes) }})
|
<h4 class="text-lg font-bold ">Расходы</h4>
|
||||||
|
|
||||||
|
<div class="font-light text-center ">
|
||||||
|
{{ formatAmount(totalExpenses) }}
|
||||||
|
₽
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
=
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full ">
|
||||||
|
<div class="group justify-end flex w-full">
|
||||||
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
|
v-tooltip="'Разница между плановыми приходами и расходами'"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<h4 class="text-lg font-bold">Остаток</h4>
|
||||||
|
|
||||||
|
<div class="font-light text-center w-full "
|
||||||
|
:class="totalIncomes - totalExpenses < 0 ? ' text-red-700 !font-bold' : ''">
|
||||||
|
{{ formatAmount(totalIncomes - totalExpenses) }}
|
||||||
₽
|
₽
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- <div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">-->
|
||||||
|
<!-- <div class="flex flex-col items-center font-bold ">-->
|
||||||
|
<!-- <p class="font-light ">в первый период</p>-->
|
||||||
|
<!-- <div class="font-light text-center w-full ">-->
|
||||||
|
<!-- +{{ formatAmount(incomesByPeriod[0]) }} ₽-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="flex flex-col items-center">-->
|
||||||
|
<!-- <p class="font-light ">в первый период</p>-->
|
||||||
|
<!-- <div class="font-light text-center w-full ">-->
|
||||||
|
<!-- -{{ formatAmount(expensesByPeriod[0]) }} ₽-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="flex flex-col items-center">-->
|
||||||
|
<!-- <p class="font-light ">во второй период</p>-->
|
||||||
|
<!-- <div class="font-light text-center w-full ">-->
|
||||||
|
<!-- +{{ formatAmount(incomesByPeriod[1]) }} ₽-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
<!-- </div>-->
|
||||||
<div class="flex flex-col items-center font-bold ">
|
<!-- <div class="flex flex-col items-center">-->
|
||||||
<p class="font-light ">в первый период</p>
|
<!-- <p class="font-light ">во второй период</p>-->
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
|
<!-- <div class="font-light text-center w-full ">-->
|
||||||
+{{ formatAmount(incomesByPeriod[0]) }} ₽
|
<!-- -{{ formatAmount(expensesByPeriod[1]) }} ₽-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<p class="font-light ">в первый период</p>
|
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
|
|
||||||
-{{ formatAmount(expensesByPeriod[0]) }} ₽
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<p class="font-light ">во второй период</p>
|
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
|
|
||||||
+{{ formatAmount(incomesByPeriod[1]) }} ₽
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<p class="font-light ">во второй период</p>
|
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
|
</div>
|
||||||
-{{ formatAmount(expensesByPeriod[1]) }} ₽
|
<div class="grid gap-5 items-center justify-items-center w-full">
|
||||||
|
<div class="w-full ">
|
||||||
|
<button class="flex flex-row justify-between gap-2 items-end w-full"
|
||||||
|
@click="detailedShowed = !detailedShowed">
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
|
<div class="group justify-end flex w-full">
|
||||||
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
|
v-tooltip="'Сумма категории Кредиты и долги в сумме лимитов категорий'"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<h4 class="text-sm lg:text-base">Долги</h4>
|
||||||
|
<div class="font-light text-center w-full ">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<CircleProgressBar :value="loansRatio.toFixed(0)" :max="100" rounded percentage strokeWidth="12"
|
||||||
|
size="64"/>
|
||||||
|
<!-- <p>Total Incomes</p>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
|
<div class="group justify-end flex w-full">
|
||||||
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
|
v-tooltip="'Для корректного подсчета нужно выставить тэг savings на категориях сбережений'"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<span class="text-sm lg:text-base font-bold">Сбережения</span>
|
||||||
|
<div class="font-light text-center w-full text-red-400 "
|
||||||
|
>
|
||||||
|
|
||||||
|
<CircleProgressBar :value="savingRatio.toFixed(0)" :max="100"
|
||||||
|
:colorUnfilled="savingRatio < 30 ? '#FF5533' : '#3BB44A'" rounded
|
||||||
|
percentage strokeWidth="12" size="64"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
|
<div class="group justify-end flex w-full">
|
||||||
|
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
|
||||||
|
v-tooltip="'Сумма остатка лимитов по остальным категориям'"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<h4 class="text-sm lg:text-base">Ежедневные</h4>
|
||||||
|
<div class="font-light text-center w-full ">
|
||||||
|
<CircleProgressBar :value="dailyRatio.toFixed(0)" :max="100" rounded percentage
|
||||||
|
strokeWidth="12" size="64"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-5 items-center justify-items-center w-full">
|
<div class=" flex flex-col gap-2 relative border-[0.1rem] rounded-xl p-2 w-full h-full pt-4">
|
||||||
|
<p class="absolute -top-3 left-1/2 -translate-x-1/2 bg-gray-100 px-2 text-gray-700 font-semibold">
|
||||||
|
Фактические</p>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<button class="grid grid-cols-3 justify-between gap-5 items-end w-full"
|
<button class="flex flex-row justify-between gap-1 items-center w-full"
|
||||||
@click="detailedShowed = !detailedShowed">
|
@click="detailedShowed = !detailedShowed">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
<h4 class="text-sm lg:text-base">Долги</h4>
|
<h4 class="text-sm lg:text-base">Приходы</h4>
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
<div class="font-light text-center w-full ">
|
||||||
{{ loansRatio.toFixed(0) }} %
|
|
||||||
</div>
|
|
||||||
<!-- <p>Total Incomes</p>-->
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center ">
|
|
||||||
<span class="text-sm lg:text-base">Сбережения</span>
|
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center"
|
|
||||||
:class="savingRatio < 30 ? '!font-bold text-red-700' : ''">
|
|
||||||
{{ savingRatio.toFixed(0) }} %
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center ">
|
|
||||||
<h4 class="text-sm lg:text-base">Ежедневные</h4>
|
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
|
||||||
{{ dailyRatio.toFixed(0) }} %
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<button class="grid grid-cols-2 justify-between gap-5 items-end w-full"
|
|
||||||
@click="detailedShowed = !detailedShowed">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<h4 class="text-sm lg:text-base">Факт. поступления ✅</h4>
|
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
|
||||||
{{ formatAmount(totalInstantIncomes) }} ₽
|
{{ formatAmount(totalInstantIncomes) }} ₽
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <p>Total Incomes</p>-->
|
<!-- <p>Total Incomes</p>-->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center ">
|
-
|
||||||
<span class="text-sm lg:text-base">Факт. траты 📛</span>
|
<div class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
<span class="text-sm lg:text-base font-bold">Траты </span>
|
||||||
|
<div class="font-light text-center w-full ">
|
||||||
{{ formatAmount(totalInstantExpenses) }} ₽
|
{{ formatAmount(totalInstantExpenses) }} ₽
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</button>
|
|
||||||
<div class="flex flex-col items-center w-full ">
|
|
||||||
<span class="text-sm lg:text-base">Остаток на траты</span>
|
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner w-full text-center">
|
|
||||||
{{ formatAmount(totalInstantIncomes - totalInstantExpenses) }} ₽
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
=
|
||||||
<div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
<div class="flex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
<div v-for="categorySum in transactionCategoriesSums" class="flex flex-col items-center font-bold ">
|
<span class="text-sm lg:text-base font-bold">Остаток</span>
|
||||||
<p class="font-light ">{{ categorySum.category.icon }} {{ categorySum.category.name }}</p>
|
<div class="font-light text-center w-full "
|
||||||
<div class="font-light bg-gray-100 p-1 rounded-lg box-shadow-inner text-center w-full ">
|
:class="totalIncomes - totalExpenses < 0 ? ' text-red-700 !font-bold' : ''">
|
||||||
|
{{ formatAmount(totalInstantIncomes - totalInstantExpenses) }} ₽
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 !gap-2 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
|
||||||
|
<div v-for="categorySum in transactionCategoriesSums"
|
||||||
|
class="lex flex-col items-center font-bold bg-white p-2 shadow-md rounded-lg w-full h-full">
|
||||||
|
<p class="font-light text-center line-clamp-1">{{ categorySum.category.icon }}
|
||||||
|
{{ categorySum.category.name }}</p>
|
||||||
|
<div class="font-light text-center w-full ">
|
||||||
{{ formatAmount(categorySum.sum) }} ₽
|
{{ formatAmount(categorySum.sum) }} ₽
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar :value="value" class="mt-2 col-span-2" style="height: 1rem !important;"></ProgressBar>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" h-fit overflow-y-auto gap-4 flex-col row-span-6 hidden lg:flex">
|
<div class=" h-fit overflow-y-auto gap-4 flex-col row-span-6 hidden lg:flex">
|
||||||
@@ -995,7 +1075,7 @@ onUnmounted(async () => {
|
|||||||
|
|
||||||
<li v-for="(category, categoryId) in categoriesIncomeTransactions" :key="categoryId"
|
<li v-for="(category, categoryId) in categoriesIncomeTransactions" :key="categoryId"
|
||||||
|
|
||||||
class="flex flex-col p-4 shadow-lg rounded-lg bg-white transition-transform duration-300 ">
|
class="flex flex-col p-4 shadow-md rounded-lg bg-white transition-transform duration-300 ">
|
||||||
|
|
||||||
<div class="flex flex-row justify-between w-full items-center transition-transform duration-300">
|
<div class="flex flex-row justify-between w-full items-center transition-transform duration-300">
|
||||||
<!-- {{category}}-->
|
<!-- {{category}}-->
|
||||||
@@ -1056,7 +1136,7 @@ onUnmounted(async () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="day, dayOne in calendar" class="flex flex-col justify-between p-4 shadow-lg rounded-lg "
|
<div v-for="day, dayOne in calendar" class="flex flex-col justify-between p-4 shadow-md rounded-lg "
|
||||||
v-if="calendarExpanded == '1'"
|
v-if="calendarExpanded == '1'"
|
||||||
:class="day.date.toISOString().split('T')[0] == new Date().toISOString().split('T')[0]? 'bg-emerald-200' : 'bg-white '">
|
:class="day.date.toISOString().split('T')[0] == new Date().toISOString().split('T')[0]? 'bg-emerald-200' : 'bg-white '">
|
||||||
<div class="flex flex-row gap-2 items-center ">
|
<div class="flex flex-row gap-2 items-center ">
|
||||||
@@ -1076,7 +1156,7 @@ onUnmounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<ul class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<ul class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<li v-for="(category, categoryId) in categoriesTransactions" :key="categoryId"
|
<li v-for="(category, categoryId) in categoriesTransactions" :key="categoryId"
|
||||||
class="flex flex-col justify-between p-4 shadow-lg rounded-lg bg-white ">
|
class="flex flex-col justify-between p-4 shadow-md rounded-lg bg-white ">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="flex flex-row justify-between w-full items-center">
|
<div class="flex flex-row justify-between w-full items-center">
|
||||||
<div class="flex flex-row col-span-2 gap-2 items-center">
|
<div class="flex flex-row col-span-2 gap-2 items-center">
|
||||||
|
|||||||
178
src/components/settings/CommonSettings.vue
Normal file
178
src/components/settings/CommonSettings.vue
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, onMounted, ref, watch} from "vue";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import FloatLabel from "primevue/floatlabel";
|
||||||
|
import InputText from "primevue/inputtext";
|
||||||
|
import InputNumber from "primevue/inputnumber";
|
||||||
|
import Tag from "primevue/tag";
|
||||||
|
import {createTagRequest, deleteTagRequest, getTagsRequest} from "@/services/categoryService";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {CategoryTag} from "@/models/Category";
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
const firstDay = ref(1)
|
||||||
|
const secondDay = ref(25)
|
||||||
|
const tags = ref([])
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
const creationShowed = ref(false)
|
||||||
|
const newTagCode = ref()
|
||||||
|
const codeError = ref()
|
||||||
|
const newTagName = ref()
|
||||||
|
const nameError = ref()
|
||||||
|
|
||||||
|
const fetchTags = async () => {
|
||||||
|
await getTagsRequest().then(res => tags.value = res)
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
toast.add({severity: 'error', summary: 'Ошибка получения тэгов', detail: err.response.data.message, life: 3000})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const createTag = async () => {
|
||||||
|
if (!newTagCode.value) {
|
||||||
|
codeError.value = "Не может быть пустым"
|
||||||
|
if (newTagCode.value.includes(" ")) {
|
||||||
|
codeError.value = "Не может содержать пробелы"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else codeError.value = null
|
||||||
|
if (!newTagCode.value || newTagName.value == '') {
|
||||||
|
nameError.value = "Не может быть пустым"
|
||||||
|
return;
|
||||||
|
} else nameError.value = null
|
||||||
|
const tag = new CategoryTag(newTagCode.value, newTagName.value);
|
||||||
|
await createTagRequest(tag).then((res) => {
|
||||||
|
creationShowed.value = false;
|
||||||
|
toast.add({severity: 'success', summary: 'Успех!', detail: 'Тэг создан', life: 3000})
|
||||||
|
fetchTags();
|
||||||
|
newTagCode.value = '';
|
||||||
|
newTagName.value = '';
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.add({severity: 'error', summary: 'Ошибка', detail: err.response.data.message, life: 3000})
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteTag = async (tag: CategoryTag) => {
|
||||||
|
await deleteTagRequest(tag).then((res) => {
|
||||||
|
creationShowed.value = false;
|
||||||
|
toast.add({severity: 'success', summary: 'Успех!', detail: 'Тэг удален!', life: 3000})
|
||||||
|
fetchTags();
|
||||||
|
newTagCode.value = '';
|
||||||
|
newTagName.value = '';
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.add({severity: 'error', summary: 'Ошибка', detail: err.response.data.message, life: 3000})
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedSpace.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
await fetchTags()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching budget infos:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (selectedSpace.value) {
|
||||||
|
await fetchTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-5 pb-5">
|
||||||
|
<h1 class="font-bold text-2xl leading-tight">Общие настройки</h1>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h2 class="font-bold text-xl leading-tight">Настройки периодов</h2>
|
||||||
|
<form class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-row items-center gap-2 ">
|
||||||
|
<label for="username">День первого поступления средств</label>
|
||||||
|
<InputNumber id="username" v-model="firstDay" class="w-10" inputId="horizontal-buttons" showButtons
|
||||||
|
buttonLayout="horizontal" :min="1" :max="31">
|
||||||
|
<template #incrementbuttonicon>
|
||||||
|
<span class="pi pi-plus"/>
|
||||||
|
</template>
|
||||||
|
<template #decrementbuttonicon>
|
||||||
|
<span class="pi pi-minus"/>
|
||||||
|
</template>
|
||||||
|
</InputNumber>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<label for="username">День второго поступления средств</label>
|
||||||
|
<InputNumber id="username" v-model="secondDay" inputId="horizontal-buttons" showButtons
|
||||||
|
buttonLayout="horizontal" :min="1" :max="31">
|
||||||
|
<template #incrementbuttonicon>
|
||||||
|
<span class="pi pi-plus"/>
|
||||||
|
</template>
|
||||||
|
<template #decrementbuttonicon>
|
||||||
|
<span class="pi pi-minus"/>
|
||||||
|
</template>
|
||||||
|
</InputNumber>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 justify-start">
|
||||||
|
<h2 class="font-bold text-xl leading-tight">Тэги категорий</h2>
|
||||||
|
<div class="flex flex-row items-center gap-2 flex-wrap">
|
||||||
|
<Tag v-for="tag in tags" :key="tag.id" class="w-fit group">{{ tag.name }} ({{ tag.code }})<i
|
||||||
|
class="pi pi-trash !hidden group-hover:!block" @click="deleteTag(tag)"/></Tag>
|
||||||
|
<button v-if=!creationShowed class="bg-emerald-100 border border-emerald-700 p-0.5 px-1 rounded"
|
||||||
|
@click="creationShowed = !creationShowed"><span
|
||||||
|
class="text-gray-500 font-bold flex flex-row gap-1 items-center"><i
|
||||||
|
class="pi pi-plus-circle"/> Добавить</span></button>
|
||||||
|
<div v-else class=" bg-emerald-50 border-spacing-0.5 border-emerald-700 rounded p-0.5">
|
||||||
|
<div class="flex flex-row gap-4 px-1">
|
||||||
|
<div class="flex flex-col items-center gap- ">
|
||||||
|
<div class="flex flex-row items-center gap-2 ">
|
||||||
|
<label for="code">Код</label>
|
||||||
|
<InputText id="name" class="rounded !h-6 " v-model="newTagCode" :invalid="!newTagCode || newTagCode==''"
|
||||||
|
v-tooltip="'Не может содержать пробелов'"/>
|
||||||
|
</div>
|
||||||
|
<span v-if="codeError" class="text-red-400">{{ codeError }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center gap-">
|
||||||
|
<div class="flex flex-row items-center gap-2 ">
|
||||||
|
<label for="name">Название</label>
|
||||||
|
|
||||||
|
<InputText id="name" class="rounded !h-6 " v-model="newTagName" :invalid="!newTagName || newTagName==''"
|
||||||
|
v-tooltip="'Должно быть введено'"/>
|
||||||
|
</div>
|
||||||
|
<span v-if="nameError" class="text-sm font-light text-red-400">{{ nameError }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-1">
|
||||||
|
<button @click="createTag"><i class="pi pi-plus-circle"></i></button>
|
||||||
|
<button @click="creationShowed=false"><i class="pi pi-times-circle"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -7,11 +7,17 @@ import {ref} from "vue";
|
|||||||
import Divider from "primevue/divider";
|
import Divider from "primevue/divider";
|
||||||
import CategoriesList from "@/components/settings/categories/CategoriesList.vue";
|
import CategoriesList from "@/components/settings/categories/CategoriesList.vue";
|
||||||
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
|
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
|
||||||
|
import CommonSettings from "@/components/settings/CommonSettings.vue";
|
||||||
|
|
||||||
const selectedModeCode = ref("categories")
|
const selectedModeCode = ref("common")
|
||||||
|
|
||||||
|
|
||||||
const pages = ref([
|
const pages = ref([
|
||||||
|
{
|
||||||
|
"code": "common",
|
||||||
|
"title": "Общие",
|
||||||
|
"icon": "pi pi-cog"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"code": "categories",
|
"code": "categories",
|
||||||
"title": "Категории",
|
"title": "Категории",
|
||||||
@@ -57,7 +63,8 @@ const pages = ref([
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class=" flex flex-col col-span-5 gap-2 max-h-[90dvh] overflow-y-scroll justify-items-start">
|
<div class=" flex flex-col col-span-5 gap-2 max-h-[90dvh] overflow-y-scroll justify-items-start">
|
||||||
<CategoriesList v-if="selectedModeCode == 'categories'"/>
|
<CommonSettings v-if="selectedModeCode == 'common'"/>
|
||||||
|
<CategoriesList v-else-if="selectedModeCode == 'categories'"/>
|
||||||
<RecurrentList v-else-if="selectedModeCode == 'recurrent'"/>
|
<RecurrentList v-else-if="selectedModeCode == 'recurrent'"/>
|
||||||
<p v-else-if="selectedModeCode == 'notifications'" class="flex justify-items-start justify-center items-center h-screen">NOTIFICATIONS UNDER CONSTRUCTIONS</p>
|
<p v-else-if="selectedModeCode == 'notifications'" class="flex justify-items-start justify-center items-center h-screen">NOTIFICATIONS UNDER CONSTRUCTIONS</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CreateCategoryModal
|
<CreateCategoryModal
|
||||||
|
v-if="isDialogVisible"
|
||||||
:show="isDialogVisible"
|
:show="isDialogVisible"
|
||||||
:categoryTypes="categoryTypes"
|
:categoryTypes="categoryTypes"
|
||||||
:selectedCategoryType="selectedCategoryType"
|
:selectedCategoryType="selectedCategoryType"
|
||||||
@@ -116,7 +117,6 @@ import {
|
|||||||
deleteCategory,
|
deleteCategory,
|
||||||
getCategories,
|
getCategories,
|
||||||
getCategoryTypes,
|
getCategoryTypes,
|
||||||
updateCategory
|
|
||||||
} from "@/services/categoryService";
|
} from "@/services/categoryService";
|
||||||
|
|
||||||
import {useConfirm} from "primevue/useconfirm";
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
@@ -201,11 +201,6 @@ const closeCreateDialog = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveCategory = async (newCategory: Category) => {
|
const saveCategory = async (newCategory: Category) => {
|
||||||
if (newCategory.id) {
|
|
||||||
await updateCategory(newCategory.id, newCategory);
|
|
||||||
} else {
|
|
||||||
await createCategory(newCategory);
|
|
||||||
}
|
|
||||||
await fetchCategories()
|
await fetchCategories()
|
||||||
closeCreateDialog();
|
closeCreateDialog();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
class="w-[95%] xl:!w-1/3">
|
class="w-[95%] xl:!w-1/3">
|
||||||
<div class="flex justify-center gap-4">
|
<div class="flex justify-center gap-4">
|
||||||
<Button rounded outlined @click="toggleEmojiPicker" class=" flex justify-center !rounded-full !text-4xl !p-4"
|
<Button rounded outlined @click="toggleEmojiPicker" class=" flex justify-center !rounded-full !text-4xl !p-4"
|
||||||
size="large" :label="icon"/>
|
size="large" :label="category.icon"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute pt-1 border rounded-lg shadow-2xl border-gray-300 bg-white grid grid-cols-6 gap-4 h-40 z-50 ml-3 mt-1 overflow-scroll"
|
class="absolute pt-1 border rounded-lg shadow-2xl border-gray-300 bg-white grid grid-cols-6 gap-4 h-40 z-50 ml-3 mt-1 overflow-scroll"
|
||||||
v-if="showEmojiPicker">
|
v-if="showEmojiPicker">
|
||||||
@@ -14,16 +15,32 @@
|
|||||||
|
|
||||||
<!-- SelectButton для выбора типа категории -->
|
<!-- SelectButton для выбора типа категории -->
|
||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4">
|
||||||
<SelectButton v-if="!isEditing" v-model="categoryType" :options="categoryTypes" optionLabel="name"/>
|
<SelectButton v-if="!isEditing" v-model="category.type" :options="categoryTypes" optionLabel="name"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Поля для создания/редактирования категории -->
|
<!-- Поля для создания/редактирования категории -->
|
||||||
<label for="newCategoryName">Название категории:</label>
|
<label for="newCategoryName">Название категории:</label>
|
||||||
<input v-model="name" type="text" id="newCategoryName"/>
|
<input v-model="category.name" type="text" id="newCategoryName"/>
|
||||||
|
|
||||||
<label for="newCategoryDesc">Описание категории:</label>
|
<label for="newCategoryDesc">Описание категории:</label>
|
||||||
<input v-model="description" type="text" id="newCategoryDesc"/>
|
<input v-model="category.description" type="text" id="newCategoryDesc"/>
|
||||||
|
<div class="flex flex-col flex-wrap gap-0">
|
||||||
|
<h2 class="text-lg ">Добавленные теги</h2>
|
||||||
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
|
<Tag v-if="category.tags" v-for="tag in category.tags" :key="tag.id" class="w-fit group">{{ tag.name }}
|
||||||
|
({{ tag.code }})<i
|
||||||
|
class="pi pi-trash !hidden group-hover:!block" @click="deleteTag(tag)"/></Tag>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-wrap gap-2">
|
||||||
|
<h2 class="text-lg ">Доступные теги</h2>
|
||||||
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
|
<Tag v-for="tag in availableTags" :key="tag.id" class="w-fit " @click="addTag(tag)">
|
||||||
|
{{ tag.name }} ({{ tag.code }})
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Кнопки -->
|
<!-- Кнопки -->
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
|
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
|
||||||
@@ -32,95 +49,151 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, watch, computed, PropType} from 'vue';
|
import {ref, watch, computed, PropType, onMounted} from 'vue';
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import Dialog from "primevue/dialog";
|
import Dialog from "primevue/dialog";
|
||||||
import SelectButton from "primevue/selectbutton";
|
import SelectButton from "primevue/selectbutton";
|
||||||
import {Category, CategoryType} from '@/models/Category';
|
import {Category, CategoryTag, CategoryType} from '@/models/Category';
|
||||||
|
import Tag from "primevue/tag";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {createCategory, editCategoryRequest, getTagsRequest} from "@/services/categoryService";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
export default {
|
const toast = useToast();
|
||||||
components: {
|
|
||||||
Dialog,
|
const props = defineProps({
|
||||||
Button, SelectButton
|
show: Boolean,
|
||||||
},
|
categoryTypes: Object as PropType<CategoryType[]>,
|
||||||
props: {
|
category: Object as PropType<Category | null>, // Категория для редактирования (null, если создание)
|
||||||
show: Boolean,
|
})
|
||||||
categoryTypes: Object as PropType<CategoryType[]>,
|
const emit = defineEmits(['saveCategory', 'close-modal'])
|
||||||
category: Object as PropType<Category | null>, // Категория для редактирования (null, если создание)
|
const category = ref<Category>()
|
||||||
},
|
const icon = ref('🐱');
|
||||||
emits: ['saveCategory', 'close-modal'],
|
const name = ref('');
|
||||||
setup(props, {emit}) {
|
const description = ref('');
|
||||||
const icon = ref('🐱');
|
const showEmojiPicker = ref(false);
|
||||||
const name = ref('');
|
const categoryType = ref(props.category?.type || props.categoryTypes[0]); // Тип по умолчанию
|
||||||
const description = ref('');
|
const isEditing = computed(() => !!props.category); // Если есть категория, значит редактирование
|
||||||
const showEmojiPicker = ref(false);
|
const availableTags = computed(() => {
|
||||||
const categoryType = ref(props.category?.type || props.categoryTypes[0]); // Тип по умолчанию
|
const availableTags = []
|
||||||
const isEditing = computed(() => !!props.category); // Если есть категория, значит редактирование
|
tags.value.forEach((tag1) => {
|
||||||
|
console.log(tag1.code)
|
||||||
|
console.log(category.value.tags)
|
||||||
|
if (category.value?.tags.filter(tag => tag.code == tag1.code).length == 0) {
|
||||||
|
availableTags.push(tag1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return availableTags;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Если мы редактируем категорию, заполняем поля данными
|
const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘', '🍎', '🍔', '🍕', '🍣', '☕', '🍺', '🚗', '🚕', '🚴♂️', '🚆', '✈️', '⛴️', '🛒', '👗', '💍', '👟', '🛍️', '🎮', '🎥', '🎧', '🎢', '🎨', '🏠', '🛏️', '🧹', '🪴', '🍼', '🏥', '💊', '🩺', '🦷', '💳', '💰', '🏦', '🌍', '🗺️', '🏝️', '🏔️', '💻', '📚', '🖋️', '🏫'];
|
||||||
watch(() => props.category, (newCategory) => {
|
|
||||||
if (newCategory) {
|
|
||||||
name.value = newCategory.name;
|
|
||||||
description.value = newCategory.description;
|
|
||||||
icon.value = newCategory.icon;
|
|
||||||
categoryType.value = newCategory.type;
|
|
||||||
} else {
|
|
||||||
resetForm(); // Сбрасываем форму, если не редактируем
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘', '🍎', '🍔', '🍕', '🍣', '☕', '🍺', '🚗', '🚕', '🚴♂️', '🚆', '✈️', '⛴️', '🛒', '👗', '💍', '👟', '🛍️', '🎮', '🎥', '🎧', '🎢', '🎨', '🏠', '🛏️', '🧹', '🪴', '🍼', '🏥', '💊', '🩺', '🦷', '💳', '💰', '🏦', '🌍', '🗺️', '🏝️', '🏔️', '💻', '📚', '🖋️', '🏫'];
|
const toggleEmojiPicker = () => {
|
||||||
|
showEmojiPicker.value = !showEmojiPicker.value;
|
||||||
const toggleEmojiPicker = () => {
|
|
||||||
showEmojiPicker.value = !showEmojiPicker.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectEmoji = (emoji: string) => {
|
|
||||||
icon.value = emoji;
|
|
||||||
showEmojiPicker.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveCategory = () => {
|
|
||||||
const categoryData = new Category(categoryType.value, name.value, description.value, icon.value);
|
|
||||||
if (isEditing.value && props.category) {
|
|
||||||
categoryData.id = props.category.id; // Сохраняем ID при редактировании
|
|
||||||
}
|
|
||||||
emit('saveCategory', categoryData);
|
|
||||||
resetForm();
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
emit('close-modal');
|
|
||||||
resetForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
name.value = '';
|
|
||||||
description.value = '';
|
|
||||||
icon.value = '🐱';
|
|
||||||
categoryType.value = 'EXPENSE';
|
|
||||||
showEmojiPicker.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
icon,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
showEmojiPicker,
|
|
||||||
categoryType,
|
|
||||||
emojis,
|
|
||||||
toggleEmojiPicker,
|
|
||||||
selectEmoji,
|
|
||||||
saveCategory,
|
|
||||||
closeModal,
|
|
||||||
isEditing
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectEmoji = (emoji: string) => {
|
||||||
|
category.value.icon = emoji
|
||||||
|
showEmojiPicker.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCategory = async () => {
|
||||||
|
// const categoryData = new Category(categoryType.value, name.value, description.value, icon.value);
|
||||||
|
if (isEditing.value) {
|
||||||
|
console.log(category.value)
|
||||||
|
await editCategoryRequest(category.value).then((res) => {
|
||||||
|
category.value = res.data
|
||||||
|
emit("saveCategory", category.value)
|
||||||
|
}).catch(err =>
|
||||||
|
toast.add({severity: 'error', summary: 'Ошибка сохранения категории', detail: err.response.data.message, life: 3000})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await createCategory(category.value).then((res) => {
|
||||||
|
category.value = res.data
|
||||||
|
}).catch(err =>
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка создания категории',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
resetForm();
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
emit('close-modal');
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
name.value = '';
|
||||||
|
description.value = '';
|
||||||
|
icon.value = '🐱';
|
||||||
|
categoryType.value = 'EXPENSE';
|
||||||
|
showEmojiPicker.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTag = (tag: CategoryType) => {
|
||||||
|
category.value.tags.push(tag);
|
||||||
|
}
|
||||||
|
const deleteTag = (tag) => {
|
||||||
|
category.value.tags = category.value.tags.filter(t => t.code !== tag.code);
|
||||||
|
}
|
||||||
|
const tags = ref<CategoryType[]>([]);
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
const fetchTags = async () => {
|
||||||
|
await getTagsRequest().then(res => tags.value = res)
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка получения тэгов',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedSpace.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
// loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
await fetchTags()
|
||||||
|
|
||||||
|
constructCategory()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching budget infos:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const constructCategory = () => {
|
||||||
|
if (props.category) {
|
||||||
|
category.value = new Category(props.category.id, props.category.type, props.category.name, props.category.description, props.category.icon, props.category.tags);
|
||||||
|
} else {
|
||||||
|
category.value = new Category(null, categoryType.value, '', '', icon.value, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
constructCategory()
|
||||||
|
// console.log(props.category)
|
||||||
|
await fetchTags();
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -232,9 +232,9 @@ onMounted(async () => {
|
|||||||
<ConfirmDialog/>
|
<ConfirmDialog/>
|
||||||
<Dialog :visible="inviteCreatedDialog" header="Приглашение" @hide="inviteCreatedDialog=false"
|
<Dialog :visible="inviteCreatedDialog" header="Приглашение" @hide="inviteCreatedDialog=false"
|
||||||
@update:visible="inviteCreatedDialog=false">
|
@update:visible="inviteCreatedDialog=false">
|
||||||
<div class="flex flex-col justify-start">
|
<div class="flex flex-col gap-4 justify-start">
|
||||||
<div class="flex flex-row gap-2 items-center"> Ссылка приглашения:
|
<div class="flex flex-row gap-2 items-center"> Ссылка приглашения:
|
||||||
<input class="p-2 border min-w-fit w-80" v-model="inviteUrl" disabled></input>
|
<input class="p-2 border w-5/6" v-model="inviteUrl" disabled></input>
|
||||||
<button @click="copyToClipboard('https://luminic.space/spaces/invite/' + invite.code)">
|
<button @click="copyToClipboard('https://luminic.space/spaces/invite/' + invite.code)">
|
||||||
{{ !copied ? 'Копировать' : 'Скопировано!' }}
|
{{ !copied ? 'Копировать' : 'Скопировано!' }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,17 +10,20 @@ export class CategorySettingType {
|
|||||||
|
|
||||||
|
|
||||||
export class Category {
|
export class Category {
|
||||||
id: number = null;
|
id: string | null = null;
|
||||||
type: CategoryType;
|
type: CategoryType;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
tags: CategoryTag[];
|
||||||
|
|
||||||
constructor(type: CategoryType, name: string, description: string, icon: string,) {
|
constructor(id: string | null,type: CategoryType, name: string, description: string, icon: string, tags: CategoryTag[]) {
|
||||||
|
this.id = id
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
|
this.tags = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Метод, который возвращает краткое описание
|
// Метод, который возвращает краткое описание
|
||||||
@@ -36,3 +39,13 @@ export class CategorySetting {
|
|||||||
settingValue: number
|
settingValue: number
|
||||||
value: number
|
value: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CategoryTag {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
constructor(code: string, name: string) {
|
||||||
|
this.code = code;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/services/categoryService.ts
|
// src/services/categoryService.ts
|
||||||
import apiClient from '@/services/axiosSetup';
|
import apiClient from '@/services/axiosSetup';
|
||||||
import {Category} from "@/models/Category";
|
import {Category, CategoryTag} from "@/models/Category";
|
||||||
import {useSpaceStore} from "@/stores/spaceStore"; // Импортируете настроенный экземпляр axios
|
import {useSpaceStore} from "@/stores/spaceStore"; // Импортируете настроенный экземпляр axios
|
||||||
|
|
||||||
export const getCategories = async (type = null) => {
|
export const getCategories = async (type = null) => {
|
||||||
@@ -22,12 +22,20 @@ export const getCategoryTypes = async () => {
|
|||||||
|
|
||||||
export const createCategory = async (category: Category) => {
|
export const createCategory = async (category: Category) => {
|
||||||
const spaceStore = useSpaceStore();
|
const spaceStore = useSpaceStore();
|
||||||
return await apiClient.post(`/spaces/${spaceStore.space?.id}/categories`, category);
|
return await apiClient.post(`/spaces/${spaceStore.space?.id}/categories`, category)
|
||||||
|
.then(res => res.data)
|
||||||
|
.catch(err => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCategory = async (id: number, category: any) => {
|
export const editCategoryRequest = async (category: any) => {
|
||||||
const spaceStore = useSpaceStore();
|
const spaceStore = useSpaceStore();
|
||||||
return await apiClient.put(`/spaces/${spaceStore.space?.id}/categories/${id}`, category);
|
return await apiClient.put(`/spaces/${spaceStore.space?.id}/categories/${category.id}`, category)
|
||||||
|
.then(res => res.data)
|
||||||
|
.catch(err => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteCategory = async (id: number) => {
|
export const deleteCategory = async (id: number) => {
|
||||||
@@ -38,5 +46,32 @@ export const deleteCategory = async (id: number) => {
|
|||||||
export const getCategoriesSumsRequest = async (spaceId: string) => {
|
export const getCategoriesSumsRequest = async (spaceId: string) => {
|
||||||
const spaceStore = useSpaceStore();
|
const spaceStore = useSpaceStore();
|
||||||
return await apiClient.get(`/spaces/${spaceStore.space?.id}/analytics/by-month`)
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/analytics/by-month`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTagsRequest = async () => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/categories/tags`)
|
||||||
|
.then(res => res.data)
|
||||||
|
.catch(err => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTagRequest = async (tag: CategoryTag) => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.post(`/spaces/${spaceStore.space?.id}/categories/tags`, tag)
|
||||||
|
.then(res => res.data)
|
||||||
|
.catch(err => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteTagRequest = async (tag: CategoryTag) => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/categories/tags/${tag.code}`)
|
||||||
|
.then(res => res.data)
|
||||||
|
.catch(err => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const deleteTransactionRequest = async (id: number) => {
|
|||||||
|
|
||||||
export const getTransactionTypes = async () => {
|
export const getTransactionTypes = async () => {
|
||||||
const spaceStore = useSpaceStore()
|
const spaceStore = useSpaceStore()
|
||||||
return await apiClient.get(`/transactions/types`);
|
return await apiClient.get(`/transactions/types/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTransactionCategoriesSums = async () => {
|
export const getTransactionCategoriesSums = async () => {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import {ref} from 'vue';
|
|||||||
import apiClient from "@/services/axiosSetup";
|
import apiClient from "@/services/axiosSetup";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {useSpaceStore} from "@/stores/spaceStore";
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const toast = useToast();
|
||||||
const user = ref(null);
|
const user = ref(null);
|
||||||
const loadingUser = ref(true);
|
const loadingUser = ref(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -31,11 +33,12 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Основная функция для логина
|
// Основная функция для логина
|
||||||
async function login(username, password, tg_id = null) {
|
async function login(username, password, tg_id: string | null = null) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
if (tg_id) {
|
if (tg_id) {
|
||||||
response = await apiClient.post('/auth/token/tg', {tg_id: tg_id});
|
response = await apiClient.post('/auth/tgLogin', {}, {headers: {'X-TG-ID': tg_id}});
|
||||||
} else {
|
} else {
|
||||||
response = await apiClient.post('/auth/login', {
|
response = await apiClient.post('/auth/login', {
|
||||||
username: username,
|
username: username,
|
||||||
@@ -46,12 +49,13 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const token = response.data.token;
|
const token = response.data.token;
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||||
|
toast.add({ severity: 'success', summary: 'Вход выполнен', detail: 'Добро пожаловать!', life: 3000 })
|
||||||
await fetchUserProfile();
|
await fetchUserProfile();
|
||||||
await spaceStore.fetchSpaces()
|
await spaceStore.fetchSpaces()
|
||||||
await router.push(route.query['back'] ? route.query['back'].toString() : '/');
|
await router.push(route.query['back'] ? route.query['back'].toString() : '/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert('Ошибка входа. Проверьте логин и пароль.');
|
toast.add({ severity: 'error', summary: 'Ошибка входа', detail: 'Ошибка Telegram авторизации', life: 3000 })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user