DONE: update logo to original official logo, update letterhead and letterfooter layout (#8)
Build Docker Image / build-backend (push) Successful in 52s
Build Docker Image / build-frontend (push) Successful in 32s

Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-07-07 14:45:20 +08:00
parent 1cc3351ca6
commit 18c0d268b1
76 changed files with 547 additions and 402 deletions
@@ -109,7 +109,7 @@ export function useImpersonate() {
title: response.message,
showConfirmButton: false,
showCloseButton: true,
timer: 2000,
timer: 500,
})
await router.push(resolvePostLoginRoute(response.redirect_path))
@@ -143,7 +143,7 @@ export function useImpersonate() {
title: response.message,
showConfirmButton: false,
showCloseButton: true,
timer: 2000,
timer: 500,
})
await router.push(resolvePostLoginRoute(response.redirect_path))
+24 -49
View File
@@ -321,7 +321,8 @@ onMounted(() => {
<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"
<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" />
@@ -333,18 +334,10 @@ onMounted(() => {
</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)"
>
<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>
@@ -385,28 +378,28 @@ onMounted(() => {
<template #item.actions="{ item }">
<div class="flex items-center">
<Button v-if="hasPermission('kemaskini pengguna')" type="button" variant="outline" size="sm"
<Button v-if="hasPermission('lihat peranan')" type="button" variant="ghost" 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"
<Button v-if="hasPermission('lihat pengguna')" type="button" variant="ghost" 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"
<Button v-if="hasPermission('kemaskini pengguna')" type="button" variant="ghost" 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"
<Button v-if="hasPermission('padam akaun pengguna')" type="button" variant="ghost" 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"
<Button v-if="showImpersonateButton(item as UserListItem)" type="button" variant="ghost" 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)">
@@ -417,6 +410,7 @@ onMounted(() => {
</template>
</DataTable>
<!-- Start of deleted users table -->
<div class="space-y-4 border-t border-foreground/10 pt-8">
<div>
<h3 class="text-lg font-medium">Pengguna Dipadam</h3>
@@ -428,19 +422,10 @@ onMounted(() => {
<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"
>
<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" />
@@ -475,16 +460,9 @@ onMounted(() => {
<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)"
>
<Button v-if="hasPermission('padam akaun pengguna')" type="button" variant="ghost" 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>
@@ -493,6 +471,7 @@ onMounted(() => {
</DataTable>
</div>
<!-- Assign roles dialog -->
<DialogRoot :open="assignRolesOpen" @openChange="(details) => (assignRolesOpen = details.open)">
<DialogContent>
<div class="p-5">
@@ -537,6 +516,7 @@ onMounted(() => {
</DialogContent>
</DialogRoot>
<!-- Delete confirmation dialog -->
<DialogRoot :open="deleteConfirmationOpen" @openChange="(details) => (deleteConfirmationOpen = details.open)">
<DialogContent>
<div class="p-5 text-center">
@@ -564,6 +544,7 @@ onMounted(() => {
</DialogContent>
</DialogRoot>
<!-- Restore confirmation dialog -->
<DialogRoot :open="restoreConfirmationOpen" @openChange="(details) => (restoreConfirmationOpen = details.open)">
<DialogContent>
<div class="p-5 text-center">
@@ -581,14 +562,8 @@ onMounted(() => {
<DialogCloseTrigger class="mr-2 w-24" :disabled="restoring">
Batal
</DialogCloseTrigger>
<Button
class="w-24"
type="button"
variant="primary"
look="outline"
:disabled="restoring"
@click="confirmRestore"
>
<Button class="w-24" type="button" variant="primary" look="outline" :disabled="restoring"
@click="confirmRestore">
{{ restoring ? 'Memulihkan...' : 'Pulihkan' }}
</Button>
</div>