init
This commit is contained in:
28
src/components/Toolbar.vue
Normal file
28
src/components/Toolbar.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import {useToolbarStore} from '@/stores/toolbar-store'
|
||||
import {RouterLink} from 'vue-router'
|
||||
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 !text-xl sticky top-10 justify-items-end 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)"
|
||||
>
|
||||
<i v-if="toolbar.current[btnKey].icon" :class="toolbar.current[btnKey].icon" style="font-size: 1.1rem" 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"/>
|
||||
</component>
|
||||
</nav>
|
||||
|
||||
|
||||
</template>
|
||||
13
src/components/dashboard/DashboardView.vue
Normal file
13
src/components/dashboard/DashboardView.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
Not implemented.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
71
src/components/settings/CategoriesList.vue
Normal file
71
src/components/settings/CategoriesList.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import {Divider} from "primevue";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {Category} from "@/models/category";
|
||||
import {categoriesService} from "@/services/categories-service";
|
||||
import {useToolbarStore} from "@/stores/toolbar-store";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const toast = useToast()
|
||||
const spaceStore = useSpaceStore()
|
||||
const toolbar = useToolbarStore()
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
|
||||
const categories = ref<Category[]>([])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
if (spaceStore.selectedSpaceId !== null) {
|
||||
let spaceId = spaceStore.selectedSpaceId!!
|
||||
categories.value = await categoriesService.fetchCategories(spaceId)
|
||||
}
|
||||
} catch (error: Error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Failed to fetch categories.',
|
||||
detail: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toCreation = () => {
|
||||
router.push(`/categories/create`)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
toolbar.registerHandler('createCategory', () => {
|
||||
console.log("create cateogiry")
|
||||
toCreation()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
|
||||
<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 text-xl">
|
||||
<div class="flex-row w-full items-center justify-between">
|
||||
<div class="flex-col items-start">
|
||||
<div class="flex-row"> {{ categories[key].icon }} {{ categories[key].name }}</div>
|
||||
<div class="flex flex-row text-sm">{{ categories[key].description }}</div>
|
||||
</div>
|
||||
<i class="pi pi-angle-right !text-xl !font-extralight"/>
|
||||
</div>
|
||||
<Divider v-if="key+1 !== categories.length" class="!m-0 !py-3"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
150
src/components/settings/CategoryCreateUpdate.vue
Normal file
150
src/components/settings/CategoryCreateUpdate.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import {useRoute} from "vue-router";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {useToolbarStore} from "@/stores/toolbar-store";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import {SelectButton} from "primevue";
|
||||
import {categoriesService} from "@/services/categories-service";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
import {CategoryType, CategoryTypeName} from "@/models/enums";
|
||||
import emojiRegex from 'emoji-regex'
|
||||
|
||||
const route = useRoute()
|
||||
const toolbar = useToolbarStore();
|
||||
const toast = useToast();
|
||||
const spaceStore = useSpaceStore();
|
||||
const categoryId = ref<string | undefined>(route.params.id)
|
||||
const mode = computed(() => {
|
||||
return categoryId.value ? "edit" : "create"
|
||||
})
|
||||
const categoryType = ref<CategoryType>(CategoryType.EXPENSE)
|
||||
const categoryName = ref<string>()
|
||||
const categoryIcon = ref<string>("🛍️")
|
||||
const categoryDescription = ref<string>()
|
||||
|
||||
|
||||
// Генерим опции: [{ label: "Расходы", value: "EXPENSE" }, ...]
|
||||
const options = Object.values(CategoryType).map(type => ({
|
||||
label: CategoryTypeName[type],
|
||||
value: type
|
||||
}))
|
||||
|
||||
const fetchCategory = async () => {
|
||||
try {
|
||||
console.log('here')
|
||||
if (spaceStore.selectedSpaceId && categoryId.value) {
|
||||
console.log('here2')
|
||||
let category = await categoriesService.fetchCategory(spaceStore.selectedSpaceId, Number(categoryId.value))
|
||||
categoryType.value = category.type
|
||||
categoryName.value = category.name
|
||||
categoryDescription.value = category.description
|
||||
categoryIcon.value = category.icon
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "Error while fetching category",
|
||||
detail: err.detail.message,
|
||||
life: 3000,
|
||||
})
|
||||
}
|
||||
}
|
||||
const re = emojiRegex()
|
||||
|
||||
function toOneEmoji(raw: string): string {
|
||||
const matches = raw.match(re) ?? [] // ← вместо [...raw.matchAll(re)]
|
||||
return matches.length ? matches[matches.length - 1] : '🛍️'
|
||||
}
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const el = e.target as HTMLInputElement
|
||||
const next = toOneEmoji(el.value)
|
||||
if (el.value !== next) {
|
||||
el.value = next // визуально заменить
|
||||
}
|
||||
categoryIcon.value = next // обновить v-model
|
||||
}
|
||||
|
||||
function handlePaste(e: ClipboardEvent) {
|
||||
const text = e.clipboardData?.getData('text') ?? ''
|
||||
const next = toOneEmoji(text)
|
||||
e.preventDefault()
|
||||
const target = e.target as HTMLInputElement
|
||||
target.value = next
|
||||
categoryIcon.value = next
|
||||
}
|
||||
|
||||
// для мобильных IME: окончание композиции
|
||||
function handleCompositionEnd(e: CompositionEvent) {
|
||||
handleInput(e as unknown as Event)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (mode.value === "edit") {
|
||||
await fetchCategory()
|
||||
toolbar.registerHandler('deleteCategory', () => {
|
||||
console.log("delete category")
|
||||
})
|
||||
toolbar.registerHandler('updateCategory', () => {
|
||||
console.log("update category")
|
||||
})
|
||||
} else {
|
||||
toolbar.registerHandler('createCategory', () => {
|
||||
console.log("create category")
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="flex flex-col w-full justify-items-start gap-7">
|
||||
|
||||
<div class="flex flex-col w-full ">
|
||||
<div class=" flex-col " v-tooltip.bottom="'Only emoji supported'">
|
||||
<input class=" !justify-items-center !justify-center font-extralight text-9xl w-full focus:outline-0"
|
||||
placeholder="Icon" v-model="categoryIcon" @input="handleInput" @paste="handlePaste"
|
||||
@compositionend="handleCompositionEnd" inputmode="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"/>
|
||||
<label class="!justify-items-center !justify-center !font-extralight text-gray-600 text-center">Category
|
||||
icon</label>
|
||||
</div>
|
||||
|
||||
<div class="w-full !justify-items-center !items-center !justify-center">
|
||||
<SelectButton
|
||||
v-model="categoryType"
|
||||
:options="options"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="!w-full !justify-items-center !items-center !justify-center "
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col w-full justify-items-start">
|
||||
<label class="!font-semibold text-2xl text-gray-600 pl-2">Category name</label>
|
||||
<div class="card !justify-start !items-start !p-4 !pl-5 ">
|
||||
<input class="font-extralight text-xl w-full focus:outline-0" placeholder="Name" v-model="categoryName"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full justify-items-start">
|
||||
<label class="!font-semibold text-2xl text-gray-600 !pl-2">Category description</label>
|
||||
<div class="card !justify-start !items-start !pl-2">
|
||||
<textarea
|
||||
class="font-extralight text-xl w-full focus:outline-0 !focus:border-0 !@focus:shadow-none !bg-white !border-0 min-h-36"
|
||||
style="box-shadow: none !important;"
|
||||
placeholder="Description" v-model="categoryDescription"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.p-togglebutton-label, .p-togglebutton {
|
||||
padding: 10rem !important;
|
||||
}
|
||||
</style>
|
||||
13
src/components/settings/NotificationSettings.vue
Normal file
13
src/components/settings/NotificationSettings.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
Not implemented
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
80
src/components/settings/RecurrentsList.vue
Normal file
80
src/components/settings/RecurrentsList.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
import {RecurrentOperation} from "@/models/recurrent-operation";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {recurrentsService} from "@/services/recurrents-service";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import {Divider} from "primevue";
|
||||
import {Category} from "@/models/category";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useToolbarStore} from "@/stores/toolbar-store";
|
||||
|
||||
const toolbar = useToolbarStore()
|
||||
const spaceStore = useSpaceStore();
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const categories = ref<Category[]>([])
|
||||
const recurrents = ref<RecurrentOperation[]>([])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
if (spaceStore.selectedSpaceId) {
|
||||
console.log('hereeee')
|
||||
let recurrentsResponse = await recurrentsService.fetchRecurrents(spaceStore.selectedSpaceId)
|
||||
recurrents.value = recurrentsResponse
|
||||
console.log(recurrentsResponse)
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Failed to fetch recurrents.',
|
||||
detail: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
toolbar.registerHandler('openRecurrentCreation', () => {
|
||||
router.push('/recurrents/create')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div v-for="key in recurrents.keys()" :key="recurrents[key].id"
|
||||
@click="router.push(`/recurrents/${recurrents[key].id}/edit`)"
|
||||
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold text-xl">
|
||||
<div class="flex-row w-full items-center justify-between">
|
||||
<div class="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"> {{ recurrents[key].name }}</div>
|
||||
<div class="flex flex-row text-sm">{{ recurrents[key].category.name }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="items-center flex-col">
|
||||
<span class="text-lg !font-semibold">{{recurrents[key].amount}}₽ </span>
|
||||
<span class="text-sm">каждое {{ recurrents[key].date }} число </span>
|
||||
</div>
|
||||
<i class="pi pi-angle-right !text-xl !font-extralight"/>
|
||||
</div>
|
||||
<Divider v-if="key+1 !== recurrents.length" class="!m-0 !py-3"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
196
src/components/settings/RecurrentyCreateUpdate.vue
Normal file
196
src/components/settings/RecurrentyCreateUpdate.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<script setup lang="ts">
|
||||
import {useRoute} from "vue-router";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {useToolbarStore} from "@/stores/toolbar-store";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import {Divider, InputNumber} from "primevue";
|
||||
import {categoriesService} from "@/services/categories-service";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
import {recurrentsService} from "@/services/recurrents-service";
|
||||
import {Category} from "@/models/category";
|
||||
|
||||
const route = useRoute()
|
||||
const toolbar = useToolbarStore();
|
||||
const toast = useToast();
|
||||
const spaceStore = useSpaceStore();
|
||||
|
||||
const isCategorySelectorOpened = ref(false);
|
||||
|
||||
const categories = ref<Category[]>([]);
|
||||
const recurrentId = ref<string | undefined>(route.params.id)
|
||||
const mode = computed(() => {
|
||||
return recurrentId.value ? "edit" : "create"
|
||||
})
|
||||
const recurrentCategory = ref<Category>({})
|
||||
const recurrentName = ref<string>()
|
||||
const recurrentAmount = ref<number>(0)
|
||||
const recurrentDate = ref<number>(Math.floor(Math.random() * 31))
|
||||
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
console.log('here')
|
||||
if (spaceStore.selectedSpaceId) {
|
||||
categories.value = await categoriesService.fetchCategories(spaceStore.selectedSpaceId)
|
||||
if (categories.value.length > 0) {
|
||||
if (mode.value === "edit") {
|
||||
console.log('here2')
|
||||
let recurrent = await recurrentsService.fetchRecurrent(spaceStore.selectedSpaceId, Number(recurrentId.value))
|
||||
recurrentCategory.value = recurrent.category
|
||||
recurrentName.value = recurrent.name
|
||||
recurrentAmount.value = recurrent.amount
|
||||
recurrentDate.value = recurrent.date
|
||||
} else {
|
||||
recurrentCategory.value = categories.value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "Error while fetching category",
|
||||
detail: err.detail.message,
|
||||
life: 3000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const el = e.target as HTMLInputElement
|
||||
const val = el.value.trim()
|
||||
|
||||
// если пусто — сбрасываем
|
||||
if (!val) {
|
||||
recurrentAmount.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
// пробуем преобразовать в число
|
||||
const num = Number(val)
|
||||
recurrentAmount.value = isNaN(num) ? 0 : num
|
||||
}
|
||||
|
||||
function handlePaste(e: ClipboardEvent) {
|
||||
e.preventDefault() // предотвратить стандартную вставку
|
||||
const text = e.clipboardData?.getData('text')?.trim() ?? ''
|
||||
|
||||
if (!text) {
|
||||
recurrentAmount.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
const num = Number(text)
|
||||
recurrentAmount.value = isNaN(num) ? 0 : num
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
if (mode.value === "edit") {
|
||||
|
||||
toolbar.registerHandler('deleteRecurrent', () => {
|
||||
console.log("delete recurrent")
|
||||
})
|
||||
toolbar.registerHandler('updateRecurrent', () => {
|
||||
console.log("update Recurrent")
|
||||
})
|
||||
} else {
|
||||
toolbar.registerHandler('createRecurrent', () => {
|
||||
console.log("create Recurrent")
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div v-if="categories.length===0" class="card !gap-4 !p-10">
|
||||
<span class="text-2xl">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>
|
||||
</div>
|
||||
<div v-else class="flex flex-col w-full justify-items-start gap-7">
|
||||
<div v-if="isCategorySelectorOpened" class="!absolute !top-0 !left-0 !h-full !w-full !z-50 !px-4 !overflow-y-auto"
|
||||
style="background: var(--primary-color)">
|
||||
<div class="card">
|
||||
<div v-for="key in categories.keys()" :key="categories[key].id"
|
||||
@click="recurrentCategory = categories[key]; isCategorySelectorOpened = false"
|
||||
class="flex flex-col w-full gap-0 pl-5 items-start justify-items-center font-bold text-xl">
|
||||
<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 justify-between">
|
||||
<div class="flex-row"> {{ categories[key].name }}</div>
|
||||
<div class="flex flex-row text-sm">{{ categories[key].description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi pi-angle-right !text-xl !font-extralight"/>
|
||||
</div>
|
||||
<Divider v-if="key+1 !== categories.length" class="!m-0 !py-3"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full ">
|
||||
<div class="flex-col w-full">
|
||||
|
||||
<InputNumber
|
||||
v-model="recurrentAmount"
|
||||
@input="handleInput"
|
||||
@paste="handlePaste"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
placeholder="Amount"
|
||||
suffix="₽"
|
||||
class="text-7xl font-bold w-full text-center focus:outline-none !p-0 !m-0"
|
||||
|
||||
/>
|
||||
<!-- <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 flex-col w-full justify-items-start">
|
||||
<label class="!font-semibold text-2xl text-gray-600 pl-2">Recurrent category</label>
|
||||
<div class="card !justify-start !items-start !p-4 !pl-5 " @click="isCategorySelectorOpened = true">
|
||||
<div class="flex-row w-full gap-2 items-center justify-between">
|
||||
<div class="flex-row gap-2 items-center">
|
||||
<span class="!text-3xl ">{{ recurrentCategory.icon }}</span>
|
||||
<div class="flex-col ">
|
||||
<span class=" !text-2xl">{{ recurrentCategory.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi pi-angle-right !text-xl !font-extralight"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full justify-items-start">
|
||||
<label class="!font-semibold text-2xl text-gray-600 pl-2">Recurrent name</label>
|
||||
<div class="card !justify-start !items-start !p-4 !pl-5 ">
|
||||
<input class="font-extralight text-xl w-full focus:outline-0" placeholder="Name" v-model="recurrentName"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full justify-items-start">
|
||||
<label class="!font-semibold text-2xl 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 v-for="i in 31" class="!w-12 !h-12 !items-center !justify-items-center !justify-center rounded-full "
|
||||
:class="recurrentDate == i ? 'bg-green-200' : 'bg-gray-100'"
|
||||
@click="recurrentDate=i">
|
||||
{{ i }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="!font-extralight text-gray-600 !pl-2">recurrent every N day of month</label>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
44
src/components/settings/SettingsList.vue
Normal file
44
src/components/settings/SettingsList.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import {Divider} from "primevue";
|
||||
|
||||
const items = [
|
||||
{name: "Space settings", link: '/space-settings'},
|
||||
{name: "Notification settings", link: '/notification-settings'},
|
||||
{name: "Categories", link: '/categories'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{name: "Recurrent Operations", link: '/recurrents'},
|
||||
{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()"
|
||||
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">
|
||||
|
||||
<span class="font-bold text-xl">{{ items[item].name }}</span>
|
||||
|
||||
<i class="pi pi-angle-right !text-xl !font-extralight"/>
|
||||
</div>
|
||||
|
||||
<Divider v-if="item+1 != items.length" class="!p-2 !m-0"/>
|
||||
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
13
src/components/settings/SpaceSettings.vue
Normal file
13
src/components/settings/SpaceSettings.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
Not implemented
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
80
src/components/space-list/SpaceList.vue
Normal file
80
src/components/space-list/SpaceList.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import type {Space} from "@/models/space";
|
||||
import {Vue3PullToRefresh} from "@amirafa/vue3-pull-to-refresh";
|
||||
// import {spaceService} from "@/services/space-service";
|
||||
|
||||
// PrimeVue Toast
|
||||
import Toast from "primevue/toast"; // <— правильный импорт компонента
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import {useSpaceStore} from "@/stores/spaceStore"; // нужен ToastService в main.ts
|
||||
const toast = useToast();
|
||||
const spaceStore = useSpaceStore();
|
||||
const emits = defineEmits(["space-selected"])
|
||||
|
||||
const spaces = ref<Space[]>([]);
|
||||
const spaceId = computed(() => {
|
||||
return spaceStore.selectedSpaceId
|
||||
})
|
||||
const spaceName = computed(() => {
|
||||
return spaceStore.selectedSpaceName
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
spaces.value = await spaceStore.getSpaces();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "Ошибка загрузки данных",
|
||||
detail: err?.message ?? "Неизвестная ошибка",
|
||||
life: 3000,
|
||||
closable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const selectSpace = (space: Space) => {
|
||||
spaceStore.setSpace(space.id, space.name);
|
||||
emits('space-selected', space);
|
||||
}
|
||||
|
||||
// если нужно дергать из родителя:
|
||||
// defineExpose({ fetchData });
|
||||
|
||||
onMounted(fetchData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast/>
|
||||
<Vue3PullToRefresh
|
||||
:distance="50"
|
||||
:duration="2000"
|
||||
:size="32"
|
||||
noreload
|
||||
:options="{ color: '#111', bgColor: '#fff' }"
|
||||
@onrefresh="
|
||||
() => {
|
||||
console.log('refreshed');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div class="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="text-2xl font-medium ">{{ space.name }}</span>
|
||||
<div class="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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
11
src/components/transactions/TransactionList.vue
Normal file
11
src/components/transactions/TransactionList.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user