add spaces
This commit is contained in:
@@ -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)
|
||||
|
||||
54
src/App.vue
54
src/App.vue
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -316,6 +338,7 @@ onMounted(async () => {
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -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>
|
||||
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 () => {
|
||||
if (selectedSpace.value) {
|
||||
loading.value = true;
|
||||
budgetInfos.value = await getBudgetInfos()
|
||||
|
||||
loading.value = false
|
||||
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,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);
|
||||
|
||||
@@ -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"/>-->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
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);
|
||||
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' : ''">
|
||||
|
||||
|
||||
@@ -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
17
src/models/Space.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {User} from "@/models/User";
|
||||
|
||||
export class Space {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
owner: User
|
||||
users: User[]
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export class SpaceInvite {
|
||||
code: string
|
||||
fromUser: User
|
||||
activeTill: Date
|
||||
isInviteActive: boolean
|
||||
}
|
||||
@@ -9,13 +9,21 @@ import SettingsView from "@/components/settings/SettingsView.vue";
|
||||
import RecurrentList from "@/components/settings/recurrent/RecurrentList.vue";
|
||||
import 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}},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`)
|
||||
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
47
src/services/spaceService.ts
Normal file
47
src/services/spaceService.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import apiClient from '@/services/axiosSetup';
|
||||
import {Space} from "@/models/Space";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
|
||||
export const getSpaces = async () => {
|
||||
return await apiClient.get('/spaces').then((res) => {
|
||||
res.data.forEach((space: Space) => {
|
||||
space.createdAt = new Date(space.createdAt);
|
||||
})
|
||||
return res.data
|
||||
});
|
||||
}
|
||||
|
||||
export const createSpaceRequest = async (space: Space) => {
|
||||
return await apiClient.post('/spaces', space).then((res) => {
|
||||
return res.data
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const deleteSpaceRequest = async (spaceId: string) => {
|
||||
return await apiClient.delete(`/spaces/${spaceId}`).then((res) => {
|
||||
return res.data
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const createSpaceInvite = async () => {
|
||||
const spaceStore = useSpaceStore();
|
||||
return await apiClient.post(`/spaces/${spaceStore.space?.id}/invite`).then((res) => res.data)
|
||||
}
|
||||
|
||||
export const acceptInviteRequest = async (code:string) => {
|
||||
await apiClient.post(`spaces/invite/${code}`).then((res) => res.data).catch((err) => {throw err})
|
||||
}
|
||||
|
||||
export const leaveSpaceRequest = async (spaceId:string) => {
|
||||
await apiClient.delete(`spaces/${spaceId}/leave`).then((res) => res.data).catch((err) => {throw err})
|
||||
}
|
||||
|
||||
export const kickMemberFromSpaceRequest = async (spaceId:string, username: string) => {
|
||||
await apiClient.delete(`spaces/${spaceId}/members/kick/${username}`).then((res) => res.data).catch((err) => {throw err})
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import apiClient from '@/services/axiosSetup';
|
||||
import {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
46
src/stores/spaceStore.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {ref, watch} from "vue";
|
||||
import {EventBus} from "@/utils/EventBus";
|
||||
import {getSpaces} from "@/services/spaceService";
|
||||
import {Space} from "@/models/Space";
|
||||
|
||||
export const useSpaceStore = defineStore('space', () => {
|
||||
const spaces = ref();
|
||||
const fetchSpaces = async () => {
|
||||
await getSpaces().then((res) => {
|
||||
spaces.value = res;
|
||||
const spaceId = localStorage.getItem("spaceId");
|
||||
space.value = spaces.value.find((s: Space) => s.id === spaceId) || null;
|
||||
})
|
||||
}
|
||||
|
||||
const space = ref<Space | null>(null);
|
||||
|
||||
|
||||
const getSpace = async () => {
|
||||
const spaceId = localStorage.getItem("spaceId");
|
||||
if (!spaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!spaces.value || spaces.value.length === 0) {
|
||||
await fetchSpaces();
|
||||
}
|
||||
|
||||
space.value = spaces.value.find((s) => s.id === spaceId) || null;
|
||||
return space.value;
|
||||
};
|
||||
|
||||
const setSpace = (newSpace: Space | null) => {
|
||||
if (newSpace != space) {
|
||||
space.value = newSpace;
|
||||
localStorage.setItem("spaceId", newSpace.id)
|
||||
EventBus.emit("spaceChanged", space);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
return {spaces, fetchSpaces, getSpace, space, setSpace};
|
||||
|
||||
})
|
||||
@@ -2,11 +2,13 @@ import {defineStore} from 'pinia';
|
||||
import {ref} from 'vue';
|
||||
import 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};
|
||||
});
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user