Add ConfirmDialog component for confirmation actions, enhance DataTable with pagination summary and links, and update phone number formatting. Integrate new UI components and improve flash toast handling.
This commit is contained in:
parent
413021c0d0
commit
960bd71fd0
@ -1,5 +1,6 @@
|
||||
import { createInertiaApp } from '@inertiajs/vue3';
|
||||
import { createApp, h } from 'vue';
|
||||
import 'vue-sonner/style.css';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { useFlashToast } from '@/composables/useFlashToast';
|
||||
|
||||
|
||||
66
resources/js/components/ConfirmDialog.vue
Normal file
66
resources/js/components/ConfirmDialog.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
description?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
destructive?: boolean;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
confirmLabel: 'Ya, Lanjutkan',
|
||||
cancelLabel: 'Batal',
|
||||
destructive: false,
|
||||
loading: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [];
|
||||
}>();
|
||||
|
||||
function handleConfirm(event: Event) {
|
||||
event.preventDefault();
|
||||
emit('confirm');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialog v-model:open="open">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ title }}</AlertDialogTitle>
|
||||
<AlertDialogDescription v-if="description">
|
||||
{{ description }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel :disabled="loading">
|
||||
{{ cancelLabel }}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
:disabled="loading"
|
||||
:class="cn(destructive && 'bg-destructive text-white hover:bg-destructive/90')"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ loading ? 'Memproses...' : confirmLabel }}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</template>
|
||||
@ -1,8 +1,10 @@
|
||||
<script setup lang="ts" generic="TData, TValue">
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table';
|
||||
import { computed, provide } from 'vue';
|
||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Empty, EmptyDescription,
|
||||
EmptyHeader, EmptyTitle
|
||||
@ -19,6 +21,7 @@ import TableEmpty from '@/components/ui/table/TableEmpty.vue';
|
||||
import type {
|
||||
DataTableFilterDef,
|
||||
DataTablePagination,
|
||||
DataTablePaginationLink,
|
||||
DataTableSort,
|
||||
} from '@/types/data-table';
|
||||
|
||||
@ -31,6 +34,7 @@ const props = withDefaults(
|
||||
filterDefs?: DataTableFilterDef[];
|
||||
filterValues?: Record<string, string>;
|
||||
searchPlaceholder?: string;
|
||||
paginationLinks?: DataTablePaginationLink[];
|
||||
}>(),
|
||||
{
|
||||
searchPlaceholder: 'Ketikkan sesuatu...',
|
||||
@ -94,6 +98,22 @@ provide('data-table-sort', {
|
||||
sort: () => props.sort,
|
||||
onSort: handleSort,
|
||||
});
|
||||
|
||||
const showingCount = computed(() => props.data.length);
|
||||
|
||||
const paginationSummary = computed(() => {
|
||||
if (!props.pagination) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { total } = props.pagination;
|
||||
|
||||
if (total === 0) {
|
||||
return 'Menampilkan 0 data';
|
||||
}
|
||||
|
||||
return `Menampilkan ${showingCount.value} data dari ${total}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -102,7 +122,7 @@ provide('data-table-sort', {
|
||||
:search-placeholder="searchPlaceholder" @filter-change="(key, value) => emit('filter-change', key, value)"
|
||||
@filters-reset="emit('filters-reset')" />
|
||||
|
||||
<div class="rounded-md border">
|
||||
<div class="min-w-0 rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||
@ -134,5 +154,33 @@ provide('data-table-sort', {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="pagination"
|
||||
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{{ paginationSummary }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="paginationLinks?.length && pagination.lastPage > 1"
|
||||
class="flex flex-wrap items-center justify-center gap-1 sm:justify-end"
|
||||
>
|
||||
<Button
|
||||
v-for="link in paginationLinks"
|
||||
:key="`${link.label}-${link.url}`"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!link.url || link.active"
|
||||
as-child
|
||||
>
|
||||
<Link v-if="link.url" :href="link.url" preserve-scroll>
|
||||
<span v-html="link.label" />
|
||||
</Link>
|
||||
<span v-else v-html="link.label" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
98
resources/js/components/hr/employees/data-table-actions.vue
Normal file
98
resources/js/components/hr/employees/data-table-actions.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, router } from '@inertiajs/vue3';
|
||||
import { KeyRound, Pencil, Trash2 } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import type { EmployeeListItem } from '@/types/employee';
|
||||
|
||||
const props = defineProps<{
|
||||
employee: EmployeeListItem;
|
||||
}>();
|
||||
|
||||
const deleteConfirmOpen = ref(false);
|
||||
const resetPasswordConfirmOpen = ref(false);
|
||||
const deleteProcessing = ref(false);
|
||||
const resetPasswordProcessing = ref(false);
|
||||
|
||||
function destroyEmployee() {
|
||||
deleteProcessing.value = true;
|
||||
|
||||
router.delete(`/admin/hr/employees/${props.employee.id}`, {
|
||||
onSuccess: () => {
|
||||
deleteConfirmOpen.value = false;
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal menghapus pegawai.');
|
||||
},
|
||||
onFinish: () => {
|
||||
deleteProcessing.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function resetPassword() {
|
||||
resetPasswordProcessing.value = true;
|
||||
|
||||
router.post(`/admin/hr/employees/${props.employee.id}/reset-password`, {}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
resetPasswordConfirmOpen.value = false;
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal mereset kata sandi.');
|
||||
},
|
||||
onFinish: () => {
|
||||
resetPasswordProcessing.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-8" as-child>
|
||||
<Link :href="`/admin/hr/employees/${employee.id}/edit`">
|
||||
<Pencil class="size-4" />
|
||||
<span class="sr-only">Ubah</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Ubah</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-8" @click="resetPasswordConfirmOpen = true">
|
||||
<KeyRound class="size-4" />
|
||||
<span class="sr-only">Reset Kata Sandi</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reset Kata Sandi</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
|
||||
@click="deleteConfirmOpen = true">
|
||||
<Trash2 class="size-4" />
|
||||
<span class="sr-only">Hapus</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Hapus</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog v-model:open="resetPasswordConfirmOpen" title="Reset kata sandi pegawai?"
|
||||
:description="`Kata sandi ${employee.full_name ?? 'pegawai'} akan direset ke kata sandi default sistem. Pengguna akan otomatis logout dari semua sesi aktif.`"
|
||||
confirm-label="Reset Kata Sandi" cancel-label="Batal" :loading="resetPasswordProcessing"
|
||||
@confirm="resetPassword" />
|
||||
|
||||
<ConfirmDialog v-model:open="deleteConfirmOpen" title="Hapus pegawai?"
|
||||
:description="`Data pegawai ${employee.full_name ?? ''} akan dihapus secara permanen beserta akun pengguna terkait. Tindakan ini tidak dapat dibatalkan.`"
|
||||
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyEmployee" />
|
||||
</template>
|
||||
@ -1,59 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, router } from '@inertiajs/vue3';
|
||||
import { MoreHorizontal, Pencil, Trash2 } from '@lucide/vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import type { EmployeeListItem } from '@/types/employee';
|
||||
|
||||
const props = defineProps<{
|
||||
employee: EmployeeListItem;
|
||||
}>();
|
||||
|
||||
function destroyEmployee() {
|
||||
if (!confirm(`Hapus pegawai ${props.employee.full_name ?? ''}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.delete(`/admin/hr/employees/${props.employee.id}`, {
|
||||
onError: () => {
|
||||
toast.error('Gagal menghapus pegawai.');
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" class="h-8 w-8 p-0">
|
||||
<span class="sr-only">Buka menu</span>
|
||||
<MoreHorizontal class="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Aksi</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem as-child>
|
||||
<Link :href="`/admin/hr/employees/${employee.id}/edit`" class="flex items-center gap-2">
|
||||
<Pencil class="size-4" />
|
||||
Ubah
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
class="text-destructive focus:text-destructive flex items-center gap-2"
|
||||
@click="destroyEmployee"
|
||||
>
|
||||
<Trash2 class="size-4" />
|
||||
Hapus
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
38
resources/js/components/ui/switch/Switch.vue
Normal file
38
resources/js/components/ui/switch/Switch.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { SwitchRootEmits, SwitchRootProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
SwitchRoot,
|
||||
SwitchThumb,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const emits = defineEmits<SwitchRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="switch"
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<SwitchThumb
|
||||
data-slot="switch-thumb"
|
||||
:class="cn('bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0')"
|
||||
>
|
||||
<slot name="thumb" v-bind="slotProps" />
|
||||
</SwitchThumb>
|
||||
</SwitchRoot>
|
||||
</template>
|
||||
@ -6,7 +6,11 @@ type Flash = {
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
function showFlash(flash: Flash): void {
|
||||
function showFlash(flash: Flash | undefined): void {
|
||||
if (!flash) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (flash.success) {
|
||||
toast.success(flash.success);
|
||||
}
|
||||
@ -17,11 +21,11 @@ function showFlash(flash: Flash): void {
|
||||
}
|
||||
|
||||
export function useFlashToast(): void {
|
||||
router.on('success', (event) => {
|
||||
const flash = event.detail.page.props.flash as Flash | undefined;
|
||||
router.on('flash', (event) => {
|
||||
showFlash(event.detail.flash as Flash);
|
||||
});
|
||||
|
||||
if (flash) {
|
||||
showFlash(flash);
|
||||
}
|
||||
router.on('success', (event) => {
|
||||
showFlash(event.detail.page.props.flash as Flash | undefined);
|
||||
});
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ export function formatPhoneNumber(value: string | number): string {
|
||||
}
|
||||
|
||||
if (digits.length <= 8) {
|
||||
return `${digits.slice(0, 4)}-${digits.slice(4)}`;
|
||||
return `${digits.slice(0, 4)} ${digits.slice(4)}`;
|
||||
}
|
||||
|
||||
return `${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8)}`;
|
||||
return `${digits.slice(0, 4)} ${digits.slice(4, 8)} ${digits.slice(8)}`;
|
||||
}
|
||||
|
||||
@ -23,6 +23,12 @@ export type DataTablePagination = {
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type DataTablePaginationLink = {
|
||||
url: string | null;
|
||||
label: string;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
export type DataTableQuery = {
|
||||
search?: string;
|
||||
sort?: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user