feat: restructure into XFlow monorepo with Nuxt 4, Modular Onion Monolith, Android client, and Desktop CLI

This commit is contained in:
2026-09-28 10:39:19 +05:00
parent edcd30c829
commit be68f5e130
204 changed files with 24616 additions and 39 deletions
+173
View File
@@ -0,0 +1,173 @@
<template>
<div class="min-h-screen bg-slate-950 p-6">
<div class="max-w-7xl mx-auto">
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl font-bold text-slate-100">Управление Группами</h1>
<p class="text-slate-400 mt-1">Создание и управление командами, участниками и правами</p>
</div>
<button
@click="showCreateModal = true"
class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition flex items-center gap-2 shadow-lg shadow-blue-900/20"
>
<span class="text-xl">+</span> Новая Группа
</button>
</div>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
<!-- Groups Grid -->
<div v-else-if="groups.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="group in groups"
:key="group.id"
class="bg-slate-900 border border-slate-800 rounded-2xl p-6 hover:border-slate-700 transition group"
>
<div class="flex items-start justify-between mb-4">
<div class="w-12 h-12 bg-blue-900/30 rounded-xl flex items-center justify-center text-blue-400 text-xl font-bold">
{{ group.name.charAt(0) }}
</div>
<div class="flex gap-2">
<span v-if="!group.isPublic" class="px-2 py-1 bg-amber-900/30 text-amber-400 text-[10px] uppercase font-bold rounded-md border border-amber-900/50">Private</span>
<span class="px-2 py-1 bg-slate-800 text-slate-400 text-[10px] uppercase font-bold rounded-md">{{ group.memberCount }} members</span>
</div>
</div>
<h3 class="text-xl font-semibold text-slate-100 mb-2">{{ group.name }}</h3>
<p class="text-slate-400 text-sm mb-6 line-clamp-2 h-10">{{ group.description || 'Нет описания' }}</p>
<div class="flex items-center justify-between pt-4 border-t border-slate-800">
<div class="flex -space-x-2">
<div v-for="i in Math.min(group.memberCount, 3)" :key="i" class="w-8 h-8 rounded-full border-2 border-slate-900 bg-slate-800 flex items-center justify-center text-[10px] text-slate-400 font-bold">
U{{ i }}
</div>
<div v-if="group.memberCount > 3" class="w-8 h-8 rounded-full border-2 border-slate-900 bg-slate-700 flex items-center justify-center text-[10px] text-slate-300 font-bold">
+{{ group.memberCount - 3 }}
</div>
</div>
<button @click="openGroupSettings(group)" class="px-4 py-1.5 bg-slate-800 hover:bg-slate-700 text-blue-400 rounded-lg text-sm font-medium transition">Управлять</button>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="bg-slate-900 border border-slate-800 border-dashed rounded-3xl p-12 text-center">
<div class="text-6xl mb-4">👥</div>
<h3 class="text-xl font-bold text-slate-200">Групп пока нет</h3>
<p class="text-slate-500 mt-2 max-w-sm mx-auto">Создайте свою первую команду, чтобы работать над проектами совместно.</p>
<button @click="showCreateModal = true" class="mt-6 px-6 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold transition">Создать группу</button>
</div>
<!-- Create Group Modal -->
<div v-if="showCreateModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm" @click.self="showCreateModal = false">
<div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-md p-6 shadow-2xl">
<h2 class="text-2xl font-bold text-slate-100 mb-4 text-center">Новая Группа</h2>
<div class="space-y-4">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Название группы</label>
<input
v-model="newGroup.name"
type="text"
class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2.5 text-slate-100 focus:border-blue-500 outline-none transition"
placeholder="Например: Отдел разработки"
/>
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Описание</label>
<textarea
v-model="newGroup.description"
rows="3"
class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-2.5 text-slate-100 focus:border-blue-500 outline-none transition resize-none"
placeholder="Чем занимается эта группа?"
></textarea>
</div>
<div class="flex items-center gap-3 py-2 bg-slate-800/50 p-3 rounded-lg border border-slate-700/50">
<input type="checkbox" v-model="newGroup.isPublic" id="isPublic" class="w-5 h-5 rounded border-slate-700 bg-slate-800 text-blue-600 focus:ring-blue-500 cursor-pointer">
<label for="isPublic" class="text-slate-300 select-none cursor-pointer text-sm">Сделать группу публичной (доступной всем)</label>
</div>
</div>
<div class="flex gap-3 mt-8">
<button
@click="showCreateModal = false"
class="flex-1 px-4 py-3 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl font-bold transition"
>
Отмена
</button>
<button
@click="createGroup"
class="flex-1 px-4 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold transition shadow-lg shadow-blue-900/40"
:disabled="creating"
>
{{ creating ? 'Создание...' : 'Создать' }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: ['auth', 'admin']
})
const auth = useAuth()
const groups = ref([])
const showCreateModal = ref(false)
const loading = ref(false)
const creating = ref(false)
const newGroup = reactive({
name: '',
description: '',
isPublic: true
})
const fetchGroups = async () => {
loading.value = true
try {
const data = await $fetch('/api/groups', {
headers: { Authorization: `Bearer ${auth.token.value}` }
})
groups.value = data.groups || []
} catch (e) {
console.error('[Groups Page] Failed to fetch groups', e)
} finally {
loading.value = false
}
}
const createGroup = async () => {
if (!newGroup.name.trim()) return
creating.value = true
try {
await $fetch('/api/groups', {
method: 'POST',
body: newGroup,
headers: { Authorization: `Bearer ${auth.token.value}` }
})
showCreateModal.value = false
newGroup.name = ''
newGroup.description = ''
await fetchGroups()
} catch (e) {
alert('Ошибка при создании группы: ' + (e.data?.message || e.message))
} finally {
creating.value = false
}
}
const openGroupSettings = (group) => {
navigateTo(`/admin/groups/${group.id}`)
}
onMounted(() => {
fetchGroups()
})
</script>