refactor: replace inline action buttons with standardized component-based row actions across data tables and forms

This commit is contained in:
Yoga Pangestu 2026-06-23 11:54:48 +07:00
parent 4414f2c050
commit 9f75a4416a
29 changed files with 303 additions and 878 deletions

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { SlidersHorizontal } from '@lucide/vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
defineProps<{
tooltip?: string;
disabled?: boolean;
}>();
</script>
<template>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" :disabled="disabled">
<SlidersHorizontal class="size-4" />
<span class="sr-only">{{ tooltip || 'Sesuaikan' }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{{ tooltip || 'Sesuaikan' }}</TooltipContent>
</Tooltip>
</template>

View File

@ -6,14 +6,16 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
defineProps<{ defineProps<{
tooltip?: string; tooltip?: string;
disabled?: boolean; disabled?: boolean;
size?: 'default' | 'sm';
}>(); }>();
</script> </script>
<template> <template>
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8 text-green-600 hover:text-green-700" :disabled="disabled"> <Button variant="ghost" :size="size === 'sm' ? 'sm' : 'icon'"
<Check class="size-4" /> :class="size === 'sm' ? '' : 'size-8'" class="text-green-600 hover:text-green-700" :disabled="disabled">
<Check :class="size === 'sm' ? 'size-3.5' : 'size-4'" />
<span class="sr-only">{{ tooltip || 'Setujui' }}</span> <span class="sr-only">{{ tooltip || 'Setujui' }}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>

View File

@ -1,61 +1,43 @@
<script setup lang="ts"> <script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { Trash2 } from '@lucide/vue'; import { Trash2 } from '@lucide/vue';
import { ref } from 'vue';
import { toast } from 'vue-sonner';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useDestroy } from '@/composables/useDestroy';
const props = defineProps<{ const props = defineProps<{
actionUrl: string;
title?: string; title?: string;
description?: string; description?: string;
actionUrl?: string; // If provided, handles the deletion request automatically.
errorMessage?: string; errorMessage?: string;
tooltip?: string;
tooltipClass?: string;
buttonClass?: string;
onSuccess?: () => void;
onError?: (errors: Record<string, string>) => string | void;
}>(); }>();
const emit = defineEmits<{ const { open, processing, destroy } = useDestroy({
confirm: []; url: props.actionUrl,
}>(); errorMessage: props.errorMessage ?? 'Gagal menghapus data.',
onSuccess: props.onSuccess,
const deleteConfirmOpen = ref(false); onError: props.onError,
const deleteProcessing = ref(false); });
function handleConfirm() {
if (props.actionUrl) {
deleteProcessing.value = true;
router.delete(props.actionUrl, {
preserveScroll: true,
onSuccess: () => {
deleteConfirmOpen.value = false;
},
onError: (errors) => {
const firstError = Object.values(errors)[0];
toast.error(firstError || props.errorMessage || 'Gagal menghapus data.');
},
onFinish: () => {
deleteProcessing.value = false;
},
});
} else {
emit('confirm');
}
}
</script> </script>
<template> <template>
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8" <Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
@click="deleteConfirmOpen = true"> :class="buttonClass" @click="open = true">
<Trash2 class="size-4" /> <Trash2 class="size-4" />
<span class="sr-only">Hapus</span> <span class="sr-only">{{ tooltip || 'Hapus' }}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Hapus</TooltipContent> <TooltipContent :class="tooltipClass">{{ tooltip || 'Hapus' }}</TooltipContent>
</Tooltip> </Tooltip>
<ConfirmDialog v-model:open="deleteConfirmOpen" :title="title || 'Hapus data?'" <ConfirmDialog v-model:open="open" :title="title || 'Hapus data?'"
:description="description || 'Data akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.'" :description="description || 'Data akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.'"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="handleConfirm" /> confirm-label="Hapus" cancel-label="Batal" destructive :loading="processing" @confirm="destroy" />
</template> </template>

View File

@ -6,23 +6,31 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
defineProps<{ defineProps<{
href?: string; href?: string;
tooltip?: string;
tooltipClass?: string;
buttonClass?: string;
}>();
const emit = defineEmits<{
click: [];
}>(); }>();
</script> </script>
<template> <template>
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" :as-child="!!href"> <Button variant="ghost" size="icon" class="size-8" :class="buttonClass" :as-child="!!href"
@click="!href && emit('click')">
<Link v-if="href" :href="href"> <Link v-if="href" :href="href">
<Pencil class="size-4" /> <Pencil class="size-4" />
<span class="sr-only">Ubah</span> <span class="sr-only">{{ tooltip || 'Ubah' }}</span>
</Link> </Link>
<template v-else> <template v-else>
<Pencil class="size-4" /> <Pencil class="size-4" />
<span class="sr-only">Ubah</span> <span class="sr-only">{{ tooltip || 'Ubah' }}</span>
</template> </template>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Ubah</TooltipContent> <TooltipContent :class="tooltipClass">{{ tooltip || 'Ubah' }}</TooltipContent>
</Tooltip> </Tooltip>
</template> </template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { Banknote } from '@lucide/vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
defineProps<{
tooltip?: string;
disabled?: boolean;
}>();
</script>
<template>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" :disabled="disabled">
<Banknote class="size-4" />
<span class="sr-only">{{ tooltip || 'Bayar' }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{{ tooltip || 'Bayar' }}</TooltipContent>
</Tooltip>
</template>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import { Printer } from '@lucide/vue';
import { Button } from '@/components/ui/button';
import Popover from '@/components/ui/popover/Popover.vue';
import PopoverContent from '@/components/ui/popover/PopoverContent.vue';
import PopoverTrigger from '@/components/ui/popover/PopoverTrigger.vue';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
defineProps<{
tooltip?: string;
disabled?: boolean;
popoverAlign?: 'start' | 'center' | 'end';
}>();
const open = defineModel<boolean>('open', { default: false });
</script>
<template>
<Tooltip>
<Popover v-model:open="open">
<TooltipTrigger as-child>
<PopoverTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" :disabled="disabled">
<Printer class="size-4" />
<span class="sr-only">{{ tooltip || 'Cetak' }}</span>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>{{ tooltip || 'Cetak' }}</TooltipContent>
<PopoverContent :align="popoverAlign || 'end'" class="w-40 space-y-2 p-3">
<slot name="popover-content" />
</PopoverContent>
</Popover>
</Tooltip>
</template>

View File

@ -6,14 +6,16 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
defineProps<{ defineProps<{
tooltip?: string; tooltip?: string;
disabled?: boolean; disabled?: boolean;
size?: 'default' | 'sm';
}>(); }>();
</script> </script>
<template> <template>
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8 text-destructive hover:text-destructive" :disabled="disabled"> <Button variant="ghost" :size="size === 'sm' ? 'sm' : 'icon'"
<X class="size-4" /> :class="size === 'sm' ? '' : 'size-8'" class="text-destructive hover:text-destructive" :disabled="disabled">
<X :class="size === 'sm' ? 'size-3.5' : 'size-4'" />
<span class="sr-only">{{ tooltip || 'Tolak' }}</span> <span class="sr-only">{{ tooltip || 'Tolak' }}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { Component } from 'vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
defineProps<{
icon: Component;
label: string;
destructive?: boolean;
size?: 'default' | 'sm';
disabled?: boolean;
tooltipClass?: string;
}>();
const emit = defineEmits<{
click: [];
}>();
</script>
<template>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" :size="size === 'sm' ? 'sm' : 'icon'"
:class="[size === 'sm' ? '' : 'size-8', destructive ? 'text-destructive hover:text-destructive' : '']"
:disabled="disabled" @click="emit('click')">
<component :is="icon" :class="size === 'sm' ? 'size-3.5' : 'size-4'" />
<span class="sr-only">{{ label }}</span>
</Button>
</TooltipTrigger>
<TooltipContent :class="tooltipClass">{{ label }}</TooltipContent>
</Tooltip>
</template>

View File

@ -2,6 +2,10 @@ export { default as RowEditAction } from './RowEditAction.vue';
export { default as RowDeleteAction } from './RowDeleteAction.vue'; export { default as RowDeleteAction } from './RowDeleteAction.vue';
export { default as RowApproveAction } from './RowApproveAction.vue'; export { default as RowApproveAction } from './RowApproveAction.vue';
export { default as RowRejectAction } from './RowRejectAction.vue'; export { default as RowRejectAction } from './RowRejectAction.vue';
export { default as RowPayAction } from './RowPayAction.vue';
export { default as RowAdjustAction } from './RowAdjustAction.vue';
export { default as RowStatusAction } from './RowStatusAction.vue';
export { default as RowPrintAction } from './RowPrintAction.vue';
export { default as RowDetailAction } from './RowDetailAction.vue'; export { default as RowDetailAction } from './RowDetailAction.vue';
export { default as CreateButton } from './CreateButton.vue'; export { default as CreateButton } from './CreateButton.vue';
export { default as BackButton } from './BackButton.vue'; export { default as BackButton } from './BackButton.vue';

View File

@ -1,11 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Pencil, Trash2 } from '@lucide/vue';
import { computed } from 'vue'; import { computed } from 'vue';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import { RowDeleteAction, RowEditAction } from '@/components/button';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { CashTransactionListItem } from '@/types/cash'; import type { CashTransactionListItem } from '@/types/cash';
import { destroy } from '@/routes/admin/finance/cash/transactions'; import { destroy } from '@/routes/admin/finance/cash/transactions';
@ -20,41 +16,15 @@ const emit = defineEmits<{
const { can } = useCan(); const { can } = useCan();
const showActions = computed(() => props.transaction.reference_type === null); const showActions = computed(() => props.transaction.reference_type === null);
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyTransaction } = useDestroy({
url: destroy.url(props.transaction.id),
onError: (errors) => errors.amount || errors.transaction || 'Gagal menghapus setor kas.',
});
</script> </script>
<template> <template>
<template v-if="showActions"> <template v-if="showActions">
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('cash.update')"> <RowEditAction v-if="can('cash.update')" @click="emit('edit', transaction)" />
<TooltipTrigger as-child> <RowDeleteAction v-if="can('cash.delete')" :action-url="destroy.url(transaction.id)" title="Hapus setor kas?"
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', transaction)"> :description="`Setor kas sebesar ${transaction.amount_formatted} akan dihapus. Saldo kas akan disesuaikan.`"
<Pencil class="size-4" /> :on-error="(errors) => errors.amount || errors.transaction || 'Gagal menghapus setor kas.'" />
<span class="sr-only">Ubah</span>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('cash.delete')">
<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> </div>
<ConfirmDialog v-if="can('cash.delete')" v-model:open="deleteConfirmOpen" title="Hapus setor kas?"
:description="`Setor kas sebesar ${transaction.amount_formatted} akan dihapus. Saldo kas akan disesuaikan.`"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing"
@confirm="destroyTransaction" />
</template> </template>
</template> </template>

View File

@ -1,13 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { router } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3';
import { Banknote, Check, Pencil, Trash2, X } from '@lucide/vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { RowApproveAction, RowDeleteAction, RowEditAction, RowPayAction, RowRejectAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { EmployeeAdvanceListItem } from '@/types/employee-advance'; import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
import { destroy, approve, pay } from '@/routes/admin/finance/employee_advances'; import { destroy, approve, pay } from '@/routes/admin/finance/employee_advances';
@ -47,11 +44,6 @@ const payConfirmOpen = ref(false);
const approveProcessing = ref(false); const approveProcessing = ref(false);
const payProcessing = ref(false); const payProcessing = ref(false);
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyEmployeeAdvance } = useDestroy({
url: destroy.url(props.employeeAdvance.id),
errorMessage: 'Gagal menghapus kasbon.',
});
function approveEmployeeAdvance() { function approveEmployeeAdvance() {
approveProcessing.value = true; approveProcessing.value = true;
@ -89,58 +81,22 @@ function payEmployeeAdvance() {
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="canEdit"> <RowEditAction v-if="canEdit" @click="emit('edit', employeeAdvance)" />
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', employeeAdvance)">
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="employeeAdvance.can_verify && can('employee_advances.verify')"> <RowApproveAction v-if="employeeAdvance.can_verify && can('employee_advances.verify')" tooltip="Setujui"
<TooltipTrigger as-child> @click="approveConfirmOpen = true" />
<Button variant="ghost" size="icon" class="size-8 text-green-600 hover:text-green-600"
@click="approveConfirmOpen = true">
<Check class="size-4" />
<span class="sr-only">Setujui</span>
</Button>
</TooltipTrigger>
<TooltipContent>Setujui</TooltipContent>
</Tooltip>
<Tooltip v-if="employeeAdvance.can_verify && can('employee_advances.verify')"> <RowRejectAction v-if="employeeAdvance.can_verify && can('employee_advances.verify')" tooltip="Tolak"
<TooltipTrigger as-child> @click="emit('reject', employeeAdvance)" />
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
@click="emit('reject', employeeAdvance)">
<X class="size-4" />
<span class="sr-only">Tolak</span>
</Button>
</TooltipTrigger>
<TooltipContent>Tolak</TooltipContent>
</Tooltip>
<Tooltip v-if="employeeAdvance.can_pay && can('employee_advances.pay')"> <RowPayAction v-if="employeeAdvance.can_pay && can('employee_advances.pay')" tooltip="Lunasi Kasbon"
<TooltipTrigger as-child> @click="payConfirmOpen = true" />
<Button variant="ghost" size="icon" class="size-8" @click="payConfirmOpen = true">
<Banknote class="size-4" />
<span class="sr-only">Lunasi</span>
</Button>
</TooltipTrigger>
<TooltipContent>Lunasi Kasbon</TooltipContent>
</Tooltip>
<Tooltip v-if="canDelete"> <template v-if="canDelete">
<TooltipTrigger as-child> <RowDeleteAction :action-url="destroy.url(employeeAdvance.id)" title="Hapus kasbon?"
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8" :description="`Pengajuan kasbon ${employeeAdvance.amount_formatted} akan dihapus.`"
@click="deleteConfirmOpen = true"> error-message="Gagal menghapus kasbon." />
<Trash2 class="size-4" /> </template>
<span class="sr-only">Hapus</span>
</Button>
</TooltipTrigger>
<TooltipContent>Hapus</TooltipContent>
</Tooltip>
</div> </div>
<ConfirmDialog v-if="can('employee_advances.verify')" v-model:open="approveConfirmOpen" title="Setujui kasbon?" <ConfirmDialog v-if="can('employee_advances.verify')" v-model:open="approveConfirmOpen" title="Setujui kasbon?"
@ -150,8 +106,4 @@ function payEmployeeAdvance() {
<ConfirmDialog v-if="can('employee_advances.pay')" v-model:open="payConfirmOpen" title="Lunasi kasbon?" <ConfirmDialog v-if="can('employee_advances.pay')" v-model:open="payConfirmOpen" title="Lunasi kasbon?"
:description="`Pelunasan kasbon ${employeeAdvance.amount_formatted} dari ${employeeAdvance.employee_name}. Saldo kas akan bertambah.`" :description="`Pelunasan kasbon ${employeeAdvance.amount_formatted} dari ${employeeAdvance.employee_name}. Saldo kas akan bertambah.`"
confirm-label="Lunasi" cancel-label="Batal" :loading="payProcessing" @confirm="payEmployeeAdvance" /> confirm-label="Lunasi" cancel-label="Batal" :loading="payProcessing" @confirm="payEmployeeAdvance" />
<ConfirmDialog v-if="canDelete" v-model:open="deleteConfirmOpen" title="Hapus kasbon?"
:description="`Pengajuan kasbon ${employeeAdvance.amount_formatted} akan dihapus.`" confirm-label="Hapus"
cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyEmployeeAdvance" />
</template> </template>

View File

@ -1,15 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Pencil, Trash2 } from '@lucide/vue'; import { RowDeleteAction, RowEditAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { ExpenseListItem } from '@/types/expense'; import type { ExpenseListItem } from '@/types/expense';
import { destroy } from '@/routes/admin/finance/expenses'; import { destroy } from '@/routes/admin/finance/expenses';
const props = defineProps<{ defineProps<{
expense: ExpenseListItem; expense: ExpenseListItem;
}>(); }>();
@ -18,38 +13,13 @@ const emit = defineEmits<{
}>(); }>();
const { can } = useCan(); const { can } = useCan();
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyExpense } = useDestroy({
url: destroy.url(props.expense.id),
onError: (errors) => errors.amount || errors.transaction || 'Gagal menghapus pengeluaran.',
});
</script> </script>
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('expenses.update')"> <RowEditAction v-if="can('expenses.update')" @click="emit('edit', expense)" />
<TooltipTrigger as-child> <RowDeleteAction v-if="can('expenses.delete')" :action-url="destroy.url(expense.id)" title="Hapus pengeluaran?"
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', expense)"> :description="`Pengeluaran ${expense.amount_formatted} akan dihapus. Saldo kas akan disesuaikan.`"
<Pencil class="size-4" /> :on-error="(errors) => errors.amount || errors.transaction || 'Gagal menghapus pengeluaran.'" />
<span class="sr-only">Ubah</span>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('expenses.delete')">
<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> </div>
<ConfirmDialog v-if="can('expenses.delete')" v-model:open="deleteConfirmOpen" title="Hapus pengeluaran?"
:description="`Pengeluaran ${expense.amount_formatted} akan dihapus. Saldo kas akan disesuaikan.`"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyExpense" />
</template> </template>

View File

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useForm, router } from '@inertiajs/vue3'; import { useForm } from '@inertiajs/vue3';
import { Pencil, Save, Trash2 } from '@lucide/vue'; import { Save } from '@lucide/vue';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { RowDeleteAction, RowEditAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { RupiahInput } from '@/components/form/rupiah-input'; import { RupiahInput } from '@/components/form/rupiah-input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -22,11 +23,6 @@ import {
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { FIELD_LIMITS } from '@/lib/field-limits'; import { FIELD_LIMITS } from '@/lib/field-limits';
import { formErrors } from '@/lib/form'; import { formErrors } from '@/lib/form';
import { parseRupiah } from '@/lib/rupiah'; import { parseRupiah } from '@/lib/rupiah';
@ -47,8 +43,6 @@ const form = useForm<PayrollAdjustmentFormData>({
}); });
const editingAdjustment = ref<PayrollAdjustmentItem | null>(null); const editingAdjustment = ref<PayrollAdjustmentItem | null>(null);
const deleteConfirmOpen = ref(false);
const deleteTarget = ref<PayrollAdjustmentItem | null>(null);
function resetForm() { function resetForm() {
form.reset(); form.reset();
@ -76,18 +70,6 @@ function cancelEdit() {
resetForm(); resetForm();
} }
function deleteAdj(adjustment: PayrollAdjustmentItem) {
router.delete(destroy.url(adjustment.id), {
preserveScroll: true,
onSuccess: () => {
toast.success('Penyesuaian berhasil dihapus.');
},
onError: () => {
toast.error('Gagal menghapus penyesuaian.');
},
});
}
function submit() { function submit() {
if (!props.payroll) { if (!props.payroll) {
return; return;
@ -150,30 +132,13 @@ function submit() {
<p class="text-xs text-muted-foreground">{{ adj.description }}</p> <p class="text-xs text-muted-foreground">{{ adj.description }}</p>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Tooltip> <RowEditAction button-class="text-muted-foreground hover:text-foreground"
<TooltipTrigger as-child> tooltip-class="z-210" @click="startEdit(adj)" />
<Button variant="ghost" size="icon" <RowDeleteAction :action-url="destroy.url(adj.id)" tooltip="Hapus"
class="size-8 text-muted-foreground hover:text-foreground" button-class="hover:bg-destructive/10" tooltip-class="z-210"
@click="startEdit(adj)"> title="Hapus penyesuaian?" :description="`Penyesuaian ${adj.type_label} ${adj.amount_formatted} akan dihapus.`"
<Pencil class="size-4" /> error-message="Gagal menghapus penyesuaian."
</Button> :on-success="() => toast.success('Penyesuaian berhasil dihapus.')" />
</TooltipTrigger>
<TooltipContent class="z-210">
<p>Ubah</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon"
class="size-8 text-destructive hover:bg-destructive/10"
@click="deleteTarget = adj; deleteConfirmOpen = true">
<Trash2 class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent class="z-210">
<p>Hapus</p>
</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>
@ -231,8 +196,4 @@ function submit() {
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<ConfirmDialog v-model:open="deleteConfirmOpen" title="Hapus Penyesuaian?"
:description="`Penyesuaian ${deleteTarget?.type_label} ${deleteTarget?.amount_formatted} akan dihapus.`"
confirm-label="Hapus" cancel-label="Batal" destructive @confirm="deleteAdj(deleteTarget!)" />
</template> </template>

View File

@ -1,7 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { SlidersHorizontal } from '@lucide/vue'; import { RowAdjustAction } from '@/components/button';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import type { PayrollListItem } from '@/types/payroll'; import type { PayrollListItem } from '@/types/payroll';
@ -18,14 +16,7 @@ const { can } = useCan();
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="payroll.can_adjust && can('payroll.adjust')"> <RowAdjustAction v-if="payroll.can_adjust && can('payroll.adjust')" tooltip="Sesuaikan Gaji"
<TooltipTrigger as-child> @click="emit('adjust', payroll)" />
<Button variant="ghost" size="icon" class="size-8" @click="emit('adjust', payroll)">
<SlidersHorizontal class="size-4" />
<span class="sr-only">Sesuaikan</span>
</Button>
</TooltipTrigger>
<TooltipContent>Sesuaikan Gaji</TooltipContent>
</Tooltip>
</div> </div>
</template> </template>

View File

@ -1,52 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { Trash2 } from '@lucide/vue'; import { RowDeleteAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { AttendanceListItem } from '@/types/attendance'; import type { AttendanceListItem } from '@/types/attendance';
import { destroy } from '@/routes/admin/hr/attendances'; import { destroy } from '@/routes/admin/hr/attendances';
const props = defineProps<{ defineProps<{
attendance: AttendanceListItem; attendance: AttendanceListItem;
}>(); }>();
const { can } = useCan(); const { can } = useCan();
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyAttendance } = useDestroy({
url: destroy.url(props.attendance.id),
errorMessage: 'Gagal menghapus data presensi.',
});
</script> </script>
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('attendances.delete')"> <RowDeleteAction v-if="can('attendances.delete')" :action-url="destroy.url(attendance.id)" title="Hapus data presensi?"
<TooltipTrigger as-child> :description="`Presensi tanggal ${attendance.attendance_date_formatted} akan dihapus secara permanen.`"
<Button error-message="Gagal menghapus data presensi." />
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> </div>
<ConfirmDialog
v-if="can('attendances.delete')"
v-model:open="deleteConfirmOpen"
title="Hapus data presensi?"
:description="`Presensi tanggal ${attendance.attendance_date_formatted} akan dihapus secara permanen.`"
confirm-label="Hapus"
cancel-label="Batal"
destructive
:loading="deleteProcessing"
@confirm="destroyAttendance"
/>
</template> </template>

View File

@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { Link, router, usePage } from '@inertiajs/vue3'; import { router, usePage } from '@inertiajs/vue3';
import { KeyRound, Pencil, Trash2 } from '@lucide/vue'; import { KeyRound } from '@lucide/vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { RowDeleteAction, RowEditAction, RowStatusAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { EmployeeListItem } from '@/types/employee'; import type { EmployeeListItem } from '@/types/employee';
import { edit, destroy, reset_password } from '@/routes/admin/hr/employees'; import { edit, destroy, reset_password } from '@/routes/admin/hr/employees';
@ -22,12 +20,6 @@ const defaultPassword = page.props.auth.password_default as string;
const resetPasswordConfirmOpen = ref(false); const resetPasswordConfirmOpen = ref(false);
const resetPasswordProcessing = ref(false); const resetPasswordProcessing = ref(false);
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyEmployee } = useDestroy({
url: destroy.url(props.employee.id),
preserveScroll: false,
errorMessage: 'Gagal menghapus pegawai.',
});
function resetPassword() { function resetPassword() {
resetPasswordProcessing.value = true; resetPasswordProcessing.value = true;
@ -48,38 +40,14 @@ function resetPassword() {
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('employees.update')"> <RowEditAction v-if="can('employees.update')" :href="edit.url(employee.id)" />
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" as-child>
<Link :href="edit.url(employee.id)">
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('employees.reset_password')"> <RowStatusAction v-if="can('employees.reset_password')" :icon="KeyRound" label="Reset Kata Sandi"
<TooltipTrigger as-child> @click="resetPasswordConfirmOpen = true" />
<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 v-if="can('employees.delete')"> <RowDeleteAction v-if="can('employees.delete')" :action-url="destroy.url(employee.id)" title="Hapus pegawai?"
<TooltipTrigger as-child> :description="`Data pegawai ${employee.profile?.full_name ?? ''} akan dihapus secara permanen beserta akun pengguna terkait. Tindakan ini tidak dapat dibatalkan.`"
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8" error-message="Gagal menghapus pegawai." />
@click="deleteConfirmOpen = true">
<Trash2 class="size-4" />
<span class="sr-only">Hapus</span>
</Button>
</TooltipTrigger>
<TooltipContent>Hapus</TooltipContent>
</Tooltip>
</div> </div>
<ConfirmDialog <ConfirmDialog
@ -89,11 +57,4 @@ function resetPassword() {
:description="`Kata sandi ${employee.profile?.full_name ?? 'pegawai'} akan direset ke kata sandi default sistem (${defaultPassword}). Pengguna akan otomatis logout dari semua sesi aktif.`" :description="`Kata sandi ${employee.profile?.full_name ?? 'pegawai'} akan direset ke kata sandi default sistem (${defaultPassword}). Pengguna akan otomatis logout dari semua sesi aktif.`"
confirm-label="Reset Kata Sandi" cancel-label="Batal" :loading="resetPasswordProcessing" confirm-label="Reset Kata Sandi" cancel-label="Batal" :loading="resetPasswordProcessing"
@confirm="resetPassword" /> @confirm="resetPassword" />
<ConfirmDialog
v-if="can('employees.delete')"
v-model:open="deleteConfirmOpen"
title="Hapus pegawai?"
:description="`Data pegawai ${employee.profile?.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> </template>

View File

@ -1,13 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { router } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3';
import { Check, Pencil, Trash2, X } from '@lucide/vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { RowApproveAction, RowDeleteAction, RowEditAction, RowRejectAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { LeaveRequestListItem } from '@/types/leave-request'; import type { LeaveRequestListItem } from '@/types/leave-request';
import { destroy, approve } from '@/routes/admin/hr/leave_requests'; import { destroy, approve } from '@/routes/admin/hr/leave_requests';
@ -45,11 +42,6 @@ const canDelete = computed(() => (
const approveConfirmOpen = ref(false); const approveConfirmOpen = ref(false);
const approveProcessing = ref(false); const approveProcessing = ref(false);
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyLeaveRequest } = useDestroy({
url: destroy.url(props.leaveRequest.id),
errorMessage: 'Gagal menghapus pengajuan cuti.',
});
function approveLeaveRequest() { function approveLeaveRequest() {
approveProcessing.value = true; approveProcessing.value = true;
@ -70,55 +62,22 @@ function approveLeaveRequest() {
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="canEdit"> <RowEditAction v-if="canEdit" @click="emit('edit', leaveRequest)" />
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', leaveRequest)">
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="leaveRequest.can_verify && can('leave_requests.verify')"> <RowApproveAction v-if="leaveRequest.can_verify && can('leave_requests.verify')" tooltip="Setujui"
<TooltipTrigger as-child> @click="approveConfirmOpen = true" />
<Button variant="ghost" size="icon" class="size-8 text-green-600 hover:text-green-600"
@click="approveConfirmOpen = true">
<Check class="size-4" />
<span class="sr-only">Setujui</span>
</Button>
</TooltipTrigger>
<TooltipContent>Setujui</TooltipContent>
</Tooltip>
<Tooltip v-if="leaveRequest.can_verify && can('leave_requests.verify')"> <RowRejectAction v-if="leaveRequest.can_verify && can('leave_requests.verify')" tooltip="Tolak"
<TooltipTrigger as-child> @click="emit('reject', leaveRequest)" />
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
@click="emit('reject', leaveRequest)">
<X class="size-4" />
<span class="sr-only">Tolak</span>
</Button>
</TooltipTrigger>
<TooltipContent>Tolak</TooltipContent>
</Tooltip>
<Tooltip v-if="canDelete"> <template v-if="canDelete">
<TooltipTrigger as-child> <RowDeleteAction :action-url="destroy.url(leaveRequest.id)" title="Hapus pengajuan cuti?"
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8" :description="`Pengajuan cuti ${leaveRequest.employee_name} akan dihapus.`"
@click="deleteConfirmOpen = true"> error-message="Gagal menghapus pengajuan cuti." />
<Trash2 class="size-4" /> </template>
<span class="sr-only">Hapus</span>
</Button>
</TooltipTrigger>
<TooltipContent>Hapus</TooltipContent>
</Tooltip>
</div> </div>
<ConfirmDialog v-if="can('leave_requests.verify')" v-model:open="approveConfirmOpen" title="Setujui cuti?" <ConfirmDialog v-if="can('leave_requests.verify')" v-model:open="approveConfirmOpen" title="Setujui cuti?"
:description="`Cuti ${leaveRequest.employee_name} (${leaveRequest.start_date_formatted} - ${leaveRequest.end_date_formatted}, ${leaveRequest.total_days} hari) akan disetujui.`" :description="`Cuti ${leaveRequest.employee_name} (${leaveRequest.start_date_formatted} - ${leaveRequest.end_date_formatted}, ${leaveRequest.total_days} hari) akan disetujui.`"
confirm-label="Setujui" cancel-label="Batal" :loading="approveProcessing" @confirm="approveLeaveRequest" /> confirm-label="Setujui" cancel-label="Batal" :loading="approveProcessing" @confirm="approveLeaveRequest" />
<ConfirmDialog v-if="canDelete" v-model:open="deleteConfirmOpen" title="Hapus pengajuan cuti?"
:description="`Pengajuan cuti ${leaveRequest.employee_name} akan dihapus.`" confirm-label="Hapus"
cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyLeaveRequest" />
</template> </template>

View File

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { Link, router, useForm } from '@inertiajs/vue3'; import { Link, router, useForm } from '@inertiajs/vue3';
import { Check, Pencil, RotateCcw, Scissors, Trash2, X } from '@lucide/vue'; import { Check, RotateCcw, Scissors, X } from '@lucide/vue';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { RowDeleteAction, RowEditAction, RowStatusAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -24,13 +25,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import { CuttingStatus } from '@/constants/cutting-status'; import { CuttingStatus } from '@/constants/cutting-status';
import { RupiahInput } from '@/components/form/rupiah-input'; import { RupiahInput } from '@/components/form/rupiah-input';
import { parseRupiah } from '@/lib/rupiah'; import { parseRupiah } from '@/lib/rupiah';
@ -55,11 +50,6 @@ const verifyDialogOpen = ref(false);
const allMatches = ref(true); const allMatches = ref(true);
const pendingAction = ref<CuttingStatusAction | null>(null); const pendingAction = ref<CuttingStatusAction | null>(null);
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyCutting } = useDestroy({
url: destroy.url(props.cutting.id),
errorMessage: 'Gagal menghapus cutting.',
});
const rejectForm = useForm({ const rejectForm = useForm({
status: CuttingStatus.REJECTED, status: CuttingStatus.REJECTED,
reason: '', reason: '',
@ -308,58 +298,17 @@ function actionIcon(status: string) {
<template> <template>
<div class="flex flex-wrap items-center justify-end gap-1"> <div class="flex flex-wrap items-center justify-end gap-1">
<template v-for="action in availableActions" :key="action.status"> <template v-for="action in availableActions" :key="action.status">
<Tooltip v-if="canPerformAction(action)"> <RowStatusAction v-if="canPerformAction(action)" :size="'sm'"
<TooltipTrigger as-child> :icon="actionIcon(action.status)" :label="action.label" :destructive="action.destructive"
<Button @click="openStatusConfirm(action)" />
size="sm"
variant="ghost"
:class="
action.destructive
? 'text-destructive hover:text-destructive'
: ''
"
@click="openStatusConfirm(action)"
>
<component
:is="actionIcon(action.status)"
class="size-3.5"
/>
<span class="sr-only">{{ action.label }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{{ action.label }}
</TooltipContent>
</Tooltip>
</template> </template>
<Tooltip v-if="canEdit"> <RowEditAction v-if="canEdit" :href="edit.url(cutting.id)" />
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" as-child>
<Link :href="edit.url(cutting.id)">
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="canDelete"> <template v-if="canDelete">
<TooltipTrigger as-child> <RowDeleteAction :action-url="destroy.url(cutting.id)" title="Hapus cutting?"
<Button description="Data cutting ini akan dihapus permanen." error-message="Gagal menghapus cutting." />
variant="ghost" </template>
size="icon"
class="size-8 text-destructive hover:text-destructive"
@click="deleteConfirmOpen = true"
>
<Trash2 class="size-4" />
<span class="sr-only">Hapus</span>
</Button>
</TooltipTrigger>
<TooltipContent>Hapus</TooltipContent>
</Tooltip>
</div> </div>
<ConfirmDialog <ConfirmDialog
@ -379,18 +328,6 @@ function actionIcon(status: string) {
@confirm="transitionStatus" @confirm="transitionStatus"
/> />
<ConfirmDialog
v-if="canDelete"
v-model:open="deleteConfirmOpen"
title="Hapus cutting?"
description="Data cutting ini akan dihapus permanen."
confirm-label="Hapus"
cancel-label="Batal"
destructive
:loading="deleteProcessing"
@confirm="destroyCutting"
/>
<Dialog v-model:open="rejectDialogOpen"> <Dialog v-model:open="rejectDialogOpen">
<DialogContent class="sm:max-w-md"> <DialogContent class="sm:max-w-md">
<DialogHeader> <DialogHeader>

View File

@ -1,15 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { usePage } from '@inertiajs/vue3'; import { usePage } from '@inertiajs/vue3';
import { Printer } from '@lucide/vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { RowPrintAction } from '@/components/button';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import Popover from '@/components/ui/popover/Popover.vue';
import PopoverContent from '@/components/ui/popover/PopoverContent.vue';
import PopoverTrigger from '@/components/ui/popover/PopoverTrigger.vue';
import Tooltip from '@/components/ui/tooltip/Tooltip.vue';
import TooltipContent from '@/components/ui/tooltip/TooltipContent.vue';
import TooltipTrigger from '@/components/ui/tooltip/TooltipTrigger.vue';
import { useThermalPrinter, getPaperSizeLabel } from '@/composables/useThermalPrinter'; import { useThermalPrinter, getPaperSizeLabel } from '@/composables/useThermalPrinter';
import type { PaperSize } from '@/lib/thermal-printer/types'; import type { PaperSize } from '@/lib/thermal-printer/types';
import type { OrderListItem } from '@/types/order'; import type { OrderListItem } from '@/types/order';
@ -69,32 +63,17 @@ async function handlePrint(paperSize: PaperSize) {
</script> </script>
<template> <template>
<Tooltip> <RowPrintAction v-model:open="open" tooltip="Cetak struk" :disabled="processing">
<Popover v-model:open="open"> <template #popover-content>
<TooltipTrigger as-child> <p class="text-sm font-medium">
<PopoverTrigger as-child> Ukuran kertas
<Button variant="ghost" size="icon" class="size-8" :disabled="processing"> </p>
<Printer class="size-4" /> <div class="grid grid-cols-2 gap-2">
<span class="sr-only">Cetak struk</span> <Button v-for="paperSize in paperSizes" :key="paperSize" variant="outline" size="sm" class="h-8"
</Button> :disabled="processing" @click="handlePrint(paperSize)">
</PopoverTrigger> {{ paperSize }}
</TooltipTrigger> </Button>
</div>
<TooltipContent> </template>
Cetak struk </RowPrintAction>
</TooltipContent>
<PopoverContent align="end" class="w-40 space-y-2 p-3">
<p class="text-sm font-medium">
Ukuran kertas
</p>
<div class="grid grid-cols-2 gap-2">
<Button v-for="paperSize in paperSizes" :key="paperSize" variant="outline" size="sm" class="h-8"
:disabled="processing" @click="handlePrint(paperSize)">
{{ paperSize }}
</Button>
</div>
</PopoverContent>
</Popover>
</Tooltip>
</template> </template>

View File

@ -1,14 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { Link, router } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3';
import { Check, Eye, Pencil, Send, Trash2, X } from '@lucide/vue'; import { Check, Send, X } from '@lucide/vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import OrderPrintButton from '../form/OrderPrintButton.vue'; import OrderPrintButton from '../form/OrderPrintButton.vue';
import { RowDeleteAction, RowDetailAction, RowEditAction, RowStatusAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import { OrderStatus } from '@/constants/order-status'; import { OrderStatus } from '@/constants/order-status';
import type { OrderListItem, OrderStatusAction } from '@/types/order'; import type { OrderListItem, OrderStatusAction } from '@/types/order';
import { show, edit, destroy, transition_status } from '@/routes/admin/manage/orders'; import { show, edit, destroy, transition_status } from '@/routes/admin/manage/orders';
@ -28,11 +27,6 @@ const availableActions = computed(() => props.order.available_actions ?? []);
const canEdit = computed(() => props.order.is_editable && can('orders.update')); const canEdit = computed(() => props.order.is_editable && can('orders.update'));
const canDelete = computed(() => can('orders.delete')); const canDelete = computed(() => can('orders.delete'));
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyOrder } = useDestroy({
url: destroy.url(props.order.id),
errorMessage: 'Gagal menghapus pesanan.',
});
function canPerformAction(action: OrderStatusAction): boolean { function canPerformAction(action: OrderStatusAction): boolean {
return can(action.permission); return can(action.permission);
} }
@ -98,21 +92,9 @@ function actionIcon(status: string) {
<OrderPrintButton :order="order" /> <OrderPrintButton :order="order" />
<template v-for="action in availableActions" :key="action.status"> <template v-for="action in availableActions" :key="action.status">
<Tooltip v-if="action.icon_only && canPerformAction(action)"> <RowStatusAction v-if="action.icon_only && canPerformAction(action)"
<TooltipTrigger as-child> :icon="actionIcon(action.status)" :label="action.label" :destructive="action.destructive"
<Button @click="openStatusConfirm(action)" />
variant="ghost"
size="icon"
class="size-8"
:class="action.destructive ? 'text-destructive hover:text-destructive' : ''"
@click="openStatusConfirm(action)"
>
<component :is="actionIcon(action.status)" class="size-4" />
<span class="sr-only">{{ action.label }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{{ action.label }}</TooltipContent>
</Tooltip>
<Button <Button
v-else-if="canPerformAction(action)" v-else-if="canPerformAction(action)"
@ -126,44 +108,15 @@ function actionIcon(status: string) {
</Button> </Button>
</template> </template>
<Tooltip> <RowDetailAction :href="show.url(order.id)" tooltip="Detail" />
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" as-child>
<Link :href="show.url(props.order.id)">
<Eye class="size-4" />
<span class="sr-only">Detail</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Detail</TooltipContent>
</Tooltip>
<Tooltip v-if="canEdit"> <RowEditAction v-if="canEdit" :href="edit.url(order.id)" />
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" as-child>
<Link :href="edit.url(props.order.id)">
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="canDelete"> <template v-if="canDelete">
<TooltipTrigger as-child> <RowDeleteAction :action-url="destroy.url(order.id)" title="Hapus pesanan?"
<Button :description="`Pesanan ${order.order_number} akan dihapus.${order.is_editable ? ' Stok produk akan dikembalikan.' : ''}`"
variant="ghost" error-message="Gagal menghapus pesanan." />
size="icon" </template>
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> </div>
<ConfirmDialog <ConfirmDialog
@ -176,16 +129,4 @@ function actionIcon(status: string) {
:loading="statusProcessing" :loading="statusProcessing"
@confirm="transitionStatus" @confirm="transitionStatus"
/> />
<ConfirmDialog
v-if="canDelete"
v-model:open="deleteConfirmOpen"
title="Hapus pesanan?"
:description="`Pesanan ${order.order_number} akan dihapus.${order.is_editable ? ' Stok produk akan dikembalikan.' : ''}`"
confirm-label="Hapus"
cancel-label="Batal"
destructive
:loading="deleteProcessing"
@confirm="destroyOrder"
/>
</template> </template>

View File

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { router, useForm } from '@inertiajs/vue3'; import { router, useForm } from '@inertiajs/vue3';
import { Check, X } from '@lucide/vue'; import { X } from '@lucide/vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { RowApproveAction, RowRejectAction } from '@/components/button';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@ -17,11 +18,6 @@ import {
FieldLabel, FieldLabel,
} from '@/components/ui/field'; } from '@/components/ui/field';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import type { CuttingListItem } from '@/types/cutting'; import type { CuttingListItem } from '@/types/cutting';
import { approve, reject } from '@/routes/admin/manage/owner_verifications'; import { approve, reject } from '@/routes/admin/manage/owner_verifications';
@ -69,36 +65,9 @@ function submitReject() {
<template> <template>
<div v-if="can('owner_verifications.verify')" class="flex items-center gap-1"> <div v-if="can('owner_verifications.verify')" class="flex items-center gap-1">
<Tooltip> <RowApproveAction size="sm" tooltip="Setujui Verifikasi" :disabled="approveForm.processing"
<TooltipTrigger as-child> @click="submitApprove" />
<Button <RowRejectAction size="sm" tooltip="Tolak Verifikasi" @click="rejectDialogOpen = true" />
size="sm"
variant="ghost"
class="text-green-600 hover:text-green-700"
:disabled="approveForm.processing"
@click="submitApprove"
>
<Check class="size-3.5" />
<span class="sr-only">Setujui</span>
</Button>
</TooltipTrigger>
<TooltipContent>Setujui Verifikasi</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button
size="sm"
variant="ghost"
class="text-red-600 hover:text-red-700"
@click="rejectDialogOpen = true"
>
<X class="size-3.5" />
<span class="sr-only">Tolak</span>
</Button>
</TooltipTrigger>
<TooltipContent>Tolak Verifikasi</TooltipContent>
</Tooltip>
</div> </div>
<!-- Reject Dialog --> <!-- Reject Dialog -->

View File

@ -1,65 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { Link } from '@inertiajs/vue3'; import { RowDeleteAction, RowEditAction } from '@/components/button';
import { Pencil, Trash2 } from '@lucide/vue';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { PurchaseListItem } from '@/types/purchase'; import type { PurchaseListItem } from '@/types/purchase';
import { edit, destroy } from '@/routes/admin/manage/purchases'; import { edit, destroy } from '@/routes/admin/manage/purchases';
const props = defineProps<{ defineProps<{
purchase: PurchaseListItem; purchase: PurchaseListItem;
}>(); }>();
const { can } = useCan(); const { can } = useCan();
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyPurchase } = useDestroy({
url: destroy.url(props.purchase.id),
errorMessage: 'Gagal menghapus belanja.',
});
</script> </script>
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('purchases.update')"> <RowEditAction v-if="can('purchases.update')" :href="edit.url(purchase.id)" />
<TooltipTrigger as-child> <RowDeleteAction v-if="can('purchases.delete')" :action-url="destroy.url(purchase.id)" title="Hapus belanja?"
<Button variant="ghost" size="icon" class="size-8" as-child> :description="`Belanja ${purchase.total_formatted} dari ${purchase.supplier_name} akan dihapus. Stok bahan baku akan dikurangi.`"
<Link :href="edit.url(props.purchase.id)"> error-message="Gagal menghapus belanja." />
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('purchases.delete')">
<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> </div>
<ConfirmDialog
v-if="can('purchases.delete')"
v-model:open="deleteConfirmOpen"
title="Hapus belanja?"
:description="`Belanja ${purchase.total_formatted} dari ${purchase.supplier_name} akan dihapus. Stok bahan baku akan dikurangi.`"
confirm-label="Hapus"
cancel-label="Batal"
destructive
:loading="deleteProcessing"
@confirm="destroyPurchase"
/>
</template> </template>

View File

@ -3,6 +3,7 @@ import { router, useForm } from '@inertiajs/vue3';
import { Check } from '@lucide/vue'; import { Check } from '@lucide/vue';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { RowApproveAction } from '@/components/button';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@ -165,20 +166,8 @@ function submitVerify() {
</script> </script>
<template> <template>
<Tooltip v-if="can('cuttings.verify')"> <RowApproveAction v-if="can('cuttings.verify')" size="sm" tooltip="Verifikasi & Tambah Stok"
<TooltipTrigger as-child> @click="verifyDialogOpen = true" />
<Button
size="sm"
variant="ghost"
class="text-green-600 hover:text-green-700"
@click="verifyDialogOpen = true"
>
<Check class="size-3.5" />
<span class="sr-only">Verifikasi</span>
</Button>
</TooltipTrigger>
<TooltipContent>Verifikasi & Tambah Stok</TooltipContent>
</Tooltip>
<Dialog v-model:open="verifyDialogOpen"> <Dialog v-model:open="verifyDialogOpen">
<DialogContent class="sm:max-w-lg"> <DialogContent class="sm:max-w-lg">

View File

@ -1,14 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Pencil, Trash2 } from '@lucide/vue'; import { RowDeleteAction, RowEditAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { CategoryListItem } from '@/types/category'; import type { CategoryListItem } from '@/types/category';
import { destroy } from '@/routes/admin/master/categories'; import { destroy } from '@/routes/admin/master/categories';
const props = defineProps<{ defineProps<{
category: CategoryListItem; category: CategoryListItem;
}>(); }>();
@ -17,38 +13,13 @@ const emit = defineEmits<{
}>(); }>();
const { can } = useCan(); const { can } = useCan();
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyCategory } = useDestroy({
url: destroy.url(props.category.id),
errorMessage: 'Gagal menghapus kategori.',
});
</script> </script>
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('categories.update')"> <RowEditAction v-if="can('categories.update')" @click="emit('edit', category)" />
<TooltipTrigger as-child> <RowDeleteAction v-if="can('categories.delete')" :action-url="destroy.url(category.id)" title="Hapus kategori?"
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', category)"> :description="`Kategori ${category.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
<Pencil class="size-4" /> error-message="Gagal menghapus kategori." />
<span class="sr-only">Ubah</span>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('categories.delete')">
<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> </div>
<ConfirmDialog v-if="can('categories.delete')" v-model:open="deleteConfirmOpen" title="Hapus kategori?"
:description="`Kategori ${category.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyCategory" />
</template> </template>

View File

@ -1,14 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Pencil, Trash2 } from '@lucide/vue'; import { RowDeleteAction, RowEditAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { CustomerListItem } from '@/types/customer'; import type { CustomerListItem } from '@/types/customer';
import { destroy } from '@/routes/admin/master/customers'; import { destroy } from '@/routes/admin/master/customers';
const props = defineProps<{ defineProps<{
customer: CustomerListItem; customer: CustomerListItem;
}>(); }>();
@ -17,38 +13,13 @@ const emit = defineEmits<{
}>(); }>();
const { can } = useCan(); const { can } = useCan();
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyCustomer } = useDestroy({
url: destroy.url(props.customer.id),
errorMessage: 'Gagal menghapus customer.',
});
</script> </script>
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('customers.update')"> <RowEditAction v-if="can('customers.update')" @click="emit('edit', customer)" />
<TooltipTrigger as-child> <RowDeleteAction v-if="can('customers.delete')" :action-url="destroy.url(customer.id)" title="Hapus customer?"
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', customer)"> :description="`Customer ${customer.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
<Pencil class="size-4" /> error-message="Gagal menghapus customer." />
<span class="sr-only">Ubah</span>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('customers.delete')">
<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> </div>
<ConfirmDialog v-if="can('customers.delete')" v-model:open="deleteConfirmOpen" title="Hapus customer?"
:description="`Customer ${customer.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyCustomer" />
</template> </template>

View File

@ -1,54 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { Link } from '@inertiajs/vue3'; import { Link } from '@inertiajs/vue3';
import { Pencil, Trash2 } from '@lucide/vue'; import { RowDeleteAction, RowEditAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { ProductListItem } from '@/types/product'; import type { ProductListItem } from '@/types/product';
import { edit, destroy } from '@/routes/admin/master/products'; import { edit, destroy } from '@/routes/admin/master/products';
const props = defineProps<{ defineProps<{
product: ProductListItem; product: ProductListItem;
}>(); }>();
const { can } = useCan(); const { can } = useCan();
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyProduct } = useDestroy({
url: destroy.url(props.product.id),
preserveScroll: false,
errorMessage: 'Gagal menghapus produk.',
});
</script> </script>
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('products.update')"> <RowEditAction v-if="can('products.update')" :href="edit.url(product.id)" />
<TooltipTrigger as-child> <RowDeleteAction v-if="can('products.delete')" :action-url="destroy.url(product.id)" title="Hapus produk?"
<Button variant="ghost" size="icon" class="size-8" as-child> :description="`Produk ${product.name} akan dihapus beserta seluruh variannya. Tindakan ini tidak dapat dibatalkan.`"
<Link :href="edit.url(props.product.id)"> error-message="Gagal menghapus produk." />
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('products.delete')">
<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> </div>
<ConfirmDialog v-if="can('products.delete')" v-model:open="deleteConfirmOpen" title="Hapus produk?"
:description="`Produk ${product.name} akan dihapus beserta seluruh variannya. Tindakan ini tidak dapat dibatalkan.`"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyProduct" />
</template> </template>

View File

@ -1,55 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { Link } from '@inertiajs/vue3'; import { RowDeleteAction, RowEditAction } from '@/components/button';
import { Pencil, Trash2 } from '@lucide/vue';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { RawMaterialListItem } from '@/types/raw-material'; import type { RawMaterialListItem } from '@/types/raw-material';
import { edit, destroy } from '@/routes/admin/master/raw_materials'; import { edit, destroy } from '@/routes/admin/master/raw_materials';
const props = defineProps<{ defineProps<{
material: RawMaterialListItem; material: RawMaterialListItem;
}>(); }>();
const { can } = useCan(); const { can } = useCan();
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyRawMaterial } = useDestroy({
url: destroy.url(props.material.id),
preserveScroll: false,
errorMessage: 'Gagal menghapus bahan baku.',
});
</script> </script>
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('raw_materials.update')"> <RowEditAction v-if="can('raw_materials.update')" :href="edit.url(material.id)" />
<TooltipTrigger as-child> <RowDeleteAction v-if="can('raw_materials.delete')" :action-url="destroy.url(material.id)" title="Hapus bahan baku?"
<Button variant="ghost" size="icon" class="size-8" as-child> :description="`Bahan baku ${material.name} akan dihapus beserta seluruh variannya. Tindakan ini tidak dapat dibatalkan.`"
<Link :href="edit.url(props.material.id)"> error-message="Gagal menghapus bahan baku." />
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('raw_materials.delete')">
<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> </div>
<ConfirmDialog v-if="can('raw_materials.delete')" v-model:open="deleteConfirmOpen" title="Hapus bahan baku?"
:description="`Bahan baku ${material.name} akan dihapus beserta seluruh variannya. Tindakan ini tidak dapat dibatalkan.`"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing"
@confirm="destroyRawMaterial" />
</template> </template>

View File

@ -1,14 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Pencil, Trash2 } from '@lucide/vue'; import { RowDeleteAction, RowEditAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { SupplierListItem } from '@/types/supplier'; import type { SupplierListItem } from '@/types/supplier';
import { destroy } from '@/routes/admin/master/suppliers'; import { destroy } from '@/routes/admin/master/suppliers';
const props = defineProps<{ defineProps<{
supplier: SupplierListItem; supplier: SupplierListItem;
}>(); }>();
@ -17,38 +13,13 @@ const emit = defineEmits<{
}>(); }>();
const { can } = useCan(); const { can } = useCan();
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroySupplier } = useDestroy({
url: destroy.url(props.supplier.id),
errorMessage: 'Gagal menghapus supplier.',
});
</script> </script>
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('suppliers.update')"> <RowEditAction v-if="can('suppliers.update')" @click="emit('edit', supplier)" />
<TooltipTrigger as-child> <RowDeleteAction v-if="can('suppliers.delete')" :action-url="destroy.url(supplier.id)" title="Hapus supplier?"
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', supplier)"> :description="`Supplier ${supplier.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
<Pencil class="size-4" /> error-message="Gagal menghapus supplier." />
<span class="sr-only">Ubah</span>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('suppliers.delete')">
<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> </div>
<ConfirmDialog v-if="can('suppliers.delete')" v-model:open="deleteConfirmOpen" title="Hapus supplier?"
:description="`Supplier ${supplier.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroySupplier" />
</template> </template>

View File

@ -1,12 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { Pencil, Trash2 } from '@lucide/vue';
import { computed } from 'vue'; import { computed } from 'vue';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import { RowDeleteAction, RowEditAction } from '@/components/button';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { RoleListItem } from './columns'; import type { RoleListItem } from './columns';
import { edit, destroy } from '@/routes/admin/system/roles'; import { edit, destroy } from '@/routes/admin/system/roles';
@ -19,44 +14,13 @@ const { can } = useCan();
const isSystemRole = computed(() => { const isSystemRole = computed(() => {
return ['developer', 'owner', 'admin-toko', 'admin-bahan-baku', 'direktur', 'marketing', 'cashier', 'non-operator'].includes(props.role.name); return ['developer', 'owner', 'admin-toko', 'admin-bahan-baku', 'direktur', 'marketing', 'cashier', 'non-operator'].includes(props.role.name);
}); });
const { open: deleteConfirmOpen, processing: deleteProcessing, destroy: destroyRole } = useDestroy({
url: destroy.url(props.role.id),
errorMessage: 'Gagal menghapus role.',
});
</script> </script>
<template> <template>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('roles.update')"> <RowEditAction v-if="can('roles.update')" :href="edit.url(role.id)" />
<TooltipTrigger as-child> <RowDeleteAction v-if="can('roles.delete') && !isSystemRole" :action-url="destroy.url(role.id)" title="Hapus role?"
<Button variant="ghost" size="icon" class="size-8" as-child> :description="`Role ${role.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
<Link :href="edit.url(props.role.id)"> error-message="Gagal menghapus role." />
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
</Tooltip>
<Tooltip v-if="can('roles.delete')">
<TooltipTrigger as-child>
<span>
<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>
</span>
</TooltipTrigger>
<TooltipContent>
Hapus
</TooltipContent>
</Tooltip>
</div> </div>
<ConfirmDialog v-if="can('roles.delete') && !isSystemRole" v-model:open="deleteConfirmOpen" title="Hapus role?"
:description="`Role ${role.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyRole" />
</template> </template>