DONE: layout letter with letterhead and footer, notification for newly...
This commit is contained in:
@@ -1,15 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { Search, HatGlasses } from '@lucide/vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import dayjs from 'dayjs'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { Search, HatGlasses, SquarePen, Trash2, Eye, Shield, RotateCcw } from '@lucide/vue'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { CheckboxRoot, CheckboxControl, CheckboxLabel } from '@/components/ui/checkbox'
|
||||
import { DialogRoot, DialogContent, DialogCloseTrigger } from '@/components/ui/dialog'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import DataTable from '@/components/ui/usage/DataTable.vue'
|
||||
import type { TableHeader } from '@/components/ui/usage/DataTable.vue'
|
||||
import { usePermissions } from '@/composables/usePermissions'
|
||||
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
||||
import { listRoles } from '@/modules/role/services/role.service'
|
||||
import type { RoleListItem } from '@/modules/role/types/role.types'
|
||||
import { useImpersonate } from '../composables/useImpersonate'
|
||||
import { useDeletedUserList } from '../composables/useDeletedUserList'
|
||||
import { useUserList } from '../composables/useUserList'
|
||||
import {
|
||||
assignUserRoles as assignUserRolesService,
|
||||
deleteUser as deleteUserService,
|
||||
restoreUser as restoreUserService,
|
||||
} from '../services/user.service'
|
||||
import type { UserRole, UserListItem } from '../types/user.types'
|
||||
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||
import type { BadgeVariants } from '@/components/ui/styles/badge.styles'
|
||||
|
||||
type StatusFilterChip = {
|
||||
label: string
|
||||
value: string
|
||||
variant: BadgeVariants['variant']
|
||||
}
|
||||
|
||||
const STATUS_FILTER_CHIPS: StatusFilterChip[] = [
|
||||
{ label: 'Semua', value: '', variant: 'ghost' },
|
||||
{ label: 'Active', value: 'active', variant: 'success' },
|
||||
{ label: 'Inactive', value: 'inactive', variant: 'danger' },
|
||||
{ label: 'Pending', value: 'pending', variant: 'pending' },
|
||||
]
|
||||
|
||||
const router = useRouter()
|
||||
const { hasPermission } = usePermissions()
|
||||
|
||||
const deleteConfirmationOpen = ref(false)
|
||||
const userToDelete = ref<UserListItem | null>(null)
|
||||
const deleting = ref(false)
|
||||
const deleteError = ref<string | null>(null)
|
||||
|
||||
const assignRolesOpen = ref(false)
|
||||
const userToAssignRoles = ref<UserListItem | null>(null)
|
||||
const availableRoles = ref<RoleListItem[]>([])
|
||||
const selectedRoleIds = ref<Set<string>>(new Set())
|
||||
const loadingRoles = ref(false)
|
||||
const savingRoles = ref(false)
|
||||
const assignRolesError = ref<string | null>(null)
|
||||
|
||||
const restoreConfirmationOpen = ref(false)
|
||||
const userToRestore = ref<UserListItem | null>(null)
|
||||
const restoring = ref(false)
|
||||
const restoreError = ref<string | null>(null)
|
||||
|
||||
function goToEditUser(userId: string) {
|
||||
router.push({ name: 'edit-user', params: { id: userId } })
|
||||
}
|
||||
|
||||
function goToCreateUser() {
|
||||
router.push({ name: 'create-user' })
|
||||
}
|
||||
|
||||
function goToViewUser(userId: string) {
|
||||
router.push({ name: 'view-user', params: { id: userId } })
|
||||
}
|
||||
|
||||
function formatUserRoles(roles: UserRole[] | undefined): string {
|
||||
return roles?.map((role) => role.name).join(', ') || '-'
|
||||
@@ -21,18 +84,166 @@ function statusBadgeVariant(status: string) {
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
function isStatusFilterActive(value: string) {
|
||||
return statusFilter.value === value
|
||||
}
|
||||
|
||||
function setStatusFilter(value: string) {
|
||||
statusFilter.value = value
|
||||
}
|
||||
|
||||
function formatDeletedAt(value: string | null | undefined): string {
|
||||
if (!value) return '-'
|
||||
return dayjs(value).format('DD MMM YYYY, HH:mm')
|
||||
}
|
||||
|
||||
function openDeleteConfirmation(user: UserListItem) {
|
||||
userToDelete.value = user
|
||||
deleteError.value = null
|
||||
deleteConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!userToDelete.value || deleting.value) {
|
||||
return
|
||||
}
|
||||
|
||||
deleting.value = true
|
||||
deleteError.value = null
|
||||
|
||||
try {
|
||||
await deleteUserService(userToDelete.value.id)
|
||||
deleteConfirmationOpen.value = false
|
||||
userToDelete.value = null
|
||||
await Promise.all([fetchUsers(page.value), fetchDeletedUsers(deletedPage.value)])
|
||||
} catch (err) {
|
||||
deleteError.value = getApiErrorMessage(err, 'Gagal mengpadam akaun pengguna.')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAvailableRoles() {
|
||||
if (availableRoles.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingRoles.value = true
|
||||
assignRolesError.value = null
|
||||
|
||||
try {
|
||||
const response = await listRoles()
|
||||
availableRoles.value = response.data.filter((role) => role.guard_name === 'api')
|
||||
} catch (err) {
|
||||
assignRolesError.value = getApiErrorMessage(err, 'Gagal memuatkan senarai peranan.')
|
||||
availableRoles.value = []
|
||||
} finally {
|
||||
loadingRoles.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openAssignRolesDialog(user: UserListItem) {
|
||||
userToAssignRoles.value = user
|
||||
selectedRoleIds.value = new Set(user.roles?.map((role) => role.id) ?? [])
|
||||
assignRolesError.value = null
|
||||
assignRolesOpen.value = true
|
||||
await loadAvailableRoles()
|
||||
}
|
||||
|
||||
function setRoleChecked(roleId: string, checked: boolean) {
|
||||
const next = new Set(selectedRoleIds.value)
|
||||
if (checked) {
|
||||
next.add(roleId)
|
||||
} else {
|
||||
next.delete(roleId)
|
||||
}
|
||||
selectedRoleIds.value = next
|
||||
}
|
||||
|
||||
async function confirmAssignRoles() {
|
||||
if (!userToAssignRoles.value || savingRoles.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedRoleIds.value.size) {
|
||||
assignRolesError.value = 'Sila pilih sekurang-kurangnya satu peranan.'
|
||||
return
|
||||
}
|
||||
|
||||
savingRoles.value = true
|
||||
assignRolesError.value = null
|
||||
|
||||
try {
|
||||
await assignUserRolesService(
|
||||
userToAssignRoles.value.id,
|
||||
Array.from(selectedRoleIds.value),
|
||||
)
|
||||
assignRolesOpen.value = false
|
||||
userToAssignRoles.value = null
|
||||
await fetchUsers(page.value)
|
||||
} catch (err) {
|
||||
assignRolesError.value = getApiErrorMessage(err, 'Gagal menetapkan peranan pengguna.')
|
||||
} finally {
|
||||
savingRoles.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openRestoreConfirmation(user: UserListItem) {
|
||||
userToRestore.value = user
|
||||
restoreError.value = null
|
||||
restoreConfirmationOpen.value = true
|
||||
}
|
||||
|
||||
async function confirmRestore() {
|
||||
if (!userToRestore.value || restoring.value) {
|
||||
return
|
||||
}
|
||||
|
||||
restoring.value = true
|
||||
restoreError.value = null
|
||||
|
||||
try {
|
||||
await restoreUserService(userToRestore.value.id)
|
||||
restoreConfirmationOpen.value = false
|
||||
userToRestore.value = null
|
||||
await Promise.all([fetchUsers(page.value), fetchDeletedUsers(deletedPage.value)])
|
||||
} catch (err) {
|
||||
restoreError.value = getApiErrorMessage(err, 'Gagal memulihkan akaun pengguna.')
|
||||
} finally {
|
||||
restoring.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const headers: TableHeader[] = [
|
||||
{ title: 'Bil.', key: '#', sortable: false },
|
||||
{ title: 'Name', key: 'name', sortable: true },
|
||||
{ title: 'Emel', key: 'email', sortable: true },
|
||||
{ title: 'Jawatan', key: 'position', sortable: true },
|
||||
{ title: 'No. Anggota', key: 'member_number', sortable: true, align: 'center' },
|
||||
{
|
||||
title: 'Peranan',
|
||||
key: 'roles',
|
||||
sortable: false,
|
||||
exportValue: (item) => formatUserRoles(item.roles),
|
||||
},
|
||||
{ title: 'Status Pengguna', key: 'status', sortable: true },
|
||||
{ title: 'Status', key: 'status', sortable: true },
|
||||
{ title: 'Tindakan', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
const deletedHeaders: TableHeader[] = [
|
||||
{ title: 'Bil.', key: '#', sortable: false },
|
||||
{ title: 'Name', key: 'name', sortable: true },
|
||||
{ title: 'Emel', key: 'email', sortable: true },
|
||||
{ title: 'Jawatan', key: 'position', sortable: true },
|
||||
{ title: 'No. Anggota', key: 'member_number', sortable: true, align: 'center' },
|
||||
{
|
||||
title: 'Peranan',
|
||||
key: 'roles',
|
||||
sortable: false,
|
||||
exportValue: (item) => formatUserRoles(item.roles),
|
||||
},
|
||||
{ title: 'Status', key: 'status', sortable: true },
|
||||
{ title: 'Dipadam Pada', key: 'deleted_at', sortable: true },
|
||||
{ title: 'Tindakan', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
@@ -41,15 +252,43 @@ const {
|
||||
loading,
|
||||
error,
|
||||
search,
|
||||
statusFilter,
|
||||
sortBy,
|
||||
page,
|
||||
itemsPerPage,
|
||||
pagination,
|
||||
handleSortUpdate,
|
||||
fetchUsers,
|
||||
} = useUserList()
|
||||
|
||||
const {
|
||||
users: deletedUsers,
|
||||
loading: deletedLoading,
|
||||
error: deletedError,
|
||||
search: deletedSearch,
|
||||
statusFilter: deletedStatusFilter,
|
||||
sortBy: deletedSortBy,
|
||||
page: deletedPage,
|
||||
itemsPerPage: deletedItemsPerPage,
|
||||
pagination: deletedPagination,
|
||||
handleSortUpdate: handleDeletedSortUpdate,
|
||||
fetchUsers: fetchDeletedUsers,
|
||||
} = useDeletedUserList({ autoWatchFilters: false, autoFetchOnMount: false })
|
||||
|
||||
const debouncedDeletedFetch = debounce(() => {
|
||||
fetchDeletedUsers(1)
|
||||
}, 400)
|
||||
|
||||
watch([search, statusFilter], () => {
|
||||
deletedSearch.value = search.value
|
||||
deletedStatusFilter.value = statusFilter.value
|
||||
debouncedDeletedFetch()
|
||||
})
|
||||
|
||||
const {
|
||||
showImpersonateButton,
|
||||
canImpersonateUser,
|
||||
impersonateButtonTitle,
|
||||
impersonating,
|
||||
loading: impersonateLoading,
|
||||
refreshImpersonationStatus,
|
||||
@@ -58,11 +297,18 @@ const {
|
||||
|
||||
onMounted(() => {
|
||||
refreshImpersonationStatus()
|
||||
deletedSearch.value = search.value
|
||||
deletedStatusFilter.value = statusFilter.value
|
||||
fetchDeletedUsers(1)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full space-y-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-medium">Senarai Pengguna</h2>
|
||||
<p class="mt-1 text-sm opacity-70">Urus dan semak pengguna koperasi.</p>
|
||||
</div>
|
||||
<AlertRoot v-if="error" class="mt-6" variant="danger">
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{{ error }}</AlertDescription>
|
||||
@@ -72,16 +318,50 @@ onMounted(() => {
|
||||
show-pagination exportable export-file-name="users" v-model:page="page" v-model:items-per-page="itemsPerPage"
|
||||
@update:sort-by="handleSortUpdate">
|
||||
<template #toolbar>
|
||||
<div class="relative w-full max-w-md">
|
||||
<Search class="pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 text-foreground/50"
|
||||
aria-hidden="true" />
|
||||
<Input v-model="search" type="search" placeholder="Search name, email, IC, phone, role..." class="w-full pl-9"
|
||||
aria-label="Search users" />
|
||||
<div class="flex w-full flex-col gap-3">
|
||||
<div class="flex w-full flex-wrap items-center gap-3">
|
||||
<div class="relative w-full max-w-md flex-1">
|
||||
<Search class="pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 text-foreground/50"
|
||||
aria-hidden="true" />
|
||||
<Input v-model="search" type="search" placeholder="Search name, email, IC, phone, role..."
|
||||
class="w-full pl-9" aria-label="Search users" />
|
||||
</div>
|
||||
<Button v-if="hasPermission('daftar pengguna baru')" type="button" variant="primary" look="outline"
|
||||
@click="goToCreateUser">
|
||||
Daftar Pengguna
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm opacity-70">Status:</span>
|
||||
<Badge
|
||||
v-for="chip in STATUS_FILTER_CHIPS"
|
||||
:key="chip.value || 'all'"
|
||||
:variant="chip.variant"
|
||||
:look="isStatusFilterActive(chip.value) ? 'filled' : 'outline'"
|
||||
class="capitalize"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-pressed="isStatusFilterActive(chip.value)"
|
||||
@click="setStatusFilter(chip.value)"
|
||||
@keydown.enter="setStatusFilter(chip.value)"
|
||||
>
|
||||
{{ chip.label }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.name="{ item }">
|
||||
<span class="font-medium">{{ item.name }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- profile image -->
|
||||
<img v-if="item.image_url" :src="item.image_url" alt="Profile Image" class="size-10 rounded-full" />
|
||||
<div v-else class="size-10 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<span class="text-gray-500">{{ item.name.charAt(0) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- name -->
|
||||
<span class="font-medium">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.email="{ item }">
|
||||
@@ -92,20 +372,227 @@ onMounted(() => {
|
||||
{{ formatUserRoles(item.roles) }}
|
||||
</template>
|
||||
|
||||
<template #item.member_number="{ item }">
|
||||
<span class="text-center">{{ item.member_number }}</span>
|
||||
</template>
|
||||
|
||||
<!-- centre align status -->
|
||||
<template #item.status="{ item }">
|
||||
<Badge :variant="statusBadgeVariant(item.status)" class="capitalize">
|
||||
<Badge :variant="statusBadgeVariant(item.status)" class="capitalize text-center">
|
||||
{{ item.status }}
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<Button v-if="canImpersonateUser(item as UserListItem)" type="button" variant="outline" size="sm"
|
||||
class="gap-1.5 bg-orange-500 text-white" :disabled="impersonateLoading || impersonating"
|
||||
:title="impersonating ? 'Anda sedang menyamar pengguna' : 'Menyamar sebagai pengguna'"
|
||||
@click="impersonateUser(item as UserListItem)">
|
||||
<HatGlasses class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<div class="flex items-center">
|
||||
<Button v-if="hasPermission('kemaskini pengguna')" type="button" variant="outline" size="sm"
|
||||
class="bg-purple-600 text-white disabled:opacity-50" :disabled="savingRoles"
|
||||
:title="savingRoles ? 'Menyimpan perubahan...' : 'Tetapkan peranan'"
|
||||
@click="openAssignRolesDialog(item as UserListItem)"> Urus Peranan
|
||||
<Shield class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button v-if="hasPermission('lihat pengguna')" type="button" variant="outline" size="sm"
|
||||
class="bg-green-600 text-white disabled:opacity-50" :disabled="savingRoles"
|
||||
:title="savingRoles ? 'Menyimpan perubahan...' : 'Lihat profil'" @click="goToViewUser(item.id)">
|
||||
<Eye class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button v-if="hasPermission('kemaskini pengguna')" type="button" variant="outline" size="sm"
|
||||
class="bg-yellow-600 text-white disabled:opacity-50" :disabled="savingRoles"
|
||||
:title="savingRoles ? 'Menyimpan perubahan...' : 'Kemaskini pengguna'" @click="goToEditUser(item.id)">
|
||||
<SquarePen class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button v-if="hasPermission('padam akaun pengguna')" type="button" variant="outline" size="sm"
|
||||
class="bg-red-600 text-white disabled:opacity-50" :disabled="deleting"
|
||||
@click="openDeleteConfirmation(item as UserListItem)">
|
||||
<Trash2 class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button v-if="showImpersonateButton(item as UserListItem)" type="button" variant="outline" size="sm"
|
||||
class="bg-orange-600 text-white disabled:opacity-50"
|
||||
:disabled="!canImpersonateUser(item as UserListItem) || impersonateLoading || impersonating"
|
||||
:title="impersonateButtonTitle(item as UserListItem)" @click="impersonateUser(item as UserListItem)">
|
||||
<HatGlasses class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<div class="space-y-4 border-t border-foreground/10 pt-8">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium">Pengguna Dipadam</h3>
|
||||
<p class="mt-1 text-sm opacity-70">Pengguna yang telah dipadam dan boleh dipulihkan.</p>
|
||||
</div>
|
||||
|
||||
<AlertRoot v-if="deletedError" variant="danger">
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{{ deletedError }}</AlertDescription>
|
||||
</AlertRoot>
|
||||
|
||||
<DataTable
|
||||
:headers="deletedHeaders"
|
||||
:items="deletedUsers"
|
||||
:loading="deletedLoading"
|
||||
:pagination="deletedPagination"
|
||||
:current-sort="deletedSortBy"
|
||||
show-pagination
|
||||
exportable
|
||||
export-file-name="deleted-users"
|
||||
v-model:page="deletedPage"
|
||||
v-model:items-per-page="deletedItemsPerPage"
|
||||
@update:sort-by="handleDeletedSortUpdate"
|
||||
>
|
||||
<template #item.name="{ item }">
|
||||
<div class="flex items-center gap-2">
|
||||
<img v-if="item.image_url" :src="item.image_url" alt="Profile Image" class="size-10 rounded-full" />
|
||||
<div v-else class="flex size-10 items-center justify-center rounded-full bg-gray-200">
|
||||
<span class="text-gray-500">{{ item.name.charAt(0) }}</span>
|
||||
</div>
|
||||
<span class="font-medium">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.email="{ item }">
|
||||
<span class="lowercase">{{ item.email }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.roles="{ item }">
|
||||
{{ formatUserRoles(item.roles) }}
|
||||
</template>
|
||||
|
||||
<template #item.member_number="{ item }">
|
||||
<span class="text-center">{{ item.member_number ?? '-' }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.status="{ item }">
|
||||
<Badge :variant="statusBadgeVariant(item.status)" class="capitalize text-center">
|
||||
{{ item.status }}
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
<template #item.deleted_at="{ item }">
|
||||
{{ formatDeletedAt(item.deleted_at) }}
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
v-if="hasPermission('padam akaun pengguna')"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="bg-blue-600 text-white disabled:opacity-50"
|
||||
:disabled="restoring"
|
||||
title="Pulihkan pengguna"
|
||||
@click="openRestoreConfirmation(item as UserListItem)"
|
||||
>
|
||||
<RotateCcw class="size-4" aria-hidden="true" />
|
||||
Pulihkan
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<DialogRoot :open="assignRolesOpen" @openChange="(details) => (assignRolesOpen = details.open)">
|
||||
<DialogContent>
|
||||
<div class="p-5">
|
||||
<div class="text-center text-2xl font-medium">Tetapkan Peranan</div>
|
||||
<div v-if="userToAssignRoles" class="mt-2 text-center opacity-70">
|
||||
Pilih peranan untuk
|
||||
<span class="font-medium">{{ userToAssignRoles.name }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="assignRolesError" class="mt-4 text-center text-sm text-danger">
|
||||
{{ assignRolesError }}
|
||||
</div>
|
||||
|
||||
<div class="mt-5 max-h-80 overflow-auto rounded-lg border border-foreground/10 p-3">
|
||||
<div v-if="loadingRoles" class="py-6 text-center opacity-70">
|
||||
Memuatkan peranan...
|
||||
</div>
|
||||
<div v-else-if="!availableRoles.length" class="py-6 text-center opacity-70">
|
||||
Tiada peranan ditemui
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-1 gap-2">
|
||||
<CheckboxRoot v-for="role in availableRoles" :key="role.id" :checked="selectedRoleIds.has(role.id)"
|
||||
:disabled="savingRoles" @checked-change="({ checked }) => setRoleChecked(role.id, checked === true)">
|
||||
<CheckboxControl />
|
||||
<CheckboxLabel>
|
||||
<span class="font-medium">{{ role.name }}</span>
|
||||
<span v-if="role.fullname" class="ml-2 opacity-70">({{ role.fullname }})</span>
|
||||
</CheckboxLabel>
|
||||
</CheckboxRoot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 pb-8 text-center">
|
||||
<DialogCloseTrigger class="mr-2 w-24" :disabled="savingRoles">
|
||||
Batal
|
||||
</DialogCloseTrigger>
|
||||
<Button class="w-24" type="button" variant="primary" look="outline" :disabled="savingRoles || loadingRoles"
|
||||
@click="confirmAssignRoles">
|
||||
{{ savingRoles ? 'Menyimpan...' : 'Simpan' }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
|
||||
<DialogRoot :open="deleteConfirmationOpen" @openChange="(details) => (deleteConfirmationOpen = details.open)">
|
||||
<DialogContent>
|
||||
<div class="p-5 text-center">
|
||||
<Lucide class="text-danger mx-auto mt-3 size-16 stroke-1" icon="CircleX" />
|
||||
<div class="mt-5 text-2xl font-medium">Adakah anda yakin?</div>
|
||||
<div class="mt-2 opacity-70">
|
||||
Adakah anda benar-benar mahu menghapus
|
||||
<span v-if="userToDelete" class="font-medium">{{ userToDelete.name }}</span>?
|
||||
<br />
|
||||
Proses ini tidak boleh dibatalkan.
|
||||
</div>
|
||||
<div v-if="deleteError" class="mt-4 text-sm text-danger">
|
||||
{{ deleteError }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 pb-8 text-center">
|
||||
<DialogCloseTrigger class="mr-2 w-24" :disabled="deleting">
|
||||
Batal
|
||||
</DialogCloseTrigger>
|
||||
<Button class="w-24" type="button" variant="danger" look="outline" :disabled="deleting"
|
||||
@click="confirmDelete">
|
||||
{{ deleting ? 'Menghapus...' : 'Hapus' }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
|
||||
<DialogRoot :open="restoreConfirmationOpen" @openChange="(details) => (restoreConfirmationOpen = details.open)">
|
||||
<DialogContent>
|
||||
<div class="p-5 text-center">
|
||||
<Lucide class="mx-auto mt-3 size-16 stroke-1 text-primary" icon="RotateCcw" />
|
||||
<div class="mt-5 text-2xl font-medium">Pulihkan pengguna?</div>
|
||||
<div class="mt-2 opacity-70">
|
||||
Adakah anda mahu memulihkan
|
||||
<span v-if="userToRestore" class="font-medium">{{ userToRestore.name }}</span>?
|
||||
</div>
|
||||
<div v-if="restoreError" class="mt-4 text-sm text-danger">
|
||||
{{ restoreError }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 pb-8 text-center">
|
||||
<DialogCloseTrigger class="mr-2 w-24" :disabled="restoring">
|
||||
Batal
|
||||
</DialogCloseTrigger>
|
||||
<Button
|
||||
class="w-24"
|
||||
type="button"
|
||||
variant="primary"
|
||||
look="outline"
|
||||
:disabled="restoring"
|
||||
@click="confirmRestore"
|
||||
>
|
||||
{{ restoring ? 'Memulihkan...' : 'Pulihkan' }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user