add spaces

This commit is contained in:
xds
2025-02-17 17:57:43 +03:00
parent a530d657d3
commit fe56505629
37 changed files with 1464 additions and 403 deletions

View File

@@ -1,9 +1,9 @@
// public/service-worker.js
self.addEventListener("push", (event) => {
console.log(event)
const data = event.data.json();
console.log(data);
const options = {
body: data.body,
icon: data.icon,
@@ -16,7 +16,7 @@ self.addEventListener("push", (event) => {
self.addEventListener('notificationclick', function (event) {
// console.log("Notification click event received."); // Сообщение об активации обработчика
console.log("Notification data:", event.notification.data); // Проверка данных уведомления
event.notification.close(); // Закрываем уведомление
// console.log(event)

View File

@@ -1,6 +1,7 @@
<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 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"/>
@@ -14,9 +15,38 @@
<div class="bg-gray-100 h-12 block lg:hidden"></div>
</div>
<div id="footer" class="flex flex-col w-full h-fit bg-gray-200 p-4 gap-4">
<div class="flex flex-row items-center gap-6 ">
<div 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"
: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>
</template>
@@ -25,13 +55,14 @@
import MenuBar from "./components/MenuBar.vue";
import ToolBar from "@/components/ToolBar.vue";
import Button from "primevue/button";
import {computed, onMounted} from "vue";
import {computed, onMounted, ref} from "vue";
import {subscribeUserToPush} from "@/services/pushManager";
import apiClient from '@/services/axiosSetup';
import {useUserStore} from "@/stores/userStore";
import {useDrawerStore} from '@/stores/drawerStore'
import TransactionForm from "@/components/transactions/TransactionForm.vue";
import {useSpaceStore} from "@/stores/spaceStore";
import Toast from "primevue/toast";
const drawerStore = useDrawerStore();
@@ -65,7 +96,7 @@ const checkSubscribe = async () => {
// Пользователь ранее отклонил запрос
}
} else {
console.log("Notification API is not supported in this browser.");
// You may want to use an alternative method, like alerts or modals
}
}
@@ -73,8 +104,6 @@ const checkSubscribe = async () => {
const sendSubscribe = async () => {
try {
const subscription = await subscribeUserToPush();
console.log("Push subscription:", subscription);
// Отправка подписки на сервер для хранения
await apiClient.post("/subscriptions/subscribe", subscription)
} catch (error) {
@@ -82,19 +111,24 @@ const sendSubscribe = async () => {
}
}
const token = ref(localStorage.getItem("token"));
const userStore = useUserStore();
const user = computed(() => userStore.user);
const spaceStore = useSpaceStore()
onMounted(async () => {
if (!userStore.user) {
if (!userStore.user && localStorage.getItem("token")) {
await userStore.fetchUserProfile();
}
await checkSubscribe();
await spaceStore.fetchSpaces()
}
});

View File

@@ -1,6 +1,5 @@
<template>
<div v-if="!loadingUser" class="card ">
<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">-->
@@ -20,17 +19,46 @@
<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>
<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>
<!-- Всплывающая плашка с 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>
@@ -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>

View File

@@ -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(() => {

View File

@@ -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
@@ -316,6 +338,7 @@ onMounted(async () => {
</Column>
</DataTable>
</div>
</div>
</template>
<style>

View File

@@ -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 }}
<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>
<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"
/>
<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 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>
<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>
</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>
</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>

View 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>

View File

@@ -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) {

View File

@@ -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>-->
<!-- &lt;!&ndash; Прогресс бар &ndash;&gt;-->
<!-- <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 () => {
if (selectedSpace.value) {
loading.value = true;
budgetInfos.value = await getBudgetInfos()
loading.value = false
loading.value = false;
}
})
</script>

View File

@@ -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')
}

View File

@@ -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;
});

View File

@@ -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,10 +379,10 @@ const updateLimitOnBackend = async (categoryId, newLimit) => {
});
};
const toast = useToast();
const budgetInfo = ref<Budget>();
const fetchBudgetInfo = async (test) => {
loading.value = test ? test : false;
try {
await getBudgetInfo(route.params.id).then((data) => {
budget.value = data
plannedExpenses.value = budget.value?.plannedExpenses
@@ -397,10 +392,24 @@ const fetchBudgetInfo = async (test) => {
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,25 +719,13 @@ watch([budget, plannedExpenses], () => {
calendar.value = result;
}, {immediate: true});
onMounted(async () => {
onMounted(() => {
updateLoading.value = true;
try {
await Promise.all([
fetchBudgetInfo(),
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);

View File

@@ -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);

View 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>

View File

@@ -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) {

View File

@@ -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"/>-->

View File

@@ -4,7 +4,9 @@
</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 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)"/>
@@ -33,7 +35,8 @@
<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')"/>
<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">
@@ -95,6 +98,7 @@
@update:visible="closeCreateDialog"
/>
</div>
</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) => {
}
});
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>

View File

@@ -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>

View File

@@ -6,9 +6,9 @@
<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 () => {
if (space.value){
await fetchRecurrentPayments()
}
await fetchCategories()
})
</script>

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {createSpaceRequest} from "@/services/spaceService";
import {Space} from "@/models/Space";
import 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>

View 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>

View 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>

View File

@@ -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");
}

View File

@@ -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>

View File

@@ -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);
if (selectedSpace.value){
await fetchTransactions(); // Первоначальная загрузка данных
await fetchUsers();
}
// 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' : ''">

View File

@@ -44,7 +44,7 @@ app.config.globalProperties.$primevue.config.locale = {
if ("serviceWorker" in navigator) {
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) => {
console.error("Service Worker registration failed:", error);
});

17
src/models/Space.ts Normal file
View 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
}

View File

@@ -9,13 +9,21 @@ import SettingsView from "@/components/settings/SettingsView.vue";
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
import TransactionList from "@/components/transactions/TransactionList.vue";
import LoginView from "@/components/auth/LoginView.vue";
import RegisterView from "@/components/auth/RegisterView.vue";
import AnalyticsView from "@/components/analytics/AnalyticsView.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 = [
{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: '/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/:id', name: 'BudgetView', component: BudgetViewboth, meta: {requiresAuth: true}},
{path: '/budgets-new/:id', name: 'BudgetView New', component: BudgetView2, meta: {requiresAuth: true}},

View File

@@ -5,9 +5,8 @@ import {format} from "date-fns";
export const getBudgetInfos = async () => {
try {
let response = await apiClient.get('/budgets');
let spaceId = localStorage.getItem("spaceId")
let response = await apiClient.get(`/spaces/${spaceId}/budgets`);
let budgetInfos = response.data;
budgetInfos.forEach((budgetInfo: Budget) => {
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) => {
try {
let spaceId = localStorage.getItem("spaceId")
let budgetToCreate = JSON.parse(JSON.stringify(budget));
budgetToCreate.dateFrom = format(budget.dateFrom, '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,
createRecurrent: createRecurrent
}
await apiClient.post('/budgets/', data);
await apiClient.post(`/spaces/${spaceId}/budgets`, data);
} catch (e){
console.error(e)

View File

@@ -1,36 +1,42 @@
// src/services/categoryService.ts
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) => {
const spaceStore = await useSpaceStore();
const params = {};
if (type) {
params.type = type;
}
return await apiClient.get('/categories', {
return await apiClient.get(`/spaces/${spaceStore.space?.id}/categories`, {
params: params
});
};
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) => {
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) => {
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) => {
return await apiClient.delete(`/categories/${id}`);
const spaceStore = useSpaceStore();
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/categories/${id}`);
};
export const getCategoriesSumsRequest = async () => {
return await apiClient.get('/categories/by-month2');
export const getCategoriesSumsRequest = async (spaceId: string) => {
const spaceStore = useSpaceStore();
return await apiClient.get(`/spaces/${spaceStore.space?.id}/analytics/by-month`)
}

View File

@@ -5,7 +5,7 @@ const applicationServerKey = ''
function urlBase64ToUint8Array(base64String: string): Uint8Array {
console.log(base64String);
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
@@ -15,12 +15,15 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array {
export async function subscribeUserToPush() {
const registration = await navigator.serviceWorker.ready;
let vapid = ''
if (localStorage.getItem("token")) {
await apiClient.get('/subscriptions/vapid').then((registration) => {
vapid = registration.data
console.log(registration.data)
})
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapid),
});
}
}

View File

@@ -1,16 +1,18 @@
// src/services/recurrentyService.ts
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 () => {
console.log('getRecurrentPayments');
return await apiClient.get('/recurrents/');
const spaceStore = useSpaceStore()
return await apiClient.get(`/spaces/${spaceStore.space?.id}/recurrents`);
};
export const saveRecurrentPayment = async (payment: RecurrentPayment) => {
console.log('saveRecurrentPayment');
return await apiClient.post('/recurrents/', payment)
const spaceStore = useSpaceStore()
return await apiClient.post(`/spaces/${spaceStore.space?.id}/recurrents`, payment)
}
//
// export const getCategoryTypes = async () => {

View 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})
}

View File

@@ -1,16 +1,17 @@
import apiClient from '@/services/axiosSetup';
import {Transaction} from "@/models/Transaction";
import {format} from "date-fns";
import {useSpaceStore} from "@/stores/spaceStore";
// Импортируете настроенный экземпляр axios
export const getTransaction = async (transactionId: int) => {
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 = {};
// params.spaceId=spaceId;
console.log(is_child)
// Add the parameters to the params object if they are not null
if (transaction_type) {
params.transaction_type = transaction_type;
@@ -35,28 +36,30 @@ export const getTransactions = async (transaction_type = null, category_type = n
if (offset) {
params.offset = offset
}
const spaceStore = useSpaceStore()
// 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
});
}
export const createTransactionRequest = async (transaction: Transaction) => {
const spaceStore = useSpaceStore()
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);
return transactionResponse.data
};
export const updateTransactionRequest = async (transaction: Transaction) => {
const spaceStore = useSpaceStore()
const id = transaction.id
// console.log(transaction.isDone)
// transaction.date = transaction.date.setHours(0,0,0,0)
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.date = new Date(transaction.date);
@@ -74,14 +77,17 @@ export const updateTransactionRequest = async (transaction: Transaction) => {
// };
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 () => {
return await apiClient.get('/transactions/types');
const spaceStore = useSpaceStore()
return await apiClient.get(`/spaces/${spaceStore.space?.id}/transactions/types`);
}
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;
}

46
src/stores/spaceStore.ts Normal file
View 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};
})

View File

@@ -2,11 +2,13 @@ import {defineStore} from 'pinia';
import {ref} from 'vue';
import apiClient from "@/services/axiosSetup";
import {useRoute, useRouter} from "vue-router";
import {useSpaceStore} from "@/stores/spaceStore";
export const useUserStore = defineStore('user', () => {
const user = ref(null);
const loadingUser = ref(true);
const router = useRouter();
const spaceStore = useSpaceStore();
const route = useRoute();
const settings = ref({"budgetViewVersion": "2", "budgetCalendarOpened": false})
@@ -45,6 +47,7 @@ export const useUserStore = defineStore('user', () => {
localStorage.setItem('token', token);
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
await fetchUserProfile();
await spaceStore.fetchSpaces()
await router.push(route.query['back'] ? route.query['back'].toString() : '/');
} catch (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};
});

View File

@@ -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) => {
switch (month) {
case 0:
@@ -68,5 +85,7 @@ export const generateRandomColors = () => {
return [r, g, b]
}
// Пример использования
export const getRandomColor = () => {
}