Files
Avtex/app/pages/admin/groups/[id].vue
T

247 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="min-h-screen bg-slate-950 p-6">
<div class="max-w-4xl mx-auto" v-if="group">
<div class="flex items-center gap-4 mb-8 text-sm">
<NuxtLink to="/admin/groups" class="text-slate-500 hover:text-white transition">Группы</NuxtLink>
<span class="text-slate-700">/</span>
<span class="text-slate-300">{{ group.name }}</span>
</div>
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-8 mb-8">
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold text-white">{{ group.name }}</h1>
<button @click="deleteGroup" class="px-4 py-2 bg-red-900/20 text-red-400 hover:bg-red-900/40 rounded-lg text-sm transition">Удалить группу</button>
</div>
<p class="text-slate-400">{{ group.description || 'Нет описания' }}</p>
</div>
<!-- Members Section -->
<div class="bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden mb-8">
<div class="p-6 border-b border-slate-800 flex items-center justify-between">
<h2 class="text-xl font-bold text-white">Участники ({{ members.length }})</h2>
<button @click="showAddMemberModal = true" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-bold transition">+ Добавить</button>
</div>
<div class="divide-y divide-slate-800">
<div v-for="m in members" :key="m.id" class="p-4 flex items-center justify-between hover:bg-slate-800/30 transition">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-slate-800 rounded-full flex items-center justify-center font-bold text-slate-400">
{{ m.user.username.charAt(0).toUpperCase() }}
</div>
<div>
<div class="text-slate-200 font-medium">{{ m.user.username }}</div>
<div class="text-xs text-slate-500">
<select
v-if="group.ownerId === auth.user.value?.id || auth.user.value?.role === 'admin'"
:value="m.role"
@change="(e) => updateMemberRole(m.userId, e.target.value)"
class="bg-transparent border-none p-0 focus:ring-0 text-blue-400 cursor-pointer outline-none"
:disabled="m.userId === group.ownerId"
>
<option value="admin">Админ</option>
<option value="editor">Редактор</option>
<option value="viewer">Зритель</option>
</select>
<span v-else>{{ m.role }}</span>
</div>
</div>
</div>
<button v-if="m.role !== 'admin'" @click="removeMember(m.userId)" class="text-slate-600 hover:text-red-400 transition text-xl">&times;</button>
</div>
</div>
</div>
<!-- Canvases Section -->
<div class="bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden">
<div class="p-6 border-b border-slate-800 flex items-center justify-between">
<h2 class="text-xl font-bold text-white">Карты группы ({{ group.canvases?.length || 0 }})</h2>
<NuxtLink to="/canvases" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm font-bold transition">Управлять в Проектах</NuxtLink>
</div>
<div class="divide-y divide-slate-800">
<div v-for="c in group.canvases" :key="c.id" class="p-4 flex items-center justify-between hover:bg-slate-800/30 transition">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-blue-900/20 rounded-lg flex items-center justify-center text-xl">🗺️</div>
<div>
<div class="text-slate-200 font-medium">{{ c.name }}</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider">Обновлена: {{ new Date(c.updatedAt).toLocaleDateString() }}</div>
</div>
</div>
<button @click="unassignCanvas(c.id)" class="text-slate-600 hover:text-red-400 transition text-sm font-bold">Отвязать</button>
</div>
<div v-if="!group.canvases?.length" class="p-8 text-center text-slate-500 italic">
В этой группе пока нет карт.
</div>
</div>
</div>
<!-- Add Member Modal -->
<div v-if="showAddMemberModal" class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4" @click.self="showAddMemberModal = false">
<div class="bg-slate-900 border border-slate-700 rounded-3xl p-8 w-full max-w-md shadow-2xl">
<h2 class="text-2xl font-bold text-white mb-6 flex items-center gap-2">
<span>👤</span> Добавить участника
</h2>
<div class="space-y-4">
<div>
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider block mb-2">Поиск пользователя</label>
<div class="relative">
<input
v-model="searchQuery"
@input="searchUsers"
placeholder="Введите имя или email..."
class="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white outline-none focus:border-blue-500 transition"
/>
<div v-if="searching" class="absolute right-4 top-3.5">
<div class="animate-spin h-5 w-5 border-2 border-blue-500 border-t-transparent rounded-full"></div>
</div>
</div>
</div>
<!-- Search Results -->
<div v-if="searchResults.length > 0" class="max-h-60 overflow-y-auto bg-slate-800/50 rounded-xl border border-slate-700 divide-y divide-slate-700">
<div v-for="u in searchResults" :key="u.id" class="p-3 flex items-center justify-between hover:bg-slate-700/50 transition">
<div class="flex flex-col">
<span class="text-sm font-medium text-white">{{ u.username }}</span>
<span class="text-[10px] text-slate-500">{{ u.email }}</span>
</div>
<button
@click="addMember(u.id)"
class="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded-lg transition"
>
Выбрать
</button>
</div>
</div>
<div v-else-if="searchQuery.length >= 2" class="text-center py-4 text-slate-500 text-sm">
Пользователи не найдены
</div>
</div>
<div class="mt-8">
<button @click="showAddMemberModal = false" class="w-full py-3 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl font-bold transition">
Закрыть
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: ['auth', 'admin']
})
const route = useRoute()
const auth = useAuth()
const group = ref(null)
const members = ref([])
const showAddMemberModal = ref(false)
const searchQuery = ref('')
const searchResults = ref([])
const searching = ref(false)
const searchUsers = async () => {
if (searchQuery.value.length < 2) {
searchResults.value = []
return
}
searching.value = true
try {
const data = await $fetch('/api/users/search', {
params: { q: searchQuery.value },
headers: { Authorization: `Bearer ${auth.token.value}` }
})
searchResults.value = data.users || []
} catch (e) {
console.error('Search failed', e)
} finally {
searching.value = false
}
}
const addMember = async (userId) => {
try {
await $fetch(`/api/groups/${route.params.id}/add-member`, {
method: 'POST',
body: { userId, role: 'member' },
headers: { Authorization: `Bearer ${auth.token.value}` }
})
showAddMemberModal.value = false
searchQuery.value = ''
searchResults.value = []
await fetchGroup()
} catch (e) {
alert(e.data?.message || 'Ошибка при добавлении')
}
}
const updateMemberRole = async (userId, role) => {
try {
await $fetch(`/api/groups/${route.params.id}/members`, {
method: 'PATCH',
body: { userId, role },
headers: { Authorization: `Bearer ${auth.token.value}` }
})
await fetchGroup()
} catch (e) {
alert('Ошибка при смене роли')
}
}
const removeMember = async (userId) => {
if (!confirm('Удалить участника из группы?')) return
try {
await $fetch(`/api/groups/${route.params.id}/members`, {
method: 'DELETE',
body: { userId },
headers: { Authorization: `Bearer ${auth.token.value}` }
})
await fetchGroup()
} catch (e) {
alert('Ошибка при удалении')
}
}
const unassignCanvas = async (canvasId) => {
if (!confirm('Отвязать карту от группы? Она станет личной картой владельца.')) return
try {
await $fetch(`/api/canvases/${canvasId}`, {
method: 'PATCH',
body: { groupId: null },
headers: { Authorization: `Bearer ${auth.token.value}` }
})
await fetchGroup()
} catch (e) {
alert('Ошибка при отвязке карты')
}
}
const fetchGroup = async () => {
try {
const data = await $fetch(`/api/groups/${route.params.id}`, {
headers: { Authorization: `Bearer ${auth.token.value}` }
})
group.value = data.group
members.value = data.group.members || []
} catch (e) {
console.error('Failed to fetch group', e)
navigateTo('/admin/groups')
}
}
const deleteGroup = async () => {
if (!confirm('Вы уверены, что хотите удалить эту группу?')) return
try {
await $fetch(`/api/groups/${route.params.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${auth.token.value}` }
})
navigateTo('/admin/groups')
} catch (e) {
alert('Ошибка при удалении')
}
}
onMounted(fetchGroup)
</script>