Files
erahn-v2-fe/src/modules/role/pages/RoleList.vue
T

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>