add spaces
This commit is contained in:
@@ -1,38 +1,66 @@
|
||||
|
||||
<template>
|
||||
<div v-if="!loadingUser" class="card ">
|
||||
<Menubar :model="items" >
|
||||
<div v-if="!loadingUser && !loading" class="card ">
|
||||
<Menubar :model="items">
|
||||
<template #start>
|
||||
<!-- <svg width="35" height="40" viewBox="0 0 35 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-8">-->
|
||||
<!-- <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"-->
|
||||
<!-- fill="var(--p-primary-color)"-->
|
||||
<!-- />-->
|
||||
<!-- <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"-->
|
||||
<!-- fill="var(--p-text-color)"-->
|
||||
<!-- />-->
|
||||
<!-- </svg>-->
|
||||
<img alt="logo" src="/apple-touch-icon.png" width="32" height="32" />
|
||||
<!-- <svg width="35" height="40" viewBox="0 0 35 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-8">-->
|
||||
<!-- <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"-->
|
||||
<!-- fill="var(--p-primary-color)"-->
|
||||
<!-- />-->
|
||||
<!-- <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"-->
|
||||
<!-- fill="var(--p-text-color)"-->
|
||||
<!-- />-->
|
||||
<!-- </svg>-->
|
||||
<img alt="logo" src="/apple-touch-icon.png" width="32" height="32"/>
|
||||
</template>
|
||||
<template #item="{ item, props, hasSubmenu, root }">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</router-link>
|
||||
|
||||
</template>
|
||||
<!-- <Button-->
|
||||
<!-- @click="drawerStore.setCategoryType('EXPENSE');drawerStore.setTransactionType('INSTANT');drawerStore.visible = true"-->
|
||||
<!-- label="Создать"/>-->
|
||||
|
||||
<template #end>
|
||||
<div class="flex items-center gap-2">
|
||||
{{ user.firstName }}
|
||||
<Button @click="drawerStore.setCategoryType('EXPENSE');drawerStore.setTransactionType('INSTANT');drawerStore.visible = true" label="Создать"/>
|
||||
<Select v-model="spaceStore.space" :options="spaces" optionLabel="name" @change="selectSpace"/>
|
||||
<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" />-->
|
||||
<!-- <Avatar image="https://primefaces.org/cdn/primevue/images/avatar/amyelsner.png" shape="circle" />-->
|
||||
<!-- Всплывающая плашка с ID -->
|
||||
<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>
|
||||
</template>
|
||||
</Menubar>
|
||||
@@ -40,14 +68,24 @@
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref, watch} from "vue";
|
||||
import Badge from "primevue/badge";
|
||||
import Button from "primevue/button"
|
||||
import Menubar from "primevue/menubar";
|
||||
import Select from "primevue/select";
|
||||
import {useUserStore} from "@/stores/userStore";
|
||||
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 user = computed(() => userStore.user);
|
||||
@@ -56,8 +94,33 @@ const loadingUser = computed(() => userStore.loadingUser);
|
||||
const drawerStore = useDrawerStore()
|
||||
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([
|
||||
{
|
||||
@@ -65,6 +128,11 @@ const items = ref([
|
||||
icon: 'pi pi-home',
|
||||
url: '/'
|
||||
},
|
||||
{
|
||||
label: 'Пространства',
|
||||
icon: 'pi pi-compass',
|
||||
url: '/spaces'
|
||||
},
|
||||
{
|
||||
label: 'Аналитика',
|
||||
icon: 'pi pi-star',
|
||||
@@ -84,7 +152,23 @@ const items = ref([
|
||||
label: 'Настройки',
|
||||
icon: 'pi pi-envelope',
|
||||
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>
|
||||
|
||||
@@ -118,8 +118,7 @@ const openDrawer = (selectedTransactionType = null, selectedCategoryType = null)
|
||||
drawerStore.setTransactionType(selectedTransactionType)
|
||||
drawerStore.setCategoryType('EXPENSE')
|
||||
}
|
||||
console.log(selectedTransactionType)
|
||||
console.log(selectedCategoryType)
|
||||
|
||||
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(() => {
|
||||
// setTimeout(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
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 DataTable from "primevue/datatable";
|
||||
import Column from "primevue/column";
|
||||
@@ -14,12 +14,15 @@ import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
import {Chart as ChartJS} from 'chart.js/auto';
|
||||
import {Colors} from 'chart.js';
|
||||
import {getMonthName} from "@/utils/utils";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
import router from "@/router";
|
||||
import Button from "primevue/button";
|
||||
|
||||
ChartJS.register(ChartDataLabels);
|
||||
ChartJS.register(Colors);
|
||||
|
||||
|
||||
const loading = ref(true);
|
||||
const loading = ref(false);
|
||||
const categoriesCatalog = ref([])
|
||||
const categoriesCatalogGrouped = computed(() => {
|
||||
const cats = {}
|
||||
@@ -210,10 +213,19 @@ const prepareTableData = (categories) => {
|
||||
return rows;
|
||||
};
|
||||
|
||||
const spaceStore = useSpaceStore()
|
||||
const selectedSpace = computed(() => spaceStore.space)
|
||||
|
||||
watch( selectedSpace, async (newValue, oldValue) => {
|
||||
if (newValue != oldValue) {
|
||||
await fetchCategoriesSums()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const fetchCategoriesSums = async () => {
|
||||
loading.value = true;
|
||||
await getCategoriesSumsRequest().then((data) => {
|
||||
await getCategoriesSumsRequest(selectedSpace.value?.id).then((data) => {
|
||||
data.data.forEach((category) => {
|
||||
category.monthlySums.forEach((monthlySum) => {
|
||||
monthlySum.date = getMonthName(new Date(monthlySum.date).getMonth()) + " " + new Date(monthlySum.date).getFullYear()
|
||||
@@ -237,15 +249,25 @@ const fetchCategoriesCatalog = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([fetchCategoriesSums(), fetchCategoriesCatalog()])
|
||||
// await fetchCategoriesSums();
|
||||
await Promise.all([ fetchCategoriesCatalog()])
|
||||
if (selectedSpace.value) {
|
||||
await fetchCategoriesSums();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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 class="!items-center w-5/6 bg-white">
|
||||
<div v-else class="p-4 pb-20 lg:pb-4 bg-gray-100 flex flex-col gap-4 h-full items-center justify-items-center ">
|
||||
<div v-if="!selectedSpace" class="flex w-full h-full items-center justify-center">
|
||||
<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"
|
||||
@tab-close="closeChart">
|
||||
<AccordionPanel value="0">
|
||||
@@ -292,7 +314,7 @@ onMounted(async () => {
|
||||
</AccordionContent>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
|
||||
<DataTable :value="dataTableCategories" responsiveLayout="scroll" filter stripedRows class="w-5/6 items-center">
|
||||
<Column
|
||||
@@ -315,6 +337,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,110 +1,130 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center h-screen bg-gray-100">
|
||||
<div 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>
|
||||
{{ tg_id }}
|
||||
<form @submit.prevent="login">
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">Логин</label>
|
||||
<input
|
||||
v-model="username"
|
||||
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 class="flex flex-col items-center justify-center h-screen bg-gray-100">
|
||||
<Card class="w-full max-w-sm p-6 bg-white rounded-lg shadow-md">
|
||||
<template #title>
|
||||
<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>
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">Пароль</label>
|
||||
<input
|
||||
v-model="password"
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="login">
|
||||
<div class="mb-4">
|
||||
<label for="username" class="block 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-6">
|
||||
<label for="password" class="block text-sm font-semibold text-gray-700">Пароль</label>
|
||||
<Password id="password" v-model="password" class="w-full" :feedback="false" toggleMask />
|
||||
<small v-if="errors.password" class="text-red-500">{{ errors.password }}</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="/register" class="text-blue-500 hover:underline">Регистрация</RouterLink>
|
||||
</p>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import apiClient from '@/services/axiosSetup';
|
||||
import {useUserStore} from "@/stores/userStore";
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, 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 { useUserStore } from '@/stores/userStore'
|
||||
import apiClient from '@/services/axiosSetup'
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const loading = ref(false)
|
||||
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(() => {
|
||||
if (window.Telegram?.WebApp) {
|
||||
const tg = window.Telegram.WebApp;
|
||||
tg.expand(); // Разворачиваем приложение на весь экран
|
||||
return tg.initDataUnsafe.user?.id ?? null; // Если tg_id нет, возвращаем null
|
||||
const tg = window.Telegram.WebApp
|
||||
tg.expand() // Разворачиваем WebApp
|
||||
return tg.initDataUnsafe.user?.id ?? null
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return null
|
||||
})
|
||||
|
||||
// Функция для автоматического входа по tg_id
|
||||
// Авто-вход по Telegram ID
|
||||
const autoLoginWithTgId = async () => {
|
||||
if (tg_id.value) {
|
||||
try {
|
||||
const response = await apiClient.post('/auth/token/tg?tg_id=' + tg_id.value );
|
||||
const token = response.data.access_token;
|
||||
localStorage.setItem('token', token);
|
||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
await router.replace(route.query['back']+"/reload" ? route.query['back'].toString() : '/');
|
||||
const response = await apiClient.post('/auth/token/tg', { tg_id: tg_id.value })
|
||||
const token = response.data.access_token
|
||||
localStorage.setItem('token', token)
|
||||
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Вход выполнен', detail: 'Добро пожаловать!', life: 3000 })
|
||||
await router.replace(route.query['back'] ? route.query['back'].toString() : '/')
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Ошибка входа. Проверьте логин и пароль.');
|
||||
console.error(error)
|
||||
toast.add({ severity: 'error', summary: 'Ошибка входа', detail: 'Ошибка Telegram авторизации', life: 3000 })
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Вызов autoLoginWithTgId при загрузке компонента
|
||||
onMounted(() => {
|
||||
autoLoginWithTgId();
|
||||
});
|
||||
// Проверка формы перед отправкой
|
||||
const validateForm = () => {
|
||||
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 () => {
|
||||
await userStore.login(username.value, password.value);
|
||||
// try {
|
||||
// let response;
|
||||
// if (tg_id.value) {
|
||||
// response = await apiClient.post('/auth/token/tg', qs.stringify({ tg_id: tg_id.value }));
|
||||
// } else {
|
||||
// response = await apiClient.post('/auth/login', {
|
||||
// username: username.value,
|
||||
// password: password.value,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// 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() : '/');
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// alert('Ошибка входа. Проверьте логин и пароль.');
|
||||
// }
|
||||
};
|
||||
if (!validateForm()) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await userStore.login(username.value, password.value)
|
||||
toast.add({ severity: 'success', summary: 'Успешный вход', detail: 'Добро пожаловать!', life: 3000 })
|
||||
// await router.push('/')
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: 'Ошибка входа', detail: 'Неверные данные', life: 3000 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Запуск авто-входа при загрузке страницы
|
||||
onMounted(() => {
|
||||
autoLoginWithTgId()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
body {
|
||||
background-color: #f0f2f5;
|
||||
.p-invalid {
|
||||
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 create = async () => {
|
||||
console.log(budget.value)
|
||||
|
||||
try {
|
||||
emits("budget-created", budget.value, createRecurrentPayments.value);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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">
|
||||
<h2 class="text-4xl font-bold">Бюджеты</h2>
|
||||
@@ -10,10 +10,21 @@
|
||||
<StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>
|
||||
</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/>
|
||||
<Toast/>
|
||||
|
||||
<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' : ''">
|
||||
<div class="flex flex-row justify-between gap-4">
|
||||
@@ -32,43 +43,15 @@
|
||||
</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 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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {computed, onMounted, ref, watch} from 'vue';
|
||||
import {Budget, BudgetInfo} from "@/models/Budget";
|
||||
import {createBudget, deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
|
||||
import {formatDate} from "@/utils/utils";
|
||||
@@ -80,6 +63,8 @@ import StatusView from "@/components/StatusView.vue";
|
||||
import {useConfirm} from "primevue/useconfirm";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import Toast from "primevue/toast";
|
||||
import router from "@/router";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
@@ -104,30 +89,6 @@ const creationSuccessShow = async (budget, createRecurrentPayments) => {
|
||||
// }
|
||||
// , 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) => {
|
||||
|
||||
@@ -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 () => {
|
||||
loading.value = true;
|
||||
budgetInfos.value = await getBudgetInfos()
|
||||
|
||||
loading.value = false
|
||||
if (selectedSpace.value) {
|
||||
loading.value = true;
|
||||
budgetInfos.value = await getBudgetInfos()
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const emits = defineEmits(['open-drawer', 'transaction-checked', 'transaction-up
|
||||
|
||||
const setIsDoneTrue = async () => {
|
||||
setTimeout(async () => {
|
||||
console.log("here")
|
||||
|
||||
await updateTransactionRequest(props.transaction)
|
||||
emits('transaction-updated')
|
||||
}, 20);
|
||||
@@ -63,7 +63,7 @@ const toggleDrawer = () => {
|
||||
}
|
||||
|
||||
const transactionUpdate = () => {
|
||||
console.log("transaction updated")
|
||||
|
||||
emits('transaction-updated')
|
||||
}
|
||||
|
||||
|
||||
@@ -409,7 +409,7 @@ const leftForUnplanned = ref(0)
|
||||
const budget = ref<Budget>()
|
||||
const warns = ref<[Warn]>()
|
||||
const checkWarnsExists = computed(() => {
|
||||
console.log(warns?.value && warns.value.length > 0 ? "true" : "false");
|
||||
|
||||
return warns?.value?.length > 0;
|
||||
});
|
||||
|
||||
|
||||
@@ -14,21 +14,16 @@ import {Budget, BudgetCategory, Warn} from "@/models/Budget";
|
||||
import {useRoute} from "vue-router";
|
||||
import {formatAmount, formatDate, getMonthName, getMonthName2} from "@/utils/utils";
|
||||
import ProgressBar from "primevue/progressbar";
|
||||
import ProgressSpinner from "primevue/progressspinner";
|
||||
import BudgetCategoryView from "@/components/budgets/BudgetCategoryView.vue";
|
||||
import {Transaction} from "@/models/Transaction";
|
||||
import Toast from "primevue/toast";
|
||||
import Button from "primevue/button";
|
||||
import LoadingView from "@/components/LoadingView.vue";
|
||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
import {Chart as ChartJS} from 'chart.js/auto';
|
||||
import SelectButton from "primevue/selectbutton";
|
||||
import Divider from "primevue/divider";
|
||||
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
import {useDrawerStore} from "@/stores/drawerStore";
|
||||
import {EventBus} from '@/utils/EventBus.ts';
|
||||
import {useUserStore} from "@/stores/userStore";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
|
||||
// Зарегистрируем плагин
|
||||
ChartJS.register(ChartDataLabels);
|
||||
@@ -60,7 +55,7 @@ const leftForUnplanned = ref(0)
|
||||
const budget = ref<Budget>()
|
||||
const warns = ref<[Warn]>()
|
||||
const checkWarnsExists = computed(() => {
|
||||
console.log(warns?.value && warns.value.length > 0 ? "true" : "false");
|
||||
|
||||
return warns?.value?.length > 0;
|
||||
});
|
||||
const categories = ref<BudgetCategory[]>([])
|
||||
@@ -384,23 +379,37 @@ const updateLimitOnBackend = async (categoryId, newLimit) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const toast = useToast();
|
||||
const budgetInfo = ref<Budget>();
|
||||
const fetchBudgetInfo = async (test) => {
|
||||
loading.value = test ? test : false;
|
||||
await getBudgetInfo(route.params.id).then((data) => {
|
||||
budget.value = data
|
||||
plannedExpenses.value = budget.value?.plannedExpenses
|
||||
plannedIncomes.value = budget.value?.plannedIncomes
|
||||
transactions.value = budget.value?.transactions
|
||||
categories.value = budget.value?.categories
|
||||
incomeCategories.value = budget.value?.incomeCategories
|
||||
updateLoading.value = false
|
||||
}
|
||||
)
|
||||
try {
|
||||
await getBudgetInfo(route.params.id).then((data) => {
|
||||
budget.value = data
|
||||
plannedExpenses.value = budget.value?.plannedExpenses
|
||||
plannedIncomes.value = budget.value?.plannedIncomes
|
||||
transactions.value = budget.value?.transactions
|
||||
categories.value = budget.value?.categories
|
||||
incomeCategories.value = budget.value?.incomeCategories
|
||||
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
|
||||
loading.value = false
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -613,7 +622,7 @@ const expandCats = (value: boolean) => {
|
||||
for (const categoryId in incomeCategoriesState) {
|
||||
if (Object.prototype.hasOwnProperty.call(incomeCategoriesState, categoryId)) {
|
||||
incomeCategoriesState[categoryId].isOpened = value;
|
||||
console.log(`Категория ${categoryId}: isOpened = ${incomeCategoriesState[categoryId].isOpened}`);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,7 +630,7 @@ const expandCats = (value: boolean) => {
|
||||
for (const categoryId in expenseCategoriesState) {
|
||||
if (Object.prototype.hasOwnProperty.call(expenseCategoriesState, categoryId)) {
|
||||
expenseCategoriesState[categoryId].isOpened = value;
|
||||
console.log(`Категория ${categoryId}: isOpened = ${expenseCategoriesState[categoryId].isOpened}`);
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -710,29 +719,17 @@ watch([budget, plannedExpenses], () => {
|
||||
calendar.value = result;
|
||||
}, {immediate: true});
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(() => {
|
||||
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 () => {
|
||||
EventBus.off('transactions-updated', fetchBudgetInfo);
|
||||
fetchBudgetInfo()
|
||||
fetchWarns()
|
||||
EventBus.on('transactions-updated', fetchBudgetInfo, true);
|
||||
})
|
||||
|
||||
onUnmounted(async () => {
|
||||
EventBus.off('transactions-updated', fetchBudgetInfo);
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -165,7 +165,7 @@ const createTransaction = async () => {
|
||||
editedTransaction.value.isDone = true;
|
||||
}
|
||||
await createTransactionRequest(editedTransaction.value).then((result) => {
|
||||
console.log("hereeeee");
|
||||
|
||||
toast.add({severity: 'success', summary: 'Transaction created!', detail: 'Транзакция создана!', life: 3000});
|
||||
emit('create-transaction', editedTransaction.value);
|
||||
computeResult(true)
|
||||
@@ -266,9 +266,9 @@ onMounted(async () => {
|
||||
|
||||
prepareData();
|
||||
|
||||
console.log("is editing " + !isEditing.value)
|
||||
|
||||
if ( !isEditing.value) {
|
||||
console.log("here blyat")
|
||||
|
||||
await getTransactions('INSTANT', 'EXPENSE',null, user.value.id, false, 3 )
|
||||
.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 () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
console.log('loaded')
|
||||
|
||||
const result = await getCategories();
|
||||
categories.value = result.data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import Button from "primevue/button";
|
||||
import {RecurrentPayment} from "@/models/Recurrent";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {getRecurrentPayments} from "@/services/recurrentService";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
|
||||
const spaceStore = useSpaceStore()
|
||||
const loading = ref(true);
|
||||
|
||||
const recurrentPayments = ref<RecurrentPayment[]>([]);
|
||||
@@ -20,6 +22,10 @@ const fetchRecurrentPayments = async () => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const space = computed(() =>
|
||||
spaceStore.space
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchRecurrentPayments()
|
||||
})
|
||||
@@ -31,7 +37,9 @@ onMounted(async () => {
|
||||
Loading...
|
||||
</div>
|
||||
<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">
|
||||
<p class="text-2xl font-bold">Повторяемые операции</p>
|
||||
<router-link to="/settings/recurrents">
|
||||
@@ -51,7 +59,8 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<!-- <Button icon="pi pi-pen-to-square" rounded @click="openEdit"/>-->
|
||||
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
|
||||
|
||||
@@ -1,99 +1,103 @@
|
||||
<template>
|
||||
<div v-if="loading">
|
||||
<LoadingView/>
|
||||
<LoadingView/>
|
||||
</div>
|
||||
<div v-else class="flex flex-col bg-gray-100 dark:bg-gray-800 h-screen p-4">
|
||||
<ConfirmDialog/>
|
||||
<div v-if="!space">Выберите сперва пространство</div>
|
||||
<!-- Заголовок и кнопка добавления категории -->
|
||||
<div class="flex flex-col gap-4 xl:flex-row justify-between bg-gray-100">
|
||||
<h2 class="text-5xl font-bold">Категории</h2>
|
||||
<Button label="Добавить категорию" icon="pi pi-plus" class="text-sm" @click="openCreateDialog(null)"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="flex flex-col gap-4 xl:flex-row justify-between bg-gray-100">
|
||||
<h2 class="text-5xl font-bold">Категории</h2>
|
||||
<Button label="Добавить категорию" icon="pi pi-plus" class="text-sm" @click="openCreateDialog(null)"/>
|
||||
</div>
|
||||
|
||||
<!-- Поле для поиска -->
|
||||
<div class="my-4 w-full">
|
||||
<!-- Поле для поиска -->
|
||||
<div class="my-4 w-full">
|
||||
<span class="p-input-icon-left flex flex-row gap-2 items-center ">
|
||||
<i class="pi pi-search"></i>
|
||||
<InputText v-model="searchTerm" placeholder="Поиск категорий..." class="w-full"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Переключатель категорий (доходы/расходы) -->
|
||||
<div class="card flex lg:hidden justify-center mb-4">
|
||||
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
|
||||
aria-labelledby="category-switch"/>
|
||||
</div>
|
||||
<!-- Переключатель категорий (доходы/расходы) -->
|
||||
<div class="card flex lg:hidden justify-center mb-4">
|
||||
<SelectButton v-model="selectedCategoryType" :options="categoryTypes" optionLabel="name"
|
||||
aria-labelledby="category-switch"/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Список категорий с прокруткой для больших экранов -->
|
||||
<div class="flex">
|
||||
<div class="hidden lg:grid grid-cols-2 gap-x-10 w-full h-full justify-items-center overflow-y-auto">
|
||||
<!-- Категории доходов -->
|
||||
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
||||
<div class=" gap-4 ">
|
||||
<div class="flex flex-row gap-2 ">
|
||||
<h3 class="text-2xl">Поступления</h3>
|
||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success" @click="openCreateDialog('INCOME')"/>
|
||||
<!-- Список категорий с прокруткой для больших экранов -->
|
||||
<div class="flex">
|
||||
<div class="hidden lg:grid grid-cols-2 gap-x-10 w-full h-full justify-items-center overflow-y-auto">
|
||||
<!-- Категории доходов -->
|
||||
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
||||
<div class=" gap-4 ">
|
||||
<div class="flex flex-row gap-2 ">
|
||||
<h3 class="text-2xl">Поступления</h3>
|
||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success"
|
||||
@click="openCreateDialog('INCOME')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" overflow-y-auto mt-2 space-y-2 px-2">
|
||||
<CategoryListItem
|
||||
class=""
|
||||
v-for="category in filteredIncomeCategories"
|
||||
:key="category.id"
|
||||
:category="category"
|
||||
v-bind="category"
|
||||
@open-edit="openEdit"
|
||||
@delete-category="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" overflow-y-auto mt-2 space-y-2 px-2">
|
||||
<CategoryListItem
|
||||
class=""
|
||||
v-for="category in filteredIncomeCategories"
|
||||
:key="category.id"
|
||||
:category="category"
|
||||
v-bind="category"
|
||||
@open-edit="openEdit"
|
||||
@delete-category="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Категории расходов -->
|
||||
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
||||
<div class=" gap-4 justify-between ">
|
||||
<div class="flex flex-row gap-2">
|
||||
<h3 class="text-2xl">Расходы</h3>
|
||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success !hover:bg-green-600"
|
||||
@click="openCreateDialog('EXPENSE')"/>
|
||||
<!-- Категории расходов -->
|
||||
<div class="grid h-full w-full min-w-fit overflow-y-auto">
|
||||
<div class=" gap-4 justify-between ">
|
||||
<div class="flex flex-row gap-2">
|
||||
<h3 class="text-2xl">Расходы</h3>
|
||||
<Button icon="pi pi-plus" rounded outlined class="p-button-success !hover:bg-green-600"
|
||||
@click="openCreateDialog('EXPENSE')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" overflow-y-auto pb-10 mt-2 space-y-2 px-2">
|
||||
<CategoryListItem
|
||||
v-for="category in filteredExpenseCategories"
|
||||
:key="category.id"
|
||||
:category="category"
|
||||
v-bind="category"
|
||||
@open-edit="openEdit"
|
||||
@delete-category="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" overflow-y-auto pb-10 mt-2 space-y-2 px-2">
|
||||
<CategoryListItem
|
||||
v-for="category in filteredExpenseCategories"
|
||||
:key="category.id"
|
||||
:category="category"
|
||||
v-bind="category"
|
||||
@open-edit="openEdit"
|
||||
@delete-category="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Для маленьких экранов -->
|
||||
<div class="flex lg:hidden flex-wrap rounded w-full">
|
||||
<CategoryListItem
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:category="category"
|
||||
v-bind="category"
|
||||
class="mt-2"
|
||||
@open-edit="openEdit"
|
||||
@delete-category="confirmDelete"
|
||||
<!-- Для маленьких экранов -->
|
||||
<div class="flex lg:hidden flex-wrap rounded w-full">
|
||||
<CategoryListItem
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:category="category"
|
||||
v-bind="category"
|
||||
class="mt-2"
|
||||
@open-edit="openEdit"
|
||||
@delete-category="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateCategoryModal
|
||||
:show="isDialogVisible"
|
||||
:categoryTypes="categoryTypes"
|
||||
:selectedCategoryType="selectedCategoryType"
|
||||
:category="editingCategory"
|
||||
@saveCategory="saveCategory"
|
||||
@close-modal="closeCreateDialog"
|
||||
@update:visible="closeCreateDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateCategoryModal
|
||||
:show="isDialogVisible"
|
||||
:categoryTypes="categoryTypes"
|
||||
:selectedCategoryType="selectedCategoryType"
|
||||
:category="editingCategory"
|
||||
@saveCategory="saveCategory"
|
||||
@close-modal="closeCreateDialog"
|
||||
@update:visible="closeCreateDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -118,6 +122,8 @@ import {
|
||||
import {useConfirm} from "primevue/useconfirm";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import LoadingView from "@/components/LoadingView.vue";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
import {getBudgetInfos} from "@/services/budgetsService";
|
||||
|
||||
|
||||
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});
|
||||
}
|
||||
});
|
||||
console.log(confirm)
|
||||
|
||||
};
|
||||
|
||||
const deleteCat = async (categoryId: number) => {
|
||||
@@ -251,11 +257,35 @@ watch(editingCategory, (newCategory) => {
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchCategories();
|
||||
await fetchCategoryTypes();
|
||||
loading.value = false;
|
||||
});
|
||||
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 fetchCategoryTypes();
|
||||
}
|
||||
loading.value = false;
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@@ -19,7 +19,7 @@ const openEdit = () => {
|
||||
|
||||
// Функция для удаления категории
|
||||
const deleteCategory = () => {
|
||||
console.log('deleteCategory ' + props.category?.id);
|
||||
|
||||
emit("delete-category", props.category); // Использование события для удаления категории
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ const deleteCategory = () => {
|
||||
|
||||
<div class="flex flex-row items-center p-x-4 gap-2 ">
|
||||
<Button icon="pi pi-pen-to-square" rounded @click="openEdit"/>
|
||||
<Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>
|
||||
<!-- <Button icon="pi pi-trash" severity="danger" rounded @click="deleteCategory"/>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<!-- RecurrentPaymentsList.vue -->
|
||||
<template>
|
||||
<div v-if="loading" >
|
||||
<LoadingView/>
|
||||
<div v-if="loading">
|
||||
<LoadingView/>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<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
|
||||
v-for="payment in recurrentPayments"
|
||||
:key="payment.id"
|
||||
@@ -42,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {computed, onMounted, ref, watch} from 'vue';
|
||||
// import RecurrentPaymentCard from './RecurrentPaymentCard.vue';
|
||||
import RecurrentListItem from "@/components/settings/recurrent/RecurrentListItem.vue";
|
||||
import Button from "primevue/button";
|
||||
@@ -52,6 +52,30 @@ import CreateRecurrentModal from "@/components/settings/recurrent/CreateRecurren
|
||||
import {Category, CategoryType} from "@/models/Category";
|
||||
import {getCategories, getCategoryTypes} from "@/services/categoryService";
|
||||
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);
|
||||
|
||||
@@ -66,7 +90,7 @@ const recurrentPayments = ref<RecurrentPayment[]>([]);
|
||||
const fetchRecurrentPayments = async () => {
|
||||
// loading.value = true;
|
||||
try {
|
||||
console.log('loaded')
|
||||
|
||||
const result = await getRecurrentPayments();
|
||||
recurrentPayments.value = result.data;
|
||||
} catch (error) {
|
||||
@@ -111,15 +135,18 @@ const savePayment = async () => {
|
||||
|
||||
// Обработчики событий
|
||||
const editPayment = (payment: any) => {
|
||||
console.log('Edit payment:', payment);
|
||||
|
||||
};
|
||||
|
||||
const deletePayment = (payment: any) => {
|
||||
console.log('Delete payment:', payment);
|
||||
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchRecurrentPayments()
|
||||
if (space.value){
|
||||
await fetchRecurrentPayments()
|
||||
}
|
||||
|
||||
await fetchCategories()
|
||||
})
|
||||
</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 = () => {
|
||||
console.log("close drawer");
|
||||
|
||||
visible.value = false;
|
||||
emit('close-drawer');
|
||||
};
|
||||
|
||||
const transactionUpdated = (text) => {
|
||||
console.log(text)
|
||||
|
||||
emit("transaction-updated");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import DatePicker from "primevue/datepicker";
|
||||
import FloatLabel from "primevue/floatlabel";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
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 {Category, CategoryType} from "@/models/Category";
|
||||
import SelectButton from "primevue/selectbutton";
|
||||
@@ -24,6 +24,7 @@ import {useUserStore} from "@/stores/userStore";
|
||||
|
||||
|
||||
import {EventBus} from '@/utils/EventBus';
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -123,7 +124,7 @@ const prepareData = () => {
|
||||
editedTransaction.value = new Transaction();
|
||||
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];
|
||||
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]
|
||||
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];
|
||||
@@ -131,7 +132,7 @@ const prepareData = () => {
|
||||
} else {
|
||||
editedTransaction.value = {...props.transaction};
|
||||
selectedCategoryType.value = editedTransaction.value.category.type;
|
||||
console.log('here')
|
||||
|
||||
selectedTransactionType.value = editedTransaction.value.transactionType;
|
||||
}
|
||||
|
||||
@@ -225,8 +226,8 @@ const createTransaction = async (): Promise<void> => {
|
||||
|
||||
|
||||
const transactionsUpdatedEmit = async () => {
|
||||
await getTransactions('INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||
console.log("here created ")
|
||||
await getTransactions(spaceStore.space?.id, 'INSTANT', 'EXPENSE', null, user.value.id, false, 3).then(transactionsResponse => transactions.value = transactionsResponse.data);
|
||||
|
||||
EventBus.emit('transactions-updated', true)
|
||||
}
|
||||
|
||||
@@ -305,6 +306,21 @@ const keyboardOpen = ref(false);
|
||||
const isMobile = ref(false);
|
||||
const userAgent = ref(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 () => {
|
||||
|
||||
@@ -313,18 +329,19 @@ onMounted(async () => {
|
||||
await fetchCategoriesAndTypes();
|
||||
|
||||
prepareData();
|
||||
console.log("is editing " + !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)
|
||||
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;
|
||||
const deviceInfo = platform;
|
||||
isMobile.value = deviceInfo.os.family === 'iOS' || deviceInfo.os.family === 'Android';
|
||||
await nextTick();
|
||||
console.log('Amount Input Ref:', amountInput.value);
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -464,7 +481,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<BudgetTransactionView v-if="!isEditing && transactions" v-for="transaction in transactions" :is-list="true"
|
||||
class="flex flexgap-4"
|
||||
|
||||
:transaction="transaction"/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 IconField from "primevue/iconfield";
|
||||
import InputIcon from "primevue/inputicon";
|
||||
@@ -11,6 +11,8 @@ import ProgressSpinner from "primevue/progressspinner";
|
||||
import {getUsers} from "@/services/userService";
|
||||
import Button from "primevue/button";
|
||||
import { EventBus } from '@/utils/EventBus.ts';
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
import router from "@/router";
|
||||
|
||||
const loading = ref(false);
|
||||
const searchText = ref("");
|
||||
@@ -21,13 +23,13 @@ const allLoaded = ref(false); // Флаг для отслеживания око
|
||||
|
||||
// Функция для получения транзакций с параметрами limit и offset
|
||||
const fetchTransactions = async (reload) => {
|
||||
console.log(reload);
|
||||
|
||||
// if (loading.value || allLoaded.value) return; // Останавливаем загрузку, если уже загружается или данные загружены полностью
|
||||
loading.value = true;
|
||||
|
||||
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;
|
||||
|
||||
// Проверка на конец данных
|
||||
@@ -45,6 +47,7 @@ const fetchTransactions = async (reload) => {
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const switchUserFilter = async (user) => {
|
||||
if (selectedUserId.value == user.id) {
|
||||
selectedUserId.value = null
|
||||
@@ -107,11 +110,22 @@ const fetchUsers = async () => {
|
||||
}
|
||||
|
||||
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([])
|
||||
onMounted(async () => {
|
||||
EventBus.on('transactions-updated', fetchTransactions,true);
|
||||
await fetchTransactions(); // Первоначальная загрузка данных
|
||||
await fetchUsers();
|
||||
if (selectedSpace.value){
|
||||
await fetchTransactions(); // Первоначальная загрузка данных
|
||||
|
||||
}
|
||||
// await fetchUsers();
|
||||
await getTransactionTypes().then( it => types.value = it.data);
|
||||
// window.addEventListener("scroll", handleScroll); // Добавляем обработчик прокрутки
|
||||
});
|
||||
@@ -125,14 +139,19 @@ onUnmounted( async () => {
|
||||
<template>
|
||||
<div class="px-4 bg-gray-100 h-full">
|
||||
<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>
|
||||
<InputIcon class="pi pi-search"/>
|
||||
<InputText v-model="searchText" placeholder="поиск"></InputText>
|
||||
</IconField>
|
||||
<div class="flex flex-row gap-2">
|
||||
<!-- <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="selectedUserId == user.id ? '!bg-blue-100' : ''">
|
||||
|
||||
|
||||
Reference in New Issue
Block a user