add network
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
VITE_API_URL=http://localhost:8086/api
|
VITE_API_URL=http://localhost:8082/api
|
||||||
VITE_ENABLE_DEVTOOLS=true
|
VITE_ENABLE_DEVTOOLS=true
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"@primevue/themes": "^4.4.1",
|
"@primevue/themes": "^4.4.1",
|
||||||
"@tailwindcss/postcss": "^4.1.16",
|
"@tailwindcss/postcss": "^4.1.16",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
|
"@wxperia/liquid-glass-vue": "^1.0.9",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"emoji-regex": "^10.6.0",
|
"emoji-regex": "^10.6.0",
|
||||||
|
|||||||
118
src/App.vue
118
src/App.vue
@@ -2,18 +2,23 @@
|
|||||||
import SpaceList from "@/components/space-list/SpaceList.vue";
|
import SpaceList from "@/components/space-list/SpaceList.vue";
|
||||||
import Toolbar from "@/components/Toolbar.vue";
|
import Toolbar from "@/components/Toolbar.vue";
|
||||||
import Toast from "primevue/toast";
|
import Toast from "primevue/toast";
|
||||||
import { useSpaceStore } from "@/stores/spaceStore";
|
import ProgressSpinner from "primevue/progressspinner";
|
||||||
import { useToolbarStore } from "@/stores/toolbar-store";
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {useToolbarStore} from "@/stores/toolbar-store";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import { useRoute, onBeforeRouteUpdate } from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import { computed, onMounted, onBeforeUnmount, ref, watch } from "vue";
|
import {computed, onBeforeUnmount, onMounted, ref, watch} from "vue";
|
||||||
import {useToast} from "primevue/usetoast";
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import {useUserStore} from "@/stores/userStore";
|
||||||
|
|
||||||
const spaceStore = useSpaceStore();
|
const spaceStore = useSpaceStore();
|
||||||
const toolbarStore = useToolbarStore();
|
const toolbarStore = useToolbarStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const platform = ref<string>("unknown")
|
const platform = ref<string>("unknown")
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const isLoading = ref(true);
|
||||||
|
|
||||||
const tgApp = (window as any)?.Telegram?.WebApp;
|
const tgApp = (window as any)?.Telegram?.WebApp;
|
||||||
const isTelegram = computed(() => !!tgApp);
|
const isTelegram = computed(() => !!tgApp);
|
||||||
@@ -24,9 +29,9 @@ const isSpaceSelected = computed(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const menu = [
|
const menu = [
|
||||||
{ name: "Dashboard", icon: "pi pi-chart-bar", link: "/", navStack: 'dashboard' },
|
{name: "Dashboard", icon: "pi pi-chart-bar", link: "/", navStack: 'dashboard'},
|
||||||
{ name: "Transactions", icon: "pi pi-cog", link: "/transactions", navStack: 'transactions' },
|
{name: "Transactions", icon: "pi pi-cog", link: "/transactions", navStack: 'transactions'},
|
||||||
{ name: "Settings", icon: "pi pi-list", link: "/settings", navStack: 'settings' },
|
{name: "Settings", icon: "pi pi-list", link: "/settings", navStack: 'settings'},
|
||||||
];
|
];
|
||||||
|
|
||||||
function spaceSelected() {
|
function spaceSelected() {
|
||||||
@@ -80,7 +85,17 @@ const blurAllInputs = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
// setTimeout(async () => {
|
||||||
|
// if (!userStore.isAuthorized) await router.push(`/login?back=${route.path}`)
|
||||||
|
// }, 1000)
|
||||||
|
//
|
||||||
|
await userStore.fetchUserProfile()
|
||||||
|
await spaceStore.getSpace()
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (!userStore.isAuthorized) await router.push(`/login?back=${route.path}`)
|
||||||
|
}, 100)
|
||||||
|
|
||||||
toolbarStore.registerHandler("openSpacePicker", () => {
|
toolbarStore.registerHandler("openSpacePicker", () => {
|
||||||
isSpaceSelectorVisible.value = true;
|
isSpaceSelectorVisible.value = true;
|
||||||
});
|
});
|
||||||
@@ -103,6 +118,7 @@ onMounted(() => {
|
|||||||
console.warn("Telegram WebApp init error:", err);
|
console.warn("Telegram WebApp init error:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isLoading.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔁 следим за изменением маршрута
|
// 🔁 следим за изменением маршрута
|
||||||
@@ -122,46 +138,54 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Toast/>
|
<Toast/>
|
||||||
<!-- {{platform}}-->
|
<div v-if="isLoading">
|
||||||
<!-- {{['ios', 'android'].includes(platform) }}-->
|
<ProgressSpinner/>
|
||||||
<div class="flex flex-col tg " :class="['ios', 'android'].includes(platform) ? '!pt-10' : ''">
|
</div>
|
||||||
<SpaceList v-if="isSpaceSelected" @space-selected="spaceSelected" />
|
|
||||||
|
|
||||||
<div v-else class="flex flex-col w-full gap-4">
|
<div v-else>
|
||||||
<div class="w-full flex flex-row items-end justify-end pt-2 pe-4">
|
<div v-if="!userStore.isAuthorized">
|
||||||
<Toolbar />
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="flex flex-col tg " :class="['ios', 'android'].includes(platform) ? '!pt-10' : ''">
|
||||||
|
<SpaceList v-if="isSpaceSelected && userStore.isAuthorized" @space-selected="spaceSelected"/>
|
||||||
|
|
||||||
<div class="flex flex-col w-full h-full items-end px-4 gap-4 pb-6">
|
<div v-else class="flex flex-col w-full gap-4">
|
||||||
<router-view class=" w-full" />
|
<div class="flex w-full flex flex-row items-end justify-end pt-2 pe-4">
|
||||||
</div>
|
<Toolbar/>
|
||||||
<button
|
|
||||||
v-if="isInputFocused"
|
|
||||||
@click="blurAllInputs"
|
|
||||||
class="fixed bottom-4 right-4 z-50 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg"
|
|
||||||
>
|
|
||||||
Готово
|
|
||||||
</button>
|
|
||||||
<nav v-if="isNavVisible"
|
|
||||||
class="fixed inset-x-0 bottom-4 z-50 w-full flex justify-center items-center"
|
|
||||||
style="padding-bottom: var(--tg-content-safe-area-inset-bottom) !important;"
|
|
||||||
>
|
|
||||||
<div class="flex h-full items-center justify-between py-2 bg-white rounded-4xl px-6 w-fit">
|
|
||||||
<!-- <div class="flex h-full justify-items-center items-center justify-between py-2 bg-white rounded-4xl !px-6 w-fit">-->
|
|
||||||
<router-link
|
|
||||||
v-for="item in menu"
|
|
||||||
:key="item.link"
|
|
||||||
:to="item.link"
|
|
||||||
class="flex w-fit h-full flex-col items-center gap-2 !py-2 !px-4"
|
|
||||||
:class="route.meta.navStack === item.navStack ? 'bg-green-100 rounded-2xl ' : ''"
|
|
||||||
>
|
|
||||||
<i class="!text-lg" :class="item.icon" />
|
|
||||||
<span class="font-medium text-gray-900">{{ item.name }}</span>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="h-16" />
|
<div class="flex flex-col w-full h-full items-end px-4 gap-4 pb-6">
|
||||||
|
<router-view class=" w-full"/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="isInputFocused"
|
||||||
|
@click="blurAllInputs"
|
||||||
|
class="fixed bottom-4 right-4 z-50 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
Готово
|
||||||
|
</button>
|
||||||
|
<nav v-if="isNavVisible"
|
||||||
|
class="fixed inset-x-0 bottom-4 z-50 w-full flex justify-center items-center "
|
||||||
|
style="padding-bottom: var(--tg-content-safe-area-inset-bottom) !important;"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="flex h-full items-center justify-between py-2 bg-white rounded-4xl px-6 w-fit shadow">
|
||||||
|
<!-- <div class="flex h-full justify-items-center items-center justify-between py-2 bg-white rounded-4xl !px-6 w-fit">-->
|
||||||
|
<router-link
|
||||||
|
v-for="item in menu"
|
||||||
|
:key="item.link"
|
||||||
|
:to="item.link"
|
||||||
|
class="flex w-fit h-full flex-col items-center gap-2 !py-2 !px-4"
|
||||||
|
:class="route.meta.navStack === item.navStack ? 'bg-green-100 rounded-2xl ' : ''"
|
||||||
|
>
|
||||||
|
<i class="!text-lg" :class="item.icon"/>
|
||||||
|
<span class="font-medium text-gray-900">{{ item.name }}</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex h-16"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -169,10 +193,6 @@ onBeforeUnmount(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.tg {
|
.tg {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin:
|
margin: var(--tg-content-safe-area-inset-top) var(--tg-content-safe-area-inset-right) 0 var(--tg-content-safe-area-inset-left) !important;
|
||||||
var(--tg-content-safe-area-inset-top)
|
|
||||||
var(--tg-content-safe-area-inset-right)
|
|
||||||
0
|
|
||||||
var(--tg-content-safe-area-inset-left) !important;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
/* PrimeVue-specific */
|
/* PrimeVue-specific */
|
||||||
--button-text-color: var(--primary-color-text);
|
--button-text-color: var(--primary-color-text);
|
||||||
--button-bg-color: var(--primary-color);
|
--button-bg-color: var(--color-green-300);
|
||||||
--button-hover-bg-color: var(--primary-hover-color);
|
--button-hover-bg-color: var(--primary-hover-color);
|
||||||
--button-active-bg-color: var(--primary-active-color);
|
--button-active-bg-color: var(--primary-active-color);
|
||||||
|
|
||||||
@@ -199,9 +199,7 @@ body {
|
|||||||
.status-warning {
|
.status-warning {
|
||||||
color: var(--warning-color);
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
span, a, i {
|
span, a, i {
|
||||||
color: var(--text-color) !important;
|
color: var(--text-color) !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ const handleConfirm = (result: boolean) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="fixed inset-0 z-[1000] flex items-center justify-center p-4 bg-black/50 !bg-opacity-10!">
|
<div class="flex fixed inset-0 z-[1000] flex items-center justify-center p-4 bg-black/50 !bg-opacity-10!">
|
||||||
<div class="bg-white rounded-2xl shadow-xl max-w-sm w-full p-6 flex-col gap-5">
|
<div class="flex bg-white rounded-2xl shadow-xl max-w-sm w-full p-6 flex-col gap-5">
|
||||||
<!-- Сообщение -->
|
<!-- Сообщение -->
|
||||||
<div class="text-center mb-6">
|
<div class="flex text-center mb-6">
|
||||||
<span class="text-lg font-semibold text-gray-900">
|
<span class="text-lg font-semibold text-gray-900">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -6,23 +6,20 @@ import {Divider} from "primevue";
|
|||||||
const toolbar = useToolbarStore()
|
const toolbar = useToolbarStore()
|
||||||
const keyOf = (b: { id?: string; text?: string }) => b.id ?? b.text ?? crypto.randomUUID()
|
const keyOf = (b: { id?: string; text?: string }) => b.id ?? b.text ?? crypto.randomUUID()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="h-12 w-fit flex flex-row items-center gap-2 p-2 bg-white rounded-full sticky top-10 justify-items-end justify-end">
|
<nav class="h-12 w-fit flex flex-row items-center gap-2 p-2 bg-white rounded-full sticky top-10 justify-end">
|
||||||
<component
|
<component
|
||||||
v-for="btnKey in toolbar.current.keys()"
|
v-for="(btn, idx) in toolbar.current"
|
||||||
:is="toolbar.current[btnKey].to ? RouterLink : 'button'"
|
:key="btn.id || btn.text || idx"
|
||||||
:key="btnKey"
|
:is="btn.to ? RouterLink : 'button'"
|
||||||
class="flex flex-row gap-2 items-center "
|
class="flex flex-row gap-2 items-center"
|
||||||
:title="toolbar.current[btnKey].title || toolbar.current[btnKey].text"
|
:title="btn.title || btn.text"
|
||||||
v-bind="toolbar.current[btnKey].to ? { to: toolbar.current[btnKey].to } : { type: 'button', disabled: toolbar.current[btnKey].disabled }"
|
v-bind="btn.to ? { to: btn.to } : { type: 'button', disabled: btn.disabled }"
|
||||||
@click="!toolbar.current[btnKey].to && toolbar.invoke(toolbar.current[btnKey].onClickId)"
|
@click="!btn.to && toolbar.invoke(btn.onClickId)"
|
||||||
>
|
>
|
||||||
<i v-if="toolbar.current[btnKey].icon" :class="toolbar.current[btnKey].icon" class="!p-2"/>
|
<i v-if="btn.icon" :class="btn.icon" class="!p-2" />
|
||||||
<span v-if="toolbar.current[btnKey].text">{{ toolbar.current[btnKey].text }}</span>
|
<span v-if="btn.text">{{ btn.text }}</span>
|
||||||
<Divider v-if="btnKey+1 != toolbar.current.length" class="!m-0" layout="vertical"/>
|
<Divider v-if="idx + 1 !== toolbar.current.length" class="!m-0" layout="vertical" />
|
||||||
</component>
|
</component>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
@@ -1,36 +1,70 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, computed } from "vue"
|
import {onMounted, ref} from "vue"
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
|
import {InputText, Password, Button} from "primevue";
|
||||||
|
import {useUserStore} from "@/stores/userStore";
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// необязательно делать реактивным, но удобно для шаблона
|
// необязательно делать реактивным, но удобно для шаблона
|
||||||
const tgApp = ref(window.Telegram.WebApp)
|
const tgApp = ref(window.Telegram.WebApp)
|
||||||
const tgData = ref()
|
const tgData = ref()
|
||||||
|
const errors = ref({username: '', password: ''})
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
// какие-то удобные вычисления для UI
|
const username = ref<string>('')
|
||||||
const userId = computed(() => tgApp.value?.user?.id?.toString() ?? "")
|
const password = ref<string>('')
|
||||||
const username = computed(() => tgApp.value?.user?.username ?? "")
|
|
||||||
const firstName = computed(() => tgData.value?.user?.first_name ?? "")
|
const userStore = useUserStore()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// сообщаем Telegram WebApp, что UI готов
|
// сообщаем Telegram WebApp, что UI готов
|
||||||
if (tgApp.initData){
|
if (tgApp.initData) {
|
||||||
tgData.value = tgApp.initDataUnsafe
|
tgData.value = tgApp.initDataUnsafe
|
||||||
if (tgData.value.user?.id != null) {
|
if (tgData.value.user?.id != null) {
|
||||||
localStorage.setItem("token", tgData.value.user.id.toString())
|
localStorage.setItem("token", tgData.value.user.id.toString())
|
||||||
router.push("/")
|
router.push("/")
|
||||||
}
|
}
|
||||||
}else {
|
} else {
|
||||||
localStorage.setItem("token", "123")
|
|
||||||
router.push("/")
|
|
||||||
}
|
}
|
||||||
// router.push('/')
|
// router.push('/')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="flex !w-full items-center justify-center h-100">
|
||||||
|
<div class="card !w-fit">
|
||||||
|
|
||||||
|
<h1 class="text-2xl font-bold text-center">Вход</h1>
|
||||||
|
|
||||||
|
|
||||||
|
<form @submit.prevent="userStore.login(username, password)" class="flex flex-col !gap-6 !w-fit !p-10">
|
||||||
|
<div class="!w-full">
|
||||||
|
<label for="username" class="block text-sm font-semibold text-gray-700">Логин</label>
|
||||||
|
<InputText id="username" v-model.trim="username" class="w-full" :class="{'p-invalid': errors.username}"/>
|
||||||
|
<small v-if="errors.username" class="text-red-500">{{ errors.username }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="password" class="block text-sm font-semibold text-gray-700">Пароль</label>
|
||||||
|
<Password id="password" v-model="password" class="w-full" :feedback="false" toggleMask/>
|
||||||
|
<small v-if="errors.password" class="text-red-500">{{ errors.password }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button label="Войти" type="submit" class="w-full mt-2 !bg-blue-300 hover:!bg-blue-400 !border-blue-300"
|
||||||
|
:disabled="loading" :loading="loading"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<p class="mt-4 text-sm text-center text-gray-600">
|
||||||
|
Нет аккаунта?
|
||||||
|
<RouterLink to="/register" class="text-blue-500 hover:underline">Регистрация</RouterLink>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="flex card">
|
||||||
Not implemented.
|
Not implemented.
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {Category} from "@/models/category";
|
|||||||
import {useToolbarStore} from "@/stores/toolbar-store";
|
import {useToolbarStore} from "@/stores/toolbar-store";
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
import {useCategoriesStore} from "@/stores/categories-store";
|
import {useCategoriesStore} from "@/stores/categories-store";
|
||||||
|
import {CategoryType, CategoryTypeName} from "@/models/enums";
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const spaceStore = useSpaceStore()
|
const spaceStore = useSpaceStore()
|
||||||
@@ -20,7 +21,7 @@ const categories = ref<Category[]>([])
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
if (spaceStore.selectedSpaceId !== null) {
|
if (spaceStore.selectedSpaceId !== undefined) {
|
||||||
let spaceId = spaceStore.selectedSpaceId!!
|
let spaceId = spaceStore.selectedSpaceId!!
|
||||||
await categoryStore.fetchCategories(spaceId)
|
await categoryStore.fetchCategories(spaceId)
|
||||||
categories.value = categoryStore.categories
|
categories.value = categoryStore.categories
|
||||||
@@ -50,21 +51,56 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full !pb-10 gap-6">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>Income categories</span>
|
||||||
|
<div class="flex card flex-col ">
|
||||||
|
|
||||||
<div class="card">
|
<span v-if="categories.filter(i => i.type == CategoryType.INCOME).length ==0 ">It looks like you haven't create any income category yet. <router-link
|
||||||
<div v-for="key in categories.keys()" :key="categories[key].id"
|
to="/categories/create" class="!text-blue-400">Try to create some first.</router-link></span>
|
||||||
@click="router.push(`/categories/${categories[key].id}/edit`)"
|
<div v-else v-for="key in categories.filter(i => i.type == CategoryType.INCOME).keys()"
|
||||||
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold ">
|
:key="categories[key].id"
|
||||||
<div class="flex-row w-full items-center justify-between">
|
@click="router.push(`/categories/${categories[key].id}/edit`)"
|
||||||
<div class="flex-row items-center gap-2 ">
|
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold ">
|
||||||
<span class="text-3xl"> {{ categories[key].icon }}</span>
|
<div class="flex flex-row w-full items-center justify-between">
|
||||||
<div class="flex-col !font-bold "> {{ categories[key].name }}
|
<div class="flex flex-row items-center gap-2 ">
|
||||||
<div class="flex flex-row text-sm">{{ categories[key].description }}</div>
|
<span class="text-3xl"> {{ categories[key].icon }}</span>
|
||||||
|
<div class="flex flex-col !font-bold "> {{ categories[key].name }}
|
||||||
|
<div class="flex flex-row text-sm">{{ categories[key].description }} |
|
||||||
|
{{ CategoryTypeName[categories[key].type] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="pi pi-angle-right !font-extralight"/>
|
||||||
</div>
|
</div>
|
||||||
|
<Divider v-if="key+1 !== categories.length" class="!m-0 !py-3"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>Expense categories</span>
|
||||||
|
<div class="flex card ">
|
||||||
|
|
||||||
|
<span v-if="categories.filter(i => i.type == CategoryType.EXPENSE).length ==0 ">It looks like you haven't create any expense category yet. <router-link
|
||||||
|
to="/categories/create" class="!text-blue-400">Try to create some first.</router-link></span>
|
||||||
|
<div v-else v-for="key in categories.filter(i => i.type == CategoryType.EXPENSE).keys()"
|
||||||
|
:key="categories[key].id"
|
||||||
|
@click="router.push(`/categories/${categories[key].id}/edit`)"
|
||||||
|
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold ">
|
||||||
|
<div class="flex flex-row w-full items-center justify-between">
|
||||||
|
<div class="flex flex-row items-center gap-2 ">
|
||||||
|
<span class="text-3xl"> {{ categories[key].icon }}</span>
|
||||||
|
<div class="flex flex-col !font-bold "> {{ categories[key].name }}
|
||||||
|
<div class="flex flex-row text-sm">{{ categories[key].description }} |
|
||||||
|
{{ CategoryTypeName[categories[key].type] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="pi pi-angle-right !font-extralight"/>
|
||||||
|
</div>
|
||||||
|
<Divider v-if="key+1 !== categories.length" class="!m-0 !py-3"/>
|
||||||
</div>
|
</div>
|
||||||
<i class="pi pi-angle-right !font-extralight"/>
|
|
||||||
</div>
|
</div>
|
||||||
<Divider v-if="key+1 !== categories.length" class="!m-0 !py-3"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ onMounted(async () => {
|
|||||||
// await categoriesStore.fetchCategories(spaceStore.selectedSpaceId)
|
// await categoriesStore.fetchCategories(spaceStore.selectedSpaceId)
|
||||||
try {
|
try {
|
||||||
let updateDTO = buildUpdate()
|
let updateDTO = buildUpdate()
|
||||||
await categoriesService.updateCategory(spaceStore.selectedSpaceId, updateDTO)
|
await categoriesService.updateCategory(spaceStore.selectedSpaceId, Number(categoryId.value), updateDTO)
|
||||||
console.log(updateDTO)
|
console.log(updateDTO)
|
||||||
await moveUser()
|
await moveUser()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -224,7 +224,7 @@ onMounted(async () => {
|
|||||||
:callback="(confirmed: boolean) => { if (confirmed) deleteCategory(); isDeleteAlertVisible = false; }"
|
:callback="(confirmed: boolean) => { if (confirmed) deleteCategory(); isDeleteAlertVisible = false; }"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col w-full ">
|
<div class="flex flex-col w-full ">
|
||||||
<div class=" flex-col " v-tooltip.focus.bottom="'Only emoji supported'">
|
<div class="flex flex-col " v-tooltip.focus.bottom="'Only emoji supported'">
|
||||||
<input
|
<input
|
||||||
class="
|
class="
|
||||||
block w-full
|
block w-full
|
||||||
@@ -249,30 +249,30 @@ onMounted(async () => {
|
|||||||
class="text-sm !text-red-500 font-extralight">Icon cannot be empty or non-emoji</span>
|
class="text-sm !text-red-500 font-extralight">Icon cannot be empty or non-emoji</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full !items-center !justify-center">
|
<div class="flex w-full !items-center !justify-center">
|
||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="categoryType"
|
v-model="categoryType"
|
||||||
:options="options"
|
:options="options"
|
||||||
optionLabel="label"
|
optionLabel="label"
|
||||||
optionValue="value"
|
optionValue="value"
|
||||||
class="!w-full !items-center !justify-center "
|
class="!w-full !items-center !justify-center !border-none "
|
||||||
/>
|
/>
|
||||||
<span v-if="isCategoryTypeError"
|
<span v-if="isCategoryTypeError"
|
||||||
class="text-sm !text-red-500 font-extralight">Category type cannot be empty</span>
|
class="text-sm !text-red-500 font-extralight">Category type cannot be empty</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full justify-items-start">
|
<div class="flex flex-col w-full justify-items-start">
|
||||||
<label class="!font-semibold text-gray-600 pl-2">Category name</label>
|
<label class="!font-semibold text-gray-600 pl-2">Category name</label>
|
||||||
<div class="card !rounded-3xl !justify-start !items-start !p-4 !pl-5 ">
|
<div class="flex card !rounded-3xl !justify-start !items-start !p-4 !pl-5 ">
|
||||||
<input class="font-extralight w-full focus:outline-0" placeholder="Name" v-model="categoryName"
|
<input class="font-extralight w-full focus:outline-0" placeholder="Name" v-model="categoryName"
|
||||||
@input="categoryName.length !== 0 ? isCategoryNameError = false : true"/>
|
@input="categoryName.length !== 0 ? isCategoryNameError = false : true"/>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="isCategoryNameError" class="text-sm !text-red-500 font-extralight">Name cannot be empty</span>
|
<span v-if="isCategoryNameError" class="text-sm !text-red-500 font-extralight">Name cannot be empty</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full justify-items-start">
|
<div class="flex flex-col w-full justify-items-start">
|
||||||
<label class="!font-semibold text-gray-600 !pl-2">Category description</label>
|
<label class="!font-semibold text-gray-600 !pl-2">Category description</label>
|
||||||
<div class="card !justify-start !items-start !pl-2">
|
<div class="flex card !justify-start !items-start !pl-2">
|
||||||
<textarea
|
<textarea
|
||||||
class="!font-extralight !text-start !pl-2 w-full focus:outline-0 !focus:border-0 !@focus:shadow-none !bg-white !border-0 min-h-36"
|
class="!font-extralight !text-start !pl-2 w-full focus:outline-0 !focus:border-0 !@focus:shadow-none !bg-white !border-0 min-h-36"
|
||||||
style="box-shadow: none !important;"
|
style="box-shadow: none !important;"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="flex card">
|
||||||
Not implemented
|
Not implemented
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -49,23 +49,24 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="!pb-10">
|
<div class="flex !pb-10">
|
||||||
<div class="card">
|
<div class="flex card">
|
||||||
<div v-for="key in recurrents.keys()" :key="recurrents[key].id"
|
<span v-if="recurrents.length==0">Looks like that you haven't create any recurrent yet. <router-link to="/recurrents/create" class="!text-blue-400">Try to create some first.</router-link></span>
|
||||||
|
<div v-else v-for="key in recurrents.keys()" :key="recurrents[key].id"
|
||||||
@click="router.push(`/recurrents/${recurrents[key].id}/edit`)"
|
@click="router.push(`/recurrents/${recurrents[key].id}/edit`)"
|
||||||
class="flex flex-col w-full pl-5 items-start justify-items-center font-bold ">
|
class="flex flex-col w-full pl-5 items-start justify-items-center font-bold ">
|
||||||
<div class="flex-row gap-2 w-full items-center justify-between">
|
<div class="flex flex-row gap-2 w-full items-center justify-between">
|
||||||
<div class="w-full flex items-center justify-between">
|
<div class="flex w-full flex items-center justify-between">
|
||||||
<div class="flex-row items-center gap-2 ">
|
<div class="flex flex-row items-center gap-2 ">
|
||||||
<span class="text-4xl">{{ recurrents[key].category.icon }}</span>
|
<span class="text-4xl">{{ recurrents[key].category.icon }}</span>
|
||||||
<div class="flex-col items-start">
|
<div class="flex flex-col items-start">
|
||||||
<div class="flex-row !font-bold "> {{ recurrents[key].name }}</div>
|
<div class="flex flex-row !font-bold "> {{ recurrents[key].name }}</div>
|
||||||
<div class="flex flex-row text-sm">{{ recurrents[key].category.name }}</div>
|
<div class="flex flex-row text-sm">{{ recurrents[key].category.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="items-end flex-col">
|
<div class="flex items-end flex-col">
|
||||||
<span class="text-lg !font-semibold">{{ recurrents[key].amount }}₽ </span>
|
<span class="text-lg !font-semibold">{{ recurrents[key].amount }}₽ </span>
|
||||||
<span class="text-sm">каждое {{ recurrents[key].date }} число </span>
|
<span class="text-sm">каждое {{ recurrents[key].date }} число </span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ const isDeleteAlertVisible = ref(false)
|
|||||||
const deleteAlertMessage = ref('Do you want to delete recurrent?')
|
const deleteAlertMessage = ref('Do you want to delete recurrent?')
|
||||||
const deleteRecurrent = async () => {
|
const deleteRecurrent = async () => {
|
||||||
await recurrentsService.deleteRecurrent(spaceStore.selectedSpaceId, recurrentId.value)
|
await recurrentsService.deleteRecurrent(spaceStore.selectedSpaceId, recurrentId.value)
|
||||||
await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
|
// await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
router.back()
|
router.back()
|
||||||
} else {
|
} else {
|
||||||
@@ -208,8 +208,8 @@ onMounted(async () => {
|
|||||||
toolbar.registerHandler('updateRecurrent', async () => {
|
toolbar.registerHandler('updateRecurrent', async () => {
|
||||||
if (spaceStore.selectedSpaceId) {
|
if (spaceStore.selectedSpaceId) {
|
||||||
try {
|
try {
|
||||||
await recurrentsService.updateRecurrent(spaceStore.selectedSpaceId, buildUpdate())
|
await recurrentsService.updateRecurrent(spaceStore.selectedSpaceId, Number(recurrentId.value), buildUpdate())
|
||||||
await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
|
// await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
|
||||||
router.back()
|
router.back()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -226,7 +226,7 @@ onMounted(async () => {
|
|||||||
toolbar.registerHandler('createRecurrent', async () => {
|
toolbar.registerHandler('createRecurrent', async () => {
|
||||||
if (spaceStore.selectedSpaceId) {
|
if (spaceStore.selectedSpaceId) {
|
||||||
await recurrentsService.createRecurrent(spaceStore.selectedSpaceId, buildCreate())
|
await recurrentsService.createRecurrent(spaceStore.selectedSpaceId, buildCreate())
|
||||||
await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
|
// await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -235,7 +235,6 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div v-if="categories.length===0" class="card !gap-4 !p-10">
|
<div v-if="categories.length===0" class="card !gap-4 !p-10">
|
||||||
<span class="">No categories available.</span>
|
<span class="">No categories available.</span>
|
||||||
<span class="text-center">Maybe you want to <router-link to="/categories" class="!text-blue-700">create a new category</router-link> first?</span>
|
<span class="text-center">Maybe you want to <router-link to="/categories" class="!text-blue-700">create a new category</router-link> first?</span>
|
||||||
@@ -250,16 +249,17 @@ onMounted(async () => {
|
|||||||
<div v-if="isCategorySelectorOpened" class="fixed inset-0 z-50 flex items-start justify-center p-4 overflow-y-auto"
|
<div v-if="isCategorySelectorOpened" class="fixed inset-0 z-50 flex items-start justify-center p-4 overflow-y-auto"
|
||||||
style="background-color: var(--primary-color); "
|
style="background-color: var(--primary-color); "
|
||||||
:style="tgApp ? `padding-top: ${insetTop}px !important` : 'padding-top: 2rem !important'">
|
:style="tgApp ? `padding-top: ${insetTop}px !important` : 'padding-top: 2rem !important'">
|
||||||
<div class="w-full max-w-md">
|
<div class="flex w-full max-w-md">
|
||||||
<div class="card justify-items-start justify-start">
|
<div class="flex card justify-items-start justify-start">
|
||||||
|
|
||||||
<div v-for="(cat, idx) in categories" :key="cat.id"
|
<div v-for="(cat, idx) in categories" :key="cat.id"
|
||||||
@click="recurrentCategory = cat; isCategorySelectorOpened = false"
|
@click="recurrentCategory = cat; isCategorySelectorOpened = false"
|
||||||
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold cursor-pointer hover:bg-gray-50 transition-colors">
|
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
<div class="flex-row w-full items-center justify-between py-3">
|
<div class="flex flex-row w-full items-center justify-between py-3">
|
||||||
<div class="flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<span class="text-3xl">{{ cat.icon }} </span>
|
<span class="text-3xl">{{ cat.icon }} </span>
|
||||||
<div class="flex-col justify-between">
|
<div class="flex flex-col justify-between">
|
||||||
<div class="flex-row"> {{ cat.name }}</div>
|
<div class="flex flex-row"> {{ cat.name }}</div>
|
||||||
<div class="flex flex-row text-sm text-gray-600">{{ cat.description }}</div>
|
<div class="flex flex-row text-sm text-gray-600">{{ cat.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,7 +272,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full ">
|
<div class="flex flex-col w-full ">
|
||||||
<div class="flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model="recurrentAmount"
|
v-model="recurrentAmount"
|
||||||
@input=" isAmountError = false"
|
@input=" isAmountError = false"
|
||||||
@@ -294,12 +294,12 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full justify-items-start">
|
<div class="flex flex-col w-full justify-items-start">
|
||||||
<label class="!font-semibold text-gray-600 pl-2">Recurrent category</label>
|
<label class="!font-semibold text-gray-600 pl-2">Recurrent category</label>
|
||||||
<div class="card !justify-start !items-start !p-4 !pl-5 cursor-pointer"
|
<div class="flex card !justify-start !items-start !p-4 !pl-5 cursor-pointer"
|
||||||
@click="isCategorySelectorOpened = true; isCategoryError=false;">
|
@click="isCategorySelectorOpened = true; isCategoryError=false;">
|
||||||
<div class="flex-row w-full gap-2 items-center justify-between">
|
<div class="flex flex-row w-full gap-2 items-center justify-between">
|
||||||
<div class="flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<span class="!text-3xl ">{{ recurrentCategory.icon }}</span>
|
<span class="!text-3xl ">{{ recurrentCategory.icon }}</span>
|
||||||
<div class="flex-col ">
|
<div class="flex flex-col ">
|
||||||
<span class=" !">{{ recurrentCategory.name }}
|
<span class=" !">{{ recurrentCategory.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,7 +311,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full justify-items-start">
|
<div class="flex flex-col w-full justify-items-start">
|
||||||
<label class="!font-semibold text-gray-600 pl-2">Recurrent name</label>
|
<label class="!font-semibold text-gray-600 pl-2">Recurrent name</label>
|
||||||
<div class="card !justify-start !items-start !p-4 !pl-5 ">
|
<div class="flex card !justify-start !items-start !p-4 !pl-5 ">
|
||||||
<input class="font-extralight w-full focus:outline-0" placeholder="Name"
|
<input class="font-extralight w-full focus:outline-0" placeholder="Name"
|
||||||
@input="recurrentName?.length ==0 ? isNameError = true : isNameError=false" v-model="recurrentName"/>
|
@input="recurrentName?.length ==0 ? isNameError = true : isNameError=false" v-model="recurrentName"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,8 +320,8 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full justify-items-start">
|
<div class="flex flex-col w-full justify-items-start">
|
||||||
<label class="!font-semibold text-gray-600 !pl-2">Recurrent date</label>
|
<label class="!font-semibold text-gray-600 !pl-2">Recurrent date</label>
|
||||||
<div class="card !justify-start !items-start !pl-2">
|
<div class="flex card !justify-start !items-start !pl-2">
|
||||||
<div class="!grid !grid-cols-7 gap-2">
|
<div class="flex !grid !grid-cols-7 gap-2">
|
||||||
<div v-for="i in 31"
|
<div v-for="i in 31"
|
||||||
class="!w-12 !h-12 !items-center !justify-items-center !justify-center rounded-full cursor-pointer flex"
|
class="!w-12 !h-12 !items-center !justify-items-center !justify-center rounded-full cursor-pointer flex"
|
||||||
:class="recurrentDate == i ? 'bg-green-200' : 'bg-gray-100'"
|
:class="recurrentDate == i ? 'bg-green-200' : 'bg-gray-100'"
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import {Divider} from "primevue";
|
|||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{name: "Space settings", link: '/space-settings'},
|
{name: "Space settings", link: '/space-settings'},
|
||||||
{name: "Notification settings", link: '/notification-settings'},
|
// {name: "Notification settings", link: '/notification-settings'},
|
||||||
{name: "Categories", link: '/categories'},
|
{name: "Categories", link: '/categories'},
|
||||||
{name: "Recurrent Operations", link: '/recurrents'},
|
{name: "Recurrent Operations", link: '/recurrents'},
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="overflow-y-auto">
|
<div class="flex overflow-y-auto">
|
||||||
<div class="card">
|
<div class="flex card">
|
||||||
<router-link :to="items[item].link" v-for="item in items.keys()"
|
|
||||||
|
<router-link :to="items[item].link" v-for="item in items.keys()"
|
||||||
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center">
|
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center">
|
||||||
|
|
||||||
<div class="flex flex-row justify-between items-center w-full pe-2 p-2">
|
<div class="flex flex-row justify-between items-center w-full pe-2 p-2">
|
||||||
|
|||||||
@@ -1,10 +1,149 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
import {Divider, ToggleSwitch} from "primevue";
|
||||||
|
import {spaceService} from "@/services/space-service";
|
||||||
|
import {SettingType} from "@/models/enums";
|
||||||
|
|
||||||
|
const spacePeriodStarts = ref(10)
|
||||||
|
const spaceSubPeriodStarts = ref(25)
|
||||||
|
|
||||||
|
const isNotificationsEnabled = ref(true)
|
||||||
|
const days = ref([
|
||||||
|
{key: 'Mon', isSelected: false},
|
||||||
|
{key: 'Tue', isSelected: false},
|
||||||
|
{key: 'Wed', isSelected: false},
|
||||||
|
{key: 'Thu', isSelected: false},
|
||||||
|
{key: 'Fri', isSelected: false},
|
||||||
|
{key: 'Sat', isSelected: false},
|
||||||
|
{key: 'Sun', isSelected: false}
|
||||||
|
]);
|
||||||
|
const selectDay = (day: string) => {
|
||||||
|
let dayValue = days.value.find(i => i.key === day)
|
||||||
|
if (dayValue) {
|
||||||
|
dayValue.isSelected = !dayValue.isSelected
|
||||||
|
settingChanged(SettingType.NOTIFICATIONS_DAYS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const selectAllDay = () => {
|
||||||
|
if (days.value.filter(i => i.isSelected).length < 7) {
|
||||||
|
days.value.forEach(i => i.isSelected = true)
|
||||||
|
} else {
|
||||||
|
days.value.forEach(i => i.isSelected = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
let settings: Record<string, string>[] = await spaceService.getSettings()
|
||||||
|
let periodStartsSetting = settings.find((i) => i.key === "period-start")
|
||||||
|
spacePeriodStarts.value = periodStartsSetting ? Number(periodStartsSetting.value) : 5
|
||||||
|
let subPeriodStartsSetting = settings.find((i) => i.key === "sub-period-start")
|
||||||
|
spaceSubPeriodStarts.value = subPeriodStartsSetting ? Number(subPeriodStartsSetting.value) : 20
|
||||||
|
let notificationEnabledSetting = settings.find((i) => i.key === 'notifications-enabled')
|
||||||
|
isNotificationsEnabled.value = notificationEnabledSetting ? notificationEnabledSetting.value == '1' : false
|
||||||
|
let notificationsDaysSettings = settings.find((i) => i.key === 'notifications-days')
|
||||||
|
if (notificationsDaysSettings) {
|
||||||
|
notificationsDaysSettings.value.split(',').forEach((element) => {
|
||||||
|
days.value.forEach((day) => {
|
||||||
|
if (day.key == element) {
|
||||||
|
day.isSelected = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingChanged = async (setting: SettingType) => {
|
||||||
|
let newValue;
|
||||||
|
switch (setting) {
|
||||||
|
case SettingType.PERIOD_START:
|
||||||
|
newValue = spacePeriodStarts.value
|
||||||
|
await spaceService.setSetting(SettingType.PERIOD_START, newValue)
|
||||||
|
break
|
||||||
|
case SettingType.SUB_PERIOD_START:
|
||||||
|
|
||||||
|
newValue = spaceSubPeriodStarts.value
|
||||||
|
await spaceService.setSetting(SettingType.SUB_PERIOD_START, newValue)
|
||||||
|
|
||||||
|
break
|
||||||
|
case SettingType.NOTIFICATIONS_ENABLED:
|
||||||
|
newValue = isNotificationsEnabled.value ? '1' : '0'
|
||||||
|
await spaceService.setSetting(SettingType.NOTIFICATIONS_ENABLED, newValue)
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SettingType.NOTIFICATIONS_DAYS:
|
||||||
|
newValue = Array.isArray(days.value)
|
||||||
|
? days.value.filter(day => day.isSelected).map(day => day.key).join(',')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
await spaceService.setSetting(SettingType.NOTIFICATIONS_DAYS, newValue)
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SettingType.NOTIFICATIONS_TIME:
|
||||||
|
newValue = '19:30'
|
||||||
|
await spaceService.setSetting(SettingType.NOTIFICATIONS_TIME, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData()
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="flex !pb-4 !w-full">
|
||||||
Not implemented
|
<div class="flex flex-col w-full gap-4">
|
||||||
|
<div class="flex periods flex-col !w-full">
|
||||||
|
<span class="text-sm !pl-2">Space period dates</span>
|
||||||
|
<div class="flex card flex-col !w-full justify-between">
|
||||||
|
<div class="flex flex-row !w-full justify-between pl-2 pt-2">
|
||||||
|
<span>Space period start</span>
|
||||||
|
<select class="custom-select" v-model="spacePeriodStarts"
|
||||||
|
@change="settingChanged(SettingType.PERIOD_START)">
|
||||||
|
<option v-for="i in 31">{{ i }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<Divider/>
|
||||||
|
<div class="flex flex-row !w-full justify-between pl-2 pb-2">
|
||||||
|
<span>Space sub-period start</span>
|
||||||
|
<select class="custom-select" v-model="spaceSubPeriodStarts"
|
||||||
|
@change="settingChanged(SettingType.SUB_PERIOD_START)">
|
||||||
|
<option v-for="i in 31">{{ i }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex notifications flex-col !w-full">
|
||||||
|
<span class="text-sm !pl-2">Space notifications</span>
|
||||||
|
<div class="flex card flex-col !w-full justify-between">
|
||||||
|
<div class="flex flex-row !w-full justify-between px-2 pt-2">
|
||||||
|
<span>Notification enabled</span>
|
||||||
|
<ToggleSwitch v-model="isNotificationsEnabled" @change="settingChanged(SettingType.NOTIFICATIONS_ENABLED)"/>
|
||||||
|
</div>
|
||||||
|
<Divider/>
|
||||||
|
<div class="flex flex-row !w-full justify-between px-2 pb-2 items-center">
|
||||||
|
<span>Days reminders</span>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<button
|
||||||
|
@click="isNotificationsEnabled ? selectAllDay() : ''">
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button v-for="day in days" class="border border-gray-200 rounded-md shadow-sm p-1"
|
||||||
|
@click="isNotificationsEnabled ? selectDay(day.key): ''"
|
||||||
|
:class="isNotificationsEnabled ? day.isSelected ? 'bg-green-100' : '' : 'bg-gray-100'">
|
||||||
|
{{ day.key.substring(0, 1) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ onMounted(fetchData);
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div class="space-list flex w-full flex-col justify-center gap-2 p-2">
|
<div class="flex space-list flex w-full flex-col justify-center gap-2 p-2">
|
||||||
<!-- твой контент -->
|
<!-- твой контент -->
|
||||||
<span class="font-bold">Selected space: {{spaceName}}</span>
|
<span class="font-bold">Selected space: {{spaceName}}</span>
|
||||||
<div v-for="space in spaces" :key="space.id" class="w-full h-full " @click="selectSpace(space)" >
|
<div v-for="space in spaces" :key="space.id" class="w-full h-full " @click="selectSpace(space)" >
|
||||||
<div class="flex w-full flex-col justify-start rounded-2xl p-2 bg-gray-50 gap-2" :class="spaceId === space.id ? '!bg-green-50' : 'bg-gray-50'">
|
<div class="flex w-full flex-col justify-start rounded-2xl p-2 bg-gray-50 gap-2" :class="spaceId === space.id ? '!bg-green-50' : 'bg-gray-50'">
|
||||||
<span class=" font-medium ">{{ space.name }}</span>
|
<span class=" font-medium ">{{ space.name }}</span>
|
||||||
<div class="w-10 h-10 rounded-full bg-green-200 flex items-center justify-center ">
|
<div class="flex w-10 h-10 rounded-full bg-green-200 flex items-center justify-center ">
|
||||||
<span class="font-bold ">{{
|
<span class="font-bold ">{{
|
||||||
space.owner.firstName.substring(0, 1).toUpperCase()
|
space.owner.firstName.substring(0, 1).toUpperCase()
|
||||||
}}</span>
|
}}</span>
|
||||||
|
|||||||
11
src/components/test.vue
Normal file
11
src/components/test.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<LiquidGlass>
|
||||||
|
<div class="p-8">
|
||||||
|
Your content here
|
||||||
|
</div>
|
||||||
|
</LiquidGlass>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { LiquidGlass } from 'liquid-glass-vue'
|
||||||
|
</script>
|
||||||
409
src/components/transactions/TransactionCreateUpdate.vue
Normal file
409
src/components/transactions/TransactionCreateUpdate.vue
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {DatePicker, Divider, InputNumber, SelectButton} from "primevue";
|
||||||
|
import ConfirmDialog from "@/components/ConfirmDialog.vue";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import {useToolbarStore} from "@/stores/toolbar-store";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {computed, onMounted, ref} from "vue";
|
||||||
|
import {Category} from "@/models/category";
|
||||||
|
import {TransactionService} from "@/services/transactions-service";
|
||||||
|
import {CreateTransactionDTO, UpdateTransactionDTO} from "@/models/transaction";
|
||||||
|
import {CategoryType, TransactionKind, TransactionKindName, TransactionType, TransactionTypeName} from "@/models/enums";
|
||||||
|
import {useTransactionStore} from "@/stores/transactions-store";
|
||||||
|
import {categoriesService} from "@/services/categories-service";
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const toolbar = useToolbarStore();
|
||||||
|
const toast = useToast();
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
const transactionService = TransactionService
|
||||||
|
const transactionStore = useTransactionStore()
|
||||||
|
const tgApp = window.Telegram.WebApp
|
||||||
|
|
||||||
|
const isCategorySelectorOpened = ref(false);
|
||||||
|
|
||||||
|
const categories = ref<Category[]>([]);
|
||||||
|
const transactionId = ref<number | undefined>(route.params.id)
|
||||||
|
const mode = computed(() => {
|
||||||
|
return transactionId.value ? "edit" : "create"
|
||||||
|
})
|
||||||
|
|
||||||
|
const isDeleteAlertVisible = ref(false)
|
||||||
|
const deleteAlertMessage = ref('Do you want to delete transaction?')
|
||||||
|
|
||||||
|
// Генерим опции: [{ label: "Расходы", value: "EXPENSE" }, ...]
|
||||||
|
const optionsType = Object.values(TransactionType).map(type => ({
|
||||||
|
label: TransactionTypeName[type],
|
||||||
|
value: type
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Генерим опции: [{ label: "Расходы", value: "EXPENSE" }, ...]
|
||||||
|
const optionsKind = Object.values(TransactionKind).map(type => ({
|
||||||
|
label: TransactionKindName[type],
|
||||||
|
value: type
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
const transactionType = ref<TransactionType>(TransactionType.EXPENSE)
|
||||||
|
const isTypeError = ref<boolean>(false);
|
||||||
|
const transactionKind = ref<TransactionKind>(TransactionKind.INSTANT)
|
||||||
|
const isKindError = ref<boolean>(false);
|
||||||
|
const transactionCategory = ref<Category>({} as Category)
|
||||||
|
const isCategoryError = ref(false)
|
||||||
|
const transactionComment = ref<string>('')
|
||||||
|
const isCommentError = ref(false)
|
||||||
|
const transactionAmount = ref<number>(0)
|
||||||
|
const isAmountError = ref(false)
|
||||||
|
const transactionDate = ref<Date>(new Date())
|
||||||
|
const isDateError = ref(false)
|
||||||
|
const isDone = ref(false)
|
||||||
|
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (spaceStore.selectedSpaceId) {
|
||||||
|
categories.value = await categoriesService.fetchCategories(spaceStore.selectedSpaceId)
|
||||||
|
if (categories.value.length > 0) {
|
||||||
|
transactionCategory.value = categories.value[0]
|
||||||
|
}
|
||||||
|
} else throw Error("No space selected")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: "Error while fetching categories.",
|
||||||
|
detail: error.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (spaceStore.selectedSpaceId && transactionId.value) {
|
||||||
|
categories.value = await categoriesService.fetchCategories(spaceStore.selectedSpaceId);
|
||||||
|
let data = await transactionService.getTransaction(spaceStore.selectedSpaceId, Number(transactionId.value));
|
||||||
|
transactionType.value = data.type;
|
||||||
|
transactionKind.value = data.kind;
|
||||||
|
transactionCategory.value = data.category;
|
||||||
|
transactionComment.value = data.comment;
|
||||||
|
transactionAmount.value = data.amount;
|
||||||
|
transactionDate.value = new Date(data.date);
|
||||||
|
} else {
|
||||||
|
throw new Error("Could not find any space")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: "Failed to load transaction data.",
|
||||||
|
detail: error,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
if (!transactionType.value) {
|
||||||
|
isTypeError.value = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!transactionKind.value) {
|
||||||
|
isKindError.value = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!transactionCategory.value) {
|
||||||
|
isCategoryError.value = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (transactionComment.value.length == 0) {
|
||||||
|
isCommentError.value = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (transactionAmount.value <= 0) {
|
||||||
|
isAmountError.value = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!transactionDate.value) {
|
||||||
|
isDateError.value = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildUpdate = (): UpdateTransactionDTO => {
|
||||||
|
if (validateForm()) {
|
||||||
|
return {
|
||||||
|
type: transactionType.value,
|
||||||
|
kind: transactionKind.value,
|
||||||
|
categoryId: transactionCategory.value.id,
|
||||||
|
comment: transactionComment.value,
|
||||||
|
amount: transactionAmount.value,
|
||||||
|
isDone: isDone.value,
|
||||||
|
date: transactionDate.value
|
||||||
|
} as UpdateTransactionDTO
|
||||||
|
} else {
|
||||||
|
throw new Error("Form is not valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildCreate = (): CreateTransactionDTO => {
|
||||||
|
if (validateForm()) {
|
||||||
|
return {
|
||||||
|
type: transactionType.value,
|
||||||
|
kind: transactionKind.value,
|
||||||
|
categoryId: transactionCategory.value.id,
|
||||||
|
comment: transactionComment.value,
|
||||||
|
amount: transactionAmount.value,
|
||||||
|
date: transactionDate.value
|
||||||
|
} as CreateTransactionDTO
|
||||||
|
} else {
|
||||||
|
throw new Error("Form is not valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveUser = async () => {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
console.log('moved back')
|
||||||
|
router.back()
|
||||||
|
} else {
|
||||||
|
console.log('moved forward')
|
||||||
|
await router.push('/categories')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteTransaction = async () => {
|
||||||
|
if (spaceStore.selectedSpaceId && transactionId.value) {
|
||||||
|
await transactionService.deleteTransaction(spaceStore.selectedSpaceId, Number(transactionId.value))
|
||||||
|
await transactionStore.fetchTransactions(spaceStore.selectedSpaceId)
|
||||||
|
await moveUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const insetTop = ref(54)
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (route.query.type === "EXPENSE") transactionType.value = TransactionType.EXPENSE
|
||||||
|
if (route.query.type === "INCOME") transactionType.value = TransactionType.INCOME
|
||||||
|
|
||||||
|
if (route.query.kind === "INSTANT") transactionKind.value = TransactionKind.INSTANT
|
||||||
|
if (route.query.kind === "PLANNING") transactionKind.value = TransactionKind.PLANNING
|
||||||
|
|
||||||
|
// Remove query params AFTER reading them
|
||||||
|
if (route.query.type || route.query.kind) {
|
||||||
|
router.replace({path: route.path}) // instead of window.location
|
||||||
|
}
|
||||||
|
await fetchCategories()
|
||||||
|
if (tgApp && ['ios', 'android'].includes(tgApp.platform)) {
|
||||||
|
insetTop.value = tgApp.contentSafeAreaInset.top + tgApp.safeAreaInset.top
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode.value === "edit") {
|
||||||
|
await fetchData()
|
||||||
|
toolbar.registerHandler('deleteTransaction', () => {
|
||||||
|
if (tgApp.initData) {
|
||||||
|
tgApp.showConfirm(deleteAlertMessage.value, async (confirmed: boolean) => {
|
||||||
|
if (confirmed) {
|
||||||
|
await deleteTransaction()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
isDeleteAlertVisible.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
toolbar.registerHandler('updateTransaction', async () => {
|
||||||
|
if (spaceStore.selectedSpaceId) {
|
||||||
|
// await categoriesStore.fetchCategories(spaceStore.selectedSpaceId)
|
||||||
|
try {
|
||||||
|
let updateDTO = buildUpdate()
|
||||||
|
await transactionService.updateTransaction(spaceStore.selectedSpaceId, Number(transactionId.value), updateDTO)
|
||||||
|
console.log(updateDTO)
|
||||||
|
await moveUser()
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: "Error while updating transaction",
|
||||||
|
detail: e,
|
||||||
|
life: 3000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toolbar.registerHandler('createTransaction', async () => {
|
||||||
|
if (spaceStore.selectedSpaceId) {
|
||||||
|
// await categoriesStore.fetchCategories(spaceStore.selectedSpaceId)
|
||||||
|
try {
|
||||||
|
let createDTO = buildCreate()
|
||||||
|
await transactionService.createTransaction(spaceStore.selectedSpaceId, createDTO)
|
||||||
|
console.log(createDTO)
|
||||||
|
await moveUser()
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: "Error while creating transaction",
|
||||||
|
detail: e,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="flex ">
|
||||||
|
<span v-if="categories.length == 0" class="card">Looks like you have no created categories yet. <router-link
|
||||||
|
to="/categories/create" class="!text-blue-400">Try to create some first.</router-link></span>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col w-full justify-items-start gap-1">
|
||||||
|
<ConfirmDialog
|
||||||
|
v-if="isDeleteAlertVisible"
|
||||||
|
:message="deleteAlertMessage"
|
||||||
|
:callback="(confirmed) => { if (confirmed) deleteTransaction(); isDeleteAlertVisible = false; }"
|
||||||
|
/>
|
||||||
|
<!-- Fixed modal container -->
|
||||||
|
<div v-if="isCategorySelectorOpened"
|
||||||
|
class="absolute inset-0 top-0 z-[1000] flex items-start justify-center p-4 overflow-y-auto "
|
||||||
|
style="background-color: var(--primary-color); padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom);"
|
||||||
|
|
||||||
|
:style="tgApp ? `padding-top: ${insetTop}px !important` : 'padding-top: 2rem !important'">
|
||||||
|
<div class="flex flex-col w-full max-w-md !h-dvh !pb-20">
|
||||||
|
<div v-if="categories.filter(i => i.type == CategoryType.INCOME).length>0" class="flex flex-col w-full">
|
||||||
|
<span>Income categories</span>
|
||||||
|
<div class="flex card justify-items-start justify-start h-fit">
|
||||||
|
<div v-for="(cat, idx) in categories.filter(i => i.type == CategoryType.INCOME)" :key="cat.id"
|
||||||
|
@click="transactionCategory = cat; transactionType = cat.type == 'EXPENSE' ? TransactionType.EXPENSE : TransactionType.INCOME; isCategorySelectorOpened = false"
|
||||||
|
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div class="flex flex-row w-full items-center justify-between py-3">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<span class="text-3xl">{{ cat.icon }} </span>
|
||||||
|
<div class="flex flex-col justify-between">
|
||||||
|
<div class="flex flex-row"> {{ cat.name }}</div>
|
||||||
|
<div class="flex flex-row text-sm text-gray-600">{{ cat.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="pi pi-angle-right !font-extralight"/>
|
||||||
|
</div>
|
||||||
|
<Divider v-if="idx + 1 !== categories.length" class="!m-0"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="categories.filter(i => i.type == CategoryType.EXPENSE).length>0" class="flex flex-col w-full">
|
||||||
|
<span>Expense categories</span>
|
||||||
|
<div class="flex card justify-items-start justify-start h-fit">
|
||||||
|
<div v-for="(cat, idx) in categories.filter(i => i.type == CategoryType.EXPENSE)" :key="cat.id"
|
||||||
|
@click="transactionCategory = cat; transactionType = cat.type == 'EXPENSE' ? TransactionType.EXPENSE : TransactionType.INCOME; isCategorySelectorOpened = false"
|
||||||
|
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold cursor-pointer hover:bg-gray-50 transition-colors">
|
||||||
|
<div class="flex flex-row w-full items-center justify-between py-3">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<span class="text-3xl">{{ cat.icon }} </span>
|
||||||
|
<div class="flex flex-col justify-between">
|
||||||
|
<div class="flex flex-row"> {{ cat.name }}</div>
|
||||||
|
<div class="flex flex-row text-sm text-gray-600">{{ cat.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="pi pi-angle-right !font-extralight"/>
|
||||||
|
</div>
|
||||||
|
<Divider v-if="idx + 1 !== categories.length" class="!m-0"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full ">
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<InputNumber
|
||||||
|
v-model="transactionAmount"
|
||||||
|
@input=" isAmountError = false"
|
||||||
|
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
placeholder="Amount"
|
||||||
|
suffix="₽"
|
||||||
|
class="text-7xl font-bold w-full text-center focus:outline-none !p-0 !m-0"
|
||||||
|
|
||||||
|
/>
|
||||||
|
<span v-if="isAmountError" class="text-sm !text-red-500 font-extralight">Amount couldn't be less then 1</span>
|
||||||
|
<!-- <span class="absolute right-2 top-1/2 -translate-y-1/2 text-7xl font-bold">₽</span>-->
|
||||||
|
|
||||||
|
<label class="!justify-items-center !justify-center !font-extralight text-gray-600 text-center">Amount</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex lg:flex-row flex-col w-full justify-between gap-4 ">
|
||||||
|
<!-- <div class="card flex flex-col w-full items-center justify-center">-->
|
||||||
|
<!-- <span class="text-lg hidden lg:flex">Тип транзакции</span>-->
|
||||||
|
<!-- <SelectButton v-model="transactionType" :options="optionsType" optionLabel="label"-->
|
||||||
|
<!-- optionValue="value"-->
|
||||||
|
<!-- class="!w-full !items-center !justify-center !border-none "/>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<div class="card flex flex-col w-full items-center justify-center">
|
||||||
|
|
||||||
|
<span class="text-lg hidden lg:flex">Вид транзакции</span>
|
||||||
|
<SelectButton v-model="transactionKind" :options="optionsKind" optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="!w-full !items-center !justify-center !border-none "/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-full justify-items-start">
|
||||||
|
<label class="!font-semibold text-gray-600 pl-2">Transaction name</label>
|
||||||
|
<div class="flex card !justify-start !items-start !p-4 !pl-5 ">
|
||||||
|
<input class="font-extralight w-full focus:outline-0" placeholder="Name"
|
||||||
|
@input="transactionComment?.length ==0 ? isCommentError = true : isCommentError=false"
|
||||||
|
v-model="transactionComment"/>
|
||||||
|
</div>
|
||||||
|
<span v-if="isCommentError" class="text-sm !text-red-500 font-extralight">Comment couldn't be empty.</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-full justify-items-start">
|
||||||
|
<label class="!font-semibold text-gray-600 pl-2">Transaction category</label>
|
||||||
|
<div class="flex card !justify-start !items-start !p-4 !pl-5 cursor-pointer"
|
||||||
|
@click="isCategorySelectorOpened = true; isCategoryError=false;">
|
||||||
|
<div class="flex flex-row w-full gap-2 items-center justify-between">
|
||||||
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
<span class="!text-3xl ">{{ transactionCategory.icon }}</span>
|
||||||
|
<div class="flex flex-col ">
|
||||||
|
<span class=" !">{{ transactionCategory.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="pi pi-angle-right !font-extralight"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="isCategoryError" class="text-sm text-red-500 font-extralight">Category should be selected</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex !flex-col w-full justify-items-start">
|
||||||
|
<label class="!font-semibold text-gray-600 !pl-2">Transaction date</label>
|
||||||
|
<div class="card flex justify-center">
|
||||||
|
<DatePicker inline v-model="transactionDate" class="w-auto !border-0"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span v-if="isDateError" class="text-sm text-red-500 font-extralight">
|
||||||
|
Date couldn't be empty or less than 1 and greater than 31.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.p-datepicker-panel) {
|
||||||
|
width: 100% !important;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,128 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {computed, onMounted, ref} from "vue";
|
||||||
|
import {Checkbox, Divider} from "primevue";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import {Transaction} from "@/models/transaction";
|
||||||
|
import {TransactionService} from "@/services/transactions-service";
|
||||||
|
import {formatAmount, formatDate} from "@/utils/utils";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import {TransactionKind} from "@/models/enums";
|
||||||
|
import {useToolbarStore} from "@/stores/toolbar-store";
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const toolbar = useToolbarStore()
|
||||||
|
const transactionService = TransactionService
|
||||||
|
const transactions = ref<Transaction[]>([])
|
||||||
|
const groupedTransactions = computed(() => {
|
||||||
|
const planned: Transaction[] = []
|
||||||
|
const instant: Transaction[] = []
|
||||||
|
|
||||||
|
for (const tx of transactions.value) {
|
||||||
|
if (tx.kind === TransactionKind.PLANNING) planned.push(tx)
|
||||||
|
else if (tx.kind === TransactionKind.INSTANT) instant.push(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { planned, instant }
|
||||||
|
})
|
||||||
|
|
||||||
|
const plannedTransactions = computed(() => groupedTransactions.value.planned)
|
||||||
|
const instantTransactions = computed(() => groupedTransactions.value.instant)
|
||||||
|
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
if (spaceStore.selectedSpaceId) {
|
||||||
|
try {
|
||||||
|
console.log('hereeeee ')
|
||||||
|
transactions.value = await transactionService.getTransactions(spaceStore.selectedSpaceId);
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: "Failed to load transactions.",
|
||||||
|
detail: e,
|
||||||
|
life: 3000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData()
|
||||||
|
toolbar.registerHandler('openTransactionCreation', () => {
|
||||||
|
router.push('/transactions/create')
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div v-if="!spaceStore.selectedSpaceId" class="card">
|
||||||
|
Try to select a space first.
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col gap-6 pb-10">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-xl !font-semibold !pl-2">Planned transactions</span>
|
||||||
|
<div class="flex card">
|
||||||
|
<span v-if="plannedTransactions.length==0">Looks like you haven't plan any transactions yet. <router-link
|
||||||
|
to="/transactions/create" class="!text-blue-400">Try to create some.</router-link></span>
|
||||||
|
<div v-else v-for="key in plannedTransactions.keys()" :key="plannedTransactions[key].id"
|
||||||
|
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold ">
|
||||||
|
<div class="flex flex-row w-full items-center gap-4">
|
||||||
|
<Checkbox v-model="plannedTransactions[key].isDone" binary class="text-3xl">
|
||||||
|
{{ plannedTransactions[key].category.icon }}
|
||||||
|
</Checkbox>
|
||||||
|
<div class="flex !flex-row !justify-between !w-full"
|
||||||
|
@click="router.push(`/transactions/${plannedTransactions[key].id}/edit`)">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<div class="flex flex-col !font-bold "> {{ plannedTransactions[key].comment }}
|
||||||
|
<div class="flex flex-row text-sm">{{ plannedTransactions[key].category.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2 items-center !w-fit">
|
||||||
|
<div class="flex flex-col justify-between items-end !w-fit whitespace-nowrap shrink-0">
|
||||||
|
<span class="text-lg !font-bold">{{ formatAmount(plannedTransactions[key].amount) }} ₽</span>
|
||||||
|
<span class="text-sm !font-extralight"> {{ formatDate(plannedTransactions[key].date) }}</span>
|
||||||
|
</div>
|
||||||
|
<i class="pi pi-angle-right !font-extralight"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider v-if="key+1 !== plannedTransactions.length" class="!m-0 !py-3"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-xl !font-semibold !pl-2">Instant transactions</span>
|
||||||
|
<div class="flex card">
|
||||||
|
<span v-if="instantTransactions.length==0">Looks like you haven't record any transaction yet.<router-link
|
||||||
|
to="/transactions/create" class="!text-blue-400">Try to create some.</router-link></span>
|
||||||
|
<div v-else v-for="key in instantTransactions.keys()" :key="instantTransactions[key].id"
|
||||||
|
@click="router.push(`/transactions/${instantTransactions[key].id}/edit`)"
|
||||||
|
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold ">
|
||||||
|
<div class="flex flex-row w-full items-center justify-between">
|
||||||
|
<div class="flex flex-row items-center gap-2 ">
|
||||||
|
<span class="text-3xl"> {{ instantTransactions[key].category.icon }}</span>
|
||||||
|
<div class="flex flex-col !font-bold "> {{ instantTransactions[key].comment }}
|
||||||
|
<div class="flex flex-row text-sm">{{ instantTransactions[key].category.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
<div class="flex flex-col justify-between items-end !w-fit whitespace-nowrap shrink-0">
|
||||||
|
<span class="text-lg !font-bold ">{{ formatAmount(instantTransactions[key].amount) }} ₽</span>
|
||||||
|
<span class="text-sm !font-extralight"> {{ formatDate(instantTransactions[key].date) }}</span>
|
||||||
|
</div>
|
||||||
|
<i class="pi pi-angle-right !font-extralight"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider v-if="key+1 !== instantTransactions.length" class="!m-0 !py-3"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -11,9 +11,11 @@ import ToastService from 'primevue/toastservice'
|
|||||||
import Tooltip from 'primevue/tooltip';
|
import Tooltip from 'primevue/tooltip';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import ConfirmationService from 'primevue/confirmationservice';
|
import ConfirmationService from 'primevue/confirmationservice';
|
||||||
|
import {LiquidGlass} from "@wxperia/liquid-glass-vue";
|
||||||
|
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
app.use(LiquidGlass)
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(ToastService);
|
app.use(ToastService);
|
||||||
app.use(ConfirmationService);
|
app.use(ConfirmationService);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export enum CategoryType {
|
export enum CategoryType {
|
||||||
EXPENSE = "EXPENSE",
|
EXPENSE = "EXPENSE",
|
||||||
INCOME = "INCOME",
|
INCOME = "INCOME",
|
||||||
}
|
}
|
||||||
@@ -8,3 +8,31 @@ export const CategoryTypeName: Record<CategoryType, string> = {
|
|||||||
[CategoryType.INCOME]: 'Поступления',
|
[CategoryType.INCOME]: 'Поступления',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SettingType {
|
||||||
|
PERIOD_START = "PERIOD_START",
|
||||||
|
SUB_PERIOD_START = "SUB_PERIOD_START",
|
||||||
|
NOTIFICATIONS_ENABLED = "NOTIFICATIONS_ENABLED",
|
||||||
|
NOTIFICATIONS_DAYS = "NOTIFICATIONS_DAYS",
|
||||||
|
NOTIFICATIONS_TIME = "NOTIFICATIONS_TIME",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export enum TransactionType {
|
||||||
|
INCOME = "INCOME",
|
||||||
|
EXPENSE = "EXPENSE",
|
||||||
|
}
|
||||||
|
export const TransactionTypeName: Record<TransactionType, string> = {
|
||||||
|
[TransactionType.EXPENSE]: 'Расходы',
|
||||||
|
[TransactionType.INCOME]: 'Поступления',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TransactionKind {
|
||||||
|
PLANNING = "PLANNING",
|
||||||
|
INSTANT = "INSTANT",
|
||||||
|
}
|
||||||
|
export const TransactionKindName: Record<TransactionKind, string> = {
|
||||||
|
[TransactionKind.INSTANT]: 'Текущие',
|
||||||
|
[TransactionKind.PLANNING]: 'Плановые',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ export interface Space {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SpaceShortInfo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
isOwner: boolean
|
||||||
|
participantsCount: number
|
||||||
|
createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateSpaceDTO {
|
export interface CreateSpaceDTO {
|
||||||
name: string;
|
name: string;
|
||||||
createBasicCategories: boolean;
|
createBasicCategories: boolean;
|
||||||
|
|||||||
41
src/models/transaction.ts
Normal file
41
src/models/transaction.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {TransactionKind, TransactionType} from "@/models/enums";
|
||||||
|
import {Category} from "@/models/category";
|
||||||
|
import {User} from "@/models/user";
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
id: number;
|
||||||
|
parentId: number | null
|
||||||
|
type: TransactionType
|
||||||
|
kind: TransactionKind
|
||||||
|
category: Category
|
||||||
|
comment: string
|
||||||
|
amount: number
|
||||||
|
fees: number | null
|
||||||
|
date: string
|
||||||
|
isDone: boolean
|
||||||
|
createdBy: User
|
||||||
|
createdAt: Date
|
||||||
|
updatedBy: User | null
|
||||||
|
updatedAt: Date | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTransactionDTO {
|
||||||
|
type: TransactionType
|
||||||
|
kind: TransactionKind
|
||||||
|
categoryId: number
|
||||||
|
comment: string
|
||||||
|
amount: number
|
||||||
|
fees: number | null
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateTransactionDTO {
|
||||||
|
type: TransactionType
|
||||||
|
kind: TransactionKind
|
||||||
|
categoryId: number
|
||||||
|
comment: string
|
||||||
|
amount: number
|
||||||
|
fees: number | null
|
||||||
|
isDone: boolean
|
||||||
|
date: string
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import DashboardView from "@/components/dashboard/DashboardView.vue";
|
|||||||
import RecurrentyCreateUpdate from "@/components/settings/RecurrentyCreateUpdate.vue";
|
import RecurrentyCreateUpdate from "@/components/settings/RecurrentyCreateUpdate.vue";
|
||||||
import TransactionList from "@/components/transactions/TransactionList.vue";
|
import TransactionList from "@/components/transactions/TransactionList.vue";
|
||||||
import LoginPage from "@/components/auth/LoginPage.vue";
|
import LoginPage from "@/components/auth/LoginPage.vue";
|
||||||
|
import TransactionCreateUpdate from "@/components/transactions/TransactionCreateUpdate.vue";
|
||||||
|
|
||||||
// 📝 Расширяем тип меты роутов (типобезопасный toolbar, requiresAuth, guestOnly)
|
// 📝 Расширяем тип меты роутов (типобезопасный toolbar, requiresAuth, guestOnly)
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
@@ -29,6 +30,8 @@ export const enum RouteName {
|
|||||||
Login = 'login',
|
Login = 'login',
|
||||||
Dashboard = 'dashboard',
|
Dashboard = 'dashboard',
|
||||||
TransactionList = 'transaction-list',
|
TransactionList = 'transaction-list',
|
||||||
|
TransactionCreate = 'transaction-create',
|
||||||
|
TransactionUpdate = 'transaction-update',
|
||||||
SettingsList = 'settings-list',
|
SettingsList = 'settings-list',
|
||||||
CategoriesList = 'categories-list',
|
CategoriesList = 'categories-list',
|
||||||
CategoryCreate = 'category-create',
|
CategoryCreate = 'category-create',
|
||||||
@@ -42,53 +45,82 @@ export const enum RouteName {
|
|||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{path: '/login', name: RouteName.Login, component: LoginPage, meta: {requiresAuth: false, navStack: 'auth'}},
|
{path: '/login', name: RouteName.Login, component: LoginPage, meta: {requiresAuth: false, navStack: 'auth'}},
|
||||||
{path: '/', name: RouteName.Dashboard, component: DashboardView, meta: {requiresAuth: true, navStack: 'dashboard'}},
|
{path: '/', name: RouteName.Dashboard, component: DashboardView, meta: {requiresAuth: true, navStack: 'dashboard', title: "Home"}},
|
||||||
{path: '/transactions', name: RouteName.TransactionList, component: TransactionList, meta: {requiresAuth: true, navStack: 'transactions'}},
|
{
|
||||||
{path: '/settings', name: RouteName.SettingsList, component: SettingsList, meta: {requiresAuth: true, navStack: 'settings'}},
|
path: '/transactions',
|
||||||
|
name: RouteName.TransactionList,
|
||||||
|
component: TransactionList,
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
||||||
|
|
||||||
|
{id: 'openTransactionCreation', text: '', icon: 'pi pi-plus', onClickId: 'openTransactionCreation'},
|
||||||
|
],
|
||||||
|
navStack: 'transactions',
|
||||||
|
title: "Transactions"
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/transactions/create', name: RouteName.TransactionCreate, component: TransactionCreateUpdate, meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
||||||
|
{id: 'createTransaction', text: '', icon: 'pi pi-save', onClickId: 'createTransaction'},
|
||||||
|
],
|
||||||
|
navStack: 'transactions',
|
||||||
|
title: "Create transaction"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/transactions/:id/edit', name: RouteName.TransactionUpdate, component: TransactionCreateUpdate, meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
||||||
|
|
||||||
|
{id: 'deleteTransaction', text: '', icon: 'pi pi-trash', onClickId: 'deleteTransaction'},
|
||||||
|
{id: 'updateTransaction', text: '', icon: 'pi pi-save', onClickId: 'updateTransaction'},
|
||||||
|
],
|
||||||
|
navStack: 'settings',
|
||||||
|
title: "Edit transaction"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: RouteName.SettingsList,
|
||||||
|
component: SettingsList,
|
||||||
|
meta: {requiresAuth: true, navStack: 'settings', title: "Settings"}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/categories', name: RouteName.CategoriesList, component: CategoriesList, meta: {
|
path: '/categories', name: RouteName.CategoriesList, component: CategoriesList, meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
||||||
{
|
|
||||||
id: 'space',
|
|
||||||
text: spaceStore.selectedSpaceName ?? 'Select Space',
|
|
||||||
icon: '',
|
|
||||||
onClickId: 'openSpacePicker',
|
|
||||||
},
|
|
||||||
{id: 'openCategoryCreation', text: '', icon: 'pi pi-plus', onClickId: 'openCategoryCreation'},
|
{id: 'openCategoryCreation', text: '', icon: 'pi pi-plus', onClickId: 'openCategoryCreation'},
|
||||||
],
|
],
|
||||||
navStack: 'settings',
|
navStack: 'settings',
|
||||||
|
title: "Categories"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/categories/create', name: RouteName.CategoryCreate, component: CategoryCreateUpdate, meta: {
|
path: '/categories/create', name: RouteName.CategoryCreate, component: CategoryCreateUpdate, meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
||||||
{
|
|
||||||
id: 'space',
|
|
||||||
text: spaceStore.selectedSpaceName ?? 'Select Space',
|
|
||||||
icon: '',
|
|
||||||
onClickId: 'openSpacePicker',
|
|
||||||
},
|
|
||||||
{id: 'createCategory', text: '', icon: 'pi pi-save', onClickId: 'createCategory'},
|
{id: 'createCategory', text: '', icon: 'pi pi-save', onClickId: 'createCategory'},
|
||||||
],
|
],
|
||||||
navStack: 'settings',
|
navStack: 'settings',
|
||||||
|
title: "Create category"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/categories/:id/edit', name: RouteName.CategoryUpdate, component: CategoryCreateUpdate, meta: {
|
path: '/categories/:id/edit', name: RouteName.CategoryUpdate, component: CategoryCreateUpdate, meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
||||||
{
|
|
||||||
id: 'space',
|
|
||||||
text: spaceStore.selectedSpaceName ?? 'Select Space',
|
|
||||||
icon: '',
|
|
||||||
onClickId: 'openSpacePicker',
|
|
||||||
},
|
|
||||||
{id: 'deleteCategory', text: '', icon: 'pi pi-trash', onClickId: 'deleteCategory'},
|
{id: 'deleteCategory', text: '', icon: 'pi pi-trash', onClickId: 'deleteCategory'},
|
||||||
{id: 'updateCategory', text: '', icon: 'pi pi-save', onClickId: 'updateCategory'},
|
{id: 'updateCategory', text: '', icon: 'pi pi-save', onClickId: 'updateCategory'},
|
||||||
],
|
],
|
||||||
navStack: 'settings',
|
navStack: 'settings',
|
||||||
|
title: "Edit category"
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -97,46 +129,34 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/recurrents', name: RouteName.RecurrentsList, component: RecurrentsList, meta: {
|
path: '/recurrents', name: RouteName.RecurrentsList, component: RecurrentsList, meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
||||||
{
|
|
||||||
id: 'space',
|
|
||||||
text: spaceStore.selectedSpaceName ?? 'Select Space',
|
|
||||||
icon: 'pi pi-home',
|
|
||||||
onClickId: 'openSpacePicker',
|
|
||||||
},
|
|
||||||
{id: 'openRecurrentCreation', text: '', icon: 'pi pi-plus', onClickId: 'openRecurrentCreation'},
|
{id: 'openRecurrentCreation', text: '', icon: 'pi pi-plus', onClickId: 'openRecurrentCreation'},
|
||||||
],
|
],
|
||||||
navStack: 'settings',
|
navStack: 'settings',
|
||||||
|
title: "Recurrents"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/recurrents/create', name: RouteName.RecurrentCreate, component: RecurrentyCreateUpdate, meta: {
|
path: '/recurrents/create', name: RouteName.RecurrentCreate, component: RecurrentyCreateUpdate, meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
||||||
{
|
|
||||||
id: 'space',
|
|
||||||
text: spaceStore.selectedSpaceName ?? 'Select Space',
|
|
||||||
icon: '',
|
|
||||||
onClickId: 'openSpacePicker',
|
|
||||||
},
|
|
||||||
{id: 'createRecurrent', text: '', icon: 'pi pi-save', onClickId: 'createRecurrent'},
|
{id: 'createRecurrent', text: '', icon: 'pi pi-save', onClickId: 'createRecurrent'},
|
||||||
],
|
],
|
||||||
navStack: 'settings',
|
navStack: 'settings',
|
||||||
|
title: "Create recurrent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/recurrents/:id/edit', name: RouteName.RecurrentUpdate, component: RecurrentyCreateUpdate, meta: {
|
path: '/recurrents/:id/edit', name: RouteName.RecurrentUpdate, component: RecurrentyCreateUpdate, meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
toolbar: ({spaceStore}: { spaceStore: ReturnType<typeof useSpaceStore> }) => [
|
||||||
{
|
|
||||||
id: 'space',
|
|
||||||
text: spaceStore.selectedSpaceName ?? 'Select Space',
|
|
||||||
icon: '',
|
|
||||||
onClickId: 'openSpacePicker',
|
|
||||||
},
|
|
||||||
{id: 'deleteRecurrent', text: '', icon: 'pi pi-trash', onClickId: 'deleteRecurrent'},
|
{id: 'deleteRecurrent', text: '', icon: 'pi pi-trash', onClickId: 'deleteRecurrent'},
|
||||||
{id: 'updateRecurrent', text: '', icon: 'pi pi-save', onClickId: 'updateRecurrent'},
|
{id: 'updateRecurrent', text: '', icon: 'pi pi-save', onClickId: 'updateRecurrent'},
|
||||||
],
|
],
|
||||||
navStack: 'settings',
|
navStack: 'settings',
|
||||||
|
title: "Edit recurrent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -152,17 +172,20 @@ const routes: RouteRecordRaw[] = [
|
|||||||
icon: 'pi pi-home',
|
icon: 'pi pi-home',
|
||||||
onClickId: 'openSpacePicker',
|
onClickId: 'openSpacePicker',
|
||||||
},
|
},
|
||||||
{id: 'save', text: 'Save', icon: 'pi pi-check', onClickId: 'saveSettings'},
|
// {id: 'save', text: 'Save', icon: 'pi pi-check', onClickId: 'saveSettings'},
|
||||||
],
|
],
|
||||||
navStack: 'settings',
|
navStack: 'settings',
|
||||||
|
title: "Space settings"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/notification-settings',
|
path: '/notification-settings',
|
||||||
name: RouteName.NotificationSettings,
|
name: RouteName.NotificationSettings,
|
||||||
component: NotificationSettings,
|
component: NotificationSettings,
|
||||||
meta: {requiresAuth: true,
|
meta: {
|
||||||
navStack: 'settings',}
|
requiresAuth: true,
|
||||||
|
navStack: 'settings',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -204,6 +227,12 @@ router.afterEach((to) => {
|
|||||||
} else {
|
} else {
|
||||||
toolbar.setByConfig(cfg)
|
toolbar.setByConfig(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (to.meta.title) {
|
||||||
|
document.title = `${to.meta.title as string} | Luminic Space`
|
||||||
|
} else {
|
||||||
|
document.title = "Luminic Space"
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1,53 +1,52 @@
|
|||||||
import {Category, CreateCategoryDTO, UpdateCategoryDTO} from "@/models/category";
|
import {Category, CreateCategoryDTO, UpdateCategoryDTO} from "@/models/category";
|
||||||
import {CategoryType} from "@/models/enums";
|
import api from "@/network/axiosSetup";
|
||||||
import {User} from "@/models/user";
|
|
||||||
|
|
||||||
async function fetchCategories(spaceId: number) {
|
async function fetchCategories(spaceId: number) {
|
||||||
let categories: Category[] = []
|
try {
|
||||||
for (let i = 0; i < 10; i++) {
|
let response = await api.get(`/spaces/${spaceId}/categories`);
|
||||||
categories.push({
|
return response.data
|
||||||
id: i,
|
} catch (error) {
|
||||||
type: Math.floor(Math.random() * 2) === 0 ? CategoryType.INCOME : CategoryType.EXPENSE,
|
console.error(error);
|
||||||
name: `Category ${i}`,
|
throw error;
|
||||||
description: `Description of Category ${i}`,
|
|
||||||
icon: "😇",
|
|
||||||
createdBy: {
|
|
||||||
id: i,
|
|
||||||
username: `username_${i}`,
|
|
||||||
firstName: `firstName ${i}`,
|
|
||||||
} as User,
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as Category)
|
|
||||||
}
|
}
|
||||||
return categories;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCategory(spaceId: number, categoryId: number): Promise<Category> {
|
async function fetchCategory(spaceId: number, categoryId: number): Promise<Category> {
|
||||||
return {
|
try {
|
||||||
id: 1,
|
|
||||||
type: Math.floor(Math.random() * 2) === 0 ? CategoryType.INCOME : CategoryType.EXPENSE,
|
let response = await api.get(`/spaces/${spaceId}/categories/${categoryId}`)
|
||||||
name: `Category ${1}`,
|
return response.data
|
||||||
description: `Description of Category ${1}`,
|
} catch (error) {
|
||||||
icon: "😇",
|
console.error(error);
|
||||||
createdBy: {
|
throw error;
|
||||||
id: 1,
|
}
|
||||||
username: `username_${1}`,
|
|
||||||
firstName: `firstName ${1}`,
|
|
||||||
} as User,
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as Category
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createCategory(spaceId: number, category: CreateCategoryDTO): Promise<number> {
|
async function createCategory(spaceId: number, category: CreateCategoryDTO): Promise<number> {
|
||||||
return 1
|
try {
|
||||||
|
return await api.post(`/spaces/${spaceId}/categories`, category)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateCategory(spaceId: number, category: UpdateCategoryDTO): Promise<void> {
|
async function updateCategory(spaceId: number, categoryId: number, category: UpdateCategoryDTO): Promise<void> {
|
||||||
|
try {
|
||||||
|
await api.put(`/spaces/${spaceId}/categories/${categoryId}`, category)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCategory(spaceId: number, categoryId: number): Promise<void> {
|
async function deleteCategory(spaceId: number, categoryId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await api.delete(`/spaces/${spaceId}/categories/${categoryId}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const categoriesService = {
|
export const categoriesService = {
|
||||||
|
|||||||
@@ -1,57 +1,56 @@
|
|||||||
import {User} from "@/models/user";
|
|
||||||
import {
|
import {
|
||||||
CreateRecurrentOperationDTO,
|
CreateRecurrentOperationDTO,
|
||||||
RecurrentOperation,
|
RecurrentOperation,
|
||||||
UpdateRecurrentOperationDTO
|
UpdateRecurrentOperationDTO
|
||||||
} from "@/models/recurrent-operation";
|
} from "@/models/recurrent-operation";
|
||||||
import {categoriesService} from "@/services/categories-service";
|
import api from "@/network/axiosSetup";
|
||||||
|
|
||||||
async function fetchRecurrents(spaceId: number): Promise<RecurrentOperation[]> {
|
async function fetchRecurrents(spaceId: number): Promise<RecurrentOperation[]> {
|
||||||
let recurrents: RecurrentOperation[] = []
|
try {
|
||||||
for (let i = 0; i < 10; i++) {
|
let response = await api.get(`/spaces/${spaceId}/recurrents`)
|
||||||
recurrents.push({
|
return response.data;
|
||||||
id: i,
|
} catch (error) {
|
||||||
category: await categoriesService.fetchCategory(1, 1),
|
console.error(error);
|
||||||
name: `Recurrent ${i}`,
|
throw error;
|
||||||
amount: Math.floor(Math.random() * 1000000),
|
|
||||||
date: Math.floor(Math.random() * 31),
|
|
||||||
createdBy: {
|
|
||||||
id: 1,
|
|
||||||
username: `username_${i}`,
|
|
||||||
firstName: `firstName ${i}`,
|
|
||||||
} as User,
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as RecurrentOperation)
|
|
||||||
}
|
}
|
||||||
return recurrents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRecurrent(spaceId: number, categoryId: number): Promise<RecurrentOperation> {
|
async function fetchRecurrent(spaceId: number, categoryId: number): Promise<RecurrentOperation> {
|
||||||
return {
|
try {
|
||||||
id: 1,
|
let response = await api.get(`/spaces/${spaceId}/recurrents/${categoryId}`)
|
||||||
category: await categoriesService.fetchCategory(1, 1),
|
return response.data;
|
||||||
name: `Recurrent ${1}`,
|
} catch (error) {
|
||||||
amount: Math.floor(Math.random() * 1000000),
|
console.error(error);
|
||||||
date: Math.floor(Math.random() * 31),
|
throw error;
|
||||||
createdBy: {
|
}
|
||||||
id: 1,
|
|
||||||
username: `username_${1}`,
|
|
||||||
firstName: `firstName ${1}`,
|
|
||||||
} as User,
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as RecurrentOperation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createRecurrent(spaceId:number, recurrent: CreateRecurrentOperationDTO): Promise<number> {
|
async function createRecurrent(spaceId: number, recurrent: CreateRecurrentOperationDTO): Promise<number> {
|
||||||
return 1
|
try {
|
||||||
|
let response = await api.post(`/spaces/${spaceId}/recurrents`, recurrent)
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateRecurrent(spaceId:number, recurrent: UpdateRecurrentOperationDTO): Promise<number> {
|
async function updateRecurrent(spaceId: number, recurrentId: number, recurrent: UpdateRecurrentOperationDTO): Promise<void> {
|
||||||
return 1
|
try {
|
||||||
|
await api.put(`/spaces/${spaceId}/recurrents/${recurrentId}`, recurrent)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteRecurrent(spaceId: number,recurrentId: number): Promise<void> {
|
async function deleteRecurrent(spaceId: number, recurrentId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await api.delete(`/spaces/${spaceId}/recurrents/${recurrentId}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {Space} from "@/models/space";
|
import type {Space} from "@/models/space";
|
||||||
import api from "@/network/axiosSetup"
|
|
||||||
import {User} from "@/models/user";
|
import {User} from "@/models/user";
|
||||||
|
import {SettingType} from "@/models/enums";
|
||||||
|
import api from "@/network/axiosSetup";
|
||||||
|
|
||||||
// async function getSpaces(): Promise<Space[]> {
|
// async function getSpaces(): Promise<Space[]> {
|
||||||
// const response = await api.get("/api/spaces");
|
// const response = await api.get("/api/spaces");
|
||||||
@@ -9,58 +10,81 @@ import {User} from "@/models/user";
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
async function fetchSpaces(): Promise<Space[]> {
|
async function fetchSpaces(): Promise<Space[]> {
|
||||||
let spaces: Space[] = [];
|
try {
|
||||||
for (let i = 0; i < 10; i++) {
|
let response = await api.get('/spaces');
|
||||||
spaces.push(
|
return response.data;
|
||||||
{
|
} catch (error) {
|
||||||
id: i,
|
console.error(error);
|
||||||
name: `Space ${i}`,
|
throw error;
|
||||||
owner: {
|
|
||||||
id: i,
|
|
||||||
username: `user_name_${i}`,
|
|
||||||
firstName: `user_name_${i}`,
|
|
||||||
} as User,
|
|
||||||
participants: [{
|
|
||||||
id: i,
|
|
||||||
username: `user_name_${i}`,
|
|
||||||
firstName: `user_name_${i}`,
|
|
||||||
} as User],
|
|
||||||
createdBy: {
|
|
||||||
id: i,
|
|
||||||
username: `user_name_${i}`,
|
|
||||||
firstName: `user_name_${i}`,
|
|
||||||
} as User,
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as Space
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return spaces;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSpace(spaceId: number): Promise<Space> {
|
async function fetchSpace(spaceId: number): Promise<Space> {
|
||||||
return {
|
try {
|
||||||
id: spaceId,
|
let response = await api.get(`/spaces/${spaceId}`);
|
||||||
name: `Space ${spaceId}`,
|
return response.data;
|
||||||
owner: {
|
} catch (error) {
|
||||||
id: spaceId,
|
console.error(error);
|
||||||
username: `user_name_${spaceId}`,
|
throw error;
|
||||||
firstName: `user_name_${spaceId}`,
|
}
|
||||||
} as User,
|
}
|
||||||
participants: [{
|
|
||||||
id: spaceId,
|
async function getSettings(): Promise<Record<string, string>[]> {
|
||||||
username: `user_name_${spaceId}`,
|
return [
|
||||||
firstName: `user_name_${spaceId}`,
|
{
|
||||||
} as User],
|
key: 'period-start',
|
||||||
createdBy: {
|
value: '15',
|
||||||
id: spaceId,
|
},
|
||||||
username: `user_name_${spaceId}`,
|
{
|
||||||
firstName: `user_name_${spaceId}`,
|
key: 'sub-period-start',
|
||||||
} as User,
|
value: '25',
|
||||||
createdAt: new Date(),
|
},
|
||||||
} as Space
|
{
|
||||||
|
key: 'notifications-enabled',
|
||||||
|
value: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notifications-days',
|
||||||
|
value: 'Mon,Tue,Thu,Fri',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notifications-time',
|
||||||
|
value: '19:30',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSetting(key: string): Promise<Record<string, string>> {
|
||||||
|
let settings = [
|
||||||
|
{
|
||||||
|
key: SettingType.PERIOD_START,
|
||||||
|
value: '10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SettingType.SUB_PERIOD_START,
|
||||||
|
value: '25',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SettingType.NOTIFICATIONS_ENABLED,
|
||||||
|
value: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SettingType.NOTIFICATIONS_DAYS,
|
||||||
|
value: 'Mon,Tue,Thu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SettingType.NOTIFICATIONS_TIME,
|
||||||
|
value: '19:30',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return settings.find((i) => i.key == key) ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setSetting(key: SettingType, value: any): Promise<void> {
|
||||||
|
console.debug(`Setting ${key} is ${JSON.stringify(value)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const spaceService = {
|
export const spaceService = {
|
||||||
fetchSpaces, fetchSpace
|
fetchSpaces, fetchSpace, getSetting, getSettings, setSetting
|
||||||
};
|
};
|
||||||
68
src/services/transactions-service.ts
Normal file
68
src/services/transactions-service.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {CreateTransactionDTO, Transaction, UpdateTransactionDTO} from "@/models/transaction";
|
||||||
|
import {TransactionKind, TransactionType} from "@/models/enums";
|
||||||
|
import {categoriesService} from "@/services/categories-service";
|
||||||
|
import {User} from "@/models/user";
|
||||||
|
import api from "@/network/axiosSetup";
|
||||||
|
|
||||||
|
// const spaceStore = useSpaceStore();
|
||||||
|
function toDateOnly(d: Date): string {
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getTransactions(spaceId: number): Promise<Transaction[]> {
|
||||||
|
try {
|
||||||
|
let response = await api.get(`/spaces/${spaceId}/transactions`);
|
||||||
|
return response.data;
|
||||||
|
}catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTransaction(spaceId: number, id: number): Promise<Transaction> {
|
||||||
|
try {
|
||||||
|
let response = await api.get(`/spaces/${spaceId}/transactions/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTransaction(spaceId: number, tx: CreateTransactionDTO): Promise<number> {
|
||||||
|
try {
|
||||||
|
tx.date = toDateOnly(tx.date)
|
||||||
|
return await api.post(`/spaces/${spaceId}/transactions`, tx)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTransaction(spaceId: number, txId: number, tx: UpdateTransactionDTO): Promise<void> {
|
||||||
|
try {
|
||||||
|
tx.date = toDateOnly(tx.date)
|
||||||
|
return await api.put(`/spaces/${spaceId}/transactions/${txId}`, tx)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTransaction(spaceId: number, txId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
return await api.delete(`/spaces/${spaceId}/transactions/${txId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransactionService = {
|
||||||
|
getTransactions, getTransaction, createTransaction, updateTransaction, deleteTransaction,
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {ref, watch} from "vue";
|
import {ref} from "vue";
|
||||||
|
|
||||||
import {spaceService} from "@/services/space-service";
|
import {spaceService} from "@/services/space-service";
|
||||||
import {Space} from "@/models/space";
|
import {Space} from "@/models/space";
|
||||||
@@ -10,6 +10,7 @@ export const useSpaceStore = defineStore('space', () => {
|
|||||||
await spaceService.fetchSpaces().then((res) => {
|
await spaceService.fetchSpaces().then((res) => {
|
||||||
spaces.value = res;
|
spaces.value = res;
|
||||||
const spaceId = localStorage.getItem("spaceId");
|
const spaceId = localStorage.getItem("spaceId");
|
||||||
|
console.log(spaces.value)
|
||||||
const selectedSpace = spaces.value.find((s: Space) => s.id.toString() === spaceId) || null;
|
const selectedSpace = spaces.value.find((s: Space) => s.id.toString() === spaceId) || null;
|
||||||
selectedSpaceId.value = selectedSpace?.id;
|
selectedSpaceId.value = selectedSpace?.id;
|
||||||
selectedSpaceName.value = selectedSpace?.name;
|
selectedSpaceName.value = selectedSpace?.name;
|
||||||
@@ -26,8 +27,7 @@ export const useSpaceStore = defineStore('space', () => {
|
|||||||
if (!spaceId) {
|
if (!spaceId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return await spaceService.fetchSpace(spaceId);
|
||||||
return spaceService.fetchSpace(spaceId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setSpace = (newSpaceId: number, newSpaceName: string) => {
|
const setSpace = (newSpaceId: number, newSpaceName: string) => {
|
||||||
@@ -35,12 +35,15 @@ export const useSpaceStore = defineStore('space', () => {
|
|||||||
selectedSpaceId.value = newSpaceId;
|
selectedSpaceId.value = newSpaceId;
|
||||||
selectedSpaceName.value = newSpaceName;
|
selectedSpaceName.value = newSpaceName;
|
||||||
localStorage.setItem("spaceId", newSpaceId.toString());
|
localStorage.setItem("spaceId", newSpaceId.toString());
|
||||||
|
localStorage.setItem("spaceName", newSpaceName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return {spaces, getSpaces, getSpace, selectedSpaceId, selectedSpaceName, setSpace};
|
return {spaces, getSpaces, getSpace, selectedSpaceId, selectedSpaceName, setSpace};
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { computed, reactive, ref } from "vue";
|
import {computed, reactive, ref, watch} from "vue";
|
||||||
import { useSpaceStore } from "@/stores/spaceStore";
|
import { useSpaceStore } from "@/stores/spaceStore";
|
||||||
|
|
||||||
export interface ToolbarButton {
|
export interface ToolbarButton {
|
||||||
@@ -16,10 +16,41 @@ export interface ToolbarButton {
|
|||||||
export type ToolbarConfig =
|
export type ToolbarConfig =
|
||||||
| ToolbarButton[]
|
| ToolbarButton[]
|
||||||
| ((ctx: { spaceStore: ReturnType<typeof useSpaceStore> }) => ToolbarButton[]);
|
| ((ctx: { spaceStore: ReturnType<typeof useSpaceStore> }) => ToolbarButton[]);
|
||||||
|
|
||||||
export const useToolbarStore = defineStore("toolbar", () => {
|
export const useToolbarStore = defineStore("toolbar", () => {
|
||||||
const spaceStore = useSpaceStore();
|
const spaceStore = useSpaceStore();
|
||||||
|
|
||||||
|
// === храним КОНФИГ, а не готовый массив
|
||||||
|
const _config = ref<ToolbarConfig | undefined>(undefined);
|
||||||
|
|
||||||
|
const setByConfig = (config?: ToolbarConfig) => {
|
||||||
|
_config.value = config;
|
||||||
|
};
|
||||||
|
const clear = () => void (_config.value = undefined);
|
||||||
|
|
||||||
|
// дефолты — уже реактивны
|
||||||
|
const defaults = computed<ToolbarButton[]>(() => [
|
||||||
|
{
|
||||||
|
id: "space",
|
||||||
|
text: spaceStore.selectedSpaceName ?? "Select Space",
|
||||||
|
icon: "",
|
||||||
|
onClickId: "openSpacePicker",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// текущие — вычисляем МГНОВЕННО из функции-конфига
|
||||||
|
const current = computed<ToolbarButton[]>(() => {
|
||||||
|
const extra =
|
||||||
|
typeof _config.value === "function"
|
||||||
|
? _config.value({ spaceStore }) // выполняется на каждый рендер
|
||||||
|
: _config.value ?? [];
|
||||||
|
|
||||||
|
// требуйте у всех кнопок стабильный id; ИНАЧЕ НЕ ИСПОЛЬЗУЙТЕ randomUUID в ключах!
|
||||||
|
const map = new Map<string, ToolbarButton>();
|
||||||
|
for (const b of defaults.value) map.set(b.id ?? b.text ?? "", b);
|
||||||
|
for (const b of extra) map.set(b.id ?? b.text ?? "", b);
|
||||||
|
return Array.from(map.values());
|
||||||
|
});
|
||||||
|
|
||||||
// обработчики
|
// обработчики
|
||||||
const handlers: Record<string, () => void> = reactive({});
|
const handlers: Record<string, () => void> = reactive({});
|
||||||
const registerHandler = (key: string, fn: () => void) => (handlers[key] = fn);
|
const registerHandler = (key: string, fn: () => void) => (handlers[key] = fn);
|
||||||
@@ -30,41 +61,12 @@ export const useToolbarStore = defineStore("toolbar", () => {
|
|||||||
if (typeof fn === "function") fn();
|
if (typeof fn === "function") fn();
|
||||||
};
|
};
|
||||||
|
|
||||||
// текущие кнопки (сделал ref — теперь реактивно)
|
|
||||||
const _current = ref<ToolbarButton[]>([]);
|
|
||||||
const set = (items: ToolbarButton[]) => void (_current.value = items);
|
|
||||||
const clear = () => void (_current.value = []);
|
|
||||||
|
|
||||||
const setByConfig = (config?: ToolbarConfig) => {
|
|
||||||
if (!config) return clear();
|
|
||||||
_current.value =
|
|
||||||
typeof config === "function" ? config({ spaceStore }) : config;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaults = computed<ToolbarButton[]>(() => [
|
|
||||||
{
|
|
||||||
id: 'space',
|
|
||||||
text: spaceStore.selectedSpaceName ?? 'Select Space',
|
|
||||||
icon: '',
|
|
||||||
onClickId: 'openSpacePicker',
|
|
||||||
},
|
|
||||||
|
|
||||||
]);
|
|
||||||
|
|
||||||
const current = computed<ToolbarButton[]>(() => {
|
|
||||||
const map = new Map<string, ToolbarButton>();
|
|
||||||
for (const b of defaults.value) map.set(b.id ?? crypto.randomUUID(), b);
|
|
||||||
for (const b of _current.value) map.set(b.id ?? crypto.randomUUID(), b);
|
|
||||||
return Array.from(map.values());
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
current,
|
current,
|
||||||
|
setByConfig,
|
||||||
|
clear,
|
||||||
registerHandler,
|
registerHandler,
|
||||||
unregisterHandler,
|
unregisterHandler,
|
||||||
invoke,
|
invoke,
|
||||||
set,
|
|
||||||
clear,
|
|
||||||
setByConfig,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
31
src/stores/transactions-store.ts
Normal file
31
src/stores/transactions-store.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {defineStore} from "pinia";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {Transaction} from "@/models/transaction";
|
||||||
|
import {TransactionService} from "@/services/transactions-service";
|
||||||
|
|
||||||
|
const transactionsService = TransactionService
|
||||||
|
|
||||||
|
export const useTransactionStore = defineStore('transactions', () => {
|
||||||
|
const transactions = ref<Transaction[]>([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const fetchTransactions = async (spaceId: number) => {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
transactions.value = await transactionsService.getTransactions(spaceId)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addTransaction = (transaction: Transaction) => {
|
||||||
|
transactions.value.push(transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactions,
|
||||||
|
isLoading,
|
||||||
|
fetchTransactions,
|
||||||
|
addTransaction
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {ref} from 'vue';
|
import {computed, ref} from 'vue';
|
||||||
import apiClient from "@/network/axiosSetup";
|
import apiClient from "@/network/axiosSetup";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {useToast} from "primevue/usetoast";
|
import {useToast} from "primevue/usetoast";
|
||||||
@@ -11,6 +11,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const loadingUser = ref(true);
|
const loadingUser = ref(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const isAuthorized = ref(false);
|
||||||
|
|
||||||
async function fetchUserProfile() {
|
async function fetchUserProfile() {
|
||||||
// Убираем проверку на `loadingUser`, чтобы не блокировать запрос
|
// Убираем проверку на `loadingUser`, чтобы не блокировать запрос
|
||||||
@@ -18,7 +19,10 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
loadingUser.value = true;
|
loadingUser.value = true;
|
||||||
try {
|
try {
|
||||||
await apiClient.get('/auth/me')
|
await apiClient.get('/auth/me')
|
||||||
.then((res: any) => user.value = res.data)
|
.then((res: any) => {
|
||||||
|
user.value = res.data
|
||||||
|
isAuthorized.value = true
|
||||||
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
throw err
|
throw err
|
||||||
@@ -77,5 +81,5 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {user, loadingUser, fetchUserProfile, login, register};
|
return {user, loadingUser, fetchUserProfile, login, register, isAuthorized};
|
||||||
});
|
});
|
||||||
|
|||||||
91
src/utils/utils.ts
Normal file
91
src/utils/utils.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
export const formatAmount = (amount: number) => {
|
||||||
|
return new Intl.NumberFormat('ru-RU').format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDate = (date) => {
|
||||||
|
const validDate = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
// Проверяем, является ли validDate корректной датой
|
||||||
|
if (isNaN(validDate.getTime())) {
|
||||||
|
return 'Invalid Date'; // Если дата неверная, возвращаем текст ошибки
|
||||||
|
}
|
||||||
|
|
||||||
|
return validDate.toLocaleDateString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
return 'Январь'
|
||||||
|
case 1:
|
||||||
|
return 'Февраль'
|
||||||
|
case 2:
|
||||||
|
return 'Март'
|
||||||
|
case 3:
|
||||||
|
return 'Апрель'
|
||||||
|
case 4:
|
||||||
|
return 'Май'
|
||||||
|
case 5:
|
||||||
|
return 'Июнь'
|
||||||
|
case 6:
|
||||||
|
return 'Июль'
|
||||||
|
case 7:
|
||||||
|
return 'Август'
|
||||||
|
case 8:
|
||||||
|
return 'Сентябрь'
|
||||||
|
case 9:
|
||||||
|
return 'Октябрь'
|
||||||
|
case 10:
|
||||||
|
return 'Ноябрь'
|
||||||
|
case 11:
|
||||||
|
return 'Декабрь'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMonthName2 = (monthIndex: number, caseName: "имен" | "род" | "дат" | "вин" | "твор" | "пред") => {
|
||||||
|
const months = {
|
||||||
|
имен: ["январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"],
|
||||||
|
род: ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"],
|
||||||
|
дат: ["январю", "февралю", "марту", "апрелю", "маю", "июню", "июлю", "августу", "сентябрю", "октябрю", "ноябрю", "декабрю"],
|
||||||
|
вин: ["январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"],
|
||||||
|
твор: ["январём", "февралём", "мартом", "апрелем", "маем", "июнем", "июлем", "августом", "сентябрем", "октябрем", "ноябрём", "декабрём"],
|
||||||
|
пред: ["январе", "феврале", "марте", "апреле", "мае", "июне", "июле", "августе", "сентябре", "октябре", "ноябре", "декабре"]
|
||||||
|
};
|
||||||
|
|
||||||
|
return months[caseName][monthIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateRandomColors = () => {
|
||||||
|
|
||||||
|
const r = Math.floor(Math.random() * 256);
|
||||||
|
const g = Math.floor(Math.random() * 256);
|
||||||
|
const b = Math.floor(Math.random() * 256);
|
||||||
|
const a = 0.5; // Прозрачность от 0.00 до 1.00
|
||||||
|
|
||||||
|
return [r, g, b]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRandomColor = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user