wishlists

This commit is contained in:
xds
2025-03-03 10:33:14 +03:00
parent a8bdb0eeab
commit 6c623918b0
20 changed files with 1471 additions and 20 deletions

View File

@@ -3,15 +3,16 @@
<div id="app" class="flex flex-col h-screen bg-gray-100">
<Toast/>
<!-- MenuBar всегда фиксирован сверху -->
<MenuBar v-if="userStore.user" class="w-full sticky hidden lg:block top-0 z-10"/>
<ToolBar class=" fixed visible lg:invisible bottom-0 z-10"/>
<MenuBar v-if="userStore.user && !route.path.startsWith('/mywishlist')" class="w-full sticky hidden lg:block top-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">
<!-- {{ tg_id }}-->
<Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>
<!-- <Button label="Sub" :class="checkNotif ? 'flex' : '!hidden'" @click="checkSubscribe"/>-->
<router-view/>
<div class="bg-gray-100 h-12 block lg:hidden"></div>
</div>
@@ -25,12 +26,13 @@
</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="/spaces" class="hover:underline">Пространства</router-link>
<router-link to="/analytics" class="hover:underline">Аналитика</router-link>
<router-link to="/budgets" class="hover:underline">Бюджеты</router-link>
<router-link to="/transactions" class="hover:underline">Транзакции</router-link>
<router-link to="/wishlists" class="hover:underline">Вишлисты</router-link>
<router-link to="/settings" class="hover:underline">Настройки</router-link>
</div>
@@ -64,8 +66,12 @@ import {useDrawerStore} from '@/stores/drawerStore'
import TransactionForm from "@/components/transactions/TransactionForm.vue";
import {useSpaceStore} from "@/stores/spaceStore";
import Toast from "primevue/toast";
import {useRoute} from "vue-router";
import Cookies from "js-cookie";
const route = useRoute()
const drawerStore = useDrawerStore();
const visible = computed(() => drawerStore.visible);
@@ -130,6 +136,11 @@ onMounted(async () => {
await spaceStore.fetchSpaces()
}
// document.cookie = `aid=${crypto.randomUUID()}`
if (!Cookies.get("aid")) {
Cookies.set("aid", crypto.randomUUID(), { expires: 36500, path: "/" })
}
});

View File

@@ -1,12 +1,14 @@
<script setup lang="ts">
import ProgressSpinner from "primevue/progressspinner";
const props = defineProps({
halfscreen: Boolean ,
})
</script>
<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>
@@ -16,7 +18,7 @@ import ProgressSpinner from "primevue/progressspinner";
style="width: 50px; height: 50px;"
strokeWidth="8"
fill="transparent"
animationDuration=".5s"
animationDuration="1s"
aria-label="Custom ProgressSpinner"
/>
</div>

View File

@@ -12,7 +12,7 @@
<!-- fill="var(&#45;&#45;p-text-color)"-->
<!-- />-->
<!-- </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 #item="{ item, props, hasSubmenu, root }">
<router-link :to="item.url" v-ripple class="flex items-center" v-bind="props.action">
@@ -145,9 +145,14 @@ const items = ref([
},
{
label: 'Транзакции',
icon: "pi pi-star",
icon: "pi pi-dollar",
url: '/transactions'
},
{
label: 'Вишлисты',
icon: "pi pi-star",
url: '/wishlists'
},
{
label: 'Настройки',
icon: 'pi pi-envelope',

View File

@@ -32,7 +32,14 @@
<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-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>
</router-link>
</div>

View File

@@ -310,7 +310,7 @@ onMounted(async () => {
<!-- «Растяжка», чтобы было за что «скроллить» -->
<div class="min-w-[550px] md:min-w-[650px] lg:min-w-[850px]">
<Chart
type="line"
type="bar"
:data="preparedChartData"
:options="chartOptions"
class="h-72 sm:h-full sm:w-full "

View File

@@ -106,7 +106,7 @@ const login = async () => {
loading.value = true
try {
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('/')
} catch (error) {
toast.add({ severity: 'error', summary: 'Ошибка входа', detail: 'Неверные данные', life: 3000 })

View 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 ">-->
<!-- &lt;!&ndash; <Button label="Back" severity="secondary" icon="pi pi-arrow-left" @click="activateCallback('1')" />&ndash;&gt;-->
<!-- <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>

View File

@@ -268,16 +268,11 @@ onMounted(async () => {
:key="user.id"
@mouseover="user.isHovered = true"
@mouseleave="user.isHovered = false">
<div
class="relative flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center"
>
<div class="relative flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center">
<!-- Первая буква имени -->
<span class="text-white text-center">
{{ user.firstName.substring(0, 1) }}
</span>
<!-- Иконка короны для владельца -->
<i
v-if="space.owner.id === user.id"

View File

@@ -116,7 +116,8 @@ const selectedSpace = computed(() => spaceStore.space)
watch(selectedSpace, async (newValue, oldValue) => {
if (newValue != oldValue) {
await fetchTransactions(false)
transactions.value = [];
await fetchTransactions(true)
}
})
const types = ref([])
@@ -183,7 +184,7 @@ onUnmounted(async () => {
@transaction-updated="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>
</div>
<!-- Показать спиннер загрузки, если идет загрузка -->

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

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

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

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

View 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"/>-->
<!-- &lt;!&ndash; Иконка для удаления &ndash;&gt;-->
<!-- <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
View 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: []
}

View File

@@ -16,11 +16,17 @@ import SpacesList from "@/components/spaces/SpacesList.vue";
import SpaceInventationView from "@/components/spaces/SpaceInventationView.vue";
import About from "@/components/faq/About.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 = [
{path: '/login', component: LoginView},
{path: '/register', component: RegisterView},
{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: '/about', name: 'About', component: About, meta: {requiresAuth: true}},
{path: '/analytics', name: 'Analytics', component: AnalyticsView, meta: {requiresAuth: true}},

View 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;
})
}

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