This commit is contained in:
2026-08-11 23:17:04 +07:00
parent bc37ab90f6
commit d7e0d81462
49 changed files with 9023 additions and 91 deletions
+146
View File
@@ -0,0 +1,146 @@
<template>
<div>
<div class="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<InputText v-model.trim="userSearch" placeholder="Search users..." class="w-full sm:w-[320px]" @input="debouncedUsers" />
<Button v-if="isOwner" label="Add Member" icon="pi pi-plus" @click="addDialog = true" />
</div>
<div class="overflow-x-auto">
<DataTable :value="members" :loading="loading" emptyMessage="No members" class="min-w-[480px]">
<Column header="User" style="width: 50%">
<template #body="{ data }">
<div class="flex items-center gap-2">
<Avatar :label="(data.displayName || data.username).slice(0, 2).toUpperCase()"
style="background: #3b82f6; color: #fff" />
<span>{{ data.displayName }}</span>
<span class="text-slate-500 dark:text-slate-400">@{{ data.username }}</span>
</div>
</template>
</Column>
<Column field="role" header="Role" style="width: 20%">
<template #body="{ data }">
<Tag :value="data.role" :severity="data.role === 'Owner' ? 'warn' : 'secondary'" />
</template>
</Column>
<Column header="" style="width: 10%">
<template #body="{ data }">
<Button
v-if="isOwner && data.role !== 'Owner'"
icon="pi pi-trash"
text
severity="danger"
@click="onRemove(data.userId)"
/>
</template>
</Column>
</DataTable>
</div>
<Dialog v-model:visible="addDialog" header="Add Member" :modal="true" style="width: min(460px, 92vw)">
<Select
v-model="selectedUserId"
:options="userOptions"
optionLabel="label"
optionValue="value"
placeholder="Select user"
filter
class="w-full"
/>
<Select v-model="newRole" :options="roleOptions" optionLabel="label" optionValue="value"
placeholder="Role" class="mt-2 w-full" />
<template #footer>
<Button label="Cancel" severity="secondary" text @click="addDialog = false" />
<Button label="Add" :loading="adding" @click="onAdd" />
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import { getMembers, addMember, removeMember, getUsers } from '../../services/backend'
import { errorMessage } from '../../services/api'
import { useAuthStore } from '../../stores/auth'
import type { ProjectMember } from '../../types'
const route = useRoute()
const toast = useToast()
const auth = useAuthStore()
const projectId = String(route.params.id)
const members = ref<ProjectMember[]>([])
const loading = ref(false)
const isOwner = ref(false)
const addDialog = ref(false)
const userSearch = ref('')
const users = ref<{ label: string; value: string }[]>([])
const selectedUserId = ref<string | null>(null)
const newRole = ref<'Owner' | 'Member'>('Member')
const roleOptions = [
{ label: 'Member', value: 'Member' },
{ label: 'Owner', value: 'Owner' },
]
const adding = ref(false)
const userOptions = computed(() => users.value.filter((u) => !members.value.some((m) => m.userId === u.value)))
let timer: ReturnType<typeof setTimeout> | undefined
async function loadMembers() {
loading.value = true
try {
members.value = await getMembers(projectId)
isOwner.value = members.value.some((m) => m.userId === auth.user?.id && m.role === 'Owner')
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally {
loading.value = false
}
}
async function loadUsers() {
users.value = (await getUsers(userSearch.value || undefined)).map((u: { id: string; username: string; displayName: string }) => ({
label: `${u.displayName} (@${u.username})`,
value: u.id,
}))
}
function debouncedUsers() {
clearTimeout(timer)
timer = setTimeout(loadUsers, 300)
}
async function onAdd() {
if (!selectedUserId.value) {
toast.add({ severity: 'warn', summary: 'Select a user', life: 3000 })
return
}
adding.value = true
try {
await addMember(projectId, selectedUserId.value, newRole.value)
addDialog.value = false
selectedUserId.value = null
toast.add({ severity: 'success', summary: 'Member added', life: 3000 })
await loadMembers()
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally {
adding.value = false
}
}
async function onRemove(userId: string) {
try {
await removeMember(projectId, userId)
toast.add({ severity: 'success', summary: 'Member removed', life: 3000 })
await loadMembers()
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
}
}
onMounted(async () => {
await loadMembers()
await loadUsers()
})
</script>