343 lines
12 KiB
Vue
343 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import {computed, onMounted, ref, watch, reactive, onUnmounted} from 'vue';
|
|
import {Space, SpaceInvite} from "@/models/Space";
|
|
import {
|
|
createSpaceInvite,
|
|
deleteSpaceRequest,
|
|
getSpaces,
|
|
kickMemberFromSpaceRequest,
|
|
leaveSpaceRequest
|
|
} from "@/services/spaceService";
|
|
import LoadingView from "@/components/LoadingView.vue";
|
|
import {formatDate, formatDateTime} from "../../utils/utils";
|
|
import Button from "primevue/button";
|
|
import Toast from "primevue/toast";
|
|
import Dialog from "primevue/dialog";
|
|
import InputText from "primevue/inputtext";
|
|
import SpaceCreationDialog from "@/components/spaces/SpaceCreationDialog.vue";
|
|
import {useToast} from "primevue/usetoast";
|
|
import {deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
|
|
import ConfirmDialog from "primevue/confirmdialog";
|
|
import {useConfirm} from "primevue/useconfirm";
|
|
import {useSpaceStore} from "@/stores/spaceStore";
|
|
import {EventBus} from "@/utils/EventBus";
|
|
import {useUserStore} from "@/stores/userStore";
|
|
import {User} from "@/models/User";
|
|
|
|
const toast = useToast()
|
|
const confirm = useConfirm();
|
|
const loading = ref(true);
|
|
const creationOpened = ref(false);
|
|
const spaceCreated = async () => {
|
|
creationOpened.value = false;
|
|
await spaceStore.fetchSpaces();
|
|
}
|
|
const invite = ref<SpaceInvite | null>(null);
|
|
const inviteUrl = computed(() => {
|
|
if (invite.value) {
|
|
return 'https://luminic.space/spaces/invite/' + invite.value.code;
|
|
}
|
|
})
|
|
|
|
const inviteCreatedDialog = ref<boolean | null>(null);
|
|
|
|
const createInvite = async (space: Space) => {
|
|
confirm.require({
|
|
message: `Вы действительно хотите создать приглашение в пространство ${space.name}?`,
|
|
header: 'Создание приграшения',
|
|
icon: 'pi pi-info-circle',
|
|
rejectLabel: 'Отмена',
|
|
rejectProps: {
|
|
label: 'Отмена',
|
|
severity: 'secondary',
|
|
outlined: true
|
|
},
|
|
acceptProps: {
|
|
label: 'Создать',
|
|
severity: 'success'
|
|
},
|
|
accept: async () => {
|
|
try {
|
|
await createSpaceInvite().then(async (res) => {
|
|
await spaceStore.fetchSpaces();
|
|
invite.value = res;
|
|
inviteCreatedDialog.value = true;
|
|
|
|
})
|
|
} catch (error) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Ошибка!',
|
|
detail: error.response?.data?.message || 'Ошибка при создании приглашения',
|
|
life: 3000
|
|
});
|
|
}
|
|
},
|
|
reject: () => {
|
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили создание приглашения', life: 3000});
|
|
}
|
|
});
|
|
|
|
}
|
|
const leaveSpace = async (space: Space) => {
|
|
|
|
confirm.require({
|
|
message: `Вы действительно хотите выйти из пространства ${space.name}?`,
|
|
header: 'Выход из пространства',
|
|
icon: 'pi pi-info-circle',
|
|
rejectLabel: 'Отмена',
|
|
rejectProps: {
|
|
label: 'Отмена',
|
|
severity: 'secondary',
|
|
outlined: true
|
|
},
|
|
acceptProps: {
|
|
label: 'Выйти',
|
|
severity: 'danger'
|
|
},
|
|
accept: async () => {
|
|
try {
|
|
await leaveSpaceRequest(space.id).then((res) => {
|
|
spaceStore.fetchSpaces();
|
|
})
|
|
} catch (error) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Ошибка!',
|
|
detail: error.response?.data?.message || 'Ошибка при выходе из пространства',
|
|
life: 3000
|
|
});
|
|
}
|
|
},
|
|
reject: () => {
|
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили выход', life: 3000});
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
const kickMember = async (space: Space, user: User) => {
|
|
|
|
confirm.require({
|
|
message: `Вы действительно хотите исключить пользователя ${user.firstName} из пространства ${space.name}?`,
|
|
header: 'Исключить из пространства',
|
|
icon: 'pi pi-info-circle',
|
|
rejectLabel: 'Отмена',
|
|
rejectProps: {
|
|
label: 'Отмена',
|
|
severity: 'secondary',
|
|
outlined: true
|
|
},
|
|
acceptProps: {
|
|
label: 'Исключить',
|
|
severity: 'danger'
|
|
},
|
|
accept: async () => {
|
|
try {
|
|
await kickMemberFromSpaceRequest(space.id, user.username).then((res) => {
|
|
spaceStore.fetchSpaces();
|
|
})
|
|
} catch (error) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Ошибка!',
|
|
detail: error.response?.data?.message || 'Ошибка при исключении из пространства',
|
|
life: 3000
|
|
});
|
|
}
|
|
},
|
|
reject: () => {
|
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили исключение', life: 3000});
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
const deleteSpace = async (space: Space) => {
|
|
|
|
confirm.require({
|
|
message: `Вы действительно хотите удалить пространство ${space.name}? Будут удалены все бюджеты!`,
|
|
header: 'Удаление пространства',
|
|
icon: 'pi pi-info-circle',
|
|
rejectLabel: 'Отмена',
|
|
rejectProps: {
|
|
label: 'Отмена',
|
|
severity: 'secondary',
|
|
outlined: true
|
|
},
|
|
acceptProps: {
|
|
label: 'Удалить',
|
|
severity: 'danger'
|
|
},
|
|
accept: async () => {
|
|
try {
|
|
await deleteSpaceRequest(space.id).then((res) => {
|
|
spaceStore.fetchSpaces();
|
|
})
|
|
} catch (error: Error) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Ошибка!',
|
|
detail: error.response?.data?.message || 'Ошибка при удалении пространства',
|
|
life: 3000
|
|
});
|
|
}
|
|
},
|
|
reject: () => {
|
|
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили удаление', life: 3000});
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
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 spaces = computed(() => spaceStore.spaces)
|
|
|
|
|
|
const spaceStore = useSpaceStore()
|
|
const selectedSpace = computed(() => spaceStore.space)
|
|
|
|
const selectSpace = (space: Space) => {
|
|
spaceStore.setSpace(space);
|
|
localStorage.setItem("spaceId", space.id);
|
|
}
|
|
const loadingValue = computed(() => {
|
|
return !spaces.value
|
|
})
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
})
|
|
|
|
</script>
|
|
|
|
<template>
|
|
|
|
<LoadingView v-if="loadingValue"/>
|
|
|
|
<div v-else class="p-4 bg-gray-100 grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 ">
|
|
<Toast/>
|
|
<ConfirmDialog/>
|
|
<Dialog :visible="inviteCreatedDialog" header="Приглашение" @hide="inviteCreatedDialog=false"
|
|
@update:visible="inviteCreatedDialog=false">
|
|
<div class="flex flex-col gap-4 justify-start">
|
|
<div class="flex flex-row gap-2 items-center"> Ссылка приглашения:
|
|
<input class="p-2 border w-5/6" v-model="inviteUrl" disabled></input>
|
|
<button @click="copyToClipboard('https://luminic.space/spaces/invite/' + invite.code)">
|
|
{{ !copied ? 'Копировать' : 'Скопировано!' }}
|
|
</button>
|
|
</div>
|
|
<p>Действует до {{ formatDateTime(invite.activeTill) }} и только на 1 использование.</p>
|
|
</div>
|
|
</Dialog>
|
|
<div v-for="space in spaces" class="w-full ">
|
|
|
|
<div
|
|
class="bg-white p-4 shadow-lg rounded-lg flex flex-col gap-2 items-start justify-between h-[200px] ">
|
|
<div class="flex flex-col w-full h-full justify-between">
|
|
|
|
<div class="flex flex-row justify-between"><p class="text-xl font-bold line-clamp-1">{{ space.name }}</p>
|
|
<button @click="selectSpace(space)">
|
|
|
|
<div class="flex p-1 rounded border"
|
|
:class="selectedSpace? selectedSpace.id === space.id ? 'bg-green-100' : 'bg-gray-100':'bg-gray-100'"
|
|
@click="spaceStore.setSpace(space)">
|
|
<span v-if="selectedSpace? space.id === selectedSpace.id : false"
|
|
class="font-bold text-gray-500 items-center"><i
|
|
class="pi pi-check"/> Выбрано</span>
|
|
<span v-else class="">Выбрать</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<p class="line-clamp-2 items-start flex ">{{ space.description }}</p>
|
|
<div class="flex flex-row gap-2">
|
|
<div v-for="user in space.users"
|
|
: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"
|
|
|
|
>
|
|
<!-- Первая буква имени -->
|
|
<span class="text-white text-center">
|
|
{{ user.firstName.substring(0, 1) }}
|
|
</span>
|
|
|
|
<!-- Иконка короны для владельца -->
|
|
<i
|
|
v-if="space.owner.id === user.id"
|
|
class="pi pi-crown absolute -top-1 -right-1 text-yellow-400 z-10 bg-white rounded-full p-[0.2rem] border"
|
|
style="font-size: 0.8rem"
|
|
></i>
|
|
|
|
<!-- Всплывающее окно -->
|
|
<div
|
|
v-if="user.isHovered"
|
|
class="absolute top-10 left-20 transform -translate-x-1/2 bg-white shadow-lg rounded-lg p-2 w-40 z-50 border"
|
|
>
|
|
<p class="text-sm font-semibold text-gray-800">{{ user.firstName }} {{ user.lastName }}</p>
|
|
<p class="text-xs text-gray-500">Роль: {{ user.role || 'Пользователь' }}</p>
|
|
|
|
<!-- Кнопка удаления (только если это не текущий пользователь) -->
|
|
<button
|
|
v-if="user.id !== useUserStore().user.id"
|
|
@click="kickMember(space, user)"
|
|
class="mt-2 bg-red-500 text-white text-xs rounded p-1 w-full hover:bg-red-600 transition"
|
|
>
|
|
Удалить
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="space.owner.id == useUserStore().user.id"
|
|
class="flex bg-gray-300 hover:bg-emerald-300 rounded-full w-10 h-10 items-center justify-center relative"
|
|
>
|
|
<i class="text-white text-center pi pi-plus" @click="createInvite(space)"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-row justify-between items-center w-full">
|
|
<p class="text-sm text-gray-600">Создано: {{ formatDate(space.createdAt) }}</p>
|
|
<button v-if="space.owner.id == useUserStore().user.id" @click="deleteSpace(space)"
|
|
class="flex justify-end"><i class="pi pi-trash"
|
|
style="color:red; font-size: 1.2rem"/>
|
|
</button>
|
|
<button v-else class="flex items-center gap-2 text-sm" @click="leaveSpace(space)">Выйти<i
|
|
class="pi pi-sign-out" style="font-size: 0.7rem"/></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="w-full h-full">
|
|
<div class="bg-white p-4 shadow-lg rounded-lg flex flex-col gap-2 justify-center items-center h-[200px]">
|
|
<button class="flex-col" @click="creationOpened = !creationOpened">
|
|
<i class="pi pi-plus-circle text-emerald-300" style="font-size: 2.5rem"></i>
|
|
<p class="text-emerald-600">Создать пространство</p>
|
|
</button>
|
|
<SpaceCreationDialog :opened="creationOpened" @spaceCreated="spaceCreated"
|
|
@close-modal="creationOpened = false"/>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
|
|
</style> |