Files
mws.frontend.vue/src/views/projects/MembersView.vue
T
2026-08-24 08:07:23 +07:00

262 lines
9.5 KiB
Vue

<template>
<div class="flex h-full flex-col">
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="sm:w-[320px]">
<IconField>
<InputIcon class="pi pi-search" />
<InputText v-model.trim="userSearch" placeholder="Search users..." class="search-input w-full" @input="debouncedUsers" />
</IconField>
</div>
<Button v-if="isOwner" label="Add Member" icon="pi pi-plus" @click="addDialog = true" />
</div>
<div class="panel flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="flex min-h-0 flex-1 flex-col overflow-x-auto">
<AppDataTable
:value="members"
:loading="loading"
:lazy="true"
:paginator="true"
:rows="pageSize"
:totalRecords="totalCount"
:first="first"
@page="onPageChange"
emptyMessage="No members yet"
scrollable
scrollHeight="flex"
class="min-h-0 flex-1 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: var(--primary); color: #fff" />
<span>{{ data.displayName }}</span>
<span class="muted-note">@{{ data.username }}</span>
</div>
</template>
</Column>
<Column field="role" header="Role" style="width: 16%">
<template #body="{ data }">
<Tag :value="data.role" :severity="data.role === 'Owner' ? 'primary' : 'secondary'" />
</template>
</Column>
<Column header="Document Access" style="width: 34%">
<template #body="{ data }">
<div v-if="data.role === 'Owner'" class="flex flex-wrap gap-1">
<Tag value="Full Access" severity="primary" />
</div>
<div v-else class="flex flex-wrap gap-1">
<Tag v-if="data.canViewDocuments" value="View" severity="secondary" />
<Tag v-if="data.canCreateDocuments" value="Create" severity="secondary" />
<Tag v-if="data.canEditDocuments" value="Edit" severity="secondary" />
<Tag v-if="data.canDeleteDocuments" value="Delete" severity="secondary" />
<Tag v-if="!data.canViewDocuments" value="No access" severity="danger" />
</div>
</template>
</Column>
<Column header="" style="width: 10%">
<template #body="{ data }">
<div v-if="isOwner && data.role !== 'Owner'" class="flex justify-end gap-1">
<Button
icon="pi pi-sliders-h"
text
severity="secondary"
:aria-label="`Set document permissions for ${data.displayName}`"
@click="openPermissions(data)"
/>
<Button
icon="pi pi-trash"
text
severity="danger"
:aria-label="`Remove ${data.displayName}`"
@click="onRemove(data.userId)"
/>
</div>
</template>
</Column>
</AppDataTable>
</div>
</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>
<Dialog v-model:visible="permDialog" header="Document Access" :modal="true" style="width: min(440px, 92vw)">
<div class="muted-note mb-3">
Permissions for <span class="font-medium" style="color: var(--ink)">{{ permTarget?.displayName }}</span>
@{{ permTarget?.username }}
</div>
<div class="flex flex-col gap-3">
<label
v-for="opt in permOptions"
:key="opt.key"
class="flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2" style="border-color: var(--hairline)"
>
<Checkbox v-model="permForm[opt.key]" binary />
<div>
<div class="text-sm font-medium">{{ opt.label }}</div>
<div class="muted-note">{{ opt.hint }}</div>
</div>
</label>
</div>
<template #footer>
<Button label="Cancel" severity="secondary" text @click="permDialog = false" />
<Button label="Save" :loading="savingPerms" @click="savePermissions" />
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import { getMembers, addMember, removeMember, getUsers, updateMemberDocumentPermissions, getMasterDataOptions } from '../../services/backend'
import { errorMessage } from '../../services/api'
import { useAuthStore } from '../../stores/auth'
import AppDataTable from '../../components/AppDataTable.vue'
import type { ProjectMember } from '../../types'
import type { DataTablePageEvent } from 'primevue/datatable'
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 page = ref(1)
const pageSize = ref(20)
const totalCount = ref(0)
const first = computed(() => (page.value - 1) * pageSize.value)
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 = ref<{ label: string; value: string }[]>([])
const adding = ref(false)
const permDialog = ref(false)
const permTarget = ref<ProjectMember | null>(null)
const permForm = reactive({ canViewDocuments: false, canCreateDocuments: false, canEditDocuments: false, canDeleteDocuments: false })
const savingPerms = ref(false)
const permOptions = [
{ key: 'canViewDocuments' as const, label: 'View', hint: 'See documents in the project' },
{ key: 'canCreateDocuments' as const, label: 'Create', hint: 'Create new documents and folders' },
{ key: 'canEditDocuments' as const, label: 'Edit', hint: 'Edit content, rename and move' },
{ key: 'canDeleteDocuments' as const, label: 'Delete', hint: 'Delete documents and folders' },
]
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 {
const res = await getMembers(projectId, page.value, pageSize.value)
members.value = res.items
totalCount.value = res.totalCount
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
}
}
function onPageChange(event: DataTablePageEvent) {
page.value = event.page + 1
pageSize.value = event.rows
void loadMembers()
}
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 })
}
}
function openPermissions(member: ProjectMember) {
permTarget.value = member
permForm.canViewDocuments = member.canViewDocuments
permForm.canCreateDocuments = member.canCreateDocuments
permForm.canEditDocuments = member.canEditDocuments
permForm.canDeleteDocuments = member.canDeleteDocuments
permDialog.value = true
}
async function savePermissions() {
if (!permTarget.value) return
savingPerms.value = true
try {
await updateMemberDocumentPermissions(projectId, permTarget.value.userId, { ...permForm })
permDialog.value = false
toast.add({ severity: 'success', summary: 'Permissions updated', life: 3000 })
await loadMembers()
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally {
savingPerms.value = false
}
}
onMounted(async () => {
roleOptions.value = await getMasterDataOptions('member_role')
await loadMembers()
await loadUsers()
})
</script>