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<{
tooltip?: string;
disabled?: boolean;
size?: 'default' | 'sm';
}>();
</script>
<template>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8 text-green-600 hover:text-green-700" :disabled="disabled">
<Check class="size-4" />
<Button variant="ghost" :size="size === 'sm' ? 'sm' : 'icon'"
: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>
</Button>
</TooltipTrigger>

View File

@ -1,61 +1,43 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { 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 { useDestroy } from '@/composables/useDestroy';
const props = defineProps<{
actionUrl: string;
title?: string;
description?: string;
actionUrl?: string; // If provided, handles the deletion request automatically.
errorMessage?: string;
tooltip?: string;
tooltipClass?: string;
buttonClass?: string;
onSuccess?: () => void;
onError?: (errors: Record<string, string>) => string | void;
}>();
const emit = defineEmits<{
confirm: [];
}>();
const deleteConfirmOpen = ref(false);
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');
}
}
const { open, processing, destroy } = useDestroy({
url: props.actionUrl,
errorMessage: props.errorMessage ?? 'Gagal menghapus data.',
onSuccess: props.onSuccess,
onError: props.onError,
});
</script>
<template>
<Tooltip>
<TooltipTrigger as-child>
<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" />
<span class="sr-only">Hapus</span>
<span class="sr-only">{{ tooltip || 'Hapus' }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>Hapus</TooltipContent>
<TooltipContent :class="tooltipClass">{{ tooltip || 'Hapus' }}</TooltipContent>
</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.'"
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="handleConfirm" />
confirm-label="Hapus" cancel-label="Batal" destructive :loading="processing" @confirm="destroy" />
</template>

View File

@ -6,23 +6,31 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
defineProps<{
href?: string;
tooltip?: string;
tooltipClass?: string;
buttonClass?: string;
}>();
const emit = defineEmits<{
click: [];
}>();
</script>
<template>
<Tooltip>
<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">
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
<span class="sr-only">{{ tooltip || 'Ubah' }}</span>
</Link>
<template v-else>
<Pencil class="size-4" />
<span class="sr-only">Ubah</span>
<span class="sr-only">{{ tooltip || 'Ubah' }}</span>
</template>
</Button>
</TooltipTrigger>
<TooltipContent>Ubah</TooltipContent>
<TooltipContent :class="tooltipClass">{{ tooltip || 'Ubah' }}</TooltipContent>
</Tooltip>
</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<{
tooltip?: string;
disabled?: boolean;
size?: 'default' | 'sm';
}>();
</script>
<template>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8 text-destructive hover:text-destructive" :disabled="disabled">
<X class="size-4" />
<Button variant="ghost" :size="size === 'sm' ? 'sm' : 'icon'"
: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>
</Button>
</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 RowApproveAction } from './RowApproveAction.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 CreateButton } from './CreateButton.vue';
export { default as BackButton } from './BackButton.vue';

View File

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

View File

@ -1,13 +1,10 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { Banknote, Check, Pencil, Trash2, X } from '@lucide/vue';
import { computed, ref } from 'vue';
import { toast } from 'vue-sonner';
import { RowApproveAction, RowDeleteAction, RowEditAction, RowPayAction, RowRejectAction } 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 { useDestroy } from '@/composables/useDestroy';
import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
import { destroy, approve, pay } from '@/routes/admin/finance/employee_advances';
@ -47,11 +44,6 @@ const payConfirmOpen = ref(false);
const approveProcessing = 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() {
approveProcessing.value = true;
@ -89,58 +81,22 @@ function payEmployeeAdvance() {
<template>
<div class="flex items-center justify-end gap-1">
<Tooltip v-if="canEdit">
<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>
<RowEditAction v-if="canEdit" @click="emit('edit', employeeAdvance)" />
<Tooltip v-if="employeeAdvance.can_verify && can('employee_advances.verify')">
<TooltipTrigger as-child>
<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>
<RowApproveAction v-if="employeeAdvance.can_verify && can('employee_advances.verify')" tooltip="Setujui"
@click="approveConfirmOpen = true" />
<Tooltip v-if="employeeAdvance.can_verify && can('employee_advances.verify')">
<TooltipTrigger as-child>
<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>
<RowRejectAction v-if="employeeAdvance.can_verify && can('employee_advances.verify')" tooltip="Tolak"
@click="emit('reject', employeeAdvance)" />
<Tooltip v-if="employeeAdvance.can_pay && can('employee_advances.pay')">
<TooltipTrigger as-child>
<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>
<RowPayAction v-if="employeeAdvance.can_pay && can('employee_advances.pay')" tooltip="Lunasi Kasbon"
@click="payConfirmOpen = true" />
<Tooltip v-if="canDelete">
<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>
<template v-if="canDelete">
<RowDeleteAction :action-url="destroy.url(employeeAdvance.id)" title="Hapus kasbon?"
:description="`Pengajuan kasbon ${employeeAdvance.amount_formatted} akan dihapus.`"
error-message="Gagal menghapus kasbon." />
</template>
</div>
<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?"
:description="`Pelunasan kasbon ${employeeAdvance.amount_formatted} dari ${employeeAdvance.employee_name}. Saldo kas akan bertambah.`"
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>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,11 @@
<script setup lang="ts">
import { Link, router, usePage } from '@inertiajs/vue3';
import { KeyRound, Pencil, Trash2 } from '@lucide/vue';
import { router, usePage } from '@inertiajs/vue3';
import { KeyRound } from '@lucide/vue';
import { ref } from 'vue';
import { toast } from 'vue-sonner';
import { RowDeleteAction, RowEditAction, RowStatusAction } 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 { useDestroy } from '@/composables/useDestroy';
import type { EmployeeListItem } from '@/types/employee';
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 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() {
resetPasswordProcessing.value = true;
@ -48,38 +40,14 @@ function resetPassword() {
<template>
<div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('employees.update')">
<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>
<RowEditAction v-if="can('employees.update')" :href="edit.url(employee.id)" />
<Tooltip v-if="can('employees.reset_password')">
<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>
<RowStatusAction v-if="can('employees.reset_password')" :icon="KeyRound" label="Reset Kata Sandi"
@click="resetPasswordConfirmOpen = true" />
<Tooltip v-if="can('employees.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>
<RowDeleteAction v-if="can('employees.delete')" :action-url="destroy.url(employee.id)" title="Hapus pegawai?"
:description="`Data pegawai ${employee.profile?.full_name ?? ''} akan dihapus secara permanen beserta akun pengguna terkait. Tindakan ini tidak dapat dibatalkan.`"
error-message="Gagal menghapus pegawai." />
</div>
<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.`"
confirm-label="Reset Kata Sandi" cancel-label="Batal" :loading="resetPasswordProcessing"
@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>

View File

@ -1,13 +1,10 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { Check, Pencil, Trash2, X } from '@lucide/vue';
import { computed, ref } from 'vue';
import { toast } from 'vue-sonner';
import { RowApproveAction, RowDeleteAction, RowEditAction, RowRejectAction } 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 { useDestroy } from '@/composables/useDestroy';
import type { LeaveRequestListItem } from '@/types/leave-request';
import { destroy, approve } from '@/routes/admin/hr/leave_requests';
@ -45,11 +42,6 @@ const canDelete = computed(() => (
const approveConfirmOpen = 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() {
approveProcessing.value = true;
@ -70,55 +62,22 @@ function approveLeaveRequest() {
<template>
<div class="flex items-center justify-end gap-1">
<Tooltip v-if="canEdit">
<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>
<RowEditAction v-if="canEdit" @click="emit('edit', leaveRequest)" />
<Tooltip v-if="leaveRequest.can_verify && can('leave_requests.verify')">
<TooltipTrigger as-child>
<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>
<RowApproveAction v-if="leaveRequest.can_verify && can('leave_requests.verify')" tooltip="Setujui"
@click="approveConfirmOpen = true" />
<Tooltip v-if="leaveRequest.can_verify && can('leave_requests.verify')">
<TooltipTrigger as-child>
<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>
<RowRejectAction v-if="leaveRequest.can_verify && can('leave_requests.verify')" tooltip="Tolak"
@click="emit('reject', leaveRequest)" />
<Tooltip v-if="canDelete">
<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>
<template v-if="canDelete">
<RowDeleteAction :action-url="destroy.url(leaveRequest.id)" title="Hapus pengajuan cuti?"
:description="`Pengajuan cuti ${leaveRequest.employee_name} akan dihapus.`"
error-message="Gagal menghapus pengajuan cuti." />
</template>
</div>
<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.`"
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>

View File

@ -1,8 +1,9 @@
<script setup lang="ts">
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 { toast } from 'vue-sonner';
import { RowDeleteAction, RowEditAction, RowStatusAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@ -24,13 +25,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import { CuttingStatus } from '@/constants/cutting-status';
import { RupiahInput } from '@/components/form/rupiah-input';
import { parseRupiah } from '@/lib/rupiah';
@ -55,11 +50,6 @@ const verifyDialogOpen = ref(false);
const allMatches = ref(true);
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({
status: CuttingStatus.REJECTED,
reason: '',
@ -308,58 +298,17 @@ function actionIcon(status: string) {
<template>
<div class="flex flex-wrap items-center justify-end gap-1">
<template v-for="action in availableActions" :key="action.status">
<Tooltip v-if="canPerformAction(action)">
<TooltipTrigger as-child>
<Button
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>
<RowStatusAction v-if="canPerformAction(action)" :size="'sm'"
:icon="actionIcon(action.status)" :label="action.label" :destructive="action.destructive"
@click="openStatusConfirm(action)" />
</template>
<Tooltip v-if="canEdit">
<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>
<RowEditAction v-if="canEdit" :href="edit.url(cutting.id)" />
<Tooltip v-if="canDelete">
<TooltipTrigger as-child>
<Button
variant="ghost"
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>
<template v-if="canDelete">
<RowDeleteAction :action-url="destroy.url(cutting.id)" title="Hapus cutting?"
description="Data cutting ini akan dihapus permanen." error-message="Gagal menghapus cutting." />
</template>
</div>
<ConfirmDialog
@ -379,18 +328,6 @@ function actionIcon(status: string) {
@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">
<DialogContent class="sm:max-w-md">
<DialogHeader>

View File

@ -1,15 +1,9 @@
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3';
import { Printer } from '@lucide/vue';
import { computed, ref } from 'vue';
import { toast } from 'vue-sonner';
import { RowPrintAction } from '@/components/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 type { PaperSize } from '@/lib/thermal-printer/types';
import type { OrderListItem } from '@/types/order';
@ -69,32 +63,17 @@ async function handlePrint(paperSize: PaperSize) {
</script>
<template>
<Tooltip>
<Popover v-model:open="open">
<TooltipTrigger as-child>
<PopoverTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" :disabled="processing">
<Printer class="size-4" />
<span class="sr-only">Cetak struk</span>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>
Cetak struk
</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>
<RowPrintAction v-model:open="open" tooltip="Cetak struk" :disabled="processing">
<template #popover-content>
<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>
</template>
</RowPrintAction>
</template>

View File

@ -1,14 +1,13 @@
<script setup lang="ts">
import { Link, router } from '@inertiajs/vue3';
import { Check, Eye, Pencil, Send, Trash2, X } from '@lucide/vue';
import { router } from '@inertiajs/vue3';
import { Check, Send, X } from '@lucide/vue';
import { computed, ref } from 'vue';
import { toast } from 'vue-sonner';
import OrderPrintButton from '../form/OrderPrintButton.vue';
import { RowDeleteAction, RowDetailAction, RowEditAction, RowStatusAction } 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 { useDestroy } from '@/composables/useDestroy';
import { OrderStatus } from '@/constants/order-status';
import type { OrderListItem, OrderStatusAction } from '@/types/order';
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 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 {
return can(action.permission);
}
@ -98,21 +92,9 @@ function actionIcon(status: string) {
<OrderPrintButton :order="order" />
<template v-for="action in availableActions" :key="action.status">
<Tooltip v-if="action.icon_only && canPerformAction(action)">
<TooltipTrigger as-child>
<Button
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>
<RowStatusAction v-if="action.icon_only && canPerformAction(action)"
:icon="actionIcon(action.status)" :label="action.label" :destructive="action.destructive"
@click="openStatusConfirm(action)" />
<Button
v-else-if="canPerformAction(action)"
@ -126,44 +108,15 @@ function actionIcon(status: string) {
</Button>
</template>
<Tooltip>
<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>
<RowDetailAction :href="show.url(order.id)" tooltip="Detail" />
<Tooltip v-if="canEdit">
<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>
<RowEditAction v-if="canEdit" :href="edit.url(order.id)" />
<Tooltip v-if="canDelete">
<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>
<template v-if="canDelete">
<RowDeleteAction :action-url="destroy.url(order.id)" title="Hapus pesanan?"
:description="`Pesanan ${order.order_number} akan dihapus.${order.is_editable ? ' Stok produk akan dikembalikan.' : ''}`"
error-message="Gagal menghapus pesanan." />
</template>
</div>
<ConfirmDialog
@ -176,16 +129,4 @@ function actionIcon(status: string) {
:loading="statusProcessing"
@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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,55 +1,21 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
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 { RowDeleteAction, RowEditAction } from '@/components/button';
import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { RawMaterialListItem } from '@/types/raw-material';
import { edit, destroy } from '@/routes/admin/master/raw_materials';
const props = defineProps<{
defineProps<{
material: RawMaterialListItem;
}>();
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>
<template>
<div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('raw_materials.update')">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" as-child>
<Link :href="edit.url(props.material.id)">
<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>
<RowEditAction v-if="can('raw_materials.update')" :href="edit.url(material.id)" />
<RowDeleteAction v-if="can('raw_materials.delete')" :action-url="destroy.url(material.id)" title="Hapus bahan baku?"
:description="`Bahan baku ${material.name} akan dihapus beserta seluruh variannya. Tindakan ini tidak dapat dibatalkan.`"
error-message="Gagal menghapus bahan baku." />
</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>

View File

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

View File

@ -1,12 +1,7 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { Pencil, Trash2 } from '@lucide/vue';
import { computed } from 'vue';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { RowDeleteAction, RowEditAction } from '@/components/button';
import { useCan } from '@/composables/useCan';
import { useDestroy } from '@/composables/useDestroy';
import type { RoleListItem } from './columns';
import { edit, destroy } from '@/routes/admin/system/roles';
@ -19,44 +14,13 @@ const { can } = useCan();
const isSystemRole = computed(() => {
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>
<template>
<div class="flex items-center justify-end gap-1">
<Tooltip v-if="can('roles.update')">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="size-8" as-child>
<Link :href="edit.url(props.role.id)">
<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>
<RowEditAction v-if="can('roles.update')" :href="edit.url(role.id)" />
<RowDeleteAction v-if="can('roles.delete') && !isSystemRole" :action-url="destroy.url(role.id)" title="Hapus role?"
:description="`Role ${role.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
error-message="Gagal menghapus role." />
</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>