add network

This commit is contained in:
xds
2025-10-31 15:22:44 +03:00
parent 6ab7a490c9
commit 5b56eb17fd
33 changed files with 1435 additions and 342 deletions

View File

@@ -10,10 +10,10 @@ const handleConfirm = (result: boolean) => {
</script>
<template>
<div class="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 fixed inset-0 z-[1000] flex items-center justify-center p-4 bg-black/50 !bg-opacity-10!">
<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">
{{ message }}
</span>

View File

@@ -6,23 +6,20 @@ import {Divider} from "primevue";
const toolbar = useToolbarStore()
const keyOf = (b: { id?: string; text?: string }) => b.id ?? b.text ?? crypto.randomUUID()
</script>
<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
v-for="btnKey in toolbar.current.keys()"
:is="toolbar.current[btnKey].to ? RouterLink : 'button'"
:key="btnKey"
class="flex flex-row gap-2 items-center "
:title="toolbar.current[btnKey].title || toolbar.current[btnKey].text"
v-bind="toolbar.current[btnKey].to ? { to: toolbar.current[btnKey].to } : { type: 'button', disabled: toolbar.current[btnKey].disabled }"
@click="!toolbar.current[btnKey].to && toolbar.invoke(toolbar.current[btnKey].onClickId)"
v-for="(btn, idx) in toolbar.current"
:key="btn.id || btn.text || idx"
:is="btn.to ? RouterLink : 'button'"
class="flex flex-row gap-2 items-center"
:title="btn.title || btn.text"
v-bind="btn.to ? { to: btn.to } : { type: 'button', disabled: btn.disabled }"
@click="!btn.to && toolbar.invoke(btn.onClickId)"
>
<i v-if="toolbar.current[btnKey].icon" :class="toolbar.current[btnKey].icon" class="!p-2"/>
<span v-if="toolbar.current[btnKey].text">{{ toolbar.current[btnKey].text }}</span>
<Divider v-if="btnKey+1 != toolbar.current.length" class="!m-0" layout="vertical"/>
<i v-if="btn.icon" :class="btn.icon" class="!p-2" />
<span v-if="btn.text">{{ btn.text }}</span>
<Divider v-if="idx + 1 !== toolbar.current.length" class="!m-0" layout="vertical" />
</component>
</nav>
</template>

View File

@@ -1,36 +1,70 @@
<script setup lang="ts">
import { onMounted, ref, computed } from "vue"
import {onMounted, ref} from "vue"
import {useRouter} from "vue-router";
import {InputText, Password, Button} from "primevue";
import {useUserStore} from "@/stores/userStore";
const router = useRouter()
// необязательно делать реактивным, но удобно для шаблона
const tgApp = ref(window.Telegram.WebApp)
const tgData = ref()
const errors = ref({username: '', password: ''})
const loading = ref(false)
// какие-то удобные вычисления для UI
const userId = computed(() => tgApp.value?.user?.id?.toString() ?? "")
const username = computed(() => tgApp.value?.user?.username ?? "")
const firstName = computed(() => tgData.value?.user?.first_name ?? "")
const username = ref<string>('')
const password = ref<string>('')
const userStore = useUserStore()
onMounted(() => {
// сообщаем Telegram WebApp, что UI готов
if (tgApp.initData){
if (tgApp.initData) {
tgData.value = tgApp.initDataUnsafe
if (tgData.value.user?.id != null) {
localStorage.setItem("token", tgData.value.user.id.toString())
router.push("/")
}
}else {
localStorage.setItem("token", "123")
router.push("/")
} else {
}
// router.push('/')
})
</script>
<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>
<style scoped>

View File

@@ -3,7 +3,7 @@
</script>
<template>
<div class="card">
<div class="flex card">
Not implemented.
</div>
</template>

View File

@@ -8,6 +8,7 @@ import {Category} from "@/models/category";
import {useToolbarStore} from "@/stores/toolbar-store";
import {useRouter} from "vue-router";
import {useCategoriesStore} from "@/stores/categories-store";
import {CategoryType, CategoryTypeName} from "@/models/enums";
const toast = useToast()
const spaceStore = useSpaceStore()
@@ -20,7 +21,7 @@ const categories = ref<Category[]>([])
const fetchData = async () => {
try {
if (spaceStore.selectedSpaceId !== null) {
if (spaceStore.selectedSpaceId !== undefined) {
let spaceId = spaceStore.selectedSpaceId!!
await categoryStore.fetchCategories(spaceId)
categories.value = categoryStore.categories
@@ -50,21 +51,56 @@ onMounted(async () => {
<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">
<div v-for="key in categories.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-row w-full items-center justify-between">
<div class="flex-row items-center gap-2 ">
<span class="text-3xl"> {{ categories[key].icon }}</span>
<div class="flex-col !font-bold "> {{ categories[key].name }}
<div class="flex flex-row text-sm">{{ categories[key].description }}</div>
<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
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.INCOME).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>
</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>
<i class="pi pi-angle-right !font-extralight"/>
</div>
<Divider v-if="key+1 !== categories.length" class="!m-0 !py-3"/>
</div>
</div>
</template>

View File

@@ -178,7 +178,7 @@ onMounted(async () => {
// await categoriesStore.fetchCategories(spaceStore.selectedSpaceId)
try {
let updateDTO = buildUpdate()
await categoriesService.updateCategory(spaceStore.selectedSpaceId, updateDTO)
await categoriesService.updateCategory(spaceStore.selectedSpaceId, Number(categoryId.value), updateDTO)
console.log(updateDTO)
await moveUser()
} catch (e) {
@@ -224,7 +224,7 @@ onMounted(async () => {
:callback="(confirmed: boolean) => { if (confirmed) deleteCategory(); isDeleteAlertVisible = false; }"
/>
<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
class="
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>
</div>
<div class="w-full !items-center !justify-center">
<div class="flex w-full !items-center !justify-center">
<SelectButton
v-model="categoryType"
:options="options"
optionLabel="label"
optionValue="value"
class="!w-full !items-center !justify-center "
class="!w-full !items-center !justify-center !border-none "
/>
<span v-if="isCategoryTypeError"
class="text-sm !text-red-500 font-extralight">Category type 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 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="categoryName.length !== 0 ? isCategoryNameError = false : true"/>
</div>
<span v-if="isCategoryNameError" class="text-sm !text-red-500 font-extralight">Name cannot be empty</span>
</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>
<div class="card !justify-start !items-start !pl-2">
<div class="flex card !justify-start !items-start !pl-2">
<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"
style="box-shadow: none !important;"

View File

@@ -3,7 +3,7 @@
</script>
<template>
<div class="card">
<div class="flex card">
Not implemented
</div>
</template>

View File

@@ -49,23 +49,24 @@ onMounted(async () => {
</script>
<template>
<div class="!pb-10">
<div class="card">
<div v-for="key in recurrents.keys()" :key="recurrents[key].id"
<div class="flex !pb-10">
<div class="flex card">
<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`)"
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="w-full flex items-center justify-between">
<div class="flex-row items-center gap-2 ">
<div class="flex flex-row gap-2 w-full items-center justify-between">
<div class="flex w-full flex items-center justify-between">
<div class="flex flex-row items-center gap-2 ">
<span class="text-4xl">{{ recurrents[key].category.icon }}</span>
<div class="flex-col items-start">
<div class="flex-row !font-bold "> {{ recurrents[key].name }}</div>
<div class="flex flex-col items-start">
<div class="flex flex-row !font-bold "> {{ recurrents[key].name }}</div>
<div class="flex flex-row text-sm">{{ recurrents[key].category.name }}</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-sm">каждое {{ recurrents[key].date }} число </span>
</div>

View File

@@ -170,7 +170,7 @@ const isDeleteAlertVisible = ref(false)
const deleteAlertMessage = ref('Do you want to delete recurrent?')
const deleteRecurrent = async () => {
await recurrentsService.deleteRecurrent(spaceStore.selectedSpaceId, recurrentId.value)
await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
// await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
if (window.history.length > 1) {
router.back()
} else {
@@ -208,8 +208,8 @@ onMounted(async () => {
toolbar.registerHandler('updateRecurrent', async () => {
if (spaceStore.selectedSpaceId) {
try {
await recurrentsService.updateRecurrent(spaceStore.selectedSpaceId, buildUpdate())
await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
await recurrentsService.updateRecurrent(spaceStore.selectedSpaceId, Number(recurrentId.value), buildUpdate())
// await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
router.back()
} catch (error) {
toast.add({
@@ -226,7 +226,7 @@ onMounted(async () => {
toolbar.registerHandler('createRecurrent', async () => {
if (spaceStore.selectedSpaceId) {
await recurrentsService.createRecurrent(spaceStore.selectedSpaceId, buildCreate())
await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
// await recurrentsStore.fetchRecurrents(spaceStore.selectedSpaceId)
router.back()
}
})
@@ -235,7 +235,6 @@ onMounted(async () => {
</script>
<template>
<div v-if="categories.length===0" class="card !gap-4 !p-10">
<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>
@@ -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"
style="background-color: var(--primary-color); "
:style="tgApp ? `padding-top: ${insetTop}px !important` : 'padding-top: 2rem !important'">
<div class="w-full max-w-md">
<div class="card justify-items-start justify-start">
<div class="flex w-full max-w-md">
<div class="flex card justify-items-start justify-start">
<div v-for="(cat, idx) in categories" :key="cat.id"
@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">
<div class="flex-row w-full items-center justify-between py-3">
<div class="flex-row items-center gap-2">
<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-col justify-between">
<div class="flex-row"> {{ cat.name }}</div>
<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>
@@ -272,7 +272,7 @@ onMounted(async () => {
</div>
<div class="flex flex-col w-full ">
<div class="flex-col w-full">
<div class="flex flex-col w-full">
<InputNumber
v-model="recurrentAmount"
@input=" isAmountError = false"
@@ -294,12 +294,12 @@ onMounted(async () => {
</div>
<div class="flex flex-col w-full justify-items-start">
<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;">
<div class="flex-row w-full gap-2 items-center justify-between">
<div class="flex-row gap-2 items-center">
<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 ">{{ recurrentCategory.icon }}</span>
<div class="flex-col ">
<div class="flex flex-col ">
<span class=" !">{{ recurrentCategory.name }}
</span>
</div>
@@ -311,7 +311,7 @@ onMounted(async () => {
</div>
<div class="flex flex-col w-full justify-items-start">
<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="recurrentName?.length ==0 ? isNameError = true : isNameError=false" v-model="recurrentName"/>
</div>
@@ -320,8 +320,8 @@ onMounted(async () => {
</div>
<div class="flex flex-col w-full justify-items-start">
<label class="!font-semibold text-gray-600 !pl-2">Recurrent date</label>
<div class="card !justify-start !items-start !pl-2">
<div class="!grid !grid-cols-7 gap-2">
<div class="flex card !justify-start !items-start !pl-2">
<div class="flex !grid !grid-cols-7 gap-2">
<div v-for="i in 31"
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'"

View File

@@ -3,16 +3,17 @@ import {Divider} from "primevue";
const items = [
{name: "Space settings", link: '/space-settings'},
{name: "Notification settings", link: '/notification-settings'},
// {name: "Notification settings", link: '/notification-settings'},
{name: "Categories", link: '/categories'},
{name: "Recurrent Operations", link: '/recurrents'},
]
</script>
<template>
<div class="overflow-y-auto">
<div class="card">
<router-link :to="items[item].link" v-for="item in items.keys()"
<div class="flex overflow-y-auto">
<div class="flex card">
<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">
<div class="flex flex-row justify-between items-center w-full pe-2 p-2">

View File

@@ -1,10 +1,149 @@
<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>
<template>
<div class="card">
Not implemented
<div class="flex !pb-4 !w-full">
<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>
</template>

View File

@@ -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>
<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'">
<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 ">{{
space.owner.firstName.substring(0, 1).toUpperCase()
}}</span>

11
src/components/test.vue Normal file
View 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>

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

View File

@@ -1,11 +1,128 @@
<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>
<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>
<style scoped>
</style>