196 lines
8.3 KiB
Vue
196 lines
8.3 KiB
Vue
<script lang="ts" setup>
|
|
import { ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Box } from '@/components/ui/box'
|
|
import {
|
|
PaginationContext,
|
|
PaginationRoot,
|
|
PaginationItem,
|
|
PaginationPrevTrigger,
|
|
PaginationNextTrigger,
|
|
PaginationEllipsis,
|
|
} from '@/components/ui/pagination'
|
|
import { DialogRoot, DialogContent, DialogCloseTrigger } from '@/components/ui/dialog'
|
|
import { Lucide } from '@/components/ui/lucide'
|
|
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
|
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
|
import { useRoleList } from '../composables/useRoleList'
|
|
import { deleteRole as deleteRoleService } from '../services/role.service'
|
|
|
|
const deleteConfirmationOpen = ref(false)
|
|
const roleToDelete = ref<string | null>(null)
|
|
const deleting = ref(false)
|
|
const deleteError = ref<string | null>(null)
|
|
const router = useRouter()
|
|
|
|
const { roles, loading, error, search, page, pagination, fetchRoles } = useRoleList()
|
|
|
|
function handlePageChange(details: { page: number }) {
|
|
page.value = details.page
|
|
}
|
|
|
|
function contextBadgeVariant(context: string) {
|
|
if (context === 'admin') return 'success'
|
|
if (context === 'member') return 'pending'
|
|
return 'danger'
|
|
}
|
|
|
|
function goToCreateRole() {
|
|
router.push({ name: 'create-role' })
|
|
}
|
|
|
|
function goToEditRole(roleId: string) {
|
|
router.push({ name: 'edit-role', params: { id: roleId } })
|
|
}
|
|
|
|
function openDeleteConfirmation(roleId: string) {
|
|
roleToDelete.value = roleId
|
|
deleteError.value = null
|
|
deleteConfirmationOpen.value = true
|
|
}
|
|
|
|
async function confirmDelete() {
|
|
if (!roleToDelete.value || deleting.value) {
|
|
return
|
|
}
|
|
|
|
deleting.value = true
|
|
deleteError.value = null
|
|
|
|
try {
|
|
await deleteRoleService(roleToDelete.value)
|
|
deleteConfirmationOpen.value = false
|
|
roleToDelete.value = null
|
|
await fetchRoles()
|
|
} catch (err) {
|
|
deleteError.value = getApiErrorMessage(err, 'Gagal menghapus peranan.')
|
|
} finally {
|
|
deleting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<h2 class="text-lg font-medium">Senarai Peranan</h2>
|
|
|
|
<AlertRoot v-if="error" class="mt-6" variant="danger">
|
|
<AlertTitle>Ralat</AlertTitle>
|
|
<AlertDescription>{{ error }}</AlertDescription>
|
|
</AlertRoot>
|
|
|
|
<div class="mt-5 grid grid-cols-12 gap-x-6 gap-y-8">
|
|
<div class="col-span-12 mt-2 flex flex-wrap items-center sm:flex-nowrap">
|
|
<!-- Search -->
|
|
<div class="w-full sm:w-auto">
|
|
<div class="relative w-56">
|
|
<Input v-model="search" class="w-56 pr-10" type="search" placeholder="Cari peranan..." />
|
|
<Lucide class="absolute inset-y-0 right-0 my-auto mr-3 h-4 w-4" icon="Search" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Button -->
|
|
<Button class="mt-3 sm:mt-0 sm:ml-auto" look="outline" variant="primary" :disabled="loading"
|
|
@click="goToCreateRole">
|
|
Tambah Peranan
|
|
</Button>
|
|
</div>
|
|
<!-- BEGIN: Data List -->
|
|
<div v-for="role in roles" :key="role.id" class="col-span-12 md:col-span-6 lg:col-span-4 xl:col-span-3">
|
|
<Box class="p-0">
|
|
<div class="p-5">
|
|
<div class="rounded-lg border border-foreground/10 bg-foreground/2 p-4">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div class="min-w-0">
|
|
<div class="truncate text-base font-medium">
|
|
{{ role.name }}
|
|
</div>
|
|
<div class="mt-1 truncate text-xs opacity-70">
|
|
{{ role.fullname || '-' }}
|
|
</div>
|
|
</div>
|
|
<Badge class="shrink-0" look="outline" :variant="contextBadgeVariant(role.context)">
|
|
{{ role.context }}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5 opacity-70">
|
|
<div class="flex items-center">
|
|
<Lucide class="mr-2 h-4 w-4" icon="Shield" />
|
|
Guard: {{ role.guard_name }}
|
|
</div>
|
|
<div class="mt-2 flex items-center">
|
|
<Lucide class="mr-2 h-4 w-4" icon="KeyRound" />
|
|
Permissions: {{ role.permissions?.length ?? 0 }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center justify-center border-t border-foreground/8 p-5 lg:justify-end">
|
|
<a class="mr-3 flex items-center" href="#" @click.prevent="goToEditRole(role.id)">
|
|
<Lucide class="mr-1 h-4 w-4" icon="CheckSquare" /> Edit
|
|
</a>
|
|
<a class="text-danger flex items-center" href="#"
|
|
@click.prevent="openDeleteConfirmation(role.id)">
|
|
<Lucide class="mr-1 h-4 w-4" icon="Trash" /> Delete
|
|
</a>
|
|
</div>
|
|
</Box>
|
|
</div>
|
|
<!-- END: Data List -->
|
|
<div v-if="!loading && !roles.length" class="col-span-12">
|
|
<Box class="p-6 text-center opacity-70">Tiada rekod ditemui</Box>
|
|
</div>
|
|
<!-- BEGIN: Pagination -->
|
|
<div class="col-span-12 flex flex-wrap items-center sm:flex-row sm:flex-nowrap">
|
|
<PaginationRoot :count="pagination.total" :page="page" :siblingCount="1"
|
|
:onPageChange="handlePageChange" class="w-full sm:mr-auto sm:w-auto">
|
|
<PaginationPrevTrigger>Previous</PaginationPrevTrigger>
|
|
<PaginationContext v-slot="{ pagination }">
|
|
<template v-for="(page, index) in pagination?.pages" :key="index">
|
|
<PaginationItem v-if="page.type === 'page'" v-bind="{ ...page }">
|
|
{{ page.value }}
|
|
</PaginationItem>
|
|
<PaginationEllipsis v-else :index="index" />
|
|
</template>
|
|
</PaginationContext>
|
|
<PaginationNextTrigger>Next</PaginationNextTrigger>
|
|
</PaginationRoot>
|
|
</div>
|
|
<!-- END: Pagination -->
|
|
</div>
|
|
<!-- BEGIN: Delete Confirmation Modal -->
|
|
<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 rekod ini? <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"> Cancel </DialogCloseTrigger>
|
|
<Button
|
|
class="w-24"
|
|
type="button"
|
|
variant="danger"
|
|
look="outline"
|
|
:disabled="deleting"
|
|
@click="confirmDelete"
|
|
>
|
|
{{ deleting ? 'Menghapus...' : 'Hapus' }}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</DialogRoot>
|
|
<!-- END: Delete Confirmation Modal -->
|
|
</div>
|
|
</template>
|