add spaces
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
// public/service-worker.js
|
// public/service-worker.js
|
||||||
|
|
||||||
self.addEventListener("push", (event) => {
|
self.addEventListener("push", (event) => {
|
||||||
console.log(event)
|
|
||||||
const data = event.data.json();
|
const data = event.data.json();
|
||||||
console.log(data);
|
|
||||||
const options = {
|
const options = {
|
||||||
body: data.body,
|
body: data.body,
|
||||||
icon: data.icon,
|
icon: data.icon,
|
||||||
@@ -16,7 +16,7 @@ self.addEventListener("push", (event) => {
|
|||||||
|
|
||||||
self.addEventListener('notificationclick', function (event) {
|
self.addEventListener('notificationclick', function (event) {
|
||||||
// console.log("Notification click event received."); // Сообщение об активации обработчика
|
// console.log("Notification click event received."); // Сообщение об активации обработчика
|
||||||
console.log("Notification data:", event.notification.data); // Проверка данных уведомления
|
|
||||||
|
|
||||||
event.notification.close(); // Закрываем уведомление
|
event.notification.close(); // Закрываем уведомление
|
||||||
// console.log(event)
|
// console.log(event)
|
||||||
|
|||||||
56
src/App.vue
56
src/App.vue
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div id="app" class="flex flex-col h-screen bg-gray-300">
|
<div id="app" class="flex flex-col h-screen bg-gray-100">
|
||||||
|
<Toast/>
|
||||||
<!-- MenuBar всегда фиксирован сверху -->
|
<!-- MenuBar всегда фиксирован сверху -->
|
||||||
<MenuBar v-if="userStore.user" class="w-full sticky hidden lg:block top-0 z-10"/>
|
<MenuBar v-if="userStore.user" class="w-full sticky hidden lg:block top-0 z-10"/>
|
||||||
<ToolBar class=" fixed visible lg:invisible bottom-0 z-10"/>
|
<ToolBar class=" fixed visible lg:invisible bottom-0 z-10"/>
|
||||||
@@ -10,13 +11,42 @@
|
|||||||
<!-- {{ tg_id }}-->
|
<!-- {{ tg_id }}-->
|
||||||
<Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>
|
<Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>
|
||||||
|
|
||||||
<router-view />
|
<router-view/>
|
||||||
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
||||||
</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 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full gap-4">
|
||||||
|
<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>
|
||||||
|
<router-link to="/budgets" class="hover:underline">Бюджеты</router-link>
|
||||||
|
<router-link to="/transactions" class="hover:underline">Транзакции</router-link>
|
||||||
|
<router-link to="/settings" class="hover:underline">Настройки</router-link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<div>Ваши предложения можно направлять в <a href="https://t.me/voroninv" class="hover:underline text-blue-600">https://t.me/@voroninv</a>
|
||||||
|
</div>
|
||||||
|
<div>v0.0.2</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<TransactionForm v-if="visible" :visible="visible"
|
<TransactionForm v-if="visible" :visible="visible"
|
||||||
:transaction-type="drawerStore.transactionType ? drawerStore.transactionType : 'INSTANT'"
|
:transaction-type="drawerStore.transactionType ? drawerStore.transactionType : 'INSTANT'"
|
||||||
:category-type="drawerStore.categoryType ? drawerStore.categoryType : 'EXPENSE'" :categoryId="drawerStore.categoryId" @close-drawer="closeDrawer"/>
|
:category-type="drawerStore.categoryType ? drawerStore.categoryType : 'EXPENSE'"
|
||||||
|
:categoryId="drawerStore.categoryId" @close-drawer="closeDrawer"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,13 +55,14 @@
|
|||||||
import MenuBar from "./components/MenuBar.vue";
|
import MenuBar from "./components/MenuBar.vue";
|
||||||
import ToolBar from "@/components/ToolBar.vue";
|
import ToolBar from "@/components/ToolBar.vue";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import {computed, onMounted} from "vue";
|
import {computed, onMounted, ref} from "vue";
|
||||||
import {subscribeUserToPush} from "@/services/pushManager";
|
import {subscribeUserToPush} from "@/services/pushManager";
|
||||||
import apiClient from '@/services/axiosSetup';
|
import apiClient from '@/services/axiosSetup';
|
||||||
import {useUserStore} from "@/stores/userStore";
|
import {useUserStore} from "@/stores/userStore";
|
||||||
import {useDrawerStore} from '@/stores/drawerStore'
|
import {useDrawerStore} from '@/stores/drawerStore'
|
||||||
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import Toast from "primevue/toast";
|
||||||
|
|
||||||
|
|
||||||
const drawerStore = useDrawerStore();
|
const drawerStore = useDrawerStore();
|
||||||
@@ -65,7 +96,7 @@ const checkSubscribe = async () => {
|
|||||||
// Пользователь ранее отклонил запрос
|
// Пользователь ранее отклонил запрос
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Notification API is not supported in this browser.");
|
|
||||||
// You may want to use an alternative method, like alerts or modals
|
// You may want to use an alternative method, like alerts or modals
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,8 +104,6 @@ const checkSubscribe = async () => {
|
|||||||
const sendSubscribe = async () => {
|
const sendSubscribe = async () => {
|
||||||
try {
|
try {
|
||||||
const subscription = await subscribeUserToPush();
|
const subscription = await subscribeUserToPush();
|
||||||
console.log("Push subscription:", subscription);
|
|
||||||
|
|
||||||
// Отправка подписки на сервер для хранения
|
// Отправка подписки на сервер для хранения
|
||||||
await apiClient.post("/subscriptions/subscribe", subscription)
|
await apiClient.post("/subscriptions/subscribe", subscription)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -82,19 +111,24 @@ const sendSubscribe = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const token = ref(localStorage.getItem("token"));
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const user = computed(() => userStore.user);
|
const user = computed(() => userStore.user);
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
||||||
if (!userStore.user) {
|
if (!userStore.user && localStorage.getItem("token")) {
|
||||||
|
|
||||||
await userStore.fetchUserProfile();
|
await userStore.fetchUserProfile();
|
||||||
|
|
||||||
}
|
|
||||||
await checkSubscribe();
|
await checkSubscribe();
|
||||||
|
await spaceStore.fetchSpaces()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,66 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!loadingUser" class="card ">
|
<div v-if="!loadingUser && !loading" class="card ">
|
||||||
<Menubar :model="items" >
|
<Menubar :model="items">
|
||||||
<template #start>
|
<template #start>
|
||||||
<!-- <svg width="35" height="40" viewBox="0 0 35 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-8">-->
|
<!-- <svg width="35" height="40" viewBox="0 0 35 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-8">-->
|
||||||
<!-- <path-->
|
<!-- <path-->
|
||||||
<!-- d="M25.87 18.05L23.16 17.45L25.27 20.46V29.78L32.49 23.76V13.53L29.18 14.73L25.87 18.04V18.05ZM25.27 35.49L29.18 31.58V27.67L25.27 30.98V35.49ZM20.16 17.14H20.03H20.17H20.16ZM30.1 5.19L34.89 4.81L33.08 12.33L24.1 15.67L30.08 5.2L30.1 5.19ZM5.72 14.74L2.41 13.54V23.77L9.63 29.79V20.47L11.74 17.46L9.03 18.06L5.72 14.75V14.74ZM9.63 30.98L5.72 27.67V31.58L9.63 35.49V30.98ZM4.8 5.2L10.78 15.67L1.81 12.33L0 4.81L4.79 5.19L4.8 5.2ZM24.37 21.05V34.59L22.56 37.29L20.46 39.4H14.44L12.34 37.29L10.53 34.59V21.05L12.42 18.23L17.45 26.8L22.48 18.23L24.37 21.05ZM22.85 0L22.57 0.69L17.45 13.08L12.33 0.69L12.05 0H22.85Z"-->
|
<!-- d="M25.87 18.05L23.16 17.45L25.27 20.46V29.78L32.49 23.76V13.53L29.18 14.73L25.87 18.04V18.05ZM25.27 35.49L29.18 31.58V27.67L25.27 30.98V35.49ZM20.16 17.14H20.03H20.17H20.16ZM30.1 5.19L34.89 4.81L33.08 12.33L24.1 15.67L30.08 5.2L30.1 5.19ZM5.72 14.74L2.41 13.54V23.77L9.63 29.79V20.47L11.74 17.46L9.03 18.06L5.72 14.75V14.74ZM9.63 30.98L5.72 27.67V31.58L9.63 35.49V30.98ZM4.8 5.2L10.78 15.67L1.81 12.33L0 4.81L4.79 5.19L4.8 5.2ZM24.37 21.05V34.59L22.56 37.29L20.46 39.4H14.44L12.34 37.29L10.53 34.59V21.05L12.42 18.23L17.45 26.8L22.48 18.23L24.37 21.05ZM22.85 0L22.57 0.69L17.45 13.08L12.33 0.69L12.05 0H22.85Z"-->
|
||||||
<!-- fill="var(--p-primary-color)"-->
|
<!-- fill="var(--p-primary-color)"-->
|
||||||
<!-- />-->
|
<!-- />-->
|
||||||
<!-- <path-->
|
<!-- <path-->
|
||||||
<!-- d="M30.69 4.21L24.37 4.81L22.57 0.69L22.86 0H26.48L30.69 4.21ZM23.75 5.67L22.66 3.08L18.05 14.24V17.14H19.7H20.03H20.16H20.2L24.1 15.7L30.11 5.19L23.75 5.67ZM4.21002 4.21L10.53 4.81L12.33 0.69L12.05 0H8.43002L4.22002 4.21H4.21002ZM21.9 17.4L20.6 18.2H14.3L13 17.4L12.4 18.2L12.42 18.23L17.45 26.8L22.48 18.23L22.5 18.2L21.9 17.4ZM4.79002 5.19L10.8 15.7L14.7 17.14H14.74H15.2H16.85V14.24L12.24 3.09L11.15 5.68L4.79002 5.2V5.19Z"-->
|
<!-- d="M30.69 4.21L24.37 4.81L22.57 0.69L22.86 0H26.48L30.69 4.21ZM23.75 5.67L22.66 3.08L18.05 14.24V17.14H19.7H20.03H20.16H20.2L24.1 15.7L30.11 5.19L23.75 5.67ZM4.21002 4.21L10.53 4.81L12.33 0.69L12.05 0H8.43002L4.22002 4.21H4.21002ZM21.9 17.4L20.6 18.2H14.3L13 17.4L12.4 18.2L12.42 18.23L17.45 26.8L22.48 18.23L22.5 18.2L21.9 17.4ZM4.79002 5.19L10.8 15.7L14.7 17.14H14.74H15.2H16.85V14.24L12.24 3.09L11.15 5.68L4.79002 5.2V5.19Z"-->
|
||||||
<!-- fill="var(--p-text-color)"-->
|
<!-- fill="var(--p-text-color)"-->
|
||||||
<!-- />-->
|
<!-- />-->
|
||||||
<!-- </svg>-->
|
<!-- </svg>-->
|
||||||
<img alt="logo" src="/apple-touch-icon.png" width="32" height="32" />
|
<img alt="logo" src="/apple-touch-icon.png" width="32" height="32"/>
|
||||||
</template>
|
</template>
|
||||||
<template #item="{ item, props, hasSubmenu, root }">
|
<template #item="{ item, props, hasSubmenu, root }">
|
||||||
<router-link :to="item.url" v-ripple class="flex items-center" v-bind="props.action">
|
<router-link :to="item.url" v-ripple class="flex items-center" v-bind="props.action">
|
||||||
<span :class="item.icon" />
|
<span :class="item.icon"/>
|
||||||
<span class="ml-2">{{ item.label }}</span>
|
<span class="ml-2">{{ item.label }}</span>
|
||||||
<Badge v-if="item.badge" :class="{ 'ml-auto': !root, 'ml-2': root }" :value="item.badge" />
|
<Badge v-if="item.badge" :class="{ 'ml-auto': !root, 'ml-2': root }" :value="item.badge"/>
|
||||||
<span v-if="item.shortcut" class="ml-auto border border-surface rounded bg-emphasis text-muted-color text-xs p-1">{{ item.shortcut }}</span>
|
|
||||||
<i v-if="hasSubmenu" :class="['pi pi-angle-down', { 'pi-angle-down ml-2': root, 'pi-angle-right ml-auto': !root }]"></i>
|
|
||||||
|
<span v-if="item.shortcut"
|
||||||
|
class="ml-auto border border-surface rounded bg-emphasis text-muted-color text-xs p-1">{{
|
||||||
|
item.shortcut
|
||||||
|
}}</span>
|
||||||
|
<i v-if="hasSubmenu"
|
||||||
|
:class="['pi pi-angle-down', { 'pi-angle-down ml-2': root, 'pi-angle-right ml-auto': !root }]"></i>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
<!-- <Button-->
|
||||||
|
<!-- @click="drawerStore.setCategoryType('EXPENSE');drawerStore.setTransactionType('INSTANT');drawerStore.visible = true"-->
|
||||||
|
<!-- label="Создать"/>-->
|
||||||
|
|
||||||
<template #end>
|
<template #end>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{{ user.firstName }}
|
<Select v-model="spaceStore.space" :options="spaces" optionLabel="name" @change="selectSpace"/>
|
||||||
<Button @click="drawerStore.setCategoryType('EXPENSE');drawerStore.setTransactionType('INSTANT');drawerStore.visible = true" label="Создать"/>
|
<div class="relative flex flex-col items-center group">
|
||||||
|
<!-- Имя пользователя -->
|
||||||
|
<div class="flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center">
|
||||||
|
<span class="text-white text-center">{{ user.firstName.substring(0, 1) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- <InputText placeholder="Search" type="text" class="w-32 sm:w-auto" />-->
|
<!-- Всплывающая плашка с ID -->
|
||||||
<!-- <Avatar image="https://primefaces.org/cdn/primevue/images/avatar/amyelsner.png" shape="circle" />-->
|
<div
|
||||||
|
class="absolute bottom-[-3rem] right-0 w-fit bg-gray-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
|
||||||
|
@click="copyToClipboard(user.id)"
|
||||||
|
>
|
||||||
|
ID: {{ user.id }}
|
||||||
|
Имя: {{ user.firstName }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Сообщение "Скопировано" -->
|
||||||
|
<div v-if="copied" class="absolute bottom-[-60px] bg-green-500 text-white text-xs px-2 py-1 rounded transition-opacity">
|
||||||
|
Скопировано!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="logout" class="text-xs">Выйти</button>
|
||||||
|
<!-- <InputText placeholder="Search" type="text" class="w-32 sm:w-auto" />-->
|
||||||
|
<!-- <Avatar image="https://primefaces.org/cdn/primevue/images/avatar/amyelsner.png" shape="circle" />-->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Menubar>
|
</Menubar>
|
||||||
@@ -40,14 +68,24 @@
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import {computed, ref} from "vue";
|
import {computed, onMounted, ref, watch} from "vue";
|
||||||
import Badge from "primevue/badge";
|
import Badge from "primevue/badge";
|
||||||
import Button from "primevue/button"
|
import Button from "primevue/button"
|
||||||
import Menubar from "primevue/menubar";
|
import Menubar from "primevue/menubar";
|
||||||
|
import Select from "primevue/select";
|
||||||
import {useUserStore} from "@/stores/userStore";
|
import {useUserStore} from "@/stores/userStore";
|
||||||
import {useDrawerStore} from "@/stores/drawerStore.ts";
|
import {useDrawerStore} from "@/stores/drawerStore.ts";
|
||||||
|
import {getSpaces} from "@/services/spaceService.ts";
|
||||||
|
import {Space} from "@/models/Space.ts";
|
||||||
|
import router from "@/router";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import {EventBus} from "@/utils/EventBus";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const user = computed(() => userStore.user);
|
const user = computed(() => userStore.user);
|
||||||
@@ -56,8 +94,33 @@ const loadingUser = computed(() => userStore.loadingUser);
|
|||||||
const drawerStore = useDrawerStore()
|
const drawerStore = useDrawerStore()
|
||||||
const visible = computed(() => drawerStore.visible);
|
const visible = computed(() => drawerStore.visible);
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const spaces = computed(() => spaceStore.spaces);
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
|
||||||
|
const selectSpace = (space: Space) => {
|
||||||
|
spaceStore.setSpace(space.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const copied = ref(false);
|
||||||
|
|
||||||
|
const copyToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
copied.value = true;
|
||||||
|
setTimeout(() => copied.value = false, 1500); // Убираем сообщение через 1.5 сек
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка копирования:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("spaceId");
|
||||||
|
userStore.user = null;
|
||||||
|
router.push("/login/?back=" + route.path);
|
||||||
|
}
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
@@ -65,6 +128,11 @@ const items = ref([
|
|||||||
icon: 'pi pi-home',
|
icon: 'pi pi-home',
|
||||||
url: '/'
|
url: '/'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Пространства',
|
||||||
|
icon: 'pi pi-compass',
|
||||||
|
url: '/spaces'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Аналитика',
|
label: 'Аналитика',
|
||||||
icon: 'pi pi-star',
|
icon: 'pi pi-star',
|
||||||
@@ -84,7 +152,23 @@ const items = ref([
|
|||||||
label: 'Настройки',
|
label: 'Настройки',
|
||||||
icon: 'pi pi-envelope',
|
icon: 'pi pi-envelope',
|
||||||
url: '/settings',
|
url: '/settings',
|
||||||
// badge: 3
|
},
|
||||||
|
{
|
||||||
|
label: 'Создать',
|
||||||
|
icon: 'pi pi-plus',
|
||||||
|
url:route.path,
|
||||||
|
command: () => {
|
||||||
|
drawerStore.setCategoryType('EXPENSE');
|
||||||
|
drawerStore.setTransactionType('INSTANT');
|
||||||
|
drawerStore.visible = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -118,8 +118,7 @@ const openDrawer = (selectedTransactionType = null, selectedCategoryType = null)
|
|||||||
drawerStore.setTransactionType(selectedTransactionType)
|
drawerStore.setTransactionType(selectedTransactionType)
|
||||||
drawerStore.setCategoryType('EXPENSE')
|
drawerStore.setCategoryType('EXPENSE')
|
||||||
}
|
}
|
||||||
console.log(selectedTransactionType)
|
|
||||||
console.log(selectedCategoryType)
|
|
||||||
drawerStore.setVisible( true)
|
drawerStore.setVisible( true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,13 +150,7 @@ const items = ref([
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
const onEditClick = () => {
|
|
||||||
console.log("Edit button clicked");
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddClick = () => {
|
|
||||||
console.log("Add button clicked");
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LoadingView from "@/components/LoadingView.vue";
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
import {computed, onMounted, ref} from "vue";
|
import {computed, onMounted, ref, watch} from "vue";
|
||||||
import {getCategories, getCategoriesSumsRequest} from "@/services/categoryService";
|
import {getCategories, getCategoriesSumsRequest} from "@/services/categoryService";
|
||||||
import DataTable from "primevue/datatable";
|
import DataTable from "primevue/datatable";
|
||||||
import Column from "primevue/column";
|
import Column from "primevue/column";
|
||||||
@@ -14,12 +14,15 @@ import ChartDataLabels from 'chartjs-plugin-datalabels';
|
|||||||
import {Chart as ChartJS} from 'chart.js/auto';
|
import {Chart as ChartJS} from 'chart.js/auto';
|
||||||
import {Colors} from 'chart.js';
|
import {Colors} from 'chart.js';
|
||||||
import {getMonthName} from "@/utils/utils";
|
import {getMonthName} from "@/utils/utils";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import router from "@/router";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
|
||||||
ChartJS.register(ChartDataLabels);
|
ChartJS.register(ChartDataLabels);
|
||||||
ChartJS.register(Colors);
|
ChartJS.register(Colors);
|
||||||
|
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(false);
|
||||||
const categoriesCatalog = ref([])
|
const categoriesCatalog = ref([])
|
||||||
const categoriesCatalogGrouped = computed(() => {
|
const categoriesCatalogGrouped = computed(() => {
|
||||||
const cats = {}
|
const cats = {}
|
||||||
@@ -210,10 +213,19 @@ const prepareTableData = (categories) => {
|
|||||||
return rows;
|
return rows;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
watch( selectedSpace, async (newValue, oldValue) => {
|
||||||
|
if (newValue != oldValue) {
|
||||||
|
await fetchCategoriesSums()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const fetchCategoriesSums = async () => {
|
const fetchCategoriesSums = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await getCategoriesSumsRequest().then((data) => {
|
await getCategoriesSumsRequest(selectedSpace.value?.id).then((data) => {
|
||||||
data.data.forEach((category) => {
|
data.data.forEach((category) => {
|
||||||
category.monthlySums.forEach((monthlySum) => {
|
category.monthlySums.forEach((monthlySum) => {
|
||||||
monthlySum.date = getMonthName(new Date(monthlySum.date).getMonth()) + " " + new Date(monthlySum.date).getFullYear()
|
monthlySum.date = getMonthName(new Date(monthlySum.date).getMonth()) + " " + new Date(monthlySum.date).getFullYear()
|
||||||
@@ -237,15 +249,25 @@ const fetchCategoriesCatalog = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([fetchCategoriesSums(), fetchCategoriesCatalog()])
|
await Promise.all([ fetchCategoriesCatalog()])
|
||||||
// await fetchCategoriesSums();
|
if (selectedSpace.value) {
|
||||||
|
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 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 class="!items-center w-5/6 bg-white">
|
<div v-if="!selectedSpace" class="flex w-full h-full items-center justify-center">
|
||||||
|
<p>Сперва нужно выбрать Пространство.
|
||||||
|
<button class="text-blue-500 hover:underline" @click="router.push('/spaces').then((res) => router.go(0))">Перейти</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="dataTableCategories.length==0">
|
||||||
|
Начните записывать траты и здесь появится информация.
|
||||||
|
</div>
|
||||||
|
<div v-else class="!items-center w-5/6 bg-white">
|
||||||
<Accordion value="1" class=" " @tab-open="isChartOpen=true"
|
<Accordion value="1" class=" " @tab-open="isChartOpen=true"
|
||||||
@tab-close="closeChart">
|
@tab-close="closeChart">
|
||||||
<AccordionPanel value="0">
|
<AccordionPanel value="0">
|
||||||
@@ -292,7 +314,7 @@ onMounted(async () => {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable :value="dataTableCategories" responsiveLayout="scroll" filter stripedRows class="w-5/6 items-center">
|
<DataTable :value="dataTableCategories" responsiveLayout="scroll" filter stripedRows class="w-5/6 items-center">
|
||||||
<Column
|
<Column
|
||||||
@@ -316,6 +338,7 @@ onMounted(async () => {
|
|||||||
</Column>
|
</Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,110 +1,130 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-center h-screen bg-gray-100">
|
<div class="flex flex-col items-center justify-center h-screen bg-gray-100">
|
||||||
<div class="w-full max-w-sm p-6 bg-white rounded-lg shadow-md">
|
<Card class="w-full max-w-sm p-6 bg-white rounded-lg shadow-md">
|
||||||
<h2 class="text-2xl font-bold text-center mb-6">Вход</h2>
|
<template #title>
|
||||||
{{ tg_id }}
|
<h2 class="text-2xl font-bold text-center">Вход</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<div v-if="tg_id" class="mb-4 text-center text-gray-600">
|
||||||
|
Авторизация через Telegram (ID: {{ tg_id }})
|
||||||
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="login">
|
<form @submit.prevent="login">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">Логин</label>
|
<label for="username" class="block text-sm font-semibold text-gray-700">Логин</label>
|
||||||
<input
|
<InputText id="username" v-model.trim="username" class="w-full" :class="{'p-invalid': errors.username}" />
|
||||||
v-model="username"
|
<small v-if="errors.username" class="text-red-500">{{ errors.username }}</small>
|
||||||
type="text"
|
|
||||||
id="username"
|
|
||||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">Пароль</label>
|
<label for="password" class="block text-sm font-semibold text-gray-700">Пароль</label>
|
||||||
<input
|
<Password id="password" v-model="password" class="w-full" :feedback="false" toggleMask />
|
||||||
v-model="password"
|
<small v-if="errors.password" class="text-red-500">{{ errors.password }}</small>
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
|
|
||||||
>
|
|
||||||
Войти
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button label="Войти" type="submit" class="w-full mt-2" :disabled="loading" :loading="loading" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<p class="mt-4 text-sm text-center text-gray-600">
|
||||||
|
Нет аккаунта? <RouterLink to="/register" class="text-blue-500 hover:underline">Регистрация</RouterLink>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import apiClient from '@/services/axiosSetup';
|
import { useToast } from 'primevue/usetoast'
|
||||||
import {useUserStore} from "@/stores/userStore";
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Password from 'primevue/password'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import Card from 'primevue/card'
|
||||||
|
import { useUserStore } from '@/stores/userStore'
|
||||||
|
import apiClient from '@/services/axiosSetup'
|
||||||
|
|
||||||
const username = ref('');
|
const username = ref('')
|
||||||
const password = ref('');
|
const password = ref('')
|
||||||
const router = useRouter();
|
const loading = ref(false)
|
||||||
const route = useRoute();
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const toast = useToast()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// Получение tg_id
|
const errors = ref({ username: '', password: '' })
|
||||||
|
|
||||||
|
// Получение 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(); // Разворачиваем приложение на весь экран
|
tg.expand() // Разворачиваем WebApp
|
||||||
return tg.initDataUnsafe.user?.id ?? null; // Если tg_id нет, возвращаем null
|
return tg.initDataUnsafe.user?.id ?? null
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
});
|
})
|
||||||
|
|
||||||
// Функция для автоматического входа по tg_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 );
|
const response = await apiClient.post('/auth/token/tg', { tg_id: tg_id.value })
|
||||||
const token = response.data.access_token;
|
const token = response.data.access_token
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token)
|
||||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
||||||
await router.replace(route.query['back']+"/reload" ? route.query['back'].toString() : '/');
|
|
||||||
|
toast.add({ severity: 'success', summary: 'Вход выполнен', detail: 'Добро пожаловать!', life: 3000 })
|
||||||
|
await router.replace(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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Вызов autoLoginWithTgId при загрузке компонента
|
// Проверка формы перед отправкой
|
||||||
onMounted(() => {
|
const validateForm = () => {
|
||||||
autoLoginWithTgId();
|
errors.value = { username: '', password: '' }
|
||||||
});
|
let valid = true
|
||||||
|
|
||||||
const userStore = useUserStore();
|
if (!username.value) {
|
||||||
// Основная функция для логина
|
errors.value.username = 'Введите логин'
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
if (!password.value) {
|
||||||
|
errors.value.password = 'Введите пароль'
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основная функция входа
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
await userStore.login(username.value, password.value);
|
if (!validateForm()) return
|
||||||
// try {
|
|
||||||
// let response;
|
loading.value = true
|
||||||
// if (tg_id.value) {
|
try {
|
||||||
// response = await apiClient.post('/auth/token/tg', qs.stringify({ tg_id: tg_id.value }));
|
await userStore.login(username.value, password.value)
|
||||||
// } else {
|
toast.add({ severity: 'success', summary: 'Успешный вход', detail: 'Добро пожаловать!', life: 3000 })
|
||||||
// response = await apiClient.post('/auth/login', {
|
// await router.push('/')
|
||||||
// username: username.value,
|
} catch (error) {
|
||||||
// password: password.value,
|
toast.add({ severity: 'error', summary: 'Ошибка входа', detail: 'Неверные данные', life: 3000 })
|
||||||
// });
|
} finally {
|
||||||
// }
|
loading.value = false
|
||||||
//
|
}
|
||||||
// const token = response.data.token;
|
}
|
||||||
// localStorage.setItem('token', token);
|
|
||||||
// apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
// Запуск авто-входа при загрузке страницы
|
||||||
// await router.push(route.query['back'] ? route.query['back'].toString() : '/');
|
onMounted(() => {
|
||||||
// } catch (error) {
|
autoLoginWithTgId()
|
||||||
// console.error(error);
|
})
|
||||||
// alert('Ошибка входа. Проверьте логин и пароль.');
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
body {
|
.p-invalid {
|
||||||
background-color: #f0f2f5;
|
border-color: red !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
120
src/components/auth/RegisterView.vue
Normal file
120
src/components/auth/RegisterView.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-center min-h-screen bg-gray-100">
|
||||||
|
<Card class="w-full max-w-md p-6 bg-white rounded-2xl shadow-lg">
|
||||||
|
<template #title>
|
||||||
|
<h2 class="text-2xl font-bold text-center text-gray-800">Регистрация</h2>
|
||||||
|
</template>
|
||||||
|
<Toast/>
|
||||||
|
<template #content>
|
||||||
|
<form @submit.prevent="submitHandler">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="username" class="block mb-1 text-sm font-semibold text-gray-700">Логин</label>
|
||||||
|
<InputText id="username" v-model.trim="username" class="w-full" :class="{'p-invalid': errors.username}" />
|
||||||
|
<small v-if="errors.username" class="text-red-500">{{ errors.username }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="firstName" class="block mb-1 text-sm font-semibold text-gray-700">Имя</label>
|
||||||
|
<InputText id="firstName" v-model.trim="firstName" class="w-full" :class="{'p-invalid': errors.firstName}" />
|
||||||
|
<small v-if="errors.firstName" class="text-red-500">{{ errors.firstName }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="password" class="block mb-1 text-sm font-semibold text-gray-700">Пароль</label>
|
||||||
|
<Password id="password" v-model="password" class="w-full" :feedback="false" :class="{'p-invalid': errors.password}" toggleMask />
|
||||||
|
<small v-if="errors.password" class="text-red-500">{{ errors.password }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="confirmPassword" class="block mb-1 text-sm font-semibold text-gray-700">Подтвердите пароль</label>
|
||||||
|
<Password id="confirmPassword" v-model="confirmPassword" class="w-full" :feedback="false" :class="{'p-invalid': errors.confirmPassword}" toggleMask />
|
||||||
|
<small v-if="errors.confirmPassword" class="text-red-500">{{ errors.confirmPassword }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button label="Зарегистрироваться" type="submit" class="w-full mt-2" :disabled="loading" :loading="loading" />
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<p class="mt-4 text-sm text-center text-gray-600">
|
||||||
|
Уже есть аккаунт? <RouterLink to="/login" class="text-blue-500 hover:underline">Войти</RouterLink>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Password from 'primevue/password'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import Card from 'primevue/card'
|
||||||
|
import Toast from "primevue/toast";
|
||||||
|
import {useUserStore} from "@/stores/userStore";
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToast()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const username = ref('')
|
||||||
|
const firstName = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const errors = ref({ username: '', firstName: '', password: '', confirmPassword: '' })
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
errors.value = { username: '', firstName: '', password: '', confirmPassword: '' }
|
||||||
|
let valid = true
|
||||||
|
|
||||||
|
if (!username.value) {
|
||||||
|
errors.value.username = 'Введите логин'
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstName.value) {
|
||||||
|
errors.value.firstName = 'Введите имя'
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password.value || password.value.length < 3) {
|
||||||
|
errors.value.password = 'Пароль должен содержать минимум 6 символов'
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.value !== confirmPassword.value) {
|
||||||
|
errors.value.confirmPassword = 'Пароли не совпадают'
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
if (!validateForm()) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// Симуляция запроса (замени на реальный API-запрос)
|
||||||
|
await userStore.register(username.value, password.value, firstName.value)
|
||||||
|
|
||||||
|
toast.add({ severity: 'success', summary: 'Успешная регистрация', detail: 'Теперь вы можете войти', life: 3000 })
|
||||||
|
await router.push('/login')
|
||||||
|
} catch (error) {
|
||||||
|
errors.value.username = error.response.data.message
|
||||||
|
toast.add({ severity: 'error', summary: 'Ошибка регистрации', detail: 'Попробуйте позже', life: 3000 })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.p-invalid {
|
||||||
|
border-color: red !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -25,7 +25,7 @@ const dateTo = ref(new Date())
|
|||||||
const budget = ref(new Budget())
|
const budget = ref(new Budget())
|
||||||
|
|
||||||
const create = async () => {
|
const create = async () => {
|
||||||
console.log(budget.value)
|
|
||||||
try {
|
try {
|
||||||
emits("budget-created", budget.value, createRecurrentPayments.value);
|
emits("budget-created", budget.value, createRecurrentPayments.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<LoadingView v-if="loading"/>
|
<LoadingView v-if="loading"/>
|
||||||
<div v-else class="px-4 bg-gray-100 h-full flex flex-col gap-4 ">
|
<div v-else class="p-4 bg-gray-100 h-full flex flex-col gap-4 ">
|
||||||
<!-- Заголовок -->
|
<!-- Заголовок -->
|
||||||
<div class="flex flex-row gap-4 items-center">
|
<div class="flex flex-row gap-4 items-center">
|
||||||
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
||||||
@@ -10,10 +10,21 @@
|
|||||||
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Плитка с бюджетами -->
|
<!-- Плитка с бюджетами -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div v-if="!selectedSpace" class="flex w-full h-full items-center justify-center">
|
||||||
|
<p>Сперва нужно выбрать Пространство.
|
||||||
|
<button class="text-blue-500 hover:underline" @click="router.push('/spaces').then((res) => router.go(0))">Перейти</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="budgetInfos.length==0" class="flex w-full h-full items-center justify-center">
|
||||||
|
<p>Кажется, в этом пространстве еще нет бюджетов
|
||||||
|
<button class="text-blue-500 hover:underline" @click="creationOpened=true">создайте один.</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<!-- Будущие и текущие бюджеты -->
|
<!-- Будущие и текущие бюджеты -->
|
||||||
<ConfirmDialog/>
|
<ConfirmDialog/>
|
||||||
<Toast/>
|
<Toast/>
|
||||||
|
|
||||||
<div v-for="budget in budgetInfos" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-white"
|
<div v-for="budget in budgetInfos" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-white"
|
||||||
:class="budget.dateTo < new Date() ? 'bg-gray-100 opacity-60' : ''">
|
:class="budget.dateTo < new Date() ? 'bg-gray-100 opacity-60' : ''">
|
||||||
<div class="flex flex-row justify-between gap-4">
|
<div class="flex flex-row justify-between gap-4">
|
||||||
@@ -32,43 +43,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="mb-4">-->
|
|
||||||
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ formatAmount(budgettotalIncomes) }} ₽</span></div>-->
|
|
||||||
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
|
||||||
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ formatAmount(budget.totalExpenses) }} ₽</span></div>-->
|
|
||||||
<!-- <div class="text-sm flex items-center">-->
|
|
||||||
<!-- Unplanned Expenses:-->
|
|
||||||
<!-- <span class="ml-2 font-bold">{{ formatAmount(budget.totalIncomes - budget.totalExpenses) }} ₽</span>-->
|
|
||||||
<!-- Прогресс бар -->
|
|
||||||
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Прошедшие бюджеты (забеленные) -->
|
|
||||||
<!-- <div v-for="budget in pastBudgets" :key="budget.id" class="p-4 shadow-lg rounded-lg bg-gray-100 opacity-60">-->
|
|
||||||
<!-- <div class="text-xl font-bold mb-2">{{ budget.month }}</div>-->
|
|
||||||
<!-- <div class="text-sm text-gray-600 mb-4">-->
|
|
||||||
<!-- {{ budget.startDate }} - {{ budget.endDate }}-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div class="mb-4">-->
|
|
||||||
<!-- <div class="text-sm">Total Income: <span class="font-bold">{{ budget.totalIncome }}</span></div>-->
|
|
||||||
<!-- <div class="text-sm">Total Expenses: <span class="font-bold">{{ budget.totalExpenses }}</span></div>-->
|
|
||||||
<!-- <div class="text-sm">Planned Expenses: <span class="font-bold">{{ budget.plannedExpenses }}</span></div>-->
|
|
||||||
<!-- <div class="text-sm flex items-center">-->
|
|
||||||
<!-- Unplanned Expenses:-->
|
|
||||||
<!-- <span class="ml-2 font-bold">{{ budget.remainingForUnplanned }}</span>-->
|
|
||||||
<!-- <!– Прогресс бар –>-->
|
|
||||||
<!-- <ProgressBar :value="budget.unplannedProgress" class="ml-4 w-full"/>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref} from 'vue';
|
import {computed, onMounted, ref, watch} from 'vue';
|
||||||
import {Budget, BudgetInfo} from "@/models/Budget";
|
import {Budget, BudgetInfo} from "@/models/Budget";
|
||||||
import {createBudget, deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
|
import {createBudget, deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
|
||||||
import {formatDate} from "@/utils/utils";
|
import {formatDate} from "@/utils/utils";
|
||||||
@@ -80,6 +63,8 @@ import StatusView from "@/components/StatusView.vue";
|
|||||||
import {useConfirm} from "primevue/useconfirm";
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
import {useToast} from "primevue/usetoast";
|
import {useToast} from "primevue/usetoast";
|
||||||
import Toast from "primevue/toast";
|
import Toast from "primevue/toast";
|
||||||
|
import router from "@/router";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -104,30 +89,6 @@ const creationSuccessShow = async (budget, createRecurrentPayments) => {
|
|||||||
// }
|
// }
|
||||||
// , 1000)
|
// , 1000)
|
||||||
}
|
}
|
||||||
const pastBudgets = ref([
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
month: 'September 2024',
|
|
||||||
startDate: '2024-09-01',
|
|
||||||
endDate: '2024-09-30',
|
|
||||||
totalIncome: '450,000 RUB',
|
|
||||||
totalExpenses: '400,000 RUB',
|
|
||||||
plannedExpenses: '350,000 RUB',
|
|
||||||
remainingForUnplanned: '50,000 RUB',
|
|
||||||
unplannedProgress: 90,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
month: 'August 2024',
|
|
||||||
startDate: '2024-08-01',
|
|
||||||
endDate: '2024-08-31',
|
|
||||||
totalIncome: '400,000 RUB',
|
|
||||||
totalExpenses: '370,000 RUB',
|
|
||||||
plannedExpenses: '320,000 RUB',
|
|
||||||
remainingForUnplanned: '50,000 RUB',
|
|
||||||
unplannedProgress: 85,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const deleteBudget = async (budget: Budget) => {
|
const deleteBudget = async (budget: Budget) => {
|
||||||
|
|
||||||
@@ -160,13 +121,34 @@ const deleteBudget = async (budget: Budget) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedSpace.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
await getBudgetInfos().then((result) => {
|
||||||
|
budgetInfos.value = result
|
||||||
|
loading.value = false;
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching budget infos:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
if (selectedSpace.value) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
budgetInfos.value = await getBudgetInfos()
|
budgetInfos.value = await getBudgetInfos()
|
||||||
|
loading.value = false;
|
||||||
loading.value = false
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const emits = defineEmits(['open-drawer', 'transaction-checked', 'transaction-up
|
|||||||
|
|
||||||
const setIsDoneTrue = async () => {
|
const setIsDoneTrue = async () => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
console.log("here")
|
|
||||||
await updateTransactionRequest(props.transaction)
|
await updateTransactionRequest(props.transaction)
|
||||||
emits('transaction-updated')
|
emits('transaction-updated')
|
||||||
}, 20);
|
}, 20);
|
||||||
@@ -63,7 +63,7 @@ const toggleDrawer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const transactionUpdate = () => {
|
const transactionUpdate = () => {
|
||||||
console.log("transaction updated")
|
|
||||||
emits('transaction-updated')
|
emits('transaction-updated')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ const leftForUnplanned = ref(0)
|
|||||||
const budget = ref<Budget>()
|
const budget = ref<Budget>()
|
||||||
const warns = ref<[Warn]>()
|
const warns = ref<[Warn]>()
|
||||||
const checkWarnsExists = computed(() => {
|
const checkWarnsExists = computed(() => {
|
||||||
console.log(warns?.value && warns.value.length > 0 ? "true" : "false");
|
|
||||||
return warns?.value?.length > 0;
|
return warns?.value?.length > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,21 +14,16 @@ import {Budget, BudgetCategory, Warn} from "@/models/Budget";
|
|||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {formatAmount, formatDate, getMonthName, getMonthName2} from "@/utils/utils";
|
import {formatAmount, formatDate, getMonthName, getMonthName2} from "@/utils/utils";
|
||||||
import ProgressBar from "primevue/progressbar";
|
import ProgressBar from "primevue/progressbar";
|
||||||
import ProgressSpinner from "primevue/progressspinner";
|
|
||||||
import BudgetCategoryView from "@/components/budgets/BudgetCategoryView.vue";
|
|
||||||
import {Transaction} from "@/models/Transaction";
|
import {Transaction} from "@/models/Transaction";
|
||||||
import Toast from "primevue/toast";
|
import Toast from "primevue/toast";
|
||||||
import Button from "primevue/button";
|
|
||||||
import LoadingView from "@/components/LoadingView.vue";
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||||
import {Chart as ChartJS} from 'chart.js/auto';
|
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 Checkbox from "primevue/checkbox";
|
|
||||||
import {useDrawerStore} from "@/stores/drawerStore";
|
import {useDrawerStore} from "@/stores/drawerStore";
|
||||||
import {EventBus} from '@/utils/EventBus.ts';
|
import {EventBus} from '@/utils/EventBus.ts';
|
||||||
import {useUserStore} from "@/stores/userStore";
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
// Зарегистрируем плагин
|
// Зарегистрируем плагин
|
||||||
ChartJS.register(ChartDataLabels);
|
ChartJS.register(ChartDataLabels);
|
||||||
@@ -60,7 +55,7 @@ const leftForUnplanned = ref(0)
|
|||||||
const budget = ref<Budget>()
|
const budget = ref<Budget>()
|
||||||
const warns = ref<[Warn]>()
|
const warns = ref<[Warn]>()
|
||||||
const checkWarnsExists = computed(() => {
|
const checkWarnsExists = computed(() => {
|
||||||
console.log(warns?.value && warns.value.length > 0 ? "true" : "false");
|
|
||||||
return warns?.value?.length > 0;
|
return warns?.value?.length > 0;
|
||||||
});
|
});
|
||||||
const categories = ref<BudgetCategory[]>([])
|
const categories = ref<BudgetCategory[]>([])
|
||||||
@@ -384,10 +379,10 @@ const updateLimitOnBackend = async (categoryId, newLimit) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
const budgetInfo = ref<Budget>();
|
const budgetInfo = ref<Budget>();
|
||||||
const fetchBudgetInfo = async (test) => {
|
const fetchBudgetInfo = async (test) => {
|
||||||
loading.value = test ? test : false;
|
try {
|
||||||
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
|
||||||
@@ -397,10 +392,24 @@ const fetchBudgetInfo = async (test) => {
|
|||||||
incomeCategories.value = budget.value?.incomeCategories
|
incomeCategories.value = budget.value?.incomeCategories
|
||||||
updateLoading.value = false
|
updateLoading.value = false
|
||||||
}
|
}
|
||||||
)
|
).catch((error) => {
|
||||||
|
loading.value = false
|
||||||
|
updateLoading.value = false
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка!',
|
||||||
|
detail: error.response?.data?.message || 'Ошибка при создании транзакции',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({severity: 'error', summary: "Ошибка при получении бюджета", detail: error.message, life: 3000});
|
||||||
|
}
|
||||||
updateLoading.value = false
|
updateLoading.value = false
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -613,7 +622,7 @@ const expandCats = (value: boolean) => {
|
|||||||
for (const categoryId in incomeCategoriesState) {
|
for (const categoryId in incomeCategoriesState) {
|
||||||
if (Object.prototype.hasOwnProperty.call(incomeCategoriesState, categoryId)) {
|
if (Object.prototype.hasOwnProperty.call(incomeCategoriesState, categoryId)) {
|
||||||
incomeCategoriesState[categoryId].isOpened = value;
|
incomeCategoriesState[categoryId].isOpened = value;
|
||||||
console.log(`Категория ${categoryId}: isOpened = ${incomeCategoriesState[categoryId].isOpened}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,7 +630,7 @@ const expandCats = (value: boolean) => {
|
|||||||
for (const categoryId in expenseCategoriesState) {
|
for (const categoryId in expenseCategoriesState) {
|
||||||
if (Object.prototype.hasOwnProperty.call(expenseCategoriesState, categoryId)) {
|
if (Object.prototype.hasOwnProperty.call(expenseCategoriesState, categoryId)) {
|
||||||
expenseCategoriesState[categoryId].isOpened = value;
|
expenseCategoriesState[categoryId].isOpened = value;
|
||||||
console.log(`Категория ${categoryId}: isOpened = ${expenseCategoriesState[categoryId].isOpened}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -710,29 +719,17 @@ watch([budget, plannedExpenses], () => {
|
|||||||
calendar.value = result;
|
calendar.value = result;
|
||||||
}, {immediate: true});
|
}, {immediate: true});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
updateLoading.value = true;
|
updateLoading.value = true;
|
||||||
try {
|
|
||||||
await Promise.all([
|
|
||||||
fetchBudgetInfo(),
|
|
||||||
fetchWarns()
|
|
||||||
// budget.value = await getBudgetInfo(route.params.id),
|
|
||||||
// fetchPlannedIncomes(),
|
|
||||||
// fetchPlannedExpenses(),
|
|
||||||
// fetchBudgetCategories(),
|
|
||||||
// fetchBudgetTransactions(),
|
|
||||||
]);
|
|
||||||
EventBus.on('transactions-updated', fetchBudgetInfo, true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during fetching data:', error);
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(async () => {
|
fetchBudgetInfo()
|
||||||
EventBus.off('transactions-updated', fetchBudgetInfo);
|
fetchWarns()
|
||||||
|
EventBus.on('transactions-updated', fetchBudgetInfo, true);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(async () => {
|
||||||
|
EventBus.off('transactions-updated', fetchBudgetInfo);
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const createTransaction = async () => {
|
|||||||
editedTransaction.value.isDone = true;
|
editedTransaction.value.isDone = true;
|
||||||
}
|
}
|
||||||
await createTransactionRequest(editedTransaction.value).then((result) => {
|
await createTransactionRequest(editedTransaction.value).then((result) => {
|
||||||
console.log("hereeeee");
|
|
||||||
toast.add({severity: 'success', summary: 'Transaction created!', detail: 'Транзакция создана!', life: 3000});
|
toast.add({severity: 'success', summary: 'Transaction created!', detail: 'Транзакция создана!', life: 3000});
|
||||||
emit('create-transaction', editedTransaction.value);
|
emit('create-transaction', editedTransaction.value);
|
||||||
computeResult(true)
|
computeResult(true)
|
||||||
@@ -266,9 +266,9 @@ onMounted(async () => {
|
|||||||
|
|
||||||
prepareData();
|
prepareData();
|
||||||
|
|
||||||
console.log("is editing " + !isEditing.value)
|
|
||||||
if ( !isEditing.value) {
|
if ( !isEditing.value) {
|
||||||
console.log("here blyat")
|
|
||||||
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id, false, 3 )
|
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id, false, 3 )
|
||||||
.then(transactionsResponse => transactions.value = transactionsResponse.data);
|
.then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||||
|
|
||||||
|
|||||||
70
src/components/faq/About.vue
Normal file
70
src/components/faq/About.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col p-4 bg-gray-100 w-full h-full gap-4">
|
||||||
|
<h1 class="font-bold text-xl leading-tight">Привет!</h1>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Не знаю как ты здесь оказался, но раз ты здесь я тебе рад.
|
||||||
|
Я немного расскажу тебе о том, где ты и как я подразумеваю как с этим работать.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Все это - Luminic.Space. Пространство для ведения бюджетов. Изначально, это предпологалось только под семейные
|
||||||
|
бюджеты и я делал этот приложение для себя с женой. Но в целом, я понимаю, что не важно, что планировать и
|
||||||
|
бюджетировать: будь то семейный бюджет, съемки, путешествие или что-то другое. Разница только в статьях расхода.
|
||||||
|
</p>
|
||||||
|
<h2 class="text-lg">Как работать?</h2>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Вот основные сущности, используемые в приложении:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Пространство - отдельное место со своим наполнением: бюджетами, категориями, плановыми платежами и
|
||||||
|
тд.
|
||||||
|
</li>
|
||||||
|
<li>Бюджет - ограниченный датами финансовый план. Содержит плановые расходы, плановые поступления, настройки по
|
||||||
|
категориям и фактические транзакции.
|
||||||
|
</li>
|
||||||
|
<li>Транзакции - движение средств. Поступления, расходы. Бывают 2 типов - Плановые и фактические. Плановые можно
|
||||||
|
пометить как выполенные и тогда к ним автоматически создастся фактическая трата.
|
||||||
|
</li>
|
||||||
|
<li>Категории - логическая разбивка транзакций: Дом, Продукты, Развлечения, спорт.</li>
|
||||||
|
<li>Повторяющиеся платежи - операции, которые повторяются из месяца в месяц: кредиты, платежи на квартиру и тд. и
|
||||||
|
тп.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h2 class="text-lg">Принципы работы пространств</h2>
|
||||||
|
<p class="text-gray-600">Каждое пространство содержит свой набор бюджетов, транзакций, категорий, повторяющихся платежей. Так же для каждого пространства считается своя аналитика.</p>
|
||||||
|
<p class="text-gray-600">Пространство может содержать безлимитный набор людей, имеющих доступ к нему. Все, что нужно - создать приглашение и передать ссылку на него нужному человеку.</p>
|
||||||
|
<p class="text-gray-600">Владелец (создатель) пространства может удалять людей из пространства, а так же само пространство.</p>
|
||||||
|
<p class="text-gray-600">Участник пространства может покидать пространство.</p>
|
||||||
|
<h2 class="text-lg">Принципы работа бюджета</h2>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Первое, что нужно понять - Бюджет и транзакция не связаны напрямую. Бюджет включает в себя все транзакции, которые
|
||||||
|
попадают в даты активности бюджета. По этому если вы хотите разные транзакции в разные бюджеты в одни и те же даты
|
||||||
|
- создавайте еще одно пространство.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Бюджет содержит настройки категорий. В такие настройки входит: лимит по категории, сумма плановых трат по
|
||||||
|
категории и сумма текущих трат по категории.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-600">При этом, важно, что лимит по категории складывается из суммы плановых трат и <span
|
||||||
|
class="text-bold">дополнительного гэпа</span>,
|
||||||
|
который вы можете заложить поверх.
|
||||||
|
Удаляете плановую трату - лимит по категории уменьшается на сумму, добавляете - увеличивается. Меняете сумму
|
||||||
|
траты - тоже самое.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-600">При создании бюджета нужно указать 2 даты: Дата старта и дата завершения. Менять их нельзя.
|
||||||
|
В одном пространстве не может быть более 1 бюджета на одни даты. Это возможно стоит поменять. Но пока это так.</p>
|
||||||
|
<p class="text-gray-600">Так же при создании бюджета можно указать необходимость создания Повторяющихся платежей.
|
||||||
|
Если выбрано, то автоматически будут созданы все Повторяющиеся платежи в пространстве в установленные даты.</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<p class="text-gray-600">Самое время начать и начни с <router-link to="/spaces/" class="text-blue-500 hover:underline">создания Пространства</router-link></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -13,7 +13,7 @@ const categories = ref<Category[]>([]);
|
|||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
console.log('loaded')
|
|
||||||
const result = await getCategories();
|
const result = await getCategories();
|
||||||
categories.value = result.data;
|
categories.value = result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import {RecurrentPayment} from "@/models/Recurrent";
|
import {RecurrentPayment} from "@/models/Recurrent";
|
||||||
import {onMounted, ref} from "vue";
|
import {computed, onMounted, ref} from "vue";
|
||||||
import {getRecurrentPayments} from "@/services/recurrentService";
|
import {getRecurrentPayments} from "@/services/recurrentService";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
const recurrentPayments = ref<RecurrentPayment[]>([]);
|
const recurrentPayments = ref<RecurrentPayment[]>([]);
|
||||||
@@ -20,6 +22,10 @@ const fetchRecurrentPayments = async () => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const space = computed(() =>
|
||||||
|
spaceStore.space
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchRecurrentPayments()
|
await fetchRecurrentPayments()
|
||||||
})
|
})
|
||||||
@@ -31,7 +37,9 @@ onMounted(async () => {
|
|||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="">
|
<div v-else class="">
|
||||||
<div class="flex flex-col bg-gray-200 outline outline-2 outline-gray-300 rounded-2xl p-4">
|
{{space}}
|
||||||
|
<div v-if="!space">Выберите сперва пространство</div>
|
||||||
|
<div v-else 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">
|
<div class="flex flex-row items-center min-w-fit justify-between">
|
||||||
<p class="text-2xl font-bold">Повторяемые операции</p>
|
<p class="text-2xl font-bold">Повторяемые операции</p>
|
||||||
<router-link to="/settings/recurrents">
|
<router-link to="/settings/recurrents">
|
||||||
@@ -51,7 +59,8 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center p-x-4 justify-items-end text-end ">
|
<div class="flex flex-col items-center p-x-4 justify-items-end text-end ">
|
||||||
<p :class="recurrent.category.type.code == 'EXPENSE' ? 'text-red-400' : 'text-green-400' " class=" font-bold">- {{ recurrent.amount }} руб.</p>
|
<p :class="recurrent.category.type.code == 'EXPENSE' ? 'text-red-400' : 'text-green-400' "
|
||||||
|
class=" font-bold">- {{ recurrent.amount }} руб.</p>
|
||||||
<p class="text-end"> {{ recurrent.atDay }} числа</p>
|
<p class="text-end"> {{ recurrent.atDay }} числа</p>
|
||||||
<!-- <Button icon="pi pi-pen-to-square" rounded @click="openEdit"/>-->
|
<!-- <Button icon="pi pi-pen-to-square" rounded @click="openEdit"/>-->
|
||||||
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
|
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
</div>
|
</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 dark:bg-gray-800 h-screen p-4">
|
||||||
<ConfirmDialog/>
|
<ConfirmDialog/>
|
||||||
|
<div v-if="!space">Выберите сперва пространство</div>
|
||||||
<!-- Заголовок и кнопка добавления категории -->
|
<!-- Заголовок и кнопка добавления категории -->
|
||||||
|
<div v-else>
|
||||||
<div class="flex flex-col gap-4 xl:flex-row justify-between bg-gray-100">
|
<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-5xl font-bold">Категории</h2>
|
||||||
<Button label="Добавить категорию" icon="pi pi-plus" class="text-sm" @click="openCreateDialog(null)"/>
|
<Button label="Добавить категорию" icon="pi pi-plus" class="text-sm" @click="openCreateDialog(null)"/>
|
||||||
@@ -33,7 +35,8 @@
|
|||||||
<div class=" gap-4 ">
|
<div class=" gap-4 ">
|
||||||
<div class="flex flex-row gap-2 ">
|
<div class="flex flex-row gap-2 ">
|
||||||
<h3 class="text-2xl">Поступления</h3>
|
<h3 class="text-2xl">Поступления</h3>
|
||||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success" @click="openCreateDialog('INCOME')"/>
|
<Button icon="pi pi-plus" rounded outlined class="p-button-success"
|
||||||
|
@click="openCreateDialog('INCOME')"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=" overflow-y-auto mt-2 space-y-2 px-2">
|
<div class=" overflow-y-auto mt-2 space-y-2 px-2">
|
||||||
@@ -95,6 +98,7 @@
|
|||||||
@update:visible="closeCreateDialog"
|
@update:visible="closeCreateDialog"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -118,6 +122,8 @@ import {
|
|||||||
import {useConfirm} from "primevue/useconfirm";
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
import {useToast} from "primevue/usetoast";
|
import {useToast} from "primevue/usetoast";
|
||||||
import LoadingView from "@/components/LoadingView.vue";
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {getBudgetInfos} from "@/services/budgetsService";
|
||||||
|
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
@@ -229,7 +235,7 @@ const confirmDelete = async (category: Category) => {
|
|||||||
toast.add({severity: 'error', summary: 'Rejected', detail: 'You have rejected', life: 3000});
|
toast.add({severity: 'error', summary: 'Rejected', detail: 'You have rejected', life: 3000});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(confirm)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCat = async (categoryId: number) => {
|
const deleteCat = async (categoryId: number) => {
|
||||||
@@ -251,11 +257,35 @@ watch(editingCategory, (newCategory) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
const spaceStore = useSpaceStore()
|
||||||
|
const space = computed(() => {
|
||||||
|
return spaceStore.space
|
||||||
|
})
|
||||||
|
watch(
|
||||||
|
space,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
await fetchCategories().then((result) => {
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching budget infos:', error);
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (space.value) {
|
||||||
await fetchCategories();
|
await fetchCategories();
|
||||||
await fetchCategoryTypes();
|
await fetchCategoryTypes();
|
||||||
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const openEdit = () => {
|
|||||||
|
|
||||||
// Функция для удаления категории
|
// Функция для удаления категории
|
||||||
const deleteCategory = () => {
|
const deleteCategory = () => {
|
||||||
console.log('deleteCategory ' + props.category?.id);
|
|
||||||
emit("delete-category", props.category); // Использование события для удаления категории
|
emit("delete-category", props.category); // Использование события для удаления категории
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ const deleteCategory = () => {
|
|||||||
|
|
||||||
<div class="flex flex-row items-center p-x-4 gap-2 ">
|
<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-pen-to-square" rounded @click="openEdit"/>
|
||||||
<Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>
|
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<!-- RecurrentPaymentsList.vue -->
|
<!-- RecurrentPaymentsList.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div v-if="loading" >
|
<div v-if="loading">
|
||||||
<LoadingView/>
|
<LoadingView/>
|
||||||
</div>
|
</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 py-15">
|
||||||
<!-- Заголовок -->
|
<!-- Заголовок -->
|
||||||
<h1 class="text-4xl font-extrabold mb-8 text-gray-800">Ежемесячные платежи</h1>
|
<h1 class="text-4xl font-extrabold mb-8 text-gray-800">Ежемесячные платежи</h1>
|
||||||
|
<div v-if="!space">Выберите сперва пространство</div>
|
||||||
<!-- Список рекуррентных платежей -->
|
<!-- Список рекуррентных платежей -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 w-full max-w-7xl px-4">
|
<div v-else class="grid grid-cols-1 lg:grid-cols-3 gap-6 w-full max-w-7xl px-4">
|
||||||
<RecurrentListItem
|
<RecurrentListItem
|
||||||
v-for="payment in recurrentPayments"
|
v-for="payment in recurrentPayments"
|
||||||
:key="payment.id"
|
:key="payment.id"
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref} from 'vue';
|
import {computed, onMounted, ref, watch} from 'vue';
|
||||||
// import RecurrentPaymentCard from './RecurrentPaymentCard.vue';
|
// import RecurrentPaymentCard from './RecurrentPaymentCard.vue';
|
||||||
import RecurrentListItem from "@/components/settings/recurrent/RecurrentListItem.vue";
|
import RecurrentListItem from "@/components/settings/recurrent/RecurrentListItem.vue";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
@@ -52,6 +52,30 @@ import CreateRecurrentModal from "@/components/settings/recurrent/CreateRecurren
|
|||||||
import {Category, CategoryType} from "@/models/Category";
|
import {Category, CategoryType} from "@/models/Category";
|
||||||
import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
||||||
import LoadingView from "@/components/LoadingView.vue";
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {getBudgetInfos} from "@/services/budgetsService";
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
|
||||||
|
const space = computed(() =>
|
||||||
|
spaceStore.space
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => space.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
await fetchRecurrentPayments()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching budget infos:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
@@ -66,7 +90,7 @@ const recurrentPayments = ref<RecurrentPayment[]>([]);
|
|||||||
const fetchRecurrentPayments = async () => {
|
const fetchRecurrentPayments = async () => {
|
||||||
// loading.value = true;
|
// loading.value = true;
|
||||||
try {
|
try {
|
||||||
console.log('loaded')
|
|
||||||
const result = await getRecurrentPayments();
|
const result = await getRecurrentPayments();
|
||||||
recurrentPayments.value = result.data;
|
recurrentPayments.value = result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -111,15 +135,18 @@ const savePayment = async () => {
|
|||||||
|
|
||||||
// Обработчики событий
|
// Обработчики событий
|
||||||
const editPayment = (payment: any) => {
|
const editPayment = (payment: any) => {
|
||||||
console.log('Edit payment:', payment);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePayment = (payment: any) => {
|
const deletePayment = (payment: any) => {
|
||||||
console.log('Delete payment:', payment);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
if (space.value){
|
||||||
await fetchRecurrentPayments()
|
await fetchRecurrentPayments()
|
||||||
|
}
|
||||||
|
|
||||||
await fetchCategories()
|
await fetchCategories()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
72
src/components/spaces/SpaceCreationDialog.vue
Normal file
72
src/components/spaces/SpaceCreationDialog.vue
Normal 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 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'])
|
||||||
|
const props = defineProps({
|
||||||
|
opened: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const spaceName = ref('')
|
||||||
|
const spaceDescription = ref('')
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
resetForm()
|
||||||
|
emits("close-modal");
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSpace = async () => {
|
||||||
|
const space = new Space()
|
||||||
|
space.name = spaceName.value
|
||||||
|
space.description = spaceDescription.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 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>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
50
src/components/spaces/SpaceInventationView.vue
Normal file
50
src/components/spaces/SpaceInventationView.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import Toast from "primevue/toast";
|
||||||
|
import {acceptInviteRequest} from "@/services/spaceService";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const code = route.path.split('/').pop();
|
||||||
|
|
||||||
|
|
||||||
|
const acceptInvite = async () => {
|
||||||
|
await acceptInviteRequest(code)
|
||||||
|
.then((response) => {
|
||||||
|
router.push( '/spaces').then(() => {
|
||||||
|
router.go(0); // Это перезагружает страницу, при этом остается на том же пути
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
toast.add({ severity: 'error', summary: 'Ошибка входа', detail: error.response.data.message, life: 3000 })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Toast/>
|
||||||
|
<div class="flex w-full h-full items-center justify-center bg-gray-100">
|
||||||
|
<div class="flex flex-col w-full items-center gap-5">
|
||||||
|
<h1 class="text-xl font-bold">Вы были приглашены в пространство по коду {{ code }}</h1>
|
||||||
|
<p class="text-lg">Принять приглашение?</p>
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<Button label="Отказаться" severity="secondary" @click="router.push('/spaces')"/>
|
||||||
|
<Button label="Принять" severity="success" @click="acceptInvite"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
343
src/components/spaces/SpacesList.vue
Normal file
343
src/components/spaces/SpacesList.vue
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, onMounted, ref, watch, reactive, onUnmounted} from 'vue';
|
||||||
|
import {Space, SpaceInvite} from "@/models/Space";
|
||||||
|
import {
|
||||||
|
createSpaceInvite,
|
||||||
|
deleteSpaceRequest,
|
||||||
|
getSpaces,
|
||||||
|
kickMemberFromSpaceRequest,
|
||||||
|
leaveSpaceRequest
|
||||||
|
} from "@/services/spaceService";
|
||||||
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
|
import {formatDate, formatDateTime} from "../../utils/utils";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import Toast from "primevue/toast";
|
||||||
|
import Dialog from "primevue/dialog";
|
||||||
|
import InputText from "primevue/inputtext";
|
||||||
|
import SpaceCreationDialog from "@/components/spaces/SpaceCreationDialog.vue";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import {deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
|
||||||
|
import ConfirmDialog from "primevue/confirmdialog";
|
||||||
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {EventBus} from "@/utils/EventBus";
|
||||||
|
import {useUserStore} from "@/stores/userStore";
|
||||||
|
import {User} from "@/models/User";
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const loading = ref(true);
|
||||||
|
const creationOpened = ref(false);
|
||||||
|
const spaceCreated = async () => {
|
||||||
|
creationOpened.value = false;
|
||||||
|
await spaceStore.fetchSpaces();
|
||||||
|
}
|
||||||
|
const invite = ref<SpaceInvite | null>(null);
|
||||||
|
const inviteUrl = computed(() => {
|
||||||
|
if (invite.value) {
|
||||||
|
return 'https://luminic.space/spaces/invite/' + invite.value.code;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const inviteCreatedDialog = ref<boolean | null>(null);
|
||||||
|
|
||||||
|
const createInvite = async (space: Space) => {
|
||||||
|
confirm.require({
|
||||||
|
message: `Вы действительно хотите создать приглашение в пространство ${space.name}?`,
|
||||||
|
header: 'Создание приграшения',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Отмена',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Отмена',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Создать',
|
||||||
|
severity: 'success'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
try {
|
||||||
|
await createSpaceInvite().then(async (res) => {
|
||||||
|
await spaceStore.fetchSpaces();
|
||||||
|
invite.value = res;
|
||||||
|
inviteCreatedDialog.value = true;
|
||||||
|
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка!',
|
||||||
|
detail: error.response?.data?.message || 'Ошибка при создании приглашения',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили создание приглашения', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
const leaveSpace = async (space: Space) => {
|
||||||
|
|
||||||
|
confirm.require({
|
||||||
|
message: `Вы действительно хотите выйти из пространства ${space.name}?`,
|
||||||
|
header: 'Выход из пространства',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Отмена',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Отмена',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Выйти',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
try {
|
||||||
|
await leaveSpaceRequest(space.id).then((res) => {
|
||||||
|
spaceStore.fetchSpaces();
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка!',
|
||||||
|
detail: error.response?.data?.message || 'Ошибка при выходе из пространства',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили выход', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const kickMember = async (space: Space, user: User) => {
|
||||||
|
|
||||||
|
confirm.require({
|
||||||
|
message: `Вы действительно хотите исключить пользователя ${user.firstName} из пространства ${space.name}?`,
|
||||||
|
header: 'Исключить из пространства',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Отмена',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Отмена',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Исключить',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
try {
|
||||||
|
await kickMemberFromSpaceRequest(space.id, user.username).then((res) => {
|
||||||
|
spaceStore.fetchSpaces();
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка!',
|
||||||
|
detail: error.response?.data?.message || 'Ошибка при исключении из пространства',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили исключение', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSpace = async (space: Space) => {
|
||||||
|
|
||||||
|
confirm.require({
|
||||||
|
message: `Вы действительно хотите удалить пространство ${space.name}? Будут удалены все бюджеты!`,
|
||||||
|
header: 'Удаление пространства',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Отмена',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Отмена',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Удалить',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
try {
|
||||||
|
await deleteSpaceRequest(space.id).then((res) => {
|
||||||
|
spaceStore.fetchSpaces();
|
||||||
|
})
|
||||||
|
} catch (error: Error) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка!',
|
||||||
|
detail: error.response?.data?.message || 'Ошибка при удалении пространства',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили удаление', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const copied = ref(false);
|
||||||
|
|
||||||
|
const copyToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
copied.value = true;
|
||||||
|
// setTimeout(() => copied.value = false, 1500); // Убираем сообщение через 1.5 сек
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка копирования:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const spaces = computed(() => spaceStore.spaces)
|
||||||
|
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
const selectSpace = (space: Space) => {
|
||||||
|
spaceStore.setSpace(space);
|
||||||
|
localStorage.setItem("spaceId", space.id);
|
||||||
|
}
|
||||||
|
const loadingValue = computed(() => {
|
||||||
|
return !spaces.value
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<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 ">
|
||||||
|
<Toast/>
|
||||||
|
<ConfirmDialog/>
|
||||||
|
<Dialog :visible="inviteCreatedDialog" header="Приглашение" @hide="inviteCreatedDialog=false"
|
||||||
|
@update:visible="inviteCreatedDialog=false">
|
||||||
|
<div class="flex flex-col justify-start">
|
||||||
|
<div class="flex flex-row gap-2 items-center"> Ссылка приглашения:
|
||||||
|
<input class="p-2 border min-w-fit w-80" v-model="inviteUrl" disabled></input>
|
||||||
|
<button @click="copyToClipboard('https://luminic.space/spaces/invite/' + invite.code)">
|
||||||
|
{{ !copied ? 'Копировать' : 'Скопировано!' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>Действует до {{ formatDateTime(invite.activeTill) }} и только на 1 использование.</p>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
<div v-for="space in spaces" class="w-full ">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-white p-4 shadow-lg rounded-lg flex flex-col gap-2 items-start justify-between h-[200px] ">
|
||||||
|
<div class="flex flex-col w-full h-full justify-between">
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between"><p class="text-xl font-bold line-clamp-1">{{ space.name }}</p>
|
||||||
|
<button @click="selectSpace(space)">
|
||||||
|
|
||||||
|
<div class="flex p-1 rounded border"
|
||||||
|
:class="selectedSpace? selectedSpace.id === space.id ? 'bg-green-100' : 'bg-gray-100':'bg-gray-100'"
|
||||||
|
@click="spaceStore.setSpace(space)">
|
||||||
|
<span v-if="selectedSpace? space.id === selectedSpace.id : false"
|
||||||
|
class="font-bold text-gray-500 items-center"><i
|
||||||
|
class="pi pi-check"/> Выбрано</span>
|
||||||
|
<span v-else class="">Выбрать</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="line-clamp-2 items-start flex ">{{ space.description }}</p>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<div v-for="user in space.users"
|
||||||
|
:key="user.id"
|
||||||
|
@mouseover="user.isHovered = true"
|
||||||
|
@mouseleave="user.isHovered = false">
|
||||||
|
<div
|
||||||
|
|
||||||
|
class="relative flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center"
|
||||||
|
|
||||||
|
>
|
||||||
|
<!-- Первая буква имени -->
|
||||||
|
<span class="text-white text-center">
|
||||||
|
{{ user.firstName.substring(0, 1) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Иконка короны для владельца -->
|
||||||
|
<i
|
||||||
|
v-if="space.owner.id === user.id"
|
||||||
|
class="pi pi-crown absolute -top-1 -right-1 text-yellow-400 z-10 bg-white rounded-full p-[0.2rem] border"
|
||||||
|
style="font-size: 0.8rem"
|
||||||
|
></i>
|
||||||
|
|
||||||
|
<!-- Всплывающее окно -->
|
||||||
|
<div
|
||||||
|
v-if="user.isHovered"
|
||||||
|
class="absolute top-10 left-20 transform -translate-x-1/2 bg-white shadow-lg rounded-lg p-2 w-40 z-50 border"
|
||||||
|
>
|
||||||
|
<p class="text-sm font-semibold text-gray-800">{{ user.firstName }} {{ user.lastName }}</p>
|
||||||
|
<p class="text-xs text-gray-500">Роль: {{ user.role || 'Пользователь' }}</p>
|
||||||
|
|
||||||
|
<!-- Кнопка удаления (только если это не текущий пользователь) -->
|
||||||
|
<button
|
||||||
|
v-if="user.id !== useUserStore().user.id"
|
||||||
|
@click="kickMember(space, user)"
|
||||||
|
class="mt-2 bg-red-500 text-white text-xs rounded p-1 w-full hover:bg-red-600 transition"
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="space.owner.id == useUserStore().user.id"
|
||||||
|
class="flex bg-gray-300 hover:bg-emerald-300 rounded-full w-10 h-10 items-center justify-center relative"
|
||||||
|
>
|
||||||
|
<i class="text-white text-center pi pi-plus" @click="createInvite(space)"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-between items-center w-full">
|
||||||
|
<p class="text-sm text-gray-600">Создано: {{ formatDate(space.createdAt) }}</p>
|
||||||
|
<button v-if="space.owner.id == useUserStore().user.id" @click="deleteSpace(space)"
|
||||||
|
class="flex justify-end"><i class="pi pi-trash"
|
||||||
|
style="color:red; font-size: 1.2rem"/>
|
||||||
|
</button>
|
||||||
|
<button v-else class="flex items-center gap-2 text-sm" @click="leaveSpace(space)">Выйти<i
|
||||||
|
class="pi pi-sign-out" style="font-size: 0.7rem"/></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<div class="bg-white p-4 shadow-lg rounded-lg flex flex-col gap-2 justify-center items-center h-[200px]">
|
||||||
|
<button class="flex-col" @click="creationOpened = !creationOpened">
|
||||||
|
<i class="pi pi-plus-circle text-emerald-300" style="font-size: 2.5rem"></i>
|
||||||
|
<p class="text-emerald-600">Создать пространство</p>
|
||||||
|
</button>
|
||||||
|
<SpaceCreationDialog :opened="creationOpened" @spaceCreated="spaceCreated"
|
||||||
|
@close-modal="creationOpened = false"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -51,13 +51,13 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const closeDrawer = () => {
|
const closeDrawer = () => {
|
||||||
console.log("close drawer");
|
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
emit('close-drawer');
|
emit('close-drawer');
|
||||||
};
|
};
|
||||||
|
|
||||||
const transactionUpdated = (text) => {
|
const transactionUpdated = (text) => {
|
||||||
console.log(text)
|
|
||||||
emit("transaction-updated");
|
emit("transaction-updated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import DatePicker from "primevue/datepicker";
|
|||||||
import FloatLabel from "primevue/floatlabel";
|
import FloatLabel from "primevue/floatlabel";
|
||||||
import InputNumber from "primevue/inputnumber";
|
import InputNumber from "primevue/inputnumber";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import {ref, onMounted, computed, nextTick} from 'vue';
|
import {ref, onMounted, computed, nextTick, watch} from 'vue';
|
||||||
import {Transaction, TransactionType} from "@/models/Transaction";
|
import {Transaction, TransactionType} from "@/models/Transaction";
|
||||||
import {Category, CategoryType} from "@/models/Category";
|
import {Category, CategoryType} from "@/models/Category";
|
||||||
import SelectButton from "primevue/selectbutton";
|
import SelectButton from "primevue/selectbutton";
|
||||||
@@ -24,6 +24,7 @@ import {useUserStore} from "@/stores/userStore";
|
|||||||
|
|
||||||
|
|
||||||
import {EventBus} from '@/utils/EventBus';
|
import {EventBus} from '@/utils/EventBus';
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ const prepareData = () => {
|
|||||||
editedTransaction.value = new Transaction();
|
editedTransaction.value = new Transaction();
|
||||||
editedTransaction.value.type = transactionTypes.value.find(type => type.code === props.transactionType) || transactionTypes.value[0];
|
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];
|
selectedCategoryType.value = categoryTypes.value.find(type => type.code === props.categoryType) || categoryTypes.value[0];
|
||||||
console.log("hui " + props.categoryId)
|
|
||||||
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 = 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.category = props.categoryType === 'EXPENSE' ? expenseCategories.value[0] : incomeCategories.value[0];
|
||||||
@@ -131,7 +132,7 @@ const prepareData = () => {
|
|||||||
} else {
|
} else {
|
||||||
editedTransaction.value = {...props.transaction};
|
editedTransaction.value = {...props.transaction};
|
||||||
selectedCategoryType.value = editedTransaction.value.category.type;
|
selectedCategoryType.value = editedTransaction.value.category.type;
|
||||||
console.log('here')
|
|
||||||
selectedTransactionType.value = editedTransaction.value.transactionType;
|
selectedTransactionType.value = editedTransaction.value.transactionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,8 +226,8 @@ const createTransaction = async (): Promise<void> => {
|
|||||||
|
|
||||||
|
|
||||||
const transactionsUpdatedEmit = async () => {
|
const transactionsUpdatedEmit = async () => {
|
||||||
await getTransactions('INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
await getTransactions(spaceStore.space?.id, 'INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||||
console.log("here created ")
|
|
||||||
EventBus.emit('transactions-updated', true)
|
EventBus.emit('transactions-updated', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,6 +306,21 @@ const keyboardOpen = ref(false);
|
|||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
const userAgent = ref(null);
|
const userAgent = ref(null);
|
||||||
const transactions = ref<Transaction[]>(null);
|
const transactions = ref<Transaction[]>(null);
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
watch( selectedSpace, async (newValue, oldValue) => {
|
||||||
|
if (newValue != oldValue) {
|
||||||
|
if (!isEditing.value) {
|
||||||
|
|
||||||
|
// transactions.value = transactions.value.slice(0,3)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
// Мониторинг при монтировании
|
// Мониторинг при монтировании
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
||||||
@@ -313,18 +329,19 @@ onMounted(async () => {
|
|||||||
await fetchCategoriesAndTypes();
|
await fetchCategoriesAndTypes();
|
||||||
|
|
||||||
prepareData();
|
prepareData();
|
||||||
console.log("is editing " + !isEditing.value)
|
|
||||||
if (!isEditing.value) {
|
if (!isEditing.value) {
|
||||||
console.log("is editing " + !isEditing.value)
|
|
||||||
await getTransactions('INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
|
||||||
// transactions.value = transactions.value.slice(0,3)
|
// transactions.value = transactions.value.slice(0,3)
|
||||||
console.log(transactions.value.slice(0, 3))
|
await getTransactions(selectedSpace.value?.id,'INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
const deviceInfo = platform;
|
const deviceInfo = platform;
|
||||||
isMobile.value = deviceInfo.os.family === 'iOS' || deviceInfo.os.family === 'Android';
|
isMobile.value = deviceInfo.os.family === 'iOS' || deviceInfo.os.family === 'Android';
|
||||||
await nextTick();
|
await nextTick();
|
||||||
console.log('Amount Input Ref:', amountInput.value);
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -464,7 +481,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
|
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
|
||||||
class="flex flexgap-4"
|
|
||||||
:transaction="transaction"/>
|
:transaction="transaction"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted, onUnmounted, ref} from "vue";
|
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
|
||||||
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
|
import BudgetTransactionView from "@/components/budgets/BudgetTransactionView.vue";
|
||||||
import IconField from "primevue/iconfield";
|
import IconField from "primevue/iconfield";
|
||||||
import InputIcon from "primevue/inputicon";
|
import InputIcon from "primevue/inputicon";
|
||||||
@@ -11,6 +11,8 @@ import ProgressSpinner from "primevue/progressspinner";
|
|||||||
import {getUsers} from "@/services/userService";
|
import {getUsers} from "@/services/userService";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import { EventBus } from '@/utils/EventBus.ts';
|
import { EventBus } from '@/utils/EventBus.ts';
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const searchText = ref("");
|
const searchText = ref("");
|
||||||
@@ -21,13 +23,13 @@ const allLoaded = ref(false); // Флаг для отслеживания око
|
|||||||
|
|
||||||
// Функция для получения транзакций с параметрами limit и offset
|
// Функция для получения транзакций с параметрами limit и offset
|
||||||
const fetchTransactions = async (reload) => {
|
const fetchTransactions = async (reload) => {
|
||||||
console.log(reload);
|
|
||||||
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const response = await getTransactions('INSTANT', null, null, selectedUserId.value ? selectedUserId.value : null, null, reload ? offset.value : limit, reload ? 0 : offset.value);
|
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 newTransactions = response.data;
|
const newTransactions = response.data;
|
||||||
|
|
||||||
// Проверка на конец данных
|
// Проверка на конец данных
|
||||||
@@ -45,6 +47,7 @@ const fetchTransactions = async (reload) => {
|
|||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchUserFilter = async (user) => {
|
const switchUserFilter = async (user) => {
|
||||||
if (selectedUserId.value == user.id) {
|
if (selectedUserId.value == user.id) {
|
||||||
selectedUserId.value = null
|
selectedUserId.value = null
|
||||||
@@ -107,11 +110,22 @@ const fetchUsers = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedTransactionType = ref(null)
|
const selectedTransactionType = ref(null)
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
watch( selectedSpace, async (newValue, oldValue) => {
|
||||||
|
if (newValue != oldValue) {
|
||||||
|
await fetchTransactions(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
const types = ref([])
|
const types = ref([])
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
EventBus.on('transactions-updated', fetchTransactions,true);
|
EventBus.on('transactions-updated', fetchTransactions,true);
|
||||||
|
if (selectedSpace.value){
|
||||||
await fetchTransactions(); // Первоначальная загрузка данных
|
await fetchTransactions(); // Первоначальная загрузка данных
|
||||||
await fetchUsers();
|
|
||||||
|
}
|
||||||
|
// await fetchUsers();
|
||||||
await getTransactionTypes().then( it => types.value = it.data);
|
await getTransactionTypes().then( it => types.value = it.data);
|
||||||
// window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
|
// window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
|
||||||
});
|
});
|
||||||
@@ -125,14 +139,19 @@ onUnmounted( async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="px-4 bg-gray-100 h-full">
|
<div class="px-4 bg-gray-100 h-full">
|
||||||
<h2 class="text-4xl mb-6 font-bold">Список транзакций </h2>
|
<h2 class="text-4xl mb-6 font-bold">Список транзакций </h2>
|
||||||
<div class="flex flex-col gap-2">
|
<div v-if="!selectedSpace" class="flex w-full h-full items-center justify-center">
|
||||||
|
<p>Сперва нужно выбрать Пространство.
|
||||||
|
<button class="text-blue-500 hover:underline" @click="router.push('/spaces').then((res) => router.go(0))">Перейти</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
<IconField>
|
<IconField>
|
||||||
<InputIcon class="pi pi-search"/>
|
<InputIcon class="pi pi-search"/>
|
||||||
<InputText v-model="searchText" placeholder="поиск"></InputText>
|
<InputText v-model="searchText" placeholder="поиск"></InputText>
|
||||||
</IconField>
|
</IconField>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<!-- <span v-for="user in users">{{user.id}}</span>-->
|
<!-- <span v-for="user in users">{{user.id}}</span>-->
|
||||||
<button v-for="user in users" @click="switchUserFilter(user)"
|
<button v-for="user in selectedSpace.users" @click="switchUserFilter(user)"
|
||||||
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
|
class="rounded-xl border p-1 bg-white border-gray-300 mb-2 min-w-fit px-2"
|
||||||
:class="selectedUserId == user.id ? '!bg-blue-100' : ''">
|
:class="selectedUserId == user.id ? '!bg-blue-100' : ''">
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ app.config.globalProperties.$primevue.config.locale = {
|
|||||||
|
|
||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
navigator.serviceWorker.register("/service-worker.js").then((registration) => {
|
navigator.serviceWorker.register("/service-worker.js").then((registration) => {
|
||||||
console.log("Service Worker registered with scope:", registration.scope);
|
// console.log("Service Worker registered with scope:", registration.scope);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error("Service Worker registration failed:", error);
|
console.error("Service Worker registration failed:", error);
|
||||||
});
|
});
|
||||||
|
|||||||
17
src/models/Space.ts
Normal file
17
src/models/Space.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {User} from "@/models/User";
|
||||||
|
|
||||||
|
export class Space {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
owner: User
|
||||||
|
users: User[]
|
||||||
|
createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpaceInvite {
|
||||||
|
code: string
|
||||||
|
fromUser: User
|
||||||
|
activeTill: Date
|
||||||
|
isInviteActive: boolean
|
||||||
|
}
|
||||||
@@ -9,13 +9,21 @@ import SettingsView from "@/components/settings/SettingsView.vue";
|
|||||||
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
|
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
|
||||||
import TransactionList from "@/components/transactions/TransactionList.vue";
|
import TransactionList from "@/components/transactions/TransactionList.vue";
|
||||||
import LoginView from "@/components/auth/LoginView.vue";
|
import LoginView from "@/components/auth/LoginView.vue";
|
||||||
|
import RegisterView from "@/components/auth/RegisterView.vue";
|
||||||
import AnalyticsView from "@/components/analytics/AnalyticsView.vue";
|
import AnalyticsView from "@/components/analytics/AnalyticsView.vue";
|
||||||
import BudgetViewboth from "@/components/budgets/BudgetViewboth.vue";
|
import BudgetViewboth from "@/components/budgets/BudgetViewboth.vue";
|
||||||
|
import SpacesList from "@/components/spaces/SpacesList.vue";
|
||||||
|
import SpaceInventationView from "@/components/spaces/SpaceInventationView.vue";
|
||||||
|
import About from "@/components/faq/About.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{path: '/login', component: LoginView},
|
{path: '/login', component: LoginView},
|
||||||
{path: '/', name: 'Budgets main', component: BudgetList, meta: {requiresAuth: true}},
|
{path: '/register', component: RegisterView},
|
||||||
|
{path: '/', name: 'Aboutmain', component: About, meta: {requiresAuth: true}},
|
||||||
|
{path: '/about', name: 'About', component: About, meta: {requiresAuth: true}},
|
||||||
{path: '/analytics', name: 'Analytics', component: AnalyticsView, meta: {requiresAuth: true}},
|
{path: '/analytics', name: 'Analytics', component: AnalyticsView, meta: {requiresAuth: true}},
|
||||||
|
{path: '/spaces', name: 'Spaces', component: SpacesList, meta: {requiresAuth: true}},
|
||||||
|
{path: '/spaces/invite/:code', name: 'Space Invite View', component: SpaceInventationView, meta: {requiresAuth: true}},
|
||||||
{path: '/budgets', name: 'Budgets', component: BudgetList, meta: {requiresAuth: true}},
|
{path: '/budgets', name: 'Budgets', component: BudgetList, meta: {requiresAuth: true}},
|
||||||
{path: '/budgets/:id', name: 'BudgetView', component: BudgetViewboth, meta: {requiresAuth: true}},
|
{path: '/budgets/:id', name: 'BudgetView', component: BudgetViewboth, meta: {requiresAuth: true}},
|
||||||
{path: '/budgets-new/:id', name: 'BudgetView New', component: BudgetView2, meta: {requiresAuth: true}},
|
{path: '/budgets-new/:id', name: 'BudgetView New', component: BudgetView2, meta: {requiresAuth: true}},
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import {format} from "date-fns";
|
|||||||
|
|
||||||
export const getBudgetInfos = async () => {
|
export const getBudgetInfos = async () => {
|
||||||
try {
|
try {
|
||||||
|
let spaceId = localStorage.getItem("spaceId")
|
||||||
|
let response = await apiClient.get(`/spaces/${spaceId}/budgets`);
|
||||||
let response = await apiClient.get('/budgets');
|
|
||||||
let budgetInfos = response.data;
|
let budgetInfos = response.data;
|
||||||
budgetInfos.forEach((budgetInfo: Budget) => {
|
budgetInfos.forEach((budgetInfo: Budget) => {
|
||||||
budgetInfo.dateFrom = new Date(budgetInfo.dateFrom);
|
budgetInfo.dateFrom = new Date(budgetInfo.dateFrom);
|
||||||
@@ -94,7 +93,7 @@ export const updateBudgetCategoryRequest = async (budget_id, category: BudgetCat
|
|||||||
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
|
export const createBudget = async (budget: Budget, createRecurrent: Boolean) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let spaceId = localStorage.getItem("spaceId")
|
||||||
let budgetToCreate = JSON.parse(JSON.stringify(budget));
|
let budgetToCreate = JSON.parse(JSON.stringify(budget));
|
||||||
budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
|
budgetToCreate.dateFrom = format(budget.dateFrom, 'yyyy-MM-dd')
|
||||||
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
|
budgetToCreate.dateTo = format(budget.dateTo, 'yyyy-MM-dd')
|
||||||
@@ -102,7 +101,7 @@ export const createBudget = async (budget: Budget, createRecurrent: Boolean) =>
|
|||||||
budget: budgetToCreate,
|
budget: budgetToCreate,
|
||||||
createRecurrent: createRecurrent
|
createRecurrent: createRecurrent
|
||||||
}
|
}
|
||||||
await apiClient.post('/budgets/', data);
|
await apiClient.post(`/spaces/${spaceId}/budgets`, data);
|
||||||
|
|
||||||
} catch (e){
|
} catch (e){
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -1,36 +1,42 @@
|
|||||||
// src/services/categoryService.ts
|
// src/services/categoryService.ts
|
||||||
import apiClient from '@/services/axiosSetup';
|
import apiClient from '@/services/axiosSetup';
|
||||||
import {Category} from "@/models/Category"; // Импортируете настроенный экземпляр axios
|
import {Category} from "@/models/Category";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore"; // Импортируете настроенный экземпляр axios
|
||||||
|
|
||||||
export const getCategories = async (type = null) => {
|
export const getCategories = async (type = null) => {
|
||||||
|
const spaceStore = await useSpaceStore();
|
||||||
|
|
||||||
const params = {};
|
const params = {};
|
||||||
if (type) {
|
if (type) {
|
||||||
params.type = type;
|
params.type = type;
|
||||||
}
|
}
|
||||||
return await apiClient.get('/categories', {
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/categories`, {
|
||||||
params: params
|
params: params
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategoryTypes = async () => {
|
export const getCategoryTypes = async () => {
|
||||||
return await apiClient.get('/categories/types');
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/categories/types`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createCategory = async (category: Category) => {
|
export const createCategory = async (category: Category) => {
|
||||||
return await apiClient.post('/categories', category);
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.post(`/spaces/${spaceStore.space?.id}/categories`, category);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCategory = async (id: number, category: any) => {
|
export const updateCategory = async (id: number, category: any) => {
|
||||||
return await apiClient.put(`/categories/${id}`, category);
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.put(`/spaces/${spaceStore.space?.id}/categories/${id}`, category);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteCategory = async (id: number) => {
|
export const deleteCategory = async (id: number) => {
|
||||||
return await apiClient.delete(`/categories/${id}`);
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/categories/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategoriesSumsRequest = async () => {
|
export const getCategoriesSumsRequest = async (spaceId: string) => {
|
||||||
return await apiClient.get('/categories/by-month2');
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/analytics/by-month`)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const applicationServerKey = ''
|
|||||||
|
|
||||||
|
|
||||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
console.log(base64String);
|
|
||||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||||
const rawData = window.atob(base64);
|
const rawData = window.atob(base64);
|
||||||
@@ -15,12 +15,15 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|||||||
export async function subscribeUserToPush() {
|
export async function subscribeUserToPush() {
|
||||||
const registration = await navigator.serviceWorker.ready;
|
const registration = await navigator.serviceWorker.ready;
|
||||||
let vapid = ''
|
let vapid = ''
|
||||||
|
if (localStorage.getItem("token")) {
|
||||||
await apiClient.get('/subscriptions/vapid').then((registration) => {
|
await apiClient.get('/subscriptions/vapid').then((registration) => {
|
||||||
vapid = registration.data
|
vapid = registration.data
|
||||||
console.log(registration.data)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return registration.pushManager.subscribe({
|
return registration.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: urlBase64ToUint8Array(vapid),
|
applicationServerKey: urlBase64ToUint8Array(vapid),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
// src/services/recurrentyService.ts
|
// src/services/recurrentyService.ts
|
||||||
import apiClient from '@/services/axiosSetup';
|
import apiClient from '@/services/axiosSetup';
|
||||||
import { RecurrentPayment} from "@/models/Recurrent"; // Импортируете настроенный экземпляр axios
|
import {RecurrentPayment} from "@/models/Recurrent";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore"; // Импортируете настроенный экземпляр axios
|
||||||
|
|
||||||
|
|
||||||
export const getRecurrentPayments = async () => {
|
export const getRecurrentPayments = async () => {
|
||||||
console.log('getRecurrentPayments');
|
const spaceStore = useSpaceStore()
|
||||||
return await apiClient.get('/recurrents/');
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/recurrents`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const saveRecurrentPayment = async (payment: RecurrentPayment) => {
|
export const saveRecurrentPayment = async (payment: RecurrentPayment) => {
|
||||||
console.log('saveRecurrentPayment');
|
const spaceStore = useSpaceStore()
|
||||||
return await apiClient.post('/recurrents/', payment)
|
return await apiClient.post(`/spaces/${spaceStore.space?.id}/recurrents`, payment)
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// export const getCategoryTypes = async () => {
|
// export const getCategoryTypes = async () => {
|
||||||
|
|||||||
47
src/services/spaceService.ts
Normal file
47
src/services/spaceService.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import apiClient from '@/services/axiosSetup';
|
||||||
|
import {Space} from "@/models/Space";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
|
||||||
|
export const getSpaces = async () => {
|
||||||
|
return await apiClient.get('/spaces').then((res) => {
|
||||||
|
res.data.forEach((space: Space) => {
|
||||||
|
space.createdAt = new Date(space.createdAt);
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSpaceRequest = async (space: Space) => {
|
||||||
|
return await apiClient.post('/spaces', space).then((res) => {
|
||||||
|
return res.data
|
||||||
|
}).catch((err) => {
|
||||||
|
throw err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const deleteSpaceRequest = async (spaceId: string) => {
|
||||||
|
return await apiClient.delete(`/spaces/${spaceId}`).then((res) => {
|
||||||
|
return res.data
|
||||||
|
}).catch((err) => {
|
||||||
|
throw err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const createSpaceInvite = async () => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.post(`/spaces/${spaceStore.space?.id}/invite`).then((res) => res.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const acceptInviteRequest = async (code:string) => {
|
||||||
|
await apiClient.post(`spaces/invite/${code}`).then((res) => res.data).catch((err) => {throw err})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const leaveSpaceRequest = async (spaceId:string) => {
|
||||||
|
await apiClient.delete(`spaces/${spaceId}/leave`).then((res) => res.data).catch((err) => {throw err})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const kickMemberFromSpaceRequest = async (spaceId:string, username: string) => {
|
||||||
|
await apiClient.delete(`spaces/${spaceId}/members/kick/${username}`).then((res) => res.data).catch((err) => {throw err})
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import apiClient from '@/services/axiosSetup';
|
import apiClient from '@/services/axiosSetup';
|
||||||
import {Transaction} from "@/models/Transaction";
|
import {Transaction} from "@/models/Transaction";
|
||||||
import {format} from "date-fns";
|
import {format} from "date-fns";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
// Импортируете настроенный экземпляр axios
|
// Импортируете настроенный экземпляр axios
|
||||||
|
|
||||||
export const getTransaction = async (transactionId: int) => {
|
export const getTransaction = async (transactionId: int) => {
|
||||||
return await apiClient.post(`/transactions/${transactionId}`,);
|
return await apiClient.post(`/transactions/${transactionId}`,);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTransactions = async (transaction_type = null, category_type = null, category_id = null, user_id = null, is_child = null, limit = null, offset = null) => {
|
export const getTransactions = async (spaceId, transaction_type = null, category_type = null, category_id = null, user_id = null, is_child = null, limit = null, offset = null) => {
|
||||||
const params = {};
|
const params = {};
|
||||||
|
// params.spaceId=spaceId;
|
||||||
|
|
||||||
console.log(is_child)
|
|
||||||
// Add the parameters to the params object if they are not null
|
// Add the parameters to the params object if they are not null
|
||||||
if (transaction_type) {
|
if (transaction_type) {
|
||||||
params.transaction_type = transaction_type;
|
params.transaction_type = transaction_type;
|
||||||
@@ -35,28 +36,30 @@ export const getTransactions = async (transaction_type = null, category_type = n
|
|||||||
if (offset) {
|
if (offset) {
|
||||||
params.offset = offset
|
params.offset = offset
|
||||||
}
|
}
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
// Use axios to make the GET request, passing the params as the second argument
|
// Use axios to make the GET request, passing the params as the second argument
|
||||||
return await apiClient.get('/transactions', {
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/transactions`, {
|
||||||
params: params
|
params: params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTransactionRequest = async (transaction: Transaction) => {
|
export const createTransactionRequest = async (transaction: Transaction) => {
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
transaction.date = format(transaction.date, 'yyyy-MM-dd')
|
||||||
let transactionResponse = await apiClient.post('/transactions', transaction);
|
let transactionResponse = await apiClient.post(`/spaces/${spaceStore.space?.id}/transactions`, transaction)
|
||||||
|
|
||||||
transaction.date = new Date(transaction.date);
|
transaction.date = new Date(transaction.date);
|
||||||
return transactionResponse.data
|
return transactionResponse.data
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTransactionRequest = async (transaction: Transaction) => {
|
export const updateTransactionRequest = async (transaction: Transaction) => {
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
const id = transaction.id
|
const id = transaction.id
|
||||||
// console.log(transaction.isDone)
|
// console.log(transaction.isDone)
|
||||||
// transaction.date = transaction.date.setHours(0,0,0,0)
|
// transaction.date = transaction.date.setHours(0,0,0,0)
|
||||||
transaction.date = format(transaction.date, "yyyy-MM-dd")
|
transaction.date = format(transaction.date, "yyyy-MM-dd")
|
||||||
|
|
||||||
const response = await apiClient.put(`/transactions/${id}`, transaction);
|
const response = await apiClient.put(`/spaces/${spaceStore.space?.id}/transactions/${id}`, transaction);
|
||||||
transaction = response.data
|
transaction = response.data
|
||||||
transaction.date = new Date(transaction.date);
|
transaction.date = new Date(transaction.date);
|
||||||
|
|
||||||
@@ -74,14 +77,17 @@ export const updateTransactionRequest = async (transaction: Transaction) => {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
export const deleteTransactionRequest = async (id: number) => {
|
export const deleteTransactionRequest = async (id: number) => {
|
||||||
return await apiClient.delete(`/transactions/${id}`);
|
const spaceStore = useSpaceStore()
|
||||||
|
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/transactions/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTransactionTypes = async () => {
|
export const getTransactionTypes = async () => {
|
||||||
return await apiClient.get('/transactions/types');
|
const spaceStore = useSpaceStore()
|
||||||
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/transactions/types`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTransactionCategoriesSums = async () => {
|
export const getTransactionCategoriesSums = async () => {
|
||||||
let response = await apiClient.get('/transactions/categories/_calc_sums');
|
const spaceStore = useSpaceStore()
|
||||||
|
let response = await apiClient.get(`/spaces/${spaceStore.space?.id}/transactions/categories/_calc_sums`);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
46
src/stores/spaceStore.ts
Normal file
46
src/stores/spaceStore.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {defineStore} from "pinia";
|
||||||
|
import {ref, watch} from "vue";
|
||||||
|
import {EventBus} from "@/utils/EventBus";
|
||||||
|
import {getSpaces} from "@/services/spaceService";
|
||||||
|
import {Space} from "@/models/Space";
|
||||||
|
|
||||||
|
export const useSpaceStore = defineStore('space', () => {
|
||||||
|
const spaces = ref();
|
||||||
|
const fetchSpaces = async () => {
|
||||||
|
await getSpaces().then((res) => {
|
||||||
|
spaces.value = res;
|
||||||
|
const spaceId = localStorage.getItem("spaceId");
|
||||||
|
space.value = spaces.value.find((s: Space) => s.id === spaceId) || null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const space = ref<Space | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
const getSpace = async () => {
|
||||||
|
const spaceId = localStorage.getItem("spaceId");
|
||||||
|
if (!spaceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spaces.value || spaces.value.length === 0) {
|
||||||
|
await fetchSpaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
space.value = spaces.value.find((s) => s.id === spaceId) || null;
|
||||||
|
return space.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSpace = (newSpace: Space | null) => {
|
||||||
|
if (newSpace != space) {
|
||||||
|
space.value = newSpace;
|
||||||
|
localStorage.setItem("spaceId", newSpace.id)
|
||||||
|
EventBus.emit("spaceChanged", space);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {spaces, fetchSpaces, getSpace, space, setSpace};
|
||||||
|
|
||||||
|
})
|
||||||
@@ -2,11 +2,13 @@ import {defineStore} from 'pinia';
|
|||||||
import {ref} from 'vue';
|
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";
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
const user = ref(null);
|
const user = ref(null);
|
||||||
const loadingUser = ref(true);
|
const loadingUser = ref(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const settings = ref({"budgetViewVersion": "2", "budgetCalendarOpened": false})
|
const settings = ref({"budgetViewVersion": "2", "budgetCalendarOpened": false})
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Основная функция для логина
|
// Основная функция для логина
|
||||||
async function login( username, password, tg_id=null) {
|
async function login(username, password, tg_id = null) {
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
if (tg_id) {
|
if (tg_id) {
|
||||||
@@ -45,6 +47,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||||
await fetchUserProfile();
|
await fetchUserProfile();
|
||||||
|
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);
|
||||||
@@ -52,5 +55,20 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {user, loadingUser, fetchUserProfile, login, settings};
|
async function register(username, password, firstName) {
|
||||||
|
try {
|
||||||
|
let response = await apiClient.post('/auth/register', {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
firstName: firstName
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(error);
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {user, loadingUser, fetchUserProfile, login, settings, register};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,23 @@ export const formatDate = (date) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatDateTime = (date) => {
|
||||||
|
const validDate = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
// Проверяем, является ли validDate корректной датой
|
||||||
|
if (isNaN(validDate.getTime())) {
|
||||||
|
return 'Invalid Date'; // Если дата неверная, возвращаем текст ошибки
|
||||||
|
}
|
||||||
|
|
||||||
|
return validDate.toLocaleString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false, // 24-часовой формат
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const getMonthName = (month: number) => {
|
export const getMonthName = (month: number) => {
|
||||||
switch (month) {
|
switch (month) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -68,5 +85,7 @@ export const generateRandomColors = () => {
|
|||||||
return [r, g, b]
|
return [r, g, b]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пример использования
|
export const getRandomColor = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user