tags and new analytics new in budget

This commit is contained in:
xds
2025-02-23 12:22:40 +03:00
parent 6d3638896f
commit 85b6d0a796
29 changed files with 1102 additions and 565 deletions

View File

@@ -15,17 +15,17 @@
<div class="bg-gray-100 h-12 block lg:hidden"></div>
</div>
<div id="footer" class="flex flex-col w-full h-fit bg-gray-200 p-4 gap-4">
<div class="flex flex-row items-center gap-6 ">
<div id="footer" class="flex flex-col w-full h-fit bg-gray-200 p-4 gap-4 ">
<div class="flex flex-row items-start gap-2 ">
<div class="flex flex-row items-center gap-2 min-w-fit">
<img alt="logo" src="/apple-touch-icon.png" width="48" height="48"/>
<div class="flex flex-col items-start">
<p class="text-lg font-bold">Luminic Space</p>
<p>Ваше пространство</p>
<p class="text-md font-bold">Luminic Space</p>
<p class="text-sm">Ваше пространство</p>
</div>
</div>
<div class="flex flex-col sm:flex-row w-full gap-4">
<div class="grid grid-cols-2 sm:flex sm:flex-row w-full gap-1">
<router-link to="/about" class="hover:underline">О проекте</router-link>
<router-link to="/spaces" class="hover:underline">Пространства</router-link>
<router-link to="/analytics" class="hover:underline">Аналитика</router-link>
@@ -40,6 +40,7 @@
</div>
<div>v0.0.2</div>
</div>
<div class="h-16 lg:h-0"/>
</div>

View File

@@ -7,6 +7,7 @@
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--p-selectbutton-border-radius: 10px;
--vt-c-indigo: #2c3e50;

View File

@@ -18,6 +18,11 @@
height: 0.5rem !important;
}
.p-selectbutton{
border-width: 1px !important;
border-color: #d1d5db !important;
}
canvas {

View File

@@ -154,7 +154,7 @@ const items = ref([
url: '/settings',
},
{
label: 'Создать',
label: 'Запись',
icon: 'pi pi-plus',
url:'',
command: () => {

View File

@@ -2,45 +2,54 @@
<div
class=" items-center toolbar-example justify-between bg-white outline rounded-xl outline-gray-300 shadow-lg h-fit fixed"
style="width: 90%; left:5%; bottom: 1.5rem;">
<!-- <TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction-type="'INSTANT'"-->
<!-- :category-type="'EXPENSE'" @close-drawer="closeDrawer"/>-->
<div class="flex flex-row rounded-full px-2 justify-between overflow-x">
<!-- <div class="flex flex-col gap-2 p-2">-->
<!-- <router-link to="/spaces" class="items-center flex flex-col gap-2">-->
<!-- <i class="pi pi-compass text-2xl" style="font-size: 1.5rem"></i>-->
<!-- <p>Пространства</p>-->
<!-- </router-link>-->
<!-- </div>-->
<!-- <TransactionForm v-if="drawerOpened" :visible="drawerOpened" :transaction-type="'INSTANT'"-->
<!-- :category-type="'EXPENSE'" @close-drawer="closeDrawer"/>-->
<div class="flex flex-row rounded-full px-2 justify-between overflow-x-scroll">
<div class=" flex-col gap-2 p-2 sm:flex">
<button class="items-center flex flex-col gap-2" @click="openDrawer('INSTANT', 'EXPENSE')">
<i class="pi pi-plus text-xl" style="font-size: 1rem"></i>
<p>Запись</p>
</button>
</div>
<div class="flex flex-col gap-2 p-2">
<router-link to="/analytics" class="items-center flex flex-col gap-2">
<i class="pi pi-chart-line text-2xl" style="font-size: 1.5rem"></i>
<p>Аналитика</p>
<router-link to="/spaces" class="items-center flex flex-col gap-2">
<i class="pi pi-compass text-2xl" style="font-size: 1rem"></i>
<p>Пространства</p>
</router-link>
</div>
<div class="flex flex-col gap-2 p-2">
<router-link to="/budgets" class="items-center flex flex-col gap-2">
<i class="pi pi-briefcase text-2xl" style="font-size: 1.5rem"></i>
<i class="pi pi-briefcase text-2xl" style="font-size: 1rem"></i>
<p>Бюджеты</p>
</router-link>
</div>
<div class="flex flex-col gap-2 p-2">
<router-link to="/analytics" class="items-center flex flex-col gap-2">
<i class="pi pi-chart-line text-2xl" style="font-size: 1rem"></i>
<p>Аналитика</p>
</router-link>
</div>
<div class="flex flex-col gap-2 p-2">
<router-link to="/transactions" class="items-center flex flex-col gap-2">
<i class="pi pi-wallet text-2xl" style="font-size: 1.5rem"></i>
<i class="pi pi-wallet text-2xl" style="font-size: 1rem"></i>
<p>Транзакции</p>
</router-link>
</div>
<div class=" flex-col gap-2 p-2 hidden sm:flex">
<div class=" flex-col gap-2 p-2 flex">
<router-link to="/settings" class="items-center flex flex-col gap-2">
<i class="pi pi-check text-2xl" style="font-size: 1.5rem"></i>
<i class="pi pi-cog text-2xl" style="font-size: 1rem"></i>
<p>Настройки</p>
</router-link>
</div>
<!-- Создать с подменю -->
<div class="relative flex-col gap-2 p-2 items-center hidden sm:flex" @click="showSubmenu = !showSubmenu"
@mouseenter="showSubmenu = true" @mouseleave="showSubmenu = false">
<!-- <router-link to="/transactions/create" class="items-center flex flex-col gap-2">-->
<i class="pi pi-cog text-2xl" style="font-size: 1.5rem"></i>
<i class="pi pi-cog text-2xl" style="font-size: 1rem"></i>
<p>Создать</p>
<!-- </router-link>-->
@@ -62,7 +71,7 @@
<div class="relative flex-col gap-2 p-2 items-center flex sm:hidden" @click="showSubmenu = !showSubmenu"
@mouseenter="showSubmenu = true" @mouseleave="showSubmenu = false">
<!-- <router-link to="/transactions/create" class="items-center flex flex-col gap-2">-->
<i class="pi pi-bars text-2xl" style="font-size: 1.5rem"></i>
<i class="pi pi-bars text-2xl" style="font-size: 1rem"></i>
<p>Меню</p>
<!-- </router-link>-->
@@ -102,6 +111,7 @@ import {useDrawerStore} from "@/stores/drawerStore";
const showSubmenu = ref(false);
const transactionType = ref<TransactionType>()
const categoryType = ref<CategoryType>()
const drawerOpened = ref(false);
@@ -125,7 +135,7 @@ const openDrawer = (selectedTransactionType = null, selectedCategoryType = null)
drawerStore.setCategoryType('EXPENSE')
}
drawerStore.setVisible( true)
drawerStore.setVisible(true)
}
@@ -157,7 +167,6 @@ const items = ref([
])
onMounted(() => {
// setTimeout(() => {
// console.log(route.params['mode']);

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import Dialog from "primevue/dialog";
</script>
<template>
<Dialog :visible="opened" modal header="Создать новый бюджет" :style="{ width: '25rem' }" @hide="cancel" @update:visible="cancel">
</Dialog>
</template>
<style scoped>
</style>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import Dialog from "primevue/dialog";
import Checkbox from "primevue/checkbox";
import Button from "primevue/button";
import InputText from "primevue/inputtext";
@@ -10,6 +10,7 @@ import {getMonthName} from "@/utils/utils";
import {Budget} from "@/models/Budget";
import {getCategories} from "@/services/categoryService";
import {useSpaceStore} from "@/stores/spaceStore";
import {createBudget, getBudgetInfos} from "@/services/budgetsService";
const props = defineProps({
opened: {
@@ -17,8 +18,8 @@ const props = defineProps({
required: true
}
})
const emits = defineEmits(['budget-created','close-modal'])
const createRecurrentPayments = ref<Boolean>(true)
const emits = defineEmits(['budget-created', 'close-modal'])
const createRecurrentPayments = ref<Boolean>(false)
const name = ref('')
const dateFrom = ref(new Date())
@@ -29,6 +30,10 @@ const budget = ref(new Budget())
const create = async () => {
try {
await createBudget(budget.value, createRecurrentPayments)
.then((res) => {
budget.value = res
})
emits("budget-created", budget.value, createRecurrentPayments.value);
} catch (e) {
console.error(e)
@@ -36,6 +41,22 @@ const create = async () => {
}
}
const creationSuccessShow = async (budget, createRecurrentPayments) => {
try {
await createBudget(budget, createRecurrentPayments)
budgetInfos.value = await getBudgetInfos()
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Бюджет создан!', life: 3000});
creationOpened.value = false
} catch (error) {
console.log(error.response.data["message"])
toast.add({severity: "error", summary: "Бюджет не создан", detail: error.response.data["message"], life: 3000});
}
// creationSuccessModal.value = true
// setTimeout(() => {
// creationSuccessModal.value = false
// }
// , 1000)
}
const categories = ref([])
const fetchCategories = async () => {
await getCategories().then(res => categories.value = res.data)
@@ -90,11 +111,9 @@ onMounted(() => {
fetchCategories()
}
})
</script>
<template>
<Dialog :visible="opened" modal header="Создать новый бюджет" :style="{ width: '25rem' }" @hide="cancel" @update:visible="cancel">
<div class="flex flex-col gap-4 mt-1">
<FloatLabel variant="on" class="w-full">
<label for="name">Название</label>
@@ -110,18 +129,16 @@ onMounted(() => {
<DatePicker v-model="budget.dateTo" id="dateTo" dateFormat="dd.mm.yy"/>
</FloatLabel>
</div>
<div class="flex flex-row items-center min-w-fit gap-4">
<Checkbox v-model="createRecurrentPayments" binary/>
Создать ежемесячные платежи?
</div>
<!-- <div class="flex flex-row items-center min-w-fit gap-4">-->
<!-- <Checkbox v-model="createRecurrentPayments" binary/>-->
<!-- Создать ежемесячные платежи?-->
<!-- </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">
<Button label="Создать" severity="success" icon="pi pi-save" @click="create"/>
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
</div>
</div>
</Dialog>
</template>
<style scoped>

View File

@@ -5,7 +5,7 @@
<div class="flex flex-row gap-4 items-center">
<h2 class="text-4xl font-bold">Бюджеты</h2>
<Button label="+ Создать" @click="creationOpened=true" size="small"/>
<BudgetCreationView :opened="creationOpened" @budget-created="creationSuccessShow"
<BudgetCreationDialogView :opened="creationOpened" @budget-created="creationSuccessShow"
@close-modal="creationOpened=false"/>
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
</div>
@@ -58,7 +58,7 @@ import {formatDate} from "@/utils/utils";
import LoadingView from "@/components/LoadingView.vue";
import Button from "primevue/button";
import ConfirmDialog from "primevue/confirmdialog";
import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
import BudgetCreationDialogView from "@/components/budgets/BudgetCreationDialogView.vue";
import StatusView from "@/components/StatusView.vue";
import {useConfirm} from "primevue/useconfirm";
import {useToast} from "primevue/usetoast";
@@ -73,22 +73,7 @@ const budgetInfos = ref<BudgetInfo[]>([])
const creationOpened = ref(false)
const creationSuccessModal = ref(false)
const creationSuccessShow = async (budget, createRecurrentPayments) => {
try {
await createBudget(budget, createRecurrentPayments)
budgetInfos.value = await getBudgetInfos()
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Бюджет создан!', life: 3000});
creationOpened.value = false
} catch (error) {
console.log(error.response.data["message"])
toast.add({severity: "error", summary: "Бюджет не создан", detail: error.response.data["message"], life: 3000});
}
// creationSuccessModal.value = true
// setTimeout(() => {
// creationSuccessModal.value = false
// }
// , 1000)
}
const deleteBudget = async (budget: Budget) => {

View File

@@ -136,7 +136,7 @@ onMounted(async () => {
<p :class="transaction.isDone && isPlanned && !props.isList ? 'line-through' : ''" class="font-bold">{{
transaction.comment
}}</p>
<p :class="transaction.isDone && isPlanned && !props.isList ? 'line-through' : ''" class="font-light">
<p :class="transaction.isDone && isPlanned && !props.isList ? 'line-through' : ''" class="font-light text-start">
{{ isPlanned ? transaction.category.icon : '' }} {{
transaction.category.name
}} |

View File

@@ -92,7 +92,14 @@ const totalIncomeLeftToGet = computed(() => {
const totalLoans = computed(() => {
let value = 0
categories.value.filter((cat) => cat.category.id == "677bc767c7857460a491bd49").forEach(cat => {
categories.value.filter((cat) =>
cat.category.tags.some(tag => {
return tag.code == "loans";
})
).forEach(cat => {
// console.log(cat)
value += cat.currentLimit
})
return value
@@ -258,13 +265,14 @@ watch(
// 4⃣ Computed для группировки транзакций расходов по категориям
const categoriesTransactions = computed(() => {
const grouped: Record<string, { name: any; transactions: any[]; isOpened: boolean }> = {};
const grouped: Record<string, { name: any; transactions: any[]; notDoneValue: any; isOpened: boolean }> = {};
// Создаем группу для каждой категории из списка категорий расходов (categories)
categories.value.forEach((category) => {
grouped[category.category.id] = {
name: category,
transactions: [],
notDoneValue: 0,
isOpened: expenseCategoriesState[category.category.id]?.isOpened ?? true,
};
});
@@ -272,14 +280,21 @@ const categoriesTransactions = computed(() => {
// Добавляем транзакции расходов (plannedExpenses)
plannedExpenses.value.forEach((plannedExpense) => {
const categoryId = plannedExpense.category.id;
// console.log(!plannedExpense.isDone ? grouped[categoryId].notDoneValue + plannedExpense.amount : grouped[categoryId].notDoneValue + 0 )
if (!grouped[categoryId]) {
grouped[categoryId] = {
name: plannedExpense.category,
transactions: [],
notDoneValue: !plannedExpense.isDone ? grouped[categoryId].notDoneValue + plannedExpense.amount : grouped[categoryId].notDoneValue + 0,
isOpened: expenseCategoriesState[categoryId]?.isOpened ?? true,
};
}
grouped[categoryId].transactions.push(plannedExpense);
if (!plannedExpense.isDone) {
grouped[categoryId].notDoneValue += plannedExpense.amount;
}
});
// Преобразуем объект в массив, сортируем, затем превращаем обратно в объект
@@ -728,7 +743,11 @@ watch([budget, plannedExpenses], () => {
calendar.value = result;
}, {immediate: true});
const mobileViewModes = ref([{code: "planned", name: "Планирование"}, {
code: "transactions",
name: "Фактические траты"
}])
const mobileViewMode = ref({code: "planned", name: "Планирование"})
const spaceStore = useSpaceStore()
const selectedSpace = computed(() => spaceStore.space)
@@ -822,7 +841,7 @@ onUnmounted(async () => {
<h3 class="text-2xl font-bold text-gray-700">Аналитика</h3>
<div class=" flex flex-col gap-3 items-start ">
<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">
<SelectButton v-model="selectedChart" :options="modes" optionLabel="label" optionIcon="icon" style=" border-width: 0px !important; ">
<template #option="slotProps">
<i :class="slotProps.option.icon"></i>
</template>
@@ -831,20 +850,22 @@ onUnmounted(async () => {
:options="incomeExpenseChartOptions" class="!w-full"
style="width: 100%"/>
<Chart v-if="selectedChart.value=='pie'" type="pie" :data="pieChartData" :options="pieChartOptions"
class="chart "/>
<div v-if="selectedChart.value=='pie'" class="chart w-full flex items-center justify-center">
<Chart type="pie" :data="pieChartData" :options="pieChartOptions"
/>
</div>
</div>
<div></div>
<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="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-row gap-1 items-center w-full">
<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">
<div class="group flex flex-row justify-between w-full">
<i class="pi pi-info-circle w-full text-end" style="font-size: 0.7rem"
v-tooltip="'Рассчитывается по принципу сумма плановых поступлений'"/>
v-tooltip.focus="'Рассчитывается по принципу сумма плановых поступлений'" tabindex="1"/>
<!-- <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>-->
@@ -861,7 +882,8 @@ onUnmounted(async () => {
<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="'Рассчитывается по принципу сумма плановых трат + разница между суммой плана категории и лимитом категории'"/>
v-tooltip.focus="'Рассчитывается по принципу сумма плановых трат + разница между суммой плана категории и лимитом категории'"
tabindex="1"/>
</div>
<div class="flex flex-col items-center">
@@ -878,7 +900,7 @@ onUnmounted(async () => {
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="'Разница между плановыми приходами и расходами'"/>
v-tooltip.focus="'Разница между плановыми приходами и расходами'" tabindex="1"/>
</div>
<h4 class="text-lg font-bold">Остаток</h4>
@@ -891,7 +913,7 @@ onUnmounted(async () => {
</div>
</button>
</div>
<!-- <div class="grid grid-cols-2 !gap-1 mt-4" :class="detailedShowed ? 'block' : 'hidden'">-->
<!-- <div class="flex flex-col items-center font-bold ">-->
@@ -926,13 +948,14 @@ onUnmounted(async () => {
</div>
<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-row justify-between gap-2 items-end w-full"
>
<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="'Сумма категории Кредиты и долги в сумме лимитов категорий'"/>
v-tooltip.focus="'Для корректного подсчета нужно выставить тэг loans на категориях долгов'"
tabindex="1"/>
</div>
<h4 class="text-sm lg:text-base">Долги</h4>
@@ -947,7 +970,8 @@ onUnmounted(async () => {
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 на категориях сбережений'"/>
v-tooltip.focus="'Для корректного подсчета нужно выставить тэг savings на категориях сбережений'"
tabindex="1"/>
</div>
<span class="text-sm lg:text-base font-bold">Сбережения</span>
@@ -963,7 +987,7 @@ onUnmounted(async () => {
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="'Сумма остатка лимитов по остальным категориям'"/>
v-tooltip.focus="'Сумма остатка лимитов по остальным категориям'" tabindex="1"/>
</div>
<h4 class="text-sm lg:text-base">Ежедневные</h4>
@@ -973,7 +997,7 @@ onUnmounted(async () => {
</div>
</div>
</button>
</div>
</div>
@@ -984,8 +1008,8 @@ onUnmounted(async () => {
<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">
<button class="flex flex-row justify-between gap-1 items-center w-full"
@click="detailedShowed = !detailedShowed">
<div class="flex flex-row justify-between gap-1 items-center w-full"
>
<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>
<div class="font-light text-center w-full ">
@@ -1010,7 +1034,7 @@ onUnmounted(async () => {
{{ formatAmount(totalInstantIncomes - totalInstantExpenses) }}
</div>
</div>
</button>
</div>
<div class="grid grid-cols-2 !gap-2 mt-4" :class="detailedShowed ? 'block' : 'hidden'">
<div v-for="categorySum in transactionCategoriesSums"
@@ -1054,11 +1078,15 @@ onUnmounted(async () => {
</div>
</div>
<div class="grid grid-cols-1 gap-1 row-span-3 col-span-2">
<div class="w-full flex items-center justify-center px-2 py-2 bg-gray-100 md:hidden flex">
<SelectButton v-model="mobileViewMode" :options="mobileViewModes" option-label="name"
class=" "/>
</div>
<div v-if="mobileViewMode.code=='planned'" class="flex flex-col gap-4">
<div class="grid grid-cols-1 gap-4 row-span-3 col-span-2">
<div>
<!-- Планируемые доходы -->
<div>
<div >
<div class="flex flex-row gap-4 items-center mb-4">
<h3 class="text-2xl font-bold text-emerald-500 ">Поступления</h3>
<button @click="openDrawer('PLANNED', 'INCOME')">
@@ -1117,18 +1145,18 @@ onUnmounted(async () => {
</li>
</ul>
</div>
</div>
<div>
<!-- Планируемые расходы -->
<div class="pb-4">
<div class="flex flex-col gap-1">
<div class="flex flex-row gap-4 items-center mb-4">
<div class="flex flex-row gap-0 items-center ">
<h3 class="text-2xl font-bold text-rose-500 ">Расходы</h3>
<button @click="openDrawer('PLANNED', 'EXPENSE')">
<span class="font-light text-sm">+ Добавить</span>
</button>
</div>
<div class="flex flex-col gap-4 pb-4">
<div class="flex flex-col gap-1 ">
<div class="flex flex-row gap-2"><span class="text-lg font-bold items-center">Календарь</span>
<button class="font-light text-sm" @click="toggleCalendar">
{{ calendarExpanded ? 'Скрыть' : 'Раскрыть' }}
@@ -1154,7 +1182,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-2">
<li v-for="(category, categoryId) in categoriesTransactions" :key="categoryId"
class="flex flex-col justify-between p-4 shadow-md rounded-lg bg-white ">
<div class="">
@@ -1210,36 +1238,48 @@ onUnmounted(async () => {
@transaction-updated="updateTransactions"
@transaction-checked="updateTransactions"
/>
<div
class="flex justify-between bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">
<div>
<p class=" font-bold text-gray-700 dark:text-gray-400">
🗓 Остаток на внеплановые
</p>
</div>
<div class="text-lg line-clamp-1 ">
{{
formatAmount(category.name.currentLimit - category.name.currentPlanned)
}}
</div>
</div>
<!-- <div-->
<!-- class="flex justify-between bg-white min-w-fit max-h-fit flex-row items-center gap-4 w-full ">-->
<!-- <div>-->
<!-- <p class=" font-bold text-gray-700 dark:text-gray-400">-->
<!-- 🗓 Остаток на внеплановые-->
<!-- </p>-->
<!-- </div>-->
<!-- <div class="text-lg line-clamp-1 ">-->
<!-- {{-->
<!-- formatAmount(category.name.currentLimit - category.name.currentPlanned)-->
<!-- }} -->
<!-- </div>-->
<!-- </div>-->
</div>
</transition>
</div>
<div class="flex flex-col gap-0">
<div class="flex flex-row justify-between w-full items-center">
<span class="font-bold">🗓 Остаток всего:</span>
<span class="font-bold text-lg">{{
formatAmount(category.name.currentLimit - category.name.currentSpent)
<div class="flex flex-row gap-1 w-full items-center">
<span class="font-bold">{{
category.name.currentLimit - category.name.currentSpent >= 0 ? 'Остаток в категории' : 'Превышение на'
}}</span>
<span class="font-bold "
:class="category.name.currentLimit - category.name.currentSpent < 0? 'text-red-500' : ''">{{
formatAmount(Math.abs(category.name.currentLimit - category.name.currentSpent))
}} </span>
</div>
<span v-if="category.transactions.filter(t => !t.isDone).length>0" class="font-light "
:class="category.name.currentLimit - category.name.currentSpent - category.notDoneValue < 0 ? 'text-red-500': ''">искл. оставшиеся плановые
<!-- {{ category.transactions.filter(t => !t.isDone ).forEach( t=> t.amount)}}-->
{{
formatAmount(category.name.currentLimit - category.name.currentSpent - category.notDoneValue)
}} </span>
</div>
</li>
</ul>
</div>
</div>
</div>
<div class=" h-fit gap-4 flex-col row-span-6 lg:hidden ">
<div v-else class=" h-fit gap-4 flex-col row-span-6 lg:hidden ">
<div class="flex flex-row ">
<h3 class="text-2xl font-bold">Транзакции</h3>
<button @click="openDrawer('INSTANT', 'EXPENSE')">
@@ -1267,6 +1307,7 @@ onUnmounted(async () => {
</div>
</div>
</div>
</div>
</div>
@@ -1300,8 +1341,7 @@ onUnmounted(async () => {
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.3);
}
.chart {
width: 70%;
}
</style>

View File

@@ -36,7 +36,7 @@ onMounted(async () => {
<div v-else class="">
<div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4">
<div class="flex flex-row items-center min-w-fit justify-between">
<p class="text-2xl font-bold">Категории</p>
<p class="text-2xl ">Категории</p>
<router-link to="/settings/categories">
<Button size="large" icon="pi pi-arrow-circle-right" severity="secondary" text rounded/>
</router-link>

View File

@@ -101,38 +101,38 @@ onMounted(async () => {
<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>
<h1 class=" text-4xl 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>
<!-- </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">

View File

@@ -3,7 +3,7 @@
import CategorySettingView from "@/components/settings/CategorySettingView.vue";
import RecurrentSettingView from "@/components/settings/RecurrentSettingView.vue";
import {ref} from "vue";
import {onMounted, ref} from "vue";
import Divider from "primevue/divider";
import CategoriesList from "@/components/settings/categories/CategoriesList.vue";
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
@@ -11,6 +11,18 @@ import CommonSettings from "@/components/settings/CommonSettings.vue";
const selectedModeCode = ref("common")
const selectMode = (mode: string) => {
localStorage.setItem("selectedSettingPage", mode);
selectedModeCode.value = mode;
}
onMounted(() => {
const localStorageMode = localStorage.getItem("selectedSettingPage");
if (localStorageMode) {
selectedModeCode.value = localStorageMode;
} else selectedModeCode.value = "common";
})
const pages = ref([
{
@@ -46,26 +58,29 @@ const pages = ref([
<ul class=" flex-col gap-1 ">
<li v-for="page in pages" :key="page.code"
class="flex flex-row gap-2 p-2 hover:bg-emerald-100 hover:text-emerald-700 hover:rounded-md items-center"
:class="selectedModeCode == page.code ? 'bg-blue-100' : ''" @click="selectedModeCode = page.code">
:class="selectedModeCode == page.code ? 'bg-blue-100' : ''" @click="selectMode(page.code)">
<i :class="page.icon"/>{{ page.title }}
</li>
</ul>
<Divider layout="vertical"/>
</div>
<div class="flex xl:hidden sm:col-span-1 w-full h-fit p-2 mb-2 overflow-y-scroll outline outline-1 outline-gray-300 rounded-xl">
<div
class="flex xl:hidden sm:col-span-1 w-full h-fit p-2 mb-2 overflow-y-scroll outline outline-1 outline-gray-300 rounded-xl">
<ul class=" min-w-fit w-full items-start">
<li v-for="page in pages"
class=" flex flex-row gap-2 px-2 py-2 hover:bg-blue-50 rounded-md line-clamp-1 w-full"
:class="selectedModeCode == page.code ? '!bg-emerald-50 text-emerald-700': '' " @click="selectedModeCode=page.code">
:class="selectedModeCode == page.code ? '!bg-emerald-50 text-emerald-700': '' "
@click="selectMode(page.code)">
<i :class="page.icon"/>{{ page.title }}
</li>
</ul>
</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 justify-start justify-items-start">
<CommonSettings v-if="selectedModeCode == 'common'"/>
<CategoriesList v-else-if="selectedModeCode == 'categories'"/>
<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>

View File

@@ -2,13 +2,13 @@
<div v-if="loading">
<LoadingView/>
</div>
<div v-else class="flex flex-col bg-gray-100 dark:bg-gray-800 h-screen p-4">
<div v-else class="flex flex-col bg-gray-100 pb-4 ">
<ConfirmDialog/>
<div v-if="!space">Выберите сперва пространство</div>
<!-- Заголовок и кнопка добавления категории -->
<div v-else>
<div class="flex flex-col gap-4 xl:flex-row justify-between bg-gray-100">
<h2 class="text-5xl font-bold">Категории</h2>
<h2 class="text-4xl ">Категории</h2>
<Button label="Добавить категорию" icon="pi pi-plus" class="text-sm" @click="openCreateDialog(null)"/>
</div>
@@ -21,62 +21,15 @@
</div>
<!-- Переключатель категорий (доходы/расходы) -->
<div class="card flex lg:hidden justify-center mb-4">
<div class="card flex justify-center w-full ">
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
class="!bg-emerald-50 !border-emerald-600 "
aria-labelledby="category-switch"/>
</div>
<!-- Список категорий с прокруткой для больших экранов -->
<div class="flex">
<div class="hidden lg:grid grid-cols-2 gap-x-10 w-full h-full justify-items-center overflow-y-auto">
<!-- Категории доходов -->
<div class="grid h-full w-full min-w-fit overflow-y-auto">
<div class=" gap-4 ">
<div class="flex flex-row gap-2 ">
<h3 class="text-2xl">Поступления</h3>
<Button icon="pi pi-plus" rounded outlined class="p-button-success"
@click="openCreateDialog('INCOME')"/>
</div>
</div>
<div class=" overflow-y-auto mt-2 space-y-2 px-2">
<CategoryListItem
class=""
v-for="category in filteredIncomeCategories"
:key="category.id"
:category="category"
v-bind="category"
@open-edit="openEdit"
@delete-category="confirmDelete"
/>
</div>
</div>
<!-- Категории расходов -->
<div class="grid h-full w-full min-w-fit overflow-y-auto">
<div class=" gap-4 justify-between ">
<div class="flex flex-row gap-2">
<h3 class="text-2xl">Расходы</h3>
<Button icon="pi pi-plus" rounded outlined class="p-button-success !hover:bg-green-600"
@click="openCreateDialog('EXPENSE')"/>
</div>
</div>
<div class=" overflow-y-auto pb-10 mt-2 space-y-2 px-2">
<CategoryListItem
v-for="category in filteredExpenseCategories"
:key="category.id"
:category="category"
v-bind="category"
@open-edit="openEdit"
@delete-category="confirmDelete"
/>
</div>
</div>
</div>
</div>
<!-- Для маленьких экранов -->
<div class="flex lg:hidden flex-wrap rounded w-full">
<div class=" flex flex-col md:grid md:grid-cols-2 lg:grid-cols-2 gap-1 justify-between">
<CategoryListItem
v-for="category in filteredCategories"
:key="category.id"
@@ -84,13 +37,14 @@
v-bind="category"
class="mt-2"
@open-edit="openEdit"
@delete-category="confirmDelete"
@delete-category="deleteCat"
/>
</div>
<CreateCategoryModal
v-if="isDialogVisible"
:show="isDialogVisible"
:show-tags="true"
:categoryTypes="categoryTypes"
:selectedCategoryType="selectedCategoryType"
:category="editingCategory"
@@ -113,8 +67,7 @@ import CreateCategoryModal from './CreateCategoryModal.vue';
import CategoryListItem from '@/components/settings/categories/CategoryListItem.vue';
import {Category, CategoryType} from '@/models/Category';
import {
createCategory,
deleteCategory,
createCategory, deleteCategoryRequest,
getCategories,
getCategoryTypes,
} from "@/services/categoryService";
@@ -132,8 +85,6 @@ const expenseCategories = ref<Category[]>([]);
const incomeCategories = ref<Category[]>([]);
const editingCategory = ref<Category | null>(null);
const isDialogVisible = ref(false);
const confirm = useConfirm();
const toast = useToast();
const fetchCategories = async () => {
loading.value = true
@@ -205,37 +156,11 @@ const saveCategory = async (newCategory: Category) => {
closeCreateDialog();
};
const confirmDelete = async (category: Category) => {
confirm.require({
message: 'Вы уверены, что хотите выполнить это действие?\n Это нельзя будет отменить.\nВсе транзакции данной категории будут перенесены в категорию "Другое".\n',
header: `Удаление категории ${category.name}`,
icon: 'pi pi-info-circle',
rejectLabel: 'Cancel',
rejectProps: {
label: 'Cancel',
severity: 'secondary',
outlined: true
},
acceptProps: {
label: 'Delete',
severity: 'danger'
},
accept: async () => {
await deleteCategory(category.id);
await fetchCategories();
toast.add({severity: 'info', summary: 'Confirmed', detail: 'Record deleted', life: 3000});
},
reject: () => {
toast.add({severity: 'error', summary: 'Rejected', detail: 'You have rejected', life: 3000});
}
});
};
const deleteCat = async (categoryId: number) => {
await deleteCategory(categoryId);
// await deleteCategory(categoryId);
await fetchCategories();
}
@@ -268,19 +193,22 @@ watch(
loading.value = false;
})
await fetchCategoryTypes();
} catch (error) {
console.error('Error fetching budget infos:', error);
}
}})
}
})
onMounted(async () => {
onMounted(async () => {
if (space.value) {
await fetchCategories();
console.log("here");
await fetchCategoryTypes();
}
loading.value = false;
})
})
</script>

View File

@@ -0,0 +1,214 @@
<script setup lang="ts">
import {ref, watch, computed, PropType, onMounted, defineProps} from 'vue';
import Button from "primevue/button";
import SelectButton from "primevue/selectbutton";
import {Category, 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";
const toast = useToast();
const props = defineProps({
categoryTypes: Object as PropType<CategoryType[]>,
categoryProp: Object as PropType<Category | null>, // Категория для редактирования (null, если создание)
})
const emit = defineEmits(['saveCategory', 'close-modal'])
const category = ref<Category>(props.categoryProp)
const icon = ref('🐱');
const name = ref('');
const description = ref('');
const showEmojiPicker = ref(false);
const categoryType = ref(props.categoryProp?.type || props.categoryTypes[0]); // Тип по умолчанию
const isEditing = computed(() => !!props.categoryProp); // Если есть категория, значит редактирование
const availableTags = computed(() => {
const availableTags = []
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 = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '😍', '🥰', '😘', '🍎', '🍔', '🍕', '🍣', '☕', '🍺', '🚗', '🚕', '🚴‍♂️', '🚆', '✈️', '⛴️', '🛒', '👗', '💍', '👟', '🛍️', '🎮', '🎥', '🎧', '🎢', '🎨', '🏠', '🛏️', '🧹', '🪴', '🍼', '🏥', '💊', '🩺', '🦷', '💳', '💰', '🏦', '🌍', '🗺️', '🏝️', '🏔️', '💻', '📚', '🖋️', '🏫'];
const toggleEmojiPicker = () => {
showEmojiPicker.value = !showEmojiPicker.value;
};
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
emit("saveCategory", category.value)
}).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 изменился, получаем новую информацию о бюджете
if (selectedSpace.value) {
await fetchTags();
}
constructCategory()
} catch (error) {
console.error('Error fetching budget infos:', error);
}
}
}
);
const constructCategory = () => {
if (props.categoryProp) {
category.value = new Category(props.categoryProp.id, props.categoryProp.type, props.categoryProp.name, props.categoryProp.description, props.categoryProp.icon, props.categoryProp.tags);
} else {
category.value = new Category(null, categoryType.value, '', '', icon.value, []);
}
}
onMounted(async () => {
console.log(props)
constructCategory()
// console.log(props.category)
if (selectedSpace.value) {
await fetchTags();
}
console.log(category.value);
})
</script>
<template>
<div class="flex flex-col gap-2">
<div class="flex justify-center gap-4">
<!-- <Button rounded outlined @click="toggleEmojiPicker" class=" flex justify-center !rounded-full !text-4xl !p-4"-->
<!-- size="large" :label="category.icon"/>-->
</div>
{{ category }}
{{props.categoryTypes}}
{{categoryType}}
<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"
v-if="showEmojiPicker">
<Button v-for="emoji in emojis" class="!p-4" @click="selectEmoji(emoji)" :label="emoji" text/>
</div>
<!-- SelectButton для выбора типа категории -->
<div class="flex justify-center mt-4">
<!-- <SelectButton v-if="!isEditing" v-model="category.type" :options="categoryTypes" optionLabel="name"/>-->
</div>
<div>
<!-- Поля для создания/редактирования категории -->
<label for="newCategoryName">Название категории:</label>
<!-- <input v-modezl="category.name" type="text" id="newCategoryName"/>-->
<label for="newCategoryDesc">Описание категории:</label>
<!-- <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-0">
<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>
<!-- Кнопки -->
<div class="button-group">
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
<button @click="closeModal" class="close-modal-btn">Отмена</button>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,13 +1,19 @@
<script setup lang="ts">
import { Category } from "@/models/Category";
import { PropType } from "vue";
import {Category} from "@/models/Category";
import {PropType} from "vue";
import Button from "primevue/button";
import Tag from "primevue/tag";
import {deleteCategoryRequest} from "@/services/categoryService";
import {useConfirm} from "primevue/useconfirm";
import {useToast} from "primevue/usetoast";
// Определение входных параметров (props)
const props = defineProps({
category: { type: Object as PropType<Category>, required: true }
category: {type: Object as PropType<Category>, required: true}
});
const confirm = useConfirm();
const toast = useToast()
// Определение событий (emits)
const emit = defineEmits(["open-edit", "delete-category"]);
@@ -18,15 +24,43 @@ const openEdit = () => {
};
// Функция для удаления категории
const deleteCategory = () => {
const confirmDelete = async () => {
confirm.require({
message: 'Вы уверены, что хотите выполнить это действие?\n Это нельзя будет отменить.\nВсе транзакции данной категории будут перенесены в категорию "Другое".\n',
header: `Удаление категории ${props.category.name}`,
icon: 'pi pi-info-circle',
rejectLabel: 'Cancel',
rejectProps: {
label: 'Cancel',
severity: 'secondary',
outlined: true
},
acceptProps: {
label: 'Delete',
severity: 'danger'
},
accept: async () => {
await deleteCategoryRequest(props.category.id).then((result) => {
emit("delete-category", props.category);
toast.add({severity: 'info', summary: 'Confirmed', detail: 'Record deleted', life: 3000});
}).catch(((err) => {
toast.add({severity: 'error', summary: 'Rejected', detail: 'Ошибка удаления категории', life: 3000});
})
)
},
reject: () => {
// toast.add({severity: 'error', summary: 'Rejected', detail: 'You have rejected', life: 3000});
}
});
emit("delete-category", props.category); // Использование события для удаления категории
};
</script>
<template>
<div class="flex rounded-xl border-2 bg-white shadow-xl min-w-fit max-h-fit gap-5 flex-row items-center justify-between w-full p-2">
<div
class="flex flex-col rounded-xl border-2 bg-white shadow-xl min-w-fit max-h-fit items-center justify-between w-full p-2 gap-2">
<div class="flex items-center justify-between w-full">
<div class="flex flex-row items-center p-x-4 gap-4">
<p class="text-6xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
<div class="flex flex-col items-start justify-items-start w-full">
@@ -34,10 +68,15 @@ const deleteCategory = () => {
<p class="font-light line-clamp-1">{{ category.description }}</p>
</div>
</div>
<div class="flex flex-row items-center p-x-4 gap-2 ">
<Button icon="pi pi-pen-to-square" rounded @click="openEdit"/>
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
<div class="flex flex-row items-center px-4 gap-2 ">
<button @click="openEdit"><i class="pi pi-pen-to-square"/></button>
<button @click="confirmDelete"><i class="pi pi-trash"/></button>
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
</div>
</div>
<div class="flex flex-row items-start w-full justify-start p-x-4 gap-2 ">
<Tag v-if="category.tags.length>0" v-for="tag in category.tags">{{ tag.name }} ({{ tag.code }})</Tag>
<span class="p-1">&nbsp;</span>
</div>
</div>
</template>

View File

@@ -1,12 +1,15 @@
<template>
<Dialog :visible="show" modal :header="isEditing ? 'Edit Category' : 'Create New Category'" :closable="false"
class="w-[95%] xl:!w-1/3">
<div class="flex flex-col gap-2">
<div class="flex justify-center gap-4">
<Button rounded outlined @click="toggleEmojiPicker" class=" flex justify-center !rounded-full !text-4xl !p-4"
size="large" :label="category.icon"/>
</div>
<!-- {{ // category }}-->
<!-- {{props.categoryTypes}}-->
<!-- {{categoryType}}-->
<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"
v-if="showEmojiPicker">
@@ -17,14 +20,14 @@
<div class="flex justify-center mt-4">
<SelectButton v-if="!isEditing" v-model="category.type" :options="categoryTypes" optionLabel="name"/>
</div>
<div>
<!-- Поля для создания/редактирования категории -->
<label for="newCategoryName">Название категории:</label>
<input v-model="category.name" type="text" id="newCategoryName"/>
<label for="newCategoryDesc">Описание категории:</label>
<input v-model="category.description" type="text" id="newCategoryDesc"/>
<div class="flex flex-col flex-wrap gap-0">
<div v-if="showTags" 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 }}
@@ -32,8 +35,8 @@
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">
<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-for="tag in availableTags" :key="tag.id" class="w-fit " @click="addTag(tag)">
@@ -41,11 +44,14 @@
</Tag>
</div>
</div>
</div>
</div>
<!-- Кнопки -->
<div class="button-group">
<button @click="saveCategory" class="create-category-btn">{{ isEditing ? 'Сохранить' : 'Создать' }}</button>
<button @click="closeModal" class="close-modal-btn">Отмена</button>
</div>
</div>
</Dialog>
</template>
@@ -59,11 +65,15 @@ import Tag from "primevue/tag";
import {useSpaceStore} from "@/stores/spaceStore";
import {createCategory, editCategoryRequest, getTagsRequest} from "@/services/categoryService";
import {useToast} from "primevue/usetoast";
import {useConfirm} from "primevue/useconfirm";
const toast = useToast();
const confirm = useConfirm();
const props = defineProps({
show: Boolean,
showTags: Boolean,
categoryTypes: Object as PropType<CategoryType[]>,
category: Object as PropType<Category | null>, // Категория для редактирования (null, если создание)
})
@@ -107,11 +117,17 @@ const saveCategory = async () => {
category.value = res.data
emit("saveCategory", category.value)
}).catch(err =>
toast.add({severity: 'error', summary: 'Ошибка сохранения категории', detail: err.response.data.message, life: 3000})
toast.add({
severity: 'error',
summary: 'Ошибка сохранения категории',
detail: err.response.data.message,
life: 3000
})
)
} else {
await createCategory(category.value).then((res) => {
category.value = res.data
emit("saveCategory", category.value)
}).catch(err =>
toast.add({
severity: 'error',

View File

@@ -1,27 +1,27 @@
<template>
<Dialog :visible="show" modal :header="isEditing ? 'Edit Recurrent Payment' : 'Create Recurrent Payment'"
:closable="true" class="!w-1/3">
<Dialog :visible="show" modal :header="isEditing ? 'Редактировать повторяющийся платеж' : 'Создать повторяющийся платеж'"
:closable="true" class="!w-5/6 xl:!w-2/4">
<div v-if="loading">
Loading...
</div>
<div v-else class="p-fluid flex flex-col gap-6 w-full py-6 items-start">
<!-- Название -->
<FloatLabel class="w-full">
<label for="paymentName">Payment Name</label>
<FloatLabel class="w-full" variant="on">
<label for="paymentName">Название платежа</label>
<InputText v-model="name" id="paymentName" class="!w-full"/>
</FloatLabel>
<!-- Категория -->
<div class="relative w-full justify-center justify-items-center ">
<div class="flex flex-col justify-items-center gap-2">
<div class="flex flex-col justify-items-center gap-2 w-full">
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
aria-labelledby="basic"
@change="categoryTypeChanged(selectedCategoryType.code)" class="justify-center"/>
@change="categoryTypeChanged(selectedCategoryType.code)" class="justify-start w-fit"/>
<button class="border border-gray-300 rounded-lg w-full z-50"
@click="isCategorySelectorOpened = !isCategorySelectorOpened">
<div class="flex flex-row items-center pe-4 py-2 gap-4">
<div class="flex flex-row justify-between w-full px-4">
<p class="text-6xl font-bold text-gray-700 dark:text-gray-400">{{ selectedCategory.icon }}</p>
<div class="flex flex-row justify-between items-center w-full px-4 gap-4">
<p class="text-3xl font-bold text-gray-700 dark:text-gray-400">{{ selectedCategory.icon }}</p>
<div class="flex flex-col items-start justify-items-start justify-around w-full">
<p class="font-bold text-start">{{ selectedCategory.name }}</p>
<p class="font-light line-clamp-1 items-start text-start">{{ selectedCategory.description }}</p>
@@ -38,12 +38,12 @@
<!-- Анимированное открытие списка категорий -->
<div v-show="isCategorySelectorOpened"
class="absolute left-0 right-0 top-full overflow-hidden z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
:class="{ 'max-h-0': !isCategorySelectorOpened, 'max-h-[500px]': isCategorySelectorOpened }">
:class="{ 'max-h-0': !isCategorySelectorOpened, '': isCategorySelectorOpened }">
<div class="grid grid-cols-2 mt-2">
<button v-for="category in selectedCategoryType.code == 'EXPENSE' ? expenseCategories : incomeCategories"
:key="category.id" class="border rounded-lg mx-2 mb-2"
@click="selectCategory(category)">
<div class="flex flex-row justify-between w-full px-2">
<div class="flex flex-row justify-between w-full p-1 gap-2">
<p class="text-4xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
<div class="flex flex-col items-start justify-items-start justify-around w-full">
<p class="font-bold text-start">{{ category.name }}</p>
@@ -56,9 +56,9 @@
</div>
<!-- Описание -->
<FloatLabel class="w-full">
<label for="description">Description</label>
<Textarea v-model="description" id="description" rows="3"/>
<FloatLabel class="!w-full">
<label for="description">Описание</label>
<Textarea v-model="description" id="description" rows="3" class="!w-full"/>
</FloatLabel>
<!-- Дата повторения (выпадающий список) -->
@@ -92,14 +92,14 @@
<!-- Сумма -->
<InputGroup class="w-full">
<InputGroupAddon></InputGroupAddon>
<InputNumber v-model="amount" placeholder="Amount"/>
<InputNumber v-model="amount" placeholder="Сумма"/>
<InputGroupAddon>.00</InputGroupAddon>
</InputGroup>
<!-- Кнопки -->
<div class="flex justify-content-end gap-2 mt-4">
<Button label="Save" icon="pi pi-check" @click="savePayment" class="p-button-success"/>
<Button label="Cancel" icon="pi pi-times" @click="closeModal" class="p-button-secondary"/>
<Button label="Сохранить" icon="pi pi-check" @click="savePayment" class="p-button-success"/>
<Button label="Отмена" icon="pi pi-times" @click="closeModal" class="p-button-secondary"/>
</div>
</div>
</Dialog>
@@ -133,7 +133,8 @@ const loading = ref(false)
// Поля для формы
const name = ref('');
const selectedCategoryType = ref(props.payment ? props.payment.type : props.categoryTypes[0]);
const selectedCategory = ref(selectedCategoryType.code == 'EXPESE' ? props.expenseCategories[0] : props.incomeCategories[0]);
console.log(props.categoryTypes)
const selectedCategory = ref(selectedCategoryType.code == 'EXPENSE' ? props.expenseCategories[0] : props.incomeCategories[0]);
const categoryTypeChanged = (code) => {

View File

@@ -0,0 +1,220 @@
<script setup lang="ts">
import InputText from "primevue/inputtext";
import SelectButton from "primevue/selectbutton";
import Button from "primevue/button";
import InputGroupAddon from "primevue/inputgroupaddon";
import FloatLabel from "primevue/floatlabel";
import Textarea from "primevue/textarea";
import InputGroup from "primevue/inputgroup";
import InputNumber from "primevue/inputnumber";
import {computed, defineEmits, ref, watch} from "vue";
import {saveRecurrentPayment} from "@/services/recurrentService";
const props = defineProps({
show: Boolean, // Показать/скрыть модальное окно
expenseCategories: Array, // Внешние данные для списка категорий
incomeCategories: Array, // Внешние данные для списка категорий
categoryTypes: Array,
payment: Object | null // Для редактирования существующего платежа
})
const emits = defineEmits(["open-edit", "save-payment", "close-modal"]);
const loading = ref(false)
// Поля для формы
const name = ref('');
const selectedCategoryType = ref(props.payment ? props.payment.type : props.categoryTypes[0]);
const selectedCategory = ref(selectedCategoryType.code == 'EXPESE' ? props.expenseCategories[0] : props.incomeCategories[0]);
const categoryTypeChanged = (code) => {
selectedCategory.value = code == "EXPENSE" ? props.expenseCategories[0] : props.incomeCategories[0];
}
const description = ref('');
const repeatDay = ref<number | null>(null);
const amount = ref<number | null>(null);
// Открытие/закрытие списка категорий
const isCategorySelectorOpened = ref(false);
const isDaySelectorOpened = ref(false);
// Список дней (131)
const days = Array.from({length: 31}, (_, i) => i + 1);
// Выбор дня
const selectDay = (day: number) => {
repeatDay.value = day;
isDaySelectorOpened.value = false;
};
// Выбор категории
const selectCategory = (category) => {
isCategorySelectorOpened.value = false;
selectedCategory.value = category;
};
// Определение, редактируем ли мы существующий платеж
const isEditing = computed(() => !!props.payment);
// Слушаем изменения, если редактируем существующий платеж
watch(() => props.payment, (newPayment) => {
if (newPayment) {
name.value = newPayment.name;
selectedCategory.value = newPayment.category;
description.value = newPayment.description;
repeatDay.value = newPayment.repeatDay;
amount.value = newPayment.amount;
} else {
resetForm();
}
});
// Функция для сохранения платежа
const savePayment = async () => {
loading.value = true;
const paymentData = {
name: name.value,
category: selectedCategory.value,
description: description.value,
atDay: repeatDay.value,
amount: amount.value
};
if (isEditing.value && props.payment) {
paymentData.id = props.payment.id; // Если редактируем, сохраняем ID
}
try {
await saveRecurrentPayment(paymentData)
loading.value = false
resetForm();
} catch (error) {
console.error('Error saving payment:', error);
}
emits('save-payment', paymentData);
resetForm();
closeModal();
};
// Закрытие окна и сброс формы
const closeModal = () => {
emits('close-modal');
resetForm();
};
const resetForm = () => {
name.value = '';
selectedCategory.value = props.expenseCategories[0];
description.value = '';
repeatDay.value = null;
amount.value = null;
};
</script>
<template>
<div class="p-fluid flex flex-col gap-6 w-full py-6 items-start">
<!-- Название -->
<FloatLabel class="w-full" variant="on">
<label for="paymentName">Название платежа</label>
<InputText v-model="name" id="paymentName" class="!w-full"/>
</FloatLabel>
<!-- Категория -->
<div class="relative w-full justify-center justify-items-center ">
<div class="flex flex-col justify-items-center gap-2 w-full">
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
aria-labelledby="basic"
@change="categoryTypeChanged(selectedCategoryType.code)" class="justify-start"/>
<button class="border border-gray-300 rounded-lg w-full z-50"
@click="isCategorySelectorOpened = !isCategorySelectorOpened">
<div class="flex flex-row items-center pe-4 py-2 gap-4">
<div class="flex flex-row justify-between items-center w-full px-4 gap-4">
<p class="text-3xl font-bold text-gray-700 dark:text-gray-400">{{ selectedCategory.icon }}</p>
<div class="flex flex-col items-start justify-items-start justify-around w-full">
<p class="font-bold text-start">{{ selectedCategory.name }}</p>
<p class="font-light line-clamp-1 items-start text-start">{{ selectedCategory.description }}</p>
</div>
</div>
<div>
<span :class="{'rotate-90': isCategorySelectorOpened}"
class="pi pi-angle-right transition-transform duration-300 text-5xl"/>
</div>
</div>
</button>
</div>
<!-- Анимированное открытие списка категорий -->
<div v-show="isCategorySelectorOpened"
class="absolute left-0 right-0 top-full overflow-hidden z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
:class="{ 'max-h-0': !isCategorySelectorOpened, '': isCategorySelectorOpened }">
<div class="grid grid-cols-2 mt-2">
<button v-for="category in selectedCategoryType.code == 'EXPENSE' ? expenseCategories : incomeCategories"
:key="category.id" class="border rounded-lg mx-2 mb-2"
@click="selectCategory(category)">
<div class="flex flex-row justify-between w-full p-1 gap-2">
<p class="text-4xl font-bold text-gray-700 dark:text-gray-400">{{ category.icon }}</p>
<div class="flex flex-col items-start justify-items-start justify-around w-full">
<p class="font-bold text-start">{{ category.name }}</p>
<p class="font-light line-clamp-1 text-start">{{ category.description }}</p>
</div>
</div>
</button>
</div>
</div>
</div>
<!-- Описание -->
<FloatLabel class="!w-full">
<label for="description">Описание</label>
<Textarea v-model="description" id="description" rows="3" class="!w-full"/>
</FloatLabel>
<!-- Дата повторения (выпадающий список) -->
<div class="w-full relative">
<button class="border border-gray-300 rounded-lg w-full z-50"
@click="isDaySelectorOpened = !isDaySelectorOpened">
<div class="flex flex-row items-center pe-4 py-2 gap-4">
<div class="flex flex-row justify-between w-full px-4">
<p class="font-bold">Повторять каждый {{ repeatDay || 'N' }} день месяца</p>
</div>
<div>
<span :class="{'rotate-90': isDaySelectorOpened}"
class="pi pi-angle-right transition-transform duration-300 text-5xl"/>
</div>
</div>
</button>
<!-- Анимированное открытие списка дней -->
<div v-show="isDaySelectorOpened"
class="absolute left-0 right-0 top-full overflow-hidden z-50 border-b-4 border-x rounded-b-lg bg-white shadow-lg transition-all duration-500"
:class="{ 'max-h-0': !isDaySelectorOpened, 'max-h-[500px]': isDaySelectorOpened }">
<div class="grid grid-cols-7 p-2">
<button v-for="day in days" :key="day" class=" border"
@click="selectDay(day)">
{{ day }}
</button>
</div>
</div>
</div>
<!-- Сумма -->
<InputGroup class="w-full">
<InputGroupAddon></InputGroupAddon>
<InputNumber v-model="amount" placeholder="Сумма"/>
<InputGroupAddon>.00</InputGroupAddon>
</InputGroup>
<!-- Кнопки -->
<div class="flex justify-content-end gap-2 mt-4">
<Button label="Сохранить" icon="pi pi-check" @click="savePayment" class="p-button-success"/>
<Button label="Отмена" icon="pi pi-times" @click="closeModal" class="p-button-secondary"/>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -3,12 +3,15 @@
<div v-if="loading">
<LoadingView/>
</div>
<div v-else class="flex flex-col h-full bg-gray-100 py-15">
<div v-else class="flex flex-col h-full bg-gray-100 pb-5 gap-4">
<!-- Заголовок -->
<h1 class="text-4xl font-extrabold mb-8 text-gray-800">Ежемесячные платежи</h1>
<div class="flex flex-col gap-4 xl:flex-row justify-between bg-gray-100">
<h1 class="text-4xl text-gray-800">Ежемесячные платежи</h1>
<Button label="Добавить платеж" icon="pi pi-plus" class="text-sm " @click="toggleModal(null)"/>
</div>
<div v-if="!space">Выберите сперва пространство</div>
<!-- Список рекуррентных платежей -->
<div v-else class="grid grid-cols-1 lg:grid-cols-3 gap-6 w-full max-w-7xl px-4">
<div v-else class="flex flex-col md:grid md:grid-cols-2 lg:grid-cols-2 gap-3 justify-between">
<RecurrentListItem
v-for="payment in recurrentPayments"
:key="payment.id"
@@ -17,25 +20,24 @@
@delete-payment="deletePayment"
/>
<div>
<div
class="recurrent-card bg-white shadow-lg rounded-lg p-6 w-full transition duration-300 transform hover:scale-105">
<div class="flex justify-between items-center">
<div class="flex items-center w-full justify-center gap-4" style="height: 160px;">
<!-- <div-->
<!-- class=" bg-white shadow-lg rounded-lg p-6 ">-->
<!-- <div class="flex justify-between items-center">-->
<!-- <div class="flex items-center w-full justify-center gap-4" style="height: 160px;">-->
<Button text class="flex-col" @click="toggleModal">
<i class="pi pi-plus-circle" style="font-size: 2.5rem"></i>
<p>Add new</p>
</Button>
<!-- <Button text class="flex-col" @click="toggleModal">-->
<!-- <i class="pi pi-plus-circle" style="font-size: 2.5rem"></i>-->
<!-- <p></p>-->
<!-- </Button>-->
<CreateRecurrentModal
v-if="showModal"
:show="showModal"
:expenseCategories="expenseCategories"
:incomeCategories="incomeCategories"
:categoryTypes="categoryTypes"
@save-payment="savePayment"
@close-modal="toggleModal"/>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -70,6 +72,7 @@ watch(
loading.value = true;
// Если выбранный space изменился, получаем новую информацию о бюджете
await fetchRecurrentPayments()
await fetchCategories()
} catch (error) {
console.error('Error fetching budget infos:', error);
}
@@ -143,11 +146,12 @@ const deletePayment = (payment: any) => {
};
onMounted(async () => {
if (space.value){
if (space.value) {
await fetchRecurrentPayments()
await fetchCategories()
}
await fetchCategories()
})
</script>

View File

@@ -1,17 +1,19 @@
<template>
<div :class="payment.category.type.code == 'INCOME' ? 'from-green-100 to-green-50' : ' from-red-100 to-red-50' "
class="recurrent-card bg-gradient-to-r shadow-xl rounded-lg p-6 w-full hover:shadow-2xl transition duration-300 transform hover:scale-105">
<div
class="recurrent-card bg-gradient-to-r shadow-xl rounded-lg p-6 w-full ">
<div class="flex flex-col gap-5 justify-between items-start">
<!-- Дата и Сумма -->
<div class="flex flex-row justify-between w-full items-center">
<div class="text-gray-500">
<strong class="text-3xl text-green-600">{{ payment.atDay }} </strong> числа
<strong class="text-xl text-green-600">{{ payment.atDay }} </strong> числа | {{payment.category.type.code=='EXPENSE' ? 'Расход' : 'Приход'}}
</div>
<div class="flex items-center gap-4">
<Button icon="pi pi-pencil" class="p-button-rounded p-button-text p-button-sm" @click="editPayment"/>
<Button icon="pi pi-trash" class="p-button-rounded p-button-text p-button-danger p-button-sm"
@click="deletePayment"/>
<button @click="editPayment"><i class="pi pi-pen-to-square"/></button>
<button @click="deletePayment"><i class="pi pi-trash"/></button>
<!-- <Button icon="pi pi-pencil" class="p-button-rounded p-button-text p-button-sm" @click="editPayment"/>-->
<!-- <Button icon="pi pi-trash" class="p-button-rounded p-button-text p-button-danger p-button-sm"-->
<!-- @click="deletePayment"/>-->
</div>
@@ -25,16 +27,16 @@
<!-- Информация о платеже -->
<div class="flex flex-row gap-2">
<div class="flex items-center ">
<span class="text-4xl">{{ payment.category.icon }}</span>
<span class="text-3xl">{{ payment.category.icon }}</span>
</div>
<div class="flex flex-col">
<h2 class="text-xl font-semibold text-gray-800">{{ payment.name }}</h2>
<h2 class="text-lg font-semibold text-gray-800">{{ payment.name }}</h2>
<p class="text-sm text-gray-500 line-clamp-1">{{ payment.description }}</p>
</div>
</div>
<div
:class="payment.category.type.code == 'EXPENSE' ? 'text-red-700' : 'text-green-700'"
class="text-2xl font-bold line-clamp-1 ">{{ formatAmount(payment.amount) }}
class="text-xl font-bold line-clamp-1 ">{{ formatAmount(payment.amount) }}
</div>
</div>
@@ -72,15 +74,7 @@ const deletePayment = () => {
</script>
<style scoped>
.recurrent-card {
transition: all 0.3s ease;
//background: linear-gradient(135deg, #f0fff4, #c6f6d5);
}
.recurrent-card:hover {
transform: scale(1.05);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
</style>

View File

@@ -1,75 +1,27 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {createSpaceRequest} from "@/services/spaceService";
import {Space} from "@/models/Space";
import Dialog from "primevue/dialog";
import FloatLabel from "primevue/floatlabel";
import Checkbox from "primevue/checkbox";
import InputText from "primevue/inputtext";
import Textarea from "primevue/textarea";
import Button from "primevue/button";
import DatePicker from "primevue/datepicker";
const emits = defineEmits(['space-created', 'close-modal', 'error-space-creation'])
import SpaceCreationFormView from "@/components/spaces/SpaceCreationFormView.vue";
import Dialog from "primevue/dialog";
const props = defineProps({
opened: {
type: Boolean,
required: true
}
})
const spaceName = ref('')
const spaceDescription = ref('')
const createCategories = ref(true)
const emits = defineEmits(['space-created', 'close-modal', 'error-space-creation'])
const cancel = () => {
resetForm()
// resetForm()
emits("close-modal");
}
const createSpace = async () => {
const space = new Space()
space.name = spaceName.value
space.description = spaceDescription.value
space.createCategories = createCategories.value
try {
await createSpaceRequest(space)
resetForm()
emits("space-created")
} catch (e) {
console.error(e)
emits('error-space-creation', e)
}
}
const resetForm = () => {
spaceName.value = ''
spaceDescription.value = ''
}
</script>
<template>
<Dialog :visible="opened" modal header="Создать новое пространство" :style="{ width: '25rem' }" @hide="cancel"
@update:visible="cancel">
<div class="flex flex-col gap-4 mt-1">
<FloatLabel variant="on" class="w-full">
<label for="name">Название</label>
<InputText v-model="spaceName" id="name" class="w-full"/>
</FloatLabel>
<FloatLabel variant="on" class="w-full">
<label for="name">Описание</label>
<Textarea v-model="spaceDescription" id="name" class="w-full"/>
</FloatLabel>
<div class="flex flex-row items-center min-w-fit gap-4">
<Checkbox v-model="createCategories" binary/>
Создать стандартный набор категорий?
</div>
<div class="flex flex-row gap-2 justify-end items-center">
<Button label="Создать" severity="success" icon="pi pi-save" @click="createSpace"/>
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
</div>
</div>
<SpaceCreationFormView @close-modal="cancel" @space-created="emits('space-created')" />
</Dialog>
</template>

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {createSpaceRequest} from "@/services/spaceService";
import {Space} from "@/models/Space";
import FloatLabel from "primevue/floatlabel";
import Checkbox from "primevue/checkbox";
import InputText from "primevue/inputtext";
import Textarea from "primevue/textarea";
import Button from "primevue/button";
import DatePicker from "primevue/datepicker";
const emits = defineEmits(['space-created', 'close-modal', 'error-space-creation'])
const spaceName = ref('')
const spaceDescription = ref('')
const createCategories = ref(true)
const cancel = () => {
resetForm()
emits("close-modal");
}
const createSpace = async () => {
const space = new Space()
space.name = spaceName.value
space.description = spaceDescription.value
space.createCategories = createCategories.value
try {
await createSpaceRequest(space).then((res) => {
resetForm()
emits("space-created", res)
})
} catch (e) {
console.error(e)
emits('error-space-creation', e)
}
}
const resetForm = () => {
spaceName.value = ''
spaceDescription.value = ''
}
</script>
<template>
<div class="flex flex-col gap-4 mt-1">
<FloatLabel variant="on" class="w-full">
<label for="name">Название</label>
<InputText v-model="spaceName" id="name" class="w-full"/>
</FloatLabel>
<FloatLabel variant="on" class="w-full">
<label for="name">Описание</label>
<Textarea v-model="spaceDescription" id="name" class="w-full"/>
</FloatLabel>
<div class="flex flex-row items-center min-w-fit gap-4">
<Checkbox v-model="createCategories" binary/>
Создать стандартный набор категорий?
</div>
<div class="flex flex-row gap-2 justify-end items-center">
<Button label="Создать" severity="success" icon="pi pi-save" @click="createSpace"/>
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancel"/>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -227,7 +227,7 @@ onMounted(async () => {
<LoadingView v-if="loadingValue"/>
<div v-else class="p-4 bg-gray-100 h-full grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 ">
<div v-else class="p-4 bg-gray-100 grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 ">
<Toast/>
<ConfirmDialog/>
<Dialog :visible="inviteCreatedDialog" header="Приглашение" @hide="inviteCreatedDialog=false"

View File

@@ -83,10 +83,12 @@ const isReady = computed(() => !loading.value && loadingUser.value)
// Получение категорий и типов транзакций
const fetchCategoriesAndTypes = async () => {
try {
const [categoriesResponse, categoryTypesResponse, transactionTypesResponse] = await Promise.all([
const [categoriesResponse, categoryTypesResponse, transactionTypesResponse, transactionsResponse] = await Promise.all([
getCategories(),
getCategoryTypes(),
getTransactionTypes()
getTransactionTypes(),
getTransactions()
]);
entireCategories.value = categoriesResponse.data;
expenseCategories.value = categoriesResponse.data.filter((category: Category) => category.type.code === 'EXPENSE');
@@ -94,6 +96,8 @@ const fetchCategoriesAndTypes = async () => {
categoryTypes.value = categoryTypesResponse.data;
transactionTypes.value = transactionTypesResponse.data;
transactions.value = transactionsResponse.data;
} catch (error) {
toast.add({severity: 'error', summary: 'Ошибка!', detail: error.response.data["message"], life: 3000});
console.error('Error fetching categories and types:', error);
@@ -125,8 +129,8 @@ const prepareData = () => {
editedTransaction.value.type = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
selectedCategoryType.value = categoryTypes.value.find(type => type.code === props.categoryType) || categoryTypes.value[0];
entireCategories.value.find(category => category.id == props.categoryId ) ? entireCategories.value.find(category => category.id == props.categoryId ) : props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0]
editedTransaction.value.category = entireCategories.value.find(category => category.id == props.categoryId ) ? entireCategories.value.find(category => category.id == props.categoryId ) : props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0]
entireCategories.value.find(category => category.id == props.categoryId) ? entireCategories.value.find(category => category.id == props.categoryId) : props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0]
editedTransaction.value.category = entireCategories.value.find(category => category.id == props.categoryId) ? entireCategories.value.find(category => category.id == props.categoryId) : props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0]
// editedTransaction.value.category = props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0];
editedTransaction.value.date = new Date();
} else {
@@ -163,6 +167,9 @@ const showError = (message) => {
result.value = true;
isError.value = true;
resultText.value = message;
setTimeout(() => {
result.value = false
}, 1000)
return false;
};
@@ -226,7 +233,7 @@ const createTransaction = async (): Promise<void> => {
const transactionsUpdatedEmit = async () => {
await getTransactions(spaceStore.space?.id, 'INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
await getTransactions( 'INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
EventBus.emit('transactions-updated', true)
}
@@ -310,7 +317,7 @@ const transactions = ref<Transaction[]>(null);
const spaceStore = useSpaceStore()
const selectedSpace = computed(() => spaceStore.space)
watch( selectedSpace, async (newValue, oldValue) => {
watch(selectedSpace, async (newValue, oldValue) => {
if (newValue != oldValue) {
if (!isEditing.value) {
@@ -333,7 +340,6 @@ onMounted(async () => {
if (!isEditing.value) {
// transactions.value = transactions.value.slice(0,3)
await getTransactions(selectedSpace.value?.id,'INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
}
@@ -369,7 +375,7 @@ onMounted(async () => {
<LoadingView v-if="loading"/>
<div v-else class=" grid gap-4 w-full ">
<div class="relative w-full justify-center justify-items-center ">
<div class="flex flex-col justify-items-center gap-2">
<div class="flex flex-col justify-items-center gap-2 w-full">
<div class="flex flex-row gap-2">
<Select v-if="!isEditing" v-model="editedTransaction.type" :allow-empty="false"
:options="transactionTypes"

View File

@@ -28,8 +28,7 @@ const fetchTransactions = async (reload) => {
loading.value = true;
try {
const response = await getTransactions(selectedSpace.value?.id, 'INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
const response = await getTransactions( 'INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
const newTransactions = response.data;
// Проверка на конец данных
@@ -115,14 +114,14 @@ const selectedSpace = computed(() => spaceStore.space)
watch( selectedSpace, async (newValue, oldValue) => {
if (newValue != oldValue) {
await fetchTransactions(true)
await fetchTransactions(false)
}
})
const types = ref([])
onMounted(async () => {
EventBus.on('transactions-updated', fetchTransactions,true);
if (selectedSpace.value){
await fetchTransactions(); // Первоначальная загрузка данных
await fetchTransactions(false); // Первоначальная загрузка данных
}
// await fetchUsers();

View File

@@ -94,8 +94,9 @@ export const updateBudgetCategoryRequest = async (budget_id, category: BudgetCat
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
try {
let spaceId = localStorage.getItem("spaceId")
const spaceStore = useSpaceStore()
console.log(budget)
let budgetToCreate = JSON.parse(JSON.stringify(budget));
budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
@@ -103,12 +104,12 @@ export const createBudget = async (budget: Budget, createRecurrent: Boolean) =>
budget: budgetToCreate,
createRecurrent: createRecurrent
}
await apiClient.post(`/spaces/${spaceId}/budgets`, data);
return await apiClient.post(`/spaces/${spaceStore.space?.id}/budgets`, budgetToCreate)
.then(res => res.data)
.catch(err => {
throw err
})
} catch (e){
console.error(e)
throw e
}
}

View File

@@ -38,9 +38,11 @@ export const editCategoryRequest = async (category: any) => {
})
};
export const deleteCategory = async (id: number) => {
export const deleteCategoryRequest = async (id: string) => {
const spaceStore = useSpaceStore();
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/categories/${id}`);
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/categories/${id}`)
.then(res => res.data)
.catch(err => {throw err})
};
export const getCategoriesSumsRequest = async (spaceId: string) => {

View File

@@ -8,7 +8,7 @@ export const getTransaction = async (transactionId: int) => {
return await apiClient.post(`/transactions/${transactionId}`,);
}
export const getTransactions = async (spaceId, transaction_type = null, category_type = null, category_id = null, user_id = null, is_child = null, limit = null, offset = null) => {
export const getTransactions = async ( transaction_type = null, category_type = null, category_id = null, user_id = null, is_child = null, limit = null, offset = null) => {
const params = {};
// params.spaceId=spaceId;