wishlists
This commit is contained in:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"circle-progress.vue": "^3.3.0",
|
"circle-progress.vue": "^3.3.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
"platform": "^1.3.6",
|
"platform": "^1.3.6",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
@@ -5177,6 +5178,15 @@
|
|||||||
"@sideway/pinpoint": "^2.0.0"
|
"@sideway/pinpoint": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-cookie": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-message": {
|
"node_modules/js-message": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz",
|
||||||
@@ -13357,6 +13367,11 @@
|
|||||||
"@sideway/pinpoint": "^2.0.0"
|
"@sideway/pinpoint": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"js-cookie": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="
|
||||||
|
},
|
||||||
"js-message": {
|
"js-message": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"circle-progress.vue": "^3.3.0",
|
"circle-progress.vue": "^3.3.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
"platform": "^1.3.6",
|
"platform": "^1.3.6",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
|
|||||||
19
src/App.vue
19
src/App.vue
@@ -3,15 +3,16 @@
|
|||||||
<div id="app" class="flex flex-col h-screen bg-gray-100">
|
<div id="app" class="flex flex-col h-screen bg-gray-100">
|
||||||
<Toast/>
|
<Toast/>
|
||||||
<!-- MenuBar всегда фиксирован сверху -->
|
<!-- MenuBar всегда фиксирован сверху -->
|
||||||
<MenuBar v-if="userStore.user" class="w-full sticky hidden lg:block top-0 z-10"/>
|
<MenuBar v-if="userStore.user && !route.path.startsWith('/mywishlist')" class="w-full sticky hidden lg:block top-0 z-10"/>
|
||||||
<ToolBar class=" fixed visible lg:invisible bottom-0 z-10"/>
|
<ToolBar v-if="userStore.user && !route.path.startsWith('/mywishlist')" class=" fixed visible lg:invisible bottom-0 z-10"/>
|
||||||
|
|
||||||
<!-- Контентная часть заполняет оставшееся пространство -->
|
<!-- Контентная часть заполняет оставшееся пространство -->
|
||||||
<div class="flex flex-col flex-grow">
|
<div class="flex flex-col flex-grow">
|
||||||
<!-- {{ tg_id }}-->
|
<!-- {{ tg_id }}-->
|
||||||
<Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>
|
<!-- <Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>-->
|
||||||
|
|
||||||
<router-view/>
|
<router-view/>
|
||||||
|
|
||||||
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
<div class="bg-gray-100 h-12 block lg:hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -25,12 +26,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 sm:flex sm:flex-row w-full gap-1">
|
<div class="grid grid-cols-2 sm:flex sm:flex-row w-full gap-2">
|
||||||
<router-link to="/about" class="hover:underline">О проекте</router-link>
|
<router-link to="/about" class="hover:underline">О проекте</router-link>
|
||||||
<router-link to="/spaces" class="hover:underline">Пространства</router-link>
|
<router-link to="/spaces" class="hover:underline">Пространства</router-link>
|
||||||
<router-link to="/analytics" class="hover:underline">Аналитика</router-link>
|
<router-link to="/analytics" class="hover:underline">Аналитика</router-link>
|
||||||
<router-link to="/budgets" class="hover:underline">Бюджеты</router-link>
|
<router-link to="/budgets" class="hover:underline">Бюджеты</router-link>
|
||||||
<router-link to="/transactions" class="hover:underline">Транзакции</router-link>
|
<router-link to="/transactions" class="hover:underline">Транзакции</router-link>
|
||||||
|
<router-link to="/wishlists" class="hover:underline">Вишлисты</router-link>
|
||||||
<router-link to="/settings" class="hover:underline">Настройки</router-link>
|
<router-link to="/settings" class="hover:underline">Настройки</router-link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -64,8 +66,12 @@ import {useDrawerStore} from '@/stores/drawerStore'
|
|||||||
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
import TransactionForm from "@/components/transactions/TransactionForm.vue";
|
||||||
import {useSpaceStore} from "@/stores/spaceStore";
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
import Toast from "primevue/toast";
|
import Toast from "primevue/toast";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const drawerStore = useDrawerStore();
|
const drawerStore = useDrawerStore();
|
||||||
|
|
||||||
const visible = computed(() => drawerStore.visible);
|
const visible = computed(() => drawerStore.visible);
|
||||||
@@ -130,6 +136,11 @@ onMounted(async () => {
|
|||||||
await spaceStore.fetchSpaces()
|
await spaceStore.fetchSpaces()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// document.cookie = `aid=${crypto.randomUUID()}`
|
||||||
|
if (!Cookies.get("aid")) {
|
||||||
|
Cookies.set("aid", crypto.randomUUID(), { expires: 36500, path: "/" })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import ProgressSpinner from "primevue/progressspinner";
|
import ProgressSpinner from "primevue/progressspinner";
|
||||||
|
const props = defineProps({
|
||||||
|
halfscreen: Boolean ,
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative w-full h-screen">
|
<div class="relative w-full " :class="!props.halfscreen ? 'h-screen' : 'h-80'">
|
||||||
<!-- Полупрозрачный белый фон -->
|
<!-- Полупрозрачный белый фон -->
|
||||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-100 z-0"></div>
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-100 z-0"></div>
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@ import ProgressSpinner from "primevue/progressspinner";
|
|||||||
style="width: 50px; height: 50px;"
|
style="width: 50px; height: 50px;"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
animationDuration=".5s"
|
animationDuration="1s"
|
||||||
aria-label="Custom ProgressSpinner"
|
aria-label="Custom ProgressSpinner"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- fill="var(--p-text-color)"-->
|
<!-- fill="var(--p-text-color)"-->
|
||||||
<!-- />-->
|
<!-- />-->
|
||||||
<!-- </svg>-->
|
<!-- </svg>-->
|
||||||
<img alt="logo" src="/apple-touch-icon.png" width="32" height="32"/>
|
<button @click="router.push('/').then(router.go(0))"><img alt="logo" src="/apple-touch-icon.png" width="32" height="32"/></button>
|
||||||
</template>
|
</template>
|
||||||
<template #item="{ item, props, hasSubmenu, root }">
|
<template #item="{ item, props, hasSubmenu, root }">
|
||||||
<router-link :to="item.url" v-ripple class="flex items-center" v-bind="props.action">
|
<router-link :to="item.url" v-ripple class="flex items-center" v-bind="props.action">
|
||||||
@@ -145,9 +145,14 @@ const items = ref([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Транзакции',
|
label: 'Транзакции',
|
||||||
icon: "pi pi-star",
|
icon: "pi pi-dollar",
|
||||||
url: '/transactions'
|
url: '/transactions'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Вишлисты',
|
||||||
|
icon: "pi pi-star",
|
||||||
|
url: '/wishlists'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Настройки',
|
label: 'Настройки',
|
||||||
icon: 'pi pi-envelope',
|
icon: 'pi pi-envelope',
|
||||||
|
|||||||
@@ -32,7 +32,14 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-2 p-2">
|
<div class="flex flex-col gap-2 p-2">
|
||||||
<router-link to="/transactions" class="items-center flex flex-col gap-2">
|
<router-link to="/transactions" class="items-center flex flex-col gap-2">
|
||||||
<i class="pi pi-wallet text-2xl" style="font-size: 1rem"></i>
|
<i class="pi pi-dollar text-2xl" style="font-size: 1rem"></i>
|
||||||
|
<p>Транзакции</p>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 p-2">
|
||||||
|
<router-link to="/transactions" class="items-center flex flex-col gap-2">
|
||||||
|
<i class="pi pi-star text-2xl" style="font-size: 1rem"></i>
|
||||||
<p>Транзакции</p>
|
<p>Транзакции</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ onMounted(async () => {
|
|||||||
<!-- «Растяжка», чтобы было за что «скроллить» -->
|
<!-- «Растяжка», чтобы было за что «скроллить» -->
|
||||||
<div class="min-w-[550px] md:min-w-[650px] lg:min-w-[850px]">
|
<div class="min-w-[550px] md:min-w-[650px] lg:min-w-[850px]">
|
||||||
<Chart
|
<Chart
|
||||||
type="line"
|
type="bar"
|
||||||
:data="preparedChartData"
|
:data="preparedChartData"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
class="h-72 sm:h-full sm:w-full "
|
class="h-72 sm:h-full sm:w-full "
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ const login = async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await userStore.login(username.value, password.value)
|
await userStore.login(username.value, password.value)
|
||||||
toast.add({ severity: 'success', summary: 'Успешный вход', detail: 'Добро пожаловать!', life: 3000 })
|
// toast.add({ severity: 'success', summary: 'Успешный вход', detail: 'Добро пожаловать!', life: 3000 })
|
||||||
// await router.push('/')
|
// await router.push('/')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({ severity: 'error', summary: 'Ошибка входа', detail: 'Неверные данные', life: 3000 })
|
toast.add({ severity: 'error', summary: 'Ошибка входа', detail: 'Неверные данные', life: 3000 })
|
||||||
|
|||||||
192
src/components/onboarding/OnboardingView.vue
Normal file
192
src/components/onboarding/OnboardingView.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, nextTick, ref} from "vue";
|
||||||
|
import Stepper from "primevue/stepper";
|
||||||
|
import StepList from "primevue/steplist";
|
||||||
|
import Step from "primevue/step";
|
||||||
|
import StepPanel from "primevue/steppanel"
|
||||||
|
import Button from "primevue/button";
|
||||||
|
|
||||||
|
import SpaceCreationFormView from "@/components/spaces/SpaceCreationFormView.vue";
|
||||||
|
import {Category, CategoryType} from "@/models/Category";
|
||||||
|
import {deleteCategoryRequest, getCategories, getCategoryTypes} from "@/services/categoryService";
|
||||||
|
import CategoryListItem from "@/components/settings/categories/CategoryListItem.vue";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
// import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
|
||||||
|
import RecurrentListItem from "@/components/settings/recurrent/RecurrentListItem.vue";
|
||||||
|
import RecurrentCreationView from "@/components/settings/recurrent/RecurrentCreationView.vue";
|
||||||
|
import {RecurrentPayment} from "@/models/Recurrent";
|
||||||
|
import {getRecurrentPayments} from "@/services/recurrentService";
|
||||||
|
import router from "@/router";
|
||||||
|
import CreateCategoryModal from "@/components/settings/categories/CreateCategoryModal.vue";
|
||||||
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const stepperValue = ref("1")
|
||||||
|
|
||||||
|
const space = computed(() => {
|
||||||
|
return spaceStore.space
|
||||||
|
})
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
|
||||||
|
const spaceCreated = async (createdSpace) => {
|
||||||
|
spaceStore.setSpace(createdSpace)
|
||||||
|
console.log(space.value)
|
||||||
|
await fetchCategories()
|
||||||
|
stepperValue.value = '2'
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = ref<Category[]>([])
|
||||||
|
const categoryTypes = ref<CategoryType[]>([]);
|
||||||
|
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
if (space.value) {
|
||||||
|
await getCategories().then((res) => {
|
||||||
|
categories.value = res.data
|
||||||
|
})
|
||||||
|
await getCategoryTypes().then((res) => {
|
||||||
|
categoryTypes.value = res.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCreateCategoryModal = ref(false)
|
||||||
|
|
||||||
|
const recurrentCreated = async () => {
|
||||||
|
await fetchRecurrents()
|
||||||
|
}
|
||||||
|
|
||||||
|
const recurrents = ref<RecurrentPayment[]>()
|
||||||
|
const fetchRecurrents = async () => {
|
||||||
|
await getRecurrentPayments().then((res) => {
|
||||||
|
recurrents.value = res.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class=" flex justify-center bg-gray-100 h-full">
|
||||||
|
<div class="flex flex-col bg-white !h-fit p-4 rounded-xl m-4 w-5/6 lg:w-3/6">
|
||||||
|
<Stepper :value="stepperValue" class="b ">
|
||||||
|
<StepList>
|
||||||
|
<Step value="1">Пространства</Step>
|
||||||
|
<Step value="2">Категории</Step>
|
||||||
|
<Step value="3">Ежемесячные платежи</Step>
|
||||||
|
<Step value="4">Бюджет</Step>
|
||||||
|
</StepList>
|
||||||
|
<StepPanels>
|
||||||
|
<StepPanel v-slot="{ activateCallback }" value="1" class="">
|
||||||
|
<div class="p-2">
|
||||||
|
<h1 class="text-xl font-light">Привет!</h1>
|
||||||
|
<p class="text-sm text-gray-500">В первую очередь для работы с Luminic Space нужно создать свое
|
||||||
|
пространство.</p>
|
||||||
|
<p class="text-sm text-gray-500">Пространство - это обособленное место, где идет хранение информации.</p>
|
||||||
|
<p class="text-sm text-gray-500">Твои транзакции, бюджеты, категории и прочие вещи хранятся в границах
|
||||||
|
одного пространства.</p>
|
||||||
|
<p class="text-sm text-gray-500">Пространство может быть твоим личным, либо ты можешь пригласить в него
|
||||||
|
доверенных тебе людей.</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
|
||||||
|
<SpaceCreationFormView opened @space-created="spaceCreated"/>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="flex pt-6 justify-end ">-->
|
||||||
|
<!-- <!– <Button label="Back" severity="secondary" icon="pi pi-arrow-left" @click="activateCallback('1')" />–>-->
|
||||||
|
<!-- <Button label="Пропустить" icon="pi pi-arrow-right" severity="secondary" iconPos="right"-->
|
||||||
|
<!-- @click="activateCallback('2')"/>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<!-- <div class="flex flex-col h-48">-->
|
||||||
|
<!-- <div class="border-2 border-dashed border-surface-200 dark:border-surface-700 rounded bg-surface-50 dark:bg-surface-950 flex-auto flex justify-center items-center font-medium">Content I</div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="flex pt-6 justify-end">-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</StepPanel>
|
||||||
|
<StepPanel v-slot="{ activateCallback }" value="2">
|
||||||
|
<div class="p-2">
|
||||||
|
<p v-if="!space">Нет пространства - нет категорий. Возвращайся и создавай пространство!</p>
|
||||||
|
<div v-else>
|
||||||
|
<h1 class="text-xl font-light">Теперь - категории!</h1>
|
||||||
|
<p class="text-sm text-gray-500">Если ты указал создание категорий на предыдущем этапе - этот можно
|
||||||
|
пропустить или просмотреть созданные категории и отредактировать их.</p>
|
||||||
|
<p class="text-sm text-gray-500">Категория - логическое разделение трат. Они могут быть 2 типов:
|
||||||
|
Поступления и расходы. Категории так же могут иметь тэги. Самые важные из них - loans и savings. На
|
||||||
|
основе них идет расчет сумм по долгам и сбережениям на странице бюджета, но об этом позже</p>
|
||||||
|
<!-- <p class="text-sm text-gray-500">Твои транзакции, бюджеты, категории и прочие вещи хранятся в границах одного пространства.</p>-->
|
||||||
|
<!-- <p class="text-sm text-gray-500">Пространство может быть твоим личным, либо ты можешь пригласить в него доверенных тебе людей.</p>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <button @click="showCreateCategoryModal=true">hui</button>-->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Button label="Добавить категорию" icon="pi pi-plus" class="text-sm"
|
||||||
|
@click="showCreateCategoryModal=true"/>
|
||||||
|
|
||||||
|
<CreateCategoryModal v-if="showCreateCategoryModal" :show="showCreateCategoryModal"
|
||||||
|
:category-types="categoryTypes" :show-tags="false" @save-category="fetchCategories"
|
||||||
|
@close-modal="showCreateCategoryModal=false"
|
||||||
|
/>
|
||||||
|
<div class="grid lg:grid-cols-2 gap-2">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex pt-6 justify-between">
|
||||||
|
<Button label="Вернуться" severity="secondary" icon="pi pi-arrow-left" @click="activateCallback('1')"/>
|
||||||
|
<Button label="Пропустить" icon="pi pi-arrow-right" iconPos="right" @click="stepperValue='3'"/>
|
||||||
|
</div>
|
||||||
|
</StepPanel>
|
||||||
|
<StepPanel v-slot="{ activateCallback }" value="3">
|
||||||
|
<div class="p-2">
|
||||||
|
<h1 class="text-xl font-light">Теперь давай заполним твои ежемесячные платежи</h1>
|
||||||
|
<p class="text-sm text-gray-500">Ежемесячные платежи - это такие платежи, которые повторяются раз из раза
|
||||||
|
каждый месяц: кредит, кварплата, арендная плата и так далее.</p>
|
||||||
|
<p class="text-sm text-gray-500">Каждый месяц первого числа мы создадим плановые транзакции по твоим
|
||||||
|
повторяющимся платежам. </p>
|
||||||
|
<!-- <p class="text-sm text-gray-500">Твои транзакции, бюджеты, категории и прочие вещи хранятся в границах одного пространства.</p>-->
|
||||||
|
<!-- <p class="text-sm text-gray-500">Пространство может быть твоим личным, либо ты можешь пригласить в него доверенных тебе людей.</p>-->
|
||||||
|
</div>
|
||||||
|
<div class="grid lg:grid-cols-1 gap-2">
|
||||||
|
<RecurrentCreationView v-if="stepperValue=='3'"
|
||||||
|
:expense-categories="categories.filter(cat => cat.type.code=='EXPENSE')"
|
||||||
|
:income-categories="categories.filter(cat => cat.type.code=='INCOME')"
|
||||||
|
:category-types="categoryTypes" @save-payment="recurrentCreated"/>
|
||||||
|
</div>
|
||||||
|
<div class="grid lg:grid-cols-2 gap-2">
|
||||||
|
<RecurrentListItem v-for="recurrent in recurrents" :payment="recurrent"/>
|
||||||
|
</div>
|
||||||
|
<div class="pt-6 flex justify-between w-full">
|
||||||
|
<Button label="Назад" severity="secondary" icon="pi pi-arrow-left" @click="activateCallback('2')"/>
|
||||||
|
<Button label="Дальше" icon="pi pi-arrow-right" iconPos="right" @click="stepperValue='4'"/>
|
||||||
|
</div>
|
||||||
|
</StepPanel>
|
||||||
|
<StepPanel v-slot="{ activateCallback }" value="4">
|
||||||
|
<div class="p-2">
|
||||||
|
<h1 class="text-xl font-light">Теперь можно создать и твой первый бюджет!</h1>
|
||||||
|
<p class="text-sm text-gray-500">Бюджет - объединение общей информации о твоих плановых и фактических
|
||||||
|
транзакциях, о категориях и лимитах по ним и другое.</p>
|
||||||
|
<p class="text-sm text-gray-500">Транзакции не привязаны к бюджету. Бюджет учитывает любые транзакции,
|
||||||
|
дата которых входит в период действия бюджета.</p>
|
||||||
|
<!-- <p class="text-sm text-gray-500">Твои транзакции, бюджеты, категории и прочие вещи хранятся в границах одного пространства.</p>-->
|
||||||
|
<!-- <p class="text-sm text-gray-500">Пространство может быть твоим личным, либо ты можешь пригласить в него доверенных тебе людей.</p>-->
|
||||||
|
</div>
|
||||||
|
<!-- <BudgetCreationView opened @budgetCreated="router.push('/budgets').then((res) => router.go(0))"/>-->
|
||||||
|
<div class="pt-6 flex justify-between w-full">
|
||||||
|
<Button label="Назад" severity="secondary" icon="pi pi-arrow-left" @click="activateCallback('3')"/>
|
||||||
|
<Button label="В работу!" icon="pi pi-arrow-right" iconPos="right"
|
||||||
|
@click="router.push('/budgets').then(router.go(0))"/>
|
||||||
|
</div>
|
||||||
|
</StepPanel>
|
||||||
|
</StepPanels>
|
||||||
|
</Stepper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -268,16 +268,11 @@ onMounted(async () => {
|
|||||||
:key="user.id"
|
:key="user.id"
|
||||||
@mouseover="user.isHovered = true"
|
@mouseover="user.isHovered = true"
|
||||||
@mouseleave="user.isHovered = false">
|
@mouseleave="user.isHovered = false">
|
||||||
<div
|
<div class="relative flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center">
|
||||||
|
|
||||||
class="relative flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center"
|
|
||||||
|
|
||||||
>
|
|
||||||
<!-- Первая буква имени -->
|
<!-- Первая буква имени -->
|
||||||
<span class="text-white text-center">
|
<span class="text-white text-center">
|
||||||
{{ user.firstName.substring(0, 1) }}
|
{{ user.firstName.substring(0, 1) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Иконка короны для владельца -->
|
<!-- Иконка короны для владельца -->
|
||||||
<i
|
<i
|
||||||
v-if="space.owner.id === user.id"
|
v-if="space.owner.id === user.id"
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ const selectedSpace = computed(() => spaceStore.space)
|
|||||||
|
|
||||||
watch(selectedSpace, async (newValue, oldValue) => {
|
watch(selectedSpace, async (newValue, oldValue) => {
|
||||||
if (newValue != oldValue) {
|
if (newValue != oldValue) {
|
||||||
await fetchTransactions(false)
|
transactions.value = [];
|
||||||
|
await fetchTransactions(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const types = ref([])
|
const types = ref([])
|
||||||
@@ -183,7 +184,7 @@ onUnmounted(async () => {
|
|||||||
@transaction-updated="fetchTransactions(true)"
|
@transaction-updated="fetchTransactions(true)"
|
||||||
@delete-transaction="fetchTransactions(true)"
|
@delete-transaction="fetchTransactions(true)"
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center justify-center px-2 py-1 mb-5">
|
<div v-if="!loading" class="flex items-center justify-center px-2 py-1 mb-5">
|
||||||
<Button @click="fetchTransactions(false)">Загрузить следующие...</Button>
|
<Button @click="fetchTransactions(false)">Загрузить следующие...</Button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Показать спиннер загрузки, если идет загрузка -->
|
<!-- Показать спиннер загрузки, если идет загрузка -->
|
||||||
|
|||||||
174
src/components/wishlists/WishListListView.vue
Normal file
174
src/components/wishlists/WishListListView.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import router from "@/router";
|
||||||
|
import {formatDate} from "@/utils/utils";
|
||||||
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
|
import StatusView from "@/components/StatusView.vue";
|
||||||
|
import BudgetCreationView from "@/components/budgets/BudgetCreationView.vue";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import ConfirmDialog from "primevue/confirmdialog";
|
||||||
|
import Toast from "primevue/toast";
|
||||||
|
import Dialog from "primevue/dialog";
|
||||||
|
import {computed, onMounted, ref, watch} from "vue";
|
||||||
|
import {WishList} from "@/models/WishList";
|
||||||
|
import {deleteWishlistRequest, getWishlists} from "@/services/WishListService";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import WishlistCreationView from "@/components/wishlists/WishlistCreationView.vue";
|
||||||
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
|
const toast = useToast()
|
||||||
|
const confirm = useConfirm();
|
||||||
|
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const creationOpened = ref(false);
|
||||||
|
|
||||||
|
const editingWishlist = ref();
|
||||||
|
|
||||||
|
const wishlists = ref<WishList[]>([]);
|
||||||
|
const fetchWishlists = async () => {
|
||||||
|
wishlists.value = await getWishlists()
|
||||||
|
}
|
||||||
|
|
||||||
|
const wishlistCreated = async () => {
|
||||||
|
creationOpened.value = false;
|
||||||
|
await fetchWishlists();
|
||||||
|
}
|
||||||
|
|
||||||
|
const wishlistCreationCanceled = () => {
|
||||||
|
creationOpened.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteWishlist = async (wishlist: WishList) => {
|
||||||
|
confirm.require({
|
||||||
|
message: `Вы действительно хотите удалить вишлист ${wishlist.name} ?`,
|
||||||
|
header: 'Удаление вишлиста',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Отмена',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Отмена',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Удалить',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
try {
|
||||||
|
await deleteWishlistRequest(wishlist.id)
|
||||||
|
await fetchWishlists()
|
||||||
|
toast.add({severity: 'success', summary: 'Успешно!', detail: 'Вишлист удален!', life: 3000});
|
||||||
|
} catch (e: Error) {
|
||||||
|
toast.add({severity: 'error', summary: "Ошибка при удалении", detail: e.response.data.message, life: 3000});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили удаление', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedSpace.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
await fetchWishlists();
|
||||||
|
loading.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching wishlists infos:', error);
|
||||||
|
toast.add({severity: 'error', summary: 'Ошибка получения вишлистов', detail: error.response.data.message});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (selectedSpace.value) {
|
||||||
|
loading.value = true;
|
||||||
|
await fetchWishlists();
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LoadingView v-if="loading"/>
|
||||||
|
<div v-else class="p-4 bg-gray-100 h-full flex flex-col gap-4 ">
|
||||||
|
<Dialog :visible="creationOpened" modal :header="editingWishlist ? 'Изменить вишлист' : 'Создать новый вишлист'" :style="{ width: '25rem' }" @hide="wishlistCreationCanceled"
|
||||||
|
@update:visible="wishlistCreationCanceled">
|
||||||
|
<WishlistCreationView :editing-wishlist="editingWishlist" @wishlist-created="wishlistCreated" @cancel-creation="wishlistCreationCanceled" />
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Заголовок -->
|
||||||
|
<div class="flex flex-row gap-4 items-center">
|
||||||
|
<h2 class="text-4xl font-bold">Вишлисты</h2>
|
||||||
|
<Button label="+ Создать" @click="creationOpened=true" size="small"/>
|
||||||
|
<!-- <StatusView :show="creationSuccessModal" :is-error="false" :message="'Бюджет создан!'"/>-->
|
||||||
|
</div>
|
||||||
|
<!-- Плитка с бюджетами -->
|
||||||
|
<div v-if="!selectedSpace" class="flex w-full h-full items-center justify-center">
|
||||||
|
<p>Сперва нужно выбрать Пространство.
|
||||||
|
<button class="text-blue-500 hover:underline" @click="router.push('/spaces').then((res) => router.go(0))">
|
||||||
|
Перейти
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="wishlists.length==0" class="flex w-full h-full items-center justify-center">
|
||||||
|
<p>Кажется, в этом пространстве еще нет вишлистов
|
||||||
|
<button class="text-blue-500 hover:underline" @click="creationOpened=true">создайте один.</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<!-- Будущие и текущие бюджеты -->
|
||||||
|
<ConfirmDialog/>
|
||||||
|
<Toast/>
|
||||||
|
|
||||||
|
<div v-for="wishlist in wishlists" :key="wishlist.id" class="p-4 shadow-lg rounded-lg bg-white">
|
||||||
|
<div class="flex flex-row justify-between gap-4">
|
||||||
|
<div class="flex flex-col justify-between gap-5">
|
||||||
|
<router-link :to="'/wishlists/'+wishlist.id">
|
||||||
|
<div class="text-xl font-bold ">{{ wishlist.name }}</div>
|
||||||
|
</router-link>
|
||||||
|
<div class="text-sm font-light ">{{ wishlist.description }}</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<div class="relative flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center">
|
||||||
|
<!-- Первая буква имени -->
|
||||||
|
<span class="text-white text-center">{{ wishlist.owner.firstName.substring(0, 1) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-semibold">{{ wishlist.owner.firstName }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-between items-end gap-4">
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button @click="editingWishlist=wishlist;creationOpened=true"><i class="pi pi-pen-to-square" style="font-size: 1rem"/></button>
|
||||||
|
<router-link :to="'/wishlists/'+wishlist.id">
|
||||||
|
<i class="pi pi-arrow-circle-right " style=""/>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<button @click="deleteWishlist(wishlist)"><i class="pi pi-trash" /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
85
src/components/wishlists/WishlistCreationView.vue
Normal file
85
src/components/wishlists/WishlistCreationView.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Checkbox from "primevue/checkbox";
|
||||||
|
import Textarea from "primevue/textarea";
|
||||||
|
import InputText from "primevue/inputtext";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import FloatLabel from "primevue/floatlabel";
|
||||||
|
import {WishList} from "@/models/WishList";
|
||||||
|
import {reactive, ref} from "vue";
|
||||||
|
import {createWishlistRequest, updateWishlistRequest} from "@/services/WishListService";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const props = defineProps({
|
||||||
|
editingWishlist: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(['wishlist-created', "cancel-creation"])
|
||||||
|
|
||||||
|
const wishlist = ref<WishList>(props.editingWishlist ? props.editingWishlist : new WishList())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const createWishlist = async () => {
|
||||||
|
if (!props.editingWishlist) {
|
||||||
|
|
||||||
|
|
||||||
|
await createWishlistRequest(wishlist.value)
|
||||||
|
.then((res) => {
|
||||||
|
emits("wishlist-created", res);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка создания вишлиста',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await updateWishlistRequest(wishlist.value)
|
||||||
|
.then((res) => {
|
||||||
|
emits("wishlist-created", res);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка изменения вишлиста',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelCreation = () => {
|
||||||
|
emits("cancel-creation")
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-col gap-4 mt-1">
|
||||||
|
<FloatLabel variant="on" class="w-full">
|
||||||
|
<label for="name">Название</label>
|
||||||
|
<InputText v-model="wishlist.name" id="name" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<FloatLabel variant="on" class="w-full">
|
||||||
|
<label for="name">Описание</label>
|
||||||
|
<Textarea v-model="wishlist.description" id="name" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<div class="flex flex-row gap-2 justify-end items-center">
|
||||||
|
<Button label="Создать" severity="success" icon="pi pi-save" @click="createWishlist"/>
|
||||||
|
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancelCreation"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
217
src/components/wishlists/WishlistExternalView.vue
Normal file
217
src/components/wishlists/WishlistExternalView.vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {formatAmount} from "@/utils/utils";
|
||||||
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import {onMounted, reactive, ref} from "vue";
|
||||||
|
import {cancelReserveWishlistItem, getWishlistExternal, reserveWishlistItem} from "@/services/WishListService";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import {WishList, WishlistItem} from "@/models/WishList";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import Image from "primevue/image";
|
||||||
|
import InputText from "primevue/inputtext";
|
||||||
|
import FloatLabel from "primevue/floatlabel";
|
||||||
|
import Dialog from "primevue/dialog";
|
||||||
|
|
||||||
|
import apiClient from "@/services/axiosSetup";
|
||||||
|
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
|
||||||
|
const aidCookie = Cookies.get("aid");
|
||||||
|
const loading = ref(true);
|
||||||
|
const wishlist = ref<WishList>()
|
||||||
|
const selectedImage = reactive(new Map<string, string>())
|
||||||
|
const selectedReserveItem = ref()
|
||||||
|
const reserveModalShow = ref(false)
|
||||||
|
const reservedBy = ref(Cookies.get("name") ? Cookies.get("name") : null);
|
||||||
|
const reserveItem = async () => {
|
||||||
|
Cookies.set("name", reservedBy.value, { expires: 36500, path: "/" })
|
||||||
|
await reserveWishlistItem(wishlist.value?.id, selectedReserveItem.value.id, reservedBy.value, aidCookie)
|
||||||
|
.then(async (res) => {
|
||||||
|
reserveModalShow.value = false
|
||||||
|
selectedReserveItem.value = false
|
||||||
|
// reservedBy.value = null
|
||||||
|
await fetchWishlist()
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Успех!',
|
||||||
|
detail: 'Успешно забронировано',
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка резервации',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelReserve = async (item) => {
|
||||||
|
await cancelReserveWishlistItem(wishlist.value?.id, item.id, reservedBy.value, aidCookie)
|
||||||
|
.then(async (res) => {
|
||||||
|
reserveModalShow.value = false
|
||||||
|
selectedReserveItem.value = false
|
||||||
|
await fetchWishlist()
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Успех!',
|
||||||
|
detail: 'Бронь снята',
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка резервации',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchWishlist = async () => {
|
||||||
|
await getWishlistExternal(route.params.id)
|
||||||
|
.then((res) => {
|
||||||
|
wishlist.value = res
|
||||||
|
wishlist.value?.items.forEach((item: WishlistItem) => {
|
||||||
|
selectedImage.set(item.id, item.images[0])
|
||||||
|
})
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка получения вишлиста',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchWishlist();
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LoadingView v-if="loading"/>
|
||||||
|
<div v-else class="p-4 bg-gray-100 h-full flex flex-col gap-10">
|
||||||
|
<Dialog :visible="reserveModalShow" header="Резерв желания"
|
||||||
|
@hide="reserveModalShow = false; selectedReserveItem=null" modal
|
||||||
|
@update:visible="reserveModalShow = false;selectedReserveItem=null" class="!w-2/6 ">
|
||||||
|
<div class="flex flex-col gap-4 mt-1">
|
||||||
|
|
||||||
|
<FloatLabel variant="on" class="w-full">
|
||||||
|
<label for="link">Ваше имя</label>
|
||||||
|
<InputText id="link" type="text" v-model="reservedBy" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<div class="flex flex-row w-full justify-between">
|
||||||
|
<Button label="Сохранить" @click="reserveItem"/>
|
||||||
|
<Button label="Отмена" @click="reserveModalShow=false;selectedReserveItem=null;reservedBy=null"
|
||||||
|
severity="secondary"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
<div class="flex flex-row items-center gap-2 min-w-fit">
|
||||||
|
<img alt="logo" src="/apple-touch-icon.png" width="48" height="48"/>
|
||||||
|
<div class="flex flex-col items-start">
|
||||||
|
<p class="text-xl font-bold">Luminic Space</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class=" flex flex-col gap-3 justify-between h-full ">
|
||||||
|
<div class="flex flex-col justify-between gap-5">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-row gap-2 items-end">
|
||||||
|
<h2 class="text-4xl font-bold text-gray-700">Вишлист {{ wishlist.name }} </h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg text-gray-500">{{ wishlist.description }}</p>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<div class="relative flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center">
|
||||||
|
<!-- Первая буква имени -->
|
||||||
|
<span class="text-white text-center">{{ wishlist.owner.firstName.substring(0, 1) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-semibold">{{ wishlist.owner.firstName }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<p class="text-2xl text-gray-700">Желания</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-2">
|
||||||
|
<div v-for="item in wishlist.items"
|
||||||
|
class=" bg-white flex flex-col p-4 flex-shrink-0 gap-4 shadow-md rounded-lg max-w-128 h-full">
|
||||||
|
<div class="flex flex-col w-full justify-center gap-2">
|
||||||
|
<div class="flex w-full justify-center">
|
||||||
|
<Image
|
||||||
|
:src="selectedImage.get(item.id).startsWith('http') ? selectedImage.get(item.id) : apiClient.defaults.baseURL+'/'+selectedImage.get(item.id) "
|
||||||
|
alt="Image"
|
||||||
|
width="128" height="128" show="show" preview
|
||||||
|
imageClass="h-64 w-64 object-cover items-center justify-center justify-items-center"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row !h-12 gap-2">
|
||||||
|
|
||||||
|
<div v-for="(image, index) in item.images" class="group relative h-12 w-12 rounded-lg shadow-md ">
|
||||||
|
<button @click="selectedImage.set(item.id, image)">
|
||||||
|
<Image
|
||||||
|
:src="image.startsWith('http') ? image : apiClient.defaults.baseURL+'/'+image " alt="Image"
|
||||||
|
width="48" height="48" show="show"
|
||||||
|
imageClass="w-12 h-12 object-cover"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-between gap-2 h-full">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-row w-full justify-between">
|
||||||
|
<p class="font-semibold text-xl text-gray-700">{{ item.name }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="font-bold text-lg text-emerald-700">{{ formatAmount(item.price) }} ₽</p>
|
||||||
|
<p class="font-light text-gray-700 text-wrap">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 ">
|
||||||
|
<a :href="item.link"
|
||||||
|
target="_blank" class="w-fit">
|
||||||
|
<Button label="В магазин" icon="pi pi-arrow-up-right" iconPos="right"/>
|
||||||
|
</a>
|
||||||
|
<Button
|
||||||
|
:label="!item.reservedBy ? 'Я беру!' : item.reservedBy.aid != aidCookie ? 'Забронировано.' : 'Отменить'"
|
||||||
|
:severity="item.reservedBy && item.reservedBy.aid == aidCookie ? 'danger' : 'secondary'"
|
||||||
|
:disabled="item.reservedBy && aidCookie != item.reservedBy.aid ? true : false"
|
||||||
|
@click="item.reservedBy && item.reservedBy.aid == aidCookie ? cancelReserve(item): reserveModalShow=true;selectedReserveItem=item"
|
||||||
|
v-tooltip="item.reservedBy ? 'Зарезервированно за ' + item.reservedBy.name : ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flex-row > Image {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-image-preview-mask {
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
125
src/components/wishlists/WishlistItemCreationView.vue
Normal file
125
src/components/wishlists/WishlistItemCreationView.vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Textarea from "primevue/textarea";
|
||||||
|
import InputText from "primevue/inputtext";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import FloatLabel from "primevue/floatlabel";
|
||||||
|
import {WishlistItem} from "@/models/WishList";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import InputGroup from "primevue/inputgroup";
|
||||||
|
import InputNumber from "primevue/inputnumber";
|
||||||
|
import InputGroupAddon from "primevue/inputgroupaddon";
|
||||||
|
import {addWishListItemRequest} from "@/services/WishListService";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
wishlistId: String,
|
||||||
|
editItem: Object,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(["item-created", "creation-cancelled"])
|
||||||
|
const isEditing = ref(!!props.editItem)
|
||||||
|
const item = ref(props.editItem ? props.editItem : new WishlistItem())
|
||||||
|
const inputNameRef = ref()
|
||||||
|
const inputDescriptionRef = ref()
|
||||||
|
const inputPriceRef = ref()
|
||||||
|
const inputLinkRef = ref()
|
||||||
|
|
||||||
|
const checkForm = () => {
|
||||||
|
console.log(inputNameRef.value)
|
||||||
|
if (!item.value.name || item.value.name.length === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка создания желания',
|
||||||
|
detail: "Название должно быть введено",
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else if (!item.value.description || item.value.description.length === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка создания желания',
|
||||||
|
detail: "Описание должно быть введено",
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else if (!item.value.link || item.value.link.length === 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка создания желания',
|
||||||
|
detail: "Ссылка должна быть введена",
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else if (!item.value.price || item.value.price == 0) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка создания желания',
|
||||||
|
detail: "Цена должна быть введена",
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else return true
|
||||||
|
}
|
||||||
|
const itemCreate = async () => {
|
||||||
|
if (checkForm()) {
|
||||||
|
await addWishListItemRequest(props.wishlistId, item.value)
|
||||||
|
.then(res => {
|
||||||
|
emits("item-created", res)
|
||||||
|
resetForm()
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка создания желания',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cancelCreation = () => {
|
||||||
|
resetForm()
|
||||||
|
emits("creation-cancelled")
|
||||||
|
}
|
||||||
|
const resetForm = () => {
|
||||||
|
item.value = new WishlistItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex flex-col gap-4 mt-1">
|
||||||
|
<FloatLabel variant="on" class="w-full">
|
||||||
|
<label for="name">Название</label>
|
||||||
|
<InputText :ref="inputNameRef" v-model="item.name" id="name" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<FloatLabel variant="on" class="w-full">
|
||||||
|
<label for="name">Описание</label>
|
||||||
|
<Textarea :ref="inputDescriptionRef" v-model="item.description" id="name" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<FloatLabel variant="on" class="w-full">
|
||||||
|
<label for="name">Ссылка на товар</label>
|
||||||
|
<InputText :ref="inputLinkRef" v-model="item.link" id="name" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<!-- Сумма -->
|
||||||
|
<InputGroup class="w-full">
|
||||||
|
<InputGroupAddon>₽</InputGroupAddon>
|
||||||
|
<InputNumber :ref="inputPriceRef" v-model="item.price" placeholder="Сумма"/>
|
||||||
|
<InputGroupAddon>.00</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
<div class="flex flex-row gap-2 justify-end items-center">
|
||||||
|
<Button label="Создать" severity="success" icon="pi pi-save" @click="itemCreate"/>
|
||||||
|
<Button label="Отмена" severity="secondary" icon="pi pi-times-circle" @click="cancelCreation"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
462
src/components/wishlists/WishlistView.vue
Normal file
462
src/components/wishlists/WishlistView.vue
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {formatAmount} from "@/utils/utils";
|
||||||
|
import LoadingView from "@/components/LoadingView.vue";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||||
|
import {
|
||||||
|
cancelReserveWishlistItem,
|
||||||
|
deleteWishListItemRequest,
|
||||||
|
deleteWishlistRequest,
|
||||||
|
getWishlist,
|
||||||
|
updateWishListItemRequest
|
||||||
|
} from "@/services/WishListService";
|
||||||
|
import {useToast} from "primevue/usetoast";
|
||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import {WishList, WishlistItem} from "@/models/WishList";
|
||||||
|
import Button from "primevue/button";
|
||||||
|
import Image from "primevue/image";
|
||||||
|
import Dialog from "primevue/dialog";
|
||||||
|
import Divider from "primevue/divider";
|
||||||
|
import FileUpload from 'primevue/fileupload';
|
||||||
|
import {uploadStatic} from "@/services/StaticService";
|
||||||
|
import apiClient from "@/services/axiosSetup";
|
||||||
|
import WishlistItemCreationView from "@/components/wishlists/WishlistItemCreationView.vue";
|
||||||
|
import InputText from "primevue/inputtext";
|
||||||
|
import FloatLabel from "primevue/floatlabel";
|
||||||
|
import ConfirmDialog from "primevue/confirmdialog";
|
||||||
|
import {useConfirm} from "primevue/useconfirm";
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const toast = useToast()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const wishlist = ref<WishList>()
|
||||||
|
const selectedImage = reactive(new Map<string, string>())
|
||||||
|
const hoveredCancelReservationButton = reactive(new Map<string, boolean>())
|
||||||
|
|
||||||
|
const fetchWishlist = async () => {
|
||||||
|
await getWishlist(route.params.id)
|
||||||
|
.then((res) => {
|
||||||
|
wishlist.value = res
|
||||||
|
wishlist.value?.items.forEach((item: WishlistItem) => {
|
||||||
|
selectedImage.set(item.id, item.images[0])
|
||||||
|
})
|
||||||
|
shareLink.value = window.location.origin + "/mywishlist/" + wishlist.value?.id
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка получения вишлиста',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const wishlistItemCreationOpened = ref(false);
|
||||||
|
const itemCreated = () => {
|
||||||
|
wishlistItemCreationOpened.value = false;
|
||||||
|
fetchWishlist();
|
||||||
|
}
|
||||||
|
const editingItem = ref()
|
||||||
|
|
||||||
|
const imageUploadVisible = ref(false);
|
||||||
|
const imageUploadForItemId = ref()
|
||||||
|
|
||||||
|
const fileupload = ref()
|
||||||
|
const onFileSelect = (event) => {
|
||||||
|
fileupload.value = event.files[0];
|
||||||
|
}
|
||||||
|
const upload = async () => {
|
||||||
|
const item = wishlist.value.items.filter((item) => item.id == imageUploadForItemId.value)[0]
|
||||||
|
if (fileUploadLink.value) {
|
||||||
|
item.images.push(fileUploadLink.value)
|
||||||
|
imageUploadVisible.value = false
|
||||||
|
await updateWishListItemRequest(wishlist.value.id, item)
|
||||||
|
.then(async (res) => {
|
||||||
|
await fetchWishlist();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка сохранения желания ',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
await uploadStatic(item.id, fileupload.value).then(async (res) => {
|
||||||
|
item.images.push(res)
|
||||||
|
imageUploadVisible.value = false
|
||||||
|
await updateWishListItemRequest(wishlist.value.id, item)
|
||||||
|
.then(async (res) => {
|
||||||
|
await fetchWishlist();
|
||||||
|
fileupload.value = null
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка сохранения желания ',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка загрузки файла',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const deleteImage = async (item: WishlistItem, index: number) => {
|
||||||
|
item.images.splice(index, 1)
|
||||||
|
await updateWishListItemRequest(wishlist.value.id, item)
|
||||||
|
.then(async (res) => {
|
||||||
|
await fetchWishlist();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка загрузки файла',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteItem = async (item: WishlistItem) => {
|
||||||
|
console.log("here")
|
||||||
|
confirm.require({
|
||||||
|
message: `Вы действительно хотите удалить желание ${item.name} ?`,
|
||||||
|
header: 'Удаление вишлиста',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Отмена',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Отмена',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Удалить',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
await deleteWishListItemRequest(wishlist.value.id, item)
|
||||||
|
.then(async (res) => {
|
||||||
|
await fetchWishlist();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка удаление желания',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили удаление', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
const cancelReserve = async (item: WishlistItem) => {
|
||||||
|
console.log("here")
|
||||||
|
confirm.require({
|
||||||
|
message: `Вы действительно хотите отменить бронь у желания ${item.name} ?`,
|
||||||
|
header: 'Отмена брони',
|
||||||
|
icon: 'pi pi-info-circle',
|
||||||
|
rejectLabel: 'Отмена',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Отмена',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Отменить',
|
||||||
|
severity: 'danger'
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
await cancelReserveWishlistItem(wishlist.value.id, item.id)
|
||||||
|
.then(async (res) => {
|
||||||
|
await fetchWishlist();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка отмены бронирования',
|
||||||
|
detail: err.response.data.message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили удаление', life: 3000});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const shareDialogOpened = ref(false)
|
||||||
|
const shareLink = ref()
|
||||||
|
|
||||||
|
const copied = ref(false);
|
||||||
|
|
||||||
|
const copyToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
copied.value = true;
|
||||||
|
// setTimeout(() => copied.value = false, 1500); // Убираем сообщение через 1.5 сек
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка копирования:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const selectedSpace = computed(() => spaceStore.space)
|
||||||
|
const fileUploadLink = ref()
|
||||||
|
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedSpace.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
|
||||||
|
if (newValue != oldValue || !oldValue) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
// Если выбранный space изменился, получаем новую информацию о бюджете
|
||||||
|
await fetchWishlist()
|
||||||
|
loading.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching wishlists infos:', error);
|
||||||
|
toast.add({severity: 'error', summary: 'Ошибка получения вишлиста', detail: error.response.data.message});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (selectedSpace.value) {
|
||||||
|
loading.value = true;
|
||||||
|
await fetchWishlist();
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LoadingView v-if="loading"/>
|
||||||
|
<div v-else class="p-4 bg-gray-100 h-full ">
|
||||||
|
<ConfirmDialog/>
|
||||||
|
<Dialog :visible="imageUploadVisible" header="Загрузка изображения"
|
||||||
|
@hide="imageUploadVisible = false; fileUploadLink=null" modal
|
||||||
|
@update:visible="imageUploadVisible = false;fileUploadLink=null" class="w-5/6 md:!w-2/6">
|
||||||
|
<div class="flex flex-col gap-2 m-1">
|
||||||
|
<FileUpload v-if="!fileUploadLink" accept="image/*/" @select="onFileSelect" :maxFileSize="5*1024*1024"
|
||||||
|
cancelLabel="cancel"
|
||||||
|
mode="basic" name="demo[]"
|
||||||
|
chooseLabel="Browse"/>
|
||||||
|
<Divider v-if="!fileUploadLink && !fileupload">или укажите ссылку</Divider>
|
||||||
|
<FloatLabel v-if="!fileupload" variant="on" class="w-full">
|
||||||
|
<label for="link">Ссылка</label>
|
||||||
|
<InputText id="link" v-model="fileUploadLink" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<Image v-if="fileUploadLink" :src="fileUploadLink" width="240" preview class="w-fit"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2 w-full justify-center">
|
||||||
|
<Button label="Загрузить" @click="upload"/>
|
||||||
|
<Button label="Сбросить" @click="fileUploadLink=null;fileupload=null" severity="secondary"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog :visible="wishlistItemCreationOpened" modal
|
||||||
|
:header=" editingItem ? 'Редактирование желания':'Создание нового желаемого'"
|
||||||
|
@hide="editingItem = null;wishlistItemCreationOpened = false"
|
||||||
|
@update:visible="editingItem = null;wishlistItemCreationOpened = false"
|
||||||
|
class="w-5/6 md:!w-2/6">
|
||||||
|
<WishlistItemCreationView :editItem="editingItem" :wishlistId="wishlist.id" @item-created="itemCreated"
|
||||||
|
@creation-cancelled="editingItem = null;wishlistItemCreationOpened = false"/>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog :visible="shareDialogOpened" header="Поделиться" @hide="shareDialogOpened = false"
|
||||||
|
@update:visible="shareDialogOpened=false" modal>
|
||||||
|
<div class="flex flex-row gap-2 w-full justify-center">
|
||||||
|
<FloatLabel variant="on" class="w-full mt-1">
|
||||||
|
<label for="link">Ссылка</label>
|
||||||
|
<InputText id="link" v-model="shareLink" class="w-full"/>
|
||||||
|
</FloatLabel>
|
||||||
|
<button @click="copyToClipboard(shareLink)">
|
||||||
|
{{ !copied ? 'Копировать' : 'Скопировано!' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<div class=" flex flex-col gap-3 justify-between h-full ">
|
||||||
|
<div class="flex flex-col justify-between gap-5">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col md:flex-row gap-2 items-start md:items-end">
|
||||||
|
<h2 class="text-4xl font-bold text-gray-700">Вишлист {{ wishlist.name }} </h2>
|
||||||
|
<button @click="shareDialogOpened=true"><span class="text-sm text-gray-600">Поделиться <i
|
||||||
|
class="pi pi-arrow-up-right" style="font-size: 0.65rem"/></span></button>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="flex flex-row gap-2 text-xl text-gray-700">{{ formatDate(budget.dateFrom) }} - -->
|
||||||
|
<!-- {{ formatDate(budget.dateTo) }}-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<p class="text-lg text-gray-500">{{ wishlist.description }}</p>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<div class="relative flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center">
|
||||||
|
<!-- Первая буква имени -->
|
||||||
|
<span class="text-white text-center">{{ wishlist.owner.firstName.substring(0, 1) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-semibold">{{ wishlist.owner.firstName }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<p class="text-2xl text-gray-700">Желания</p>
|
||||||
|
<Button label="+ Создать" @click="wishlistItemCreationOpened=true" size="small"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-2">
|
||||||
|
<div v-for="item in wishlist.items"
|
||||||
|
class=" bg-white flex flex-col p-4 flex-shrink-0 gap-4 shadow-md rounded-lg max-w-128 h-full">
|
||||||
|
<!-- <div v-if="true" >-->
|
||||||
|
<!-- <LoadingView :halfscreen="true"/>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col w-full justify-center gap-2">
|
||||||
|
<div v-if="item.images.length > 0 && selectedImage.get(item.id)" class="flex w-full justify-center">
|
||||||
|
<Image
|
||||||
|
:src="selectedImage.get(item.id).startsWith('http') ? selectedImage.get(item.id) : apiClient.defaults.baseURL+'/'+selectedImage.get(item.id) "
|
||||||
|
alt="Image"
|
||||||
|
width="128" height="128" show="show" preview
|
||||||
|
imageClass="h-64 w-64 object-cover items-center justify-center justify-items-center"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row !h-12 gap-2">
|
||||||
|
|
||||||
|
<div v-for="(image, index) in item.images" class="group relative h-12 w-12 rounded-lg shadow-md ">
|
||||||
|
<button @click="selectedImage.set(item.id, image)">
|
||||||
|
<Image
|
||||||
|
:src="image.startsWith('http') ? image : apiClient.defaults.baseURL+'/'+image " alt="Image"
|
||||||
|
width="48" height="48" show="show"
|
||||||
|
imageClass="w-12 h-12 object-cover"/>
|
||||||
|
</button>
|
||||||
|
<!-- Иконка для удаления -->
|
||||||
|
<button @click="deleteImage(item, index)">
|
||||||
|
<i class="pi pi-minus absolute -top-2 right-0 text-red-400 z-10 bg-white rounded-full p-[0.2rem] border-2 !hidden group-hover:!block "
|
||||||
|
style="font-size: 0.6rem"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-gray-100 rounded flex items-center justify-center group">
|
||||||
|
<div
|
||||||
|
class="rounded-full w-9 h-9 bg-gray-400 opacity-30 group-hover:opacity-70 flex items-center justify-center">
|
||||||
|
<button @click="imageUploadVisible=true;imageUploadForItemId=item.id">
|
||||||
|
<i class="pi pi-plus !font-bold !text-xl text-white"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-between gap-2 h-full">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-row w-full justify-between">
|
||||||
|
<p class="font-semibold text-xl text-gray-700">{{ item.name }}</p>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button @click="editingItem=item;wishlistItemCreationOpened=true"><i
|
||||||
|
class="pi pi-pen-to-square"/>
|
||||||
|
</button>
|
||||||
|
<button @click="deleteItem(item)"><i class="pi pi-trash"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="font-bold text-lg text-emerald-700">{{ formatAmount(item.price) }} ₽</p>
|
||||||
|
<p class="font-light text-gray-700 text-wrap">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <div class="flex flex-row !h-12 gap-2">-->
|
||||||
|
<!-- <div v-for="(image, index) in item.images" class="group relative h-12 w-12 rounded-lg shadow-md ">-->
|
||||||
|
<!-- <Image-->
|
||||||
|
<!-- :src="image.startsWith('http') ? image : apiClient.defaults.baseURL+'/'+image " alt="Image"-->
|
||||||
|
<!-- width="48" height="48" show="show" preview-->
|
||||||
|
<!-- imageClass="w-12 h-12 object-cover"/>-->
|
||||||
|
<!-- <!– Иконка для удаления –>-->
|
||||||
|
<!-- <button @click="deleteImage(item, index)">-->
|
||||||
|
<!-- <i class="pi pi-minus absolute -top-2 right-0 text-red-400 z-10 bg-white rounded-full p-[0.2rem] border-2 !hidden group-hover:!block "-->
|
||||||
|
<!-- style="font-size: 0.6rem"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </button>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="w-12 h-12 bg-gray-100 rounded flex items-center justify-center group">-->
|
||||||
|
<!-- <div-->
|
||||||
|
<!-- class="rounded-full w-9 h-9 bg-gray-400 opacity-30 group-hover:opacity-70 flex items-center justify-center">-->
|
||||||
|
<!-- <button @click="imageUploadVisible=true;imageUploadForItemId=item.id">-->
|
||||||
|
<!-- <i class="pi pi-plus !font-bold !text-xl text-white"/>-->
|
||||||
|
<!-- </button>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 w-full justify-between items-center">
|
||||||
|
<a :href="item.link"
|
||||||
|
target="_blank">
|
||||||
|
<Button label="В магазин" icon="pi pi-arrow-up-right" iconPos="right"/>
|
||||||
|
</a>
|
||||||
|
<span v-if="!item.reservedBy" class="text-gray-500">Не забронировано</span>
|
||||||
|
<Button v-else @mouseover="hoveredCancelReservationButton.set(item.id, true)"
|
||||||
|
@mouseleave="hoveredCancelReservationButton.delete(item.id)"
|
||||||
|
:label="hoveredCancelReservationButton.get(item.id) ? 'Отменить' : 'Забронировано'"
|
||||||
|
:disabled="!hoveredCancelReservationButton.get(item.id)"
|
||||||
|
:severity="hoveredCancelReservationButton.get(item.id) ? 'danger' : ''"
|
||||||
|
class="w-full"
|
||||||
|
@click="hoveredCancelReservationButton.get(item.id) ? cancelReserve(item): ''"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flex-row > Image {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-image-preview-mask {
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
22
src/models/WishList.ts
Normal file
22
src/models/WishList.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
export class WishList {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
isPrivate: boolean
|
||||||
|
items: WishlistItem[]
|
||||||
|
updatedAt: Date
|
||||||
|
createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WishlistItem {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
price: number
|
||||||
|
link: string
|
||||||
|
images: string[]
|
||||||
|
updatedAt: Date
|
||||||
|
createdAt: Date
|
||||||
|
imagesWithLinks: []
|
||||||
|
}
|
||||||
@@ -16,11 +16,17 @@ import SpacesList from "@/components/spaces/SpacesList.vue";
|
|||||||
import SpaceInventationView from "@/components/spaces/SpaceInventationView.vue";
|
import SpaceInventationView from "@/components/spaces/SpaceInventationView.vue";
|
||||||
import About from "@/components/faq/About.vue";
|
import About from "@/components/faq/About.vue";
|
||||||
import OnboardingView from "@/components/onboarding/OnboardingView.vue";
|
import OnboardingView from "@/components/onboarding/OnboardingView.vue";
|
||||||
|
import WishListListView from "@/components/wishlists/WishListListView.vue";
|
||||||
|
import WishlistView from "@/components/wishlists/WishlistView.vue";
|
||||||
|
import WishlistExternalView from "@/components/wishlists/WishlistExternalView.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{path: '/login', component: LoginView},
|
{path: '/login', component: LoginView},
|
||||||
{path: '/register', component: RegisterView},
|
{path: '/register', component: RegisterView},
|
||||||
{path: '/', name: 'Budgets Main', component: BudgetList, meta: {requiresAuth: true}},
|
{path: '/', name: 'Budgets Main', component: BudgetList, meta: {requiresAuth: true}},
|
||||||
|
{path: '/wishlists', name: 'Wishlists', component: WishListListView, meta: {requiresAuth: true}},
|
||||||
|
{path: '/wishlists/:id', name: 'Wishlist view', component: WishlistView, meta: {requiresAuth: true}},
|
||||||
|
{path: '/mywishlist/:id', name: 'Wishlist view export', component: WishlistExternalView},
|
||||||
// {path: '/onboarding', name: 'Onboarding', component: OnboardingView, meta: {requiresAuth: true}},
|
// {path: '/onboarding', name: 'Onboarding', component: OnboardingView, meta: {requiresAuth: true}},
|
||||||
{path: '/about', name: 'About', component: About, meta: {requiresAuth: true}},
|
{path: '/about', name: 'About', component: About, meta: {requiresAuth: true}},
|
||||||
{path: '/analytics', name: 'Analytics', component: AnalyticsView, meta: {requiresAuth: true}},
|
{path: '/analytics', name: 'Analytics', component: AnalyticsView, meta: {requiresAuth: true}},
|
||||||
|
|||||||
13
src/services/StaticService.ts
Normal file
13
src/services/StaticService.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import apiClient from "@/services/axiosSetup";
|
||||||
|
|
||||||
|
export const uploadStatic = async ( wishlistItemId: string, file) => {
|
||||||
|
const spaceStore = useSpaceStore()
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file", file);
|
||||||
|
return await apiClient.post(`/static/${spaceStore.space?.id}/wishlists/${wishlistItemId}`, form)
|
||||||
|
.then((res) => res.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
118
src/services/WishListService.ts
Normal file
118
src/services/WishListService.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import {useSpaceStore} from "@/stores/spaceStore";
|
||||||
|
import apiClient from "@/services/axiosSetup";
|
||||||
|
import {WishList, WishlistItem} from "@/models/WishList";
|
||||||
|
|
||||||
|
export const getWishlists = async () => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/wishlists`)
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getWishlist = async (id: string) => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.get(`/spaces/${spaceStore.space?.id}/wishlists/${id}`).then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export const getWishlistExternal = async (id: string) => {
|
||||||
|
return await apiClient.get(`/wishlistexternal/${id}`).then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export const reserveWishlistItem = async (wishlistId: string, wishlistItemId: string, reservedBy: string, aidCookie: string) => {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("reservedBy", reservedBy);
|
||||||
|
return await apiClient.post(`/wishlistexternal/${wishlistId}/${wishlistItemId}/reserve/_create`, {
|
||||||
|
'aid': aidCookie,
|
||||||
|
'name': reservedBy
|
||||||
|
}, {headers: {"Content-Type": "application/json"}})
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const cancelReserveWishlistItem = async (wishlistId: string, wishlistItemId: string, reservedBy = null, aidCookie = null) => {
|
||||||
|
if (reservedBy) {
|
||||||
|
return await apiClient.post(`/wishlistexternal/${wishlistId}/${wishlistItemId}/reserve/_cancel`, {
|
||||||
|
'aid': aidCookie,
|
||||||
|
'name': reservedBy
|
||||||
|
}, {headers: {"Content-Type": "application/json"}})
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.post(`/spaces/${spaceStore.space?.id}/wishlists/${wishlistId}/${wishlistItemId}/reserve/_cancel`, {
|
||||||
|
'aid': aidCookie,
|
||||||
|
'name': reservedBy
|
||||||
|
}, {headers: {"Content-Type": "application/json"}})
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createWishlistRequest = async (data: WishList) => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.post(`/spaces/${spaceStore.space?.id}/wishlists`, data)
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateWishlistRequest = async (data: WishList) => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.patch(`/spaces/${spaceStore.space?.id}/wishlists/${data.id}`, data)
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addWishListItemRequest = async (wishlistId: string, data: WishlistItem) => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.post(`/spaces/${spaceStore.space?.id}/wishlists/${wishlistId}/items`, data)
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateWishListItemRequest = async (wishlistId: string, data: WishlistItem) => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.patch(`/spaces/${spaceStore.space?.id}/wishlists/${wishlistId}/items/${data.id}`, data)
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteWishListItemRequest = async (wishlistId: string, data: WishlistItem) => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/wishlists/${wishlistId}/items/${data.id}`)
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const deleteWishlistRequest = async (data: string) => {
|
||||||
|
const spaceStore = useSpaceStore();
|
||||||
|
return await apiClient.delete(`/spaces/${spaceStore.space?.id}/wishlists/${data}`)
|
||||||
|
.then((data: any) => data.data)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user