feat: update UI

This commit is contained in:
2026-08-24 08:07:23 +07:00
parent d7e0d81462
commit 8fe5e44f52
41 changed files with 3117 additions and 993 deletions
+138 -23
View File
@@ -1,39 +1,82 @@
<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" />
<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="overflow-x-auto">
<DataTable :value="members" :loading="loading" emptyMessage="No members" class="min-w-[480px]">
<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: #3b82f6; color: #fff" />
style="background: var(--primary); color: #fff" />
<span>{{ data.displayName }}</span>
<span class="text-slate-500 dark:text-slate-400">@{{ data.username }}</span>
<span class="muted-note">@{{ data.username }}</span>
</div>
</template>
</Column>
<Column field="role" header="Role" style="width: 20%">
<Column field="role" header="Role" style="width: 16%">
<template #body="{ data }">
<Tag :value="data.role" :severity="data.role === 'Owner' ? 'warn' : 'secondary'" />
<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 }">
<Button
v-if="isOwner && data.role !== 'Owner'"
icon="pi pi-trash"
text
severity="danger"
@click="onRemove(data.userId)"
/>
<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>
</DataTable>
</AppDataTable>
</div>
</div>
<Dialog v-model:visible="addDialog" header="Add Member" :modal="true" style="width: min(460px, 92vw)">
@@ -53,14 +96,40 @@
<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 } from '../../services/backend'
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()
@@ -71,17 +140,30 @@ 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 = [
{ label: 'Member', value: 'Member' },
{ label: 'Owner', value: 'Owner' },
]
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
@@ -89,7 +171,9 @@ let timer: ReturnType<typeof setTimeout> | undefined
async function loadMembers() {
loading.value = true
try {
members.value = await getMembers(projectId)
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 })
@@ -98,6 +182,12 @@ async function loadMembers() {
}
}
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})`,
@@ -139,7 +229,32 @@ async function onRemove(userId: string) {
}
}
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()
})