feat: remove unused Business Types and Plans components and their related files
This commit is contained in:
parent
d8edede37a
commit
bc05551f89
@ -1,8 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
BriefcaseBusiness,
|
|
||||||
CreditCard,
|
|
||||||
Database,
|
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
LayoutDashboard
|
LayoutDashboard
|
||||||
} from "@lucide/vue"
|
} from "@lucide/vue"
|
||||||
@ -33,8 +30,6 @@ const groups = [
|
|||||||
icon: FolderKanban,
|
icon: FolderKanban,
|
||||||
items: [
|
items: [
|
||||||
{ title: 'Dashboard', url: '/admin/dashboard', icon: LayoutDashboard },
|
{ title: 'Dashboard', url: '/admin/dashboard', icon: LayoutDashboard },
|
||||||
{ title: 'Jenis Bisnis', url: '/admin/business-types', icon: BriefcaseBusiness },
|
|
||||||
{ title: 'Plans', url: '/admin/plans', icon: CreditCard },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,130 +0,0 @@
|
|||||||
import type { ColumnDef, FilterFn } from '@tanstack/vue-table'
|
|
||||||
import { ArrowUpDown, Pencil, Trash2 } from '@lucide/vue'
|
|
||||||
import { h } from 'vue'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
export interface BusinessType {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string | null
|
|
||||||
is_active: boolean | number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function copy(id: string) {
|
|
||||||
navigator.clipboard.writeText(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameFilter: FilterFn<BusinessType> = (row, _columnId, filterValue) => {
|
|
||||||
const search = (filterValue as string).toLowerCase()
|
|
||||||
const name = (row.original.name as string).toLowerCase()
|
|
||||||
return name.includes(search)
|
|
||||||
}
|
|
||||||
|
|
||||||
const booleanFilter: FilterFn<BusinessType> = (row, columnId, filterValue) => {
|
|
||||||
if (!filterValue || filterValue === '') return true
|
|
||||||
const raw = row.original[columnId as keyof BusinessType]
|
|
||||||
const cellValue = raw === true || raw === 1 || raw === '1'
|
|
||||||
const filterBool = filterValue === 'true'
|
|
||||||
return cellValue === filterBool
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getColumns(callbacks: {
|
|
||||||
onEdit: (businessType: BusinessType) => void
|
|
||||||
onDelete: (businessType: BusinessType) => void
|
|
||||||
onToggleStatus: (businessType: BusinessType, value: boolean) => void
|
|
||||||
}): ColumnDef<BusinessType>[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'name_search',
|
|
||||||
accessorFn: row => row.name,
|
|
||||||
header: ({ column }) => {
|
|
||||||
return h(Button, {
|
|
||||||
variant: 'ghost',
|
|
||||||
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
||||||
}, () => ['Nama', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
|
|
||||||
},
|
|
||||||
cell: ({ row }) => h('div', { class: 'font-medium' }, row.original.name),
|
|
||||||
filterFn: nameFilter,
|
|
||||||
enableHiding: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'description',
|
|
||||||
header: 'Deskripsi',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const desc = row.getValue('description') as string | null
|
|
||||||
return h('div', { class: 'text-muted-foreground truncate max-w-[300px]' }, desc ?? '-')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'is_active',
|
|
||||||
header: 'Status',
|
|
||||||
filterFn: booleanFilter,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const raw = row.getValue('is_active')
|
|
||||||
const isActive = raw === true || raw === 1 || raw === '1'
|
|
||||||
const businessType = row.original
|
|
||||||
return h('div', { class: 'flex items-center gap-2' }, [
|
|
||||||
h(Switch, {
|
|
||||||
modelValue: isActive,
|
|
||||||
'onUpdate:modelValue': (val: boolean) => callbacks.onToggleStatus(businessType, val),
|
|
||||||
}),
|
|
||||||
h(Badge, {
|
|
||||||
variant: 'outline',
|
|
||||||
class: isActive
|
|
||||||
? 'border-emerald-500 text-emerald-600 bg-emerald-50'
|
|
||||||
: 'border-red-500 text-red-600 bg-red-50',
|
|
||||||
}, () => isActive ? 'Aktif' : 'Nonaktif'),
|
|
||||||
])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
header: ' ',
|
|
||||||
size: 0,
|
|
||||||
minSize: 0,
|
|
||||||
enableHiding: false,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const businessType = row.original
|
|
||||||
|
|
||||||
return h(TooltipProvider, null, () =>
|
|
||||||
h('div', { class: 'flex items-center gap-1' }, [
|
|
||||||
h(Tooltip, null, () => [
|
|
||||||
h(TooltipTrigger, { asChild: true }, () =>
|
|
||||||
h(Button, {
|
|
||||||
variant: 'ghost',
|
|
||||||
size: 'icon',
|
|
||||||
class: 'h-8 w-8 text-yellow-500',
|
|
||||||
onClick: () => callbacks.onEdit(businessType),
|
|
||||||
}, () =>
|
|
||||||
h(Pencil, { class: 'h-4 w-4' })
|
|
||||||
)
|
|
||||||
),
|
|
||||||
h(TooltipContent, null, () => 'Edit'),
|
|
||||||
]),
|
|
||||||
h(Tooltip, null, () => [
|
|
||||||
h(TooltipTrigger, { asChild: true }, () =>
|
|
||||||
h(Button, {
|
|
||||||
variant: 'ghost',
|
|
||||||
size: 'icon',
|
|
||||||
class: 'h-8 w-8 text-destructive',
|
|
||||||
onClick: () => callbacks.onDelete(businessType),
|
|
||||||
}, () =>
|
|
||||||
h(Trash2, { class: 'h-4 w-4' })
|
|
||||||
)
|
|
||||||
),
|
|
||||||
h(TooltipContent, null, () => 'Hapus'),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,320 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import AppSidebar from '@/components/AppSidebar.vue'
|
|
||||||
import SiteHeader from '@/components/SiteHeader.vue'
|
|
||||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'
|
|
||||||
|
|
||||||
import { ChevronDown, Loader2, Plus, X } from '@lucide/vue'
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
|
|
||||||
import DataTable from '@/components/data-table/DataTable.vue'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
} from '@/components/ui/field'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { type BusinessType, getColumns } from './columns'
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
|
|
||||||
const { data: businessTypes, status, refresh } = useFetch<BusinessType[]>(`${config.public.apiBase}/business-types`)
|
|
||||||
|
|
||||||
const tableRef = ref<InstanceType<typeof DataTable>>()
|
|
||||||
|
|
||||||
// --- Dialog states ---
|
|
||||||
const formOpen = ref(false)
|
|
||||||
const deleteOpen = ref(false)
|
|
||||||
|
|
||||||
// --- Form state ---
|
|
||||||
type FormMode = 'create' | 'edit'
|
|
||||||
|
|
||||||
const formMode = ref<FormMode>('create')
|
|
||||||
const selectedBusinessType = ref<BusinessType | null>(null)
|
|
||||||
|
|
||||||
interface FormData {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = ref<FormData>({ name: '', description: '' })
|
|
||||||
|
|
||||||
// --- Loading states ---
|
|
||||||
const formLoading = ref(false)
|
|
||||||
const deleteLoading = ref(false)
|
|
||||||
|
|
||||||
// --- Computed ---
|
|
||||||
const formTitle = computed(() => formMode.value === 'create' ? 'Tambah Jenis Bisnis' : 'Edit Jenis Bisnis')
|
|
||||||
|
|
||||||
// --- Table callbacks ---
|
|
||||||
function openCreateForm() {
|
|
||||||
formMode.value = 'create'
|
|
||||||
selectedBusinessType.value = null
|
|
||||||
form.value = { name: '', description: '' }
|
|
||||||
formOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEdit(bt: BusinessType) {
|
|
||||||
formMode.value = 'edit'
|
|
||||||
selectedBusinessType.value = bt
|
|
||||||
form.value = {
|
|
||||||
name: bt.name,
|
|
||||||
description: bt.description ?? '',
|
|
||||||
}
|
|
||||||
formOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDelete(bt: BusinessType) {
|
|
||||||
selectedBusinessType.value = bt
|
|
||||||
deleteOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleToggleStatus(bt: BusinessType, value: boolean) {
|
|
||||||
try {
|
|
||||||
await $fetch(`${config.public.apiBase}/business-types/${bt.id}/active`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: { is_active: value },
|
|
||||||
})
|
|
||||||
if (businessTypes.value) {
|
|
||||||
businessTypes.value = businessTypes.value.map(b =>
|
|
||||||
b.id === bt.id ? { ...b, is_active: value } : b
|
|
||||||
)
|
|
||||||
}
|
|
||||||
toast.success(`Jenis bisnis berhasil di${value ? 'aktifkan' : 'nonaktifkan'}`)
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(e?.data?.message ?? 'Gagal mengubah status')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = computed(() => getColumns({ onEdit: handleEdit, onDelete: handleDelete, onToggleStatus: handleToggleStatus }))
|
|
||||||
|
|
||||||
// --- CRUD operations ---
|
|
||||||
async function handleFormSubmit() {
|
|
||||||
if (!form.value.name.trim()) {
|
|
||||||
toast.error('Nama wajib diisi')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
formLoading.value = true
|
|
||||||
try {
|
|
||||||
if (formMode.value === 'create') {
|
|
||||||
await $fetch(`${config.public.apiBase}/business-types`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
name: form.value.name.trim(),
|
|
||||||
description: form.value.description.trim() || null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
toast.success('Jenis bisnis berhasil ditambahkan')
|
|
||||||
} else {
|
|
||||||
await $fetch(`${config.public.apiBase}/business-types/${selectedBusinessType.value!.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: {
|
|
||||||
name: form.value.name.trim(),
|
|
||||||
description: form.value.description.trim() || null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
toast.success('Jenis bisnis berhasil diperbarui')
|
|
||||||
}
|
|
||||||
|
|
||||||
formOpen.value = false
|
|
||||||
await refresh()
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(e?.data?.message ?? 'Gagal menyimpan jenis bisnis')
|
|
||||||
} finally {
|
|
||||||
formLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleConfirmDelete() {
|
|
||||||
if (!selectedBusinessType.value) return
|
|
||||||
|
|
||||||
deleteLoading.value = true
|
|
||||||
try {
|
|
||||||
await $fetch(`${config.public.apiBase}/business-types/${selectedBusinessType.value.id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
toast.success('Jenis bisnis berhasil dihapus')
|
|
||||||
deleteOpen.value = false
|
|
||||||
selectedBusinessType.value = null
|
|
||||||
await refresh()
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(e?.data?.message ?? 'Gagal menghapus jenis bisnis')
|
|
||||||
} finally {
|
|
||||||
deleteLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Filters ---
|
|
||||||
function getStatusFilter(table: any) {
|
|
||||||
const val = table.getColumn('is_active')?.getFilterValue()
|
|
||||||
if (val === true || val === 'true' || val === 1) return 'true'
|
|
||||||
if (val === false || val === 'false' || val === 0) return 'false'
|
|
||||||
return 'all'
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasActiveFilter(table: any) {
|
|
||||||
const search = table.getColumn('name_search')?.getFilterValue()
|
|
||||||
const status = table.getColumn('is_active')?.getFilterValue()
|
|
||||||
return (search && search !== '') || (status !== '' && status !== undefined && status !== null)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFilters(table: any) {
|
|
||||||
table.getColumn('name_search')?.setFilterValue('')
|
|
||||||
table.getColumn('is_active')?.setFilterValue('')
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Reset on dialog close ---
|
|
||||||
watch(formOpen, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
form.value = { name: '', description: '' }
|
|
||||||
selectedBusinessType.value = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(deleteOpen, (val) => {
|
|
||||||
if (!val) selectedBusinessType.value = null
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<SidebarProvider :style="{
|
|
||||||
'--sidebar-width': 'calc(var(--spacing) * 72)',
|
|
||||||
'--header-height': 'calc(var(--spacing) * 12)',
|
|
||||||
}">
|
|
||||||
<AppSidebar variant="inset" />
|
|
||||||
<SidebarInset>
|
|
||||||
<SiteHeader />
|
|
||||||
<div class="flex flex-1 flex-col">
|
|
||||||
<div class="@container/main flex flex-1 flex-col gap-2">
|
|
||||||
<div class="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
|
||||||
<div class="px-4 lg:px-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h1 class="text-2xl font-bold">Jenis Bisnis</h1>
|
|
||||||
<Button size="sm" @click="openCreateForm">
|
|
||||||
<Plus class="h-4 w-4 mr-1" />
|
|
||||||
Tambah
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-4 lg:px-6">
|
|
||||||
<DataTable ref="tableRef" :columns="columns" :data="businessTypes ?? []" :loading="status === 'pending'">
|
|
||||||
<template #filters="{ table }">
|
|
||||||
<div class="flex items-center gap-2 py-4">
|
|
||||||
<Input class="max-w-sm" placeholder="Cari nama..."
|
|
||||||
:model-value="(table.getColumn('name_search')?.getFilterValue() as string) ?? ''"
|
|
||||||
@update:model-value="table.getColumn('name_search')?.setFilterValue($event)" />
|
|
||||||
<Select :model-value="getStatusFilter(table)"
|
|
||||||
@update:model-value="table.getColumn('is_active')?.setFilterValue($event === 'all' ? '' : $event)">
|
|
||||||
<SelectTrigger class="hidden h-8 w-auto md:flex">
|
|
||||||
<SelectValue placeholder="Semua Status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Semua Status</SelectItem>
|
|
||||||
<SelectItem value="true">Aktif</SelectItem>
|
|
||||||
<SelectItem value="false">Nonaktif</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button v-if="hasActiveFilter(table)" variant="ghost" size="sm" class="h-8"
|
|
||||||
@click="resetFilters(table)">
|
|
||||||
<X class="h-4 w-4 mr-1" />
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger as-child>
|
|
||||||
<Button variant="outline" class="ml-auto md:hidden">
|
|
||||||
Filter
|
|
||||||
<ChevronDown class="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
|
||||||
<div class="px-2 pb-2">
|
|
||||||
<Select :model-value="getStatusFilter(table)"
|
|
||||||
@update:model-value="table.getColumn('is_active')?.setFilterValue($event === 'all' ? '' : $event)">
|
|
||||||
<SelectTrigger class="h-8 w-37.5">
|
|
||||||
<SelectValue placeholder="Semua Status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Semua Status</SelectItem>
|
|
||||||
<SelectItem value="true">Aktif</SelectItem>
|
|
||||||
<SelectItem value="false">Nonaktif</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem :disabled="!hasActiveFilter(table)" @click="resetFilters(table)">
|
|
||||||
Reset Filter
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</DataTable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarInset>
|
|
||||||
|
|
||||||
<!-- ==================== FORM DIALOG (CREATE / EDIT) ==================== -->
|
|
||||||
<Dialog v-model:open="formOpen">
|
|
||||||
<DialogContent class="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{{ formTitle }}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<form id="businessTypeForm" @submit.prevent="handleFormSubmit">
|
|
||||||
<FieldGroup class="gap-4">
|
|
||||||
<Field>
|
|
||||||
<FieldLabel for="form-name">Nama <span class="text-red-500">*</span></FieldLabel>
|
|
||||||
<Input id="form-name" v-model="form.name" placeholder="Masukkan nama" :disabled="formLoading" />
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel for="form-description">Deskripsi</FieldLabel>
|
|
||||||
<textarea id="form-description" v-model="form.description" placeholder="Masukkan deskripsi (opsional)"
|
|
||||||
:disabled="formLoading" rows="3"
|
|
||||||
class="flex w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50" />
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose as-child>
|
|
||||||
<Button variant="outline" :disabled="formLoading">Batal</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button type="submit" form="businessTypeForm" :disabled="formLoading">
|
|
||||||
<Loader2 v-if="formLoading" class="h-4 w-4 mr-1 animate-spin" />
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<!-- ==================== DELETE DIALOG ==================== -->
|
|
||||||
<DeleteDialog v-model:open="deleteOpen" title="Hapus Jenis Bisnis" :item-name="selectedBusinessType?.name"
|
|
||||||
:loading="deleteLoading" @confirm="handleConfirmDelete" />
|
|
||||||
</SidebarProvider>
|
|
||||||
</template>
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
import type { ColumnDef, FilterFn } from '@tanstack/vue-table'
|
|
||||||
import { ArrowUpDown, Pencil, Trash2 } from '@lucide/vue'
|
|
||||||
import { h } from 'vue'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
export interface Plan {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string | null
|
|
||||||
price: number
|
|
||||||
limits: Record<string, any> | null
|
|
||||||
is_active: boolean | number
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameFilter: FilterFn<Plan> = (row, _columnId, filterValue) => {
|
|
||||||
const search = (filterValue as string).toLowerCase()
|
|
||||||
const name = (row.original.name as string).toLowerCase()
|
|
||||||
return name.includes(search)
|
|
||||||
}
|
|
||||||
|
|
||||||
const booleanFilter: FilterFn<Plan> = (row, columnId, filterValue) => {
|
|
||||||
if (!filterValue || filterValue === '') return true
|
|
||||||
const raw = row.original[columnId as keyof Plan]
|
|
||||||
const cellValue = raw === true || raw === 1 || raw === '1'
|
|
||||||
const filterBool = filterValue === 'true'
|
|
||||||
return cellValue === filterBool
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getColumns(callbacks: {
|
|
||||||
onEdit: (plan: Plan) => void
|
|
||||||
onDelete: (plan: Plan) => void
|
|
||||||
onToggleStatus: (plan: Plan, value: boolean) => void
|
|
||||||
}): ColumnDef<Plan>[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'name_search',
|
|
||||||
accessorFn: row => row.name,
|
|
||||||
header: ({ column }) => {
|
|
||||||
return h(Button, {
|
|
||||||
variant: 'ghost',
|
|
||||||
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
||||||
}, () => ['Nama', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
|
|
||||||
},
|
|
||||||
cell: ({ row }) => h('div', { class: 'font-medium' }, row.original.name),
|
|
||||||
filterFn: nameFilter,
|
|
||||||
enableHiding: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'price',
|
|
||||||
header: ({ column }) => {
|
|
||||||
return h(Button, {
|
|
||||||
variant: 'ghost',
|
|
||||||
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
||||||
}, () => ['Harga', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const val = row.original.price
|
|
||||||
const formatted = val.toLocaleString('id-ID')
|
|
||||||
return h('div', { class: 'flex items-center gap-1 text-sm' }, [
|
|
||||||
h('span', { class: 'text-muted-foreground' }, 'Rp'),
|
|
||||||
h('span', { class: 'font-medium' }, formatted),
|
|
||||||
])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'description',
|
|
||||||
header: 'Deskripsi',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const desc = row.getValue('description') as string | null
|
|
||||||
return h('div', { class: 'text-muted-foreground truncate max-w-[300px]' }, desc ?? '-')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'is_active',
|
|
||||||
header: 'Status',
|
|
||||||
filterFn: booleanFilter,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const raw = row.getValue('is_active')
|
|
||||||
const isActive = raw === true || raw === 1 || raw === '1'
|
|
||||||
const plan = row.original
|
|
||||||
return h('div', { class: 'flex items-center gap-2' }, [
|
|
||||||
h(Switch, {
|
|
||||||
modelValue: isActive,
|
|
||||||
'onUpdate:modelValue': (val: boolean) => callbacks.onToggleStatus(plan, val),
|
|
||||||
}),
|
|
||||||
h(Badge, {
|
|
||||||
variant: 'outline',
|
|
||||||
class: isActive
|
|
||||||
? 'border-emerald-500 text-emerald-600 bg-emerald-50'
|
|
||||||
: 'border-red-500 text-red-600 bg-red-50',
|
|
||||||
}, () => isActive ? 'Aktif' : 'Nonaktif'),
|
|
||||||
])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
header: ' ',
|
|
||||||
size: 0,
|
|
||||||
minSize: 0,
|
|
||||||
enableHiding: false,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const plan = row.original
|
|
||||||
|
|
||||||
return h(TooltipProvider, null, () =>
|
|
||||||
h('div', { class: 'flex items-center gap-1' }, [
|
|
||||||
h(Tooltip, null, () => [
|
|
||||||
h(TooltipTrigger, { asChild: true }, () =>
|
|
||||||
h(Button, {
|
|
||||||
variant: 'ghost',
|
|
||||||
size: 'icon',
|
|
||||||
class: 'h-8 w-8 text-yellow-500',
|
|
||||||
onClick: () => callbacks.onEdit(plan),
|
|
||||||
}, () =>
|
|
||||||
h(Pencil, { class: 'h-4 w-4' })
|
|
||||||
)
|
|
||||||
),
|
|
||||||
h(TooltipContent, null, () => 'Edit'),
|
|
||||||
]),
|
|
||||||
h(Tooltip, null, () => [
|
|
||||||
h(TooltipTrigger, { asChild: true }, () =>
|
|
||||||
h(Button, {
|
|
||||||
variant: 'ghost',
|
|
||||||
size: 'icon',
|
|
||||||
class: 'h-8 w-8 text-destructive',
|
|
||||||
onClick: () => callbacks.onDelete(plan),
|
|
||||||
}, () =>
|
|
||||||
h(Trash2, { class: 'h-4 w-4' })
|
|
||||||
)
|
|
||||||
),
|
|
||||||
h(TooltipContent, null, () => 'Hapus'),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,330 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import AppSidebar from '@/components/AppSidebar.vue'
|
|
||||||
import SiteHeader from '@/components/SiteHeader.vue'
|
|
||||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'
|
|
||||||
|
|
||||||
import { ChevronDown, Loader2, Plus, X } from '@lucide/vue'
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
|
|
||||||
import DataTable from '@/components/data-table/DataTable.vue'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
} from '@/components/ui/field'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { type Plan, getColumns } from './columns'
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
|
|
||||||
const { data: plans, status, refresh } = useFetch<Plan[]>(`${config.public.apiBase}/plans`)
|
|
||||||
|
|
||||||
const tableRef = ref<InstanceType<typeof DataTable>>()
|
|
||||||
|
|
||||||
// --- Dialog states ---
|
|
||||||
const formOpen = ref(false)
|
|
||||||
const deleteOpen = ref(false)
|
|
||||||
|
|
||||||
// --- Form state ---
|
|
||||||
type FormMode = 'create' | 'edit'
|
|
||||||
|
|
||||||
const formMode = ref<FormMode>('create')
|
|
||||||
const selectedPlan = ref<Plan | null>(null)
|
|
||||||
|
|
||||||
interface FormData {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
price: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = ref<FormData>({ name: '', description: '', price: '' })
|
|
||||||
|
|
||||||
// --- Loading states ---
|
|
||||||
const formLoading = ref(false)
|
|
||||||
const deleteLoading = ref(false)
|
|
||||||
|
|
||||||
// --- Computed ---
|
|
||||||
const formTitle = computed(() => formMode.value === 'create' ? 'Tambah Plan' : 'Edit Plan')
|
|
||||||
|
|
||||||
// --- Table callbacks ---
|
|
||||||
function openCreateForm() {
|
|
||||||
formMode.value = 'create'
|
|
||||||
selectedPlan.value = null
|
|
||||||
form.value = { name: '', description: '', price: '' }
|
|
||||||
formOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEdit(plan: Plan) {
|
|
||||||
formMode.value = 'edit'
|
|
||||||
selectedPlan.value = plan
|
|
||||||
form.value = {
|
|
||||||
name: plan.name,
|
|
||||||
description: plan.description ?? '',
|
|
||||||
price: String(plan.price),
|
|
||||||
}
|
|
||||||
formOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDelete(plan: Plan) {
|
|
||||||
selectedPlan.value = plan
|
|
||||||
deleteOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleToggleStatus(plan: Plan, value: boolean) {
|
|
||||||
try {
|
|
||||||
await $fetch(`${config.public.apiBase}/plans/${plan.id}/active`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: { is_active: value },
|
|
||||||
})
|
|
||||||
if (plans.value) {
|
|
||||||
plans.value = plans.value.map(p =>
|
|
||||||
p.id === plan.id ? { ...p, is_active: value } : p
|
|
||||||
)
|
|
||||||
}
|
|
||||||
toast.success(`Plan berhasil di${value ? 'aktifkan' : 'nonaktifkan'}`)
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(e?.data?.message ?? 'Gagal mengubah status')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = computed(() => getColumns({ onEdit: handleEdit, onDelete: handleDelete, onToggleStatus: handleToggleStatus }))
|
|
||||||
|
|
||||||
// --- CRUD operations ---
|
|
||||||
async function handleFormSubmit() {
|
|
||||||
if (!form.value.name.trim()) {
|
|
||||||
toast.error('Nama wajib diisi')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.value.price.trim() || isNaN(Number(form.value.price))) {
|
|
||||||
toast.error('Harga wajib diisi dan harus berupa angka')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
formLoading.value = true
|
|
||||||
try {
|
|
||||||
const body = {
|
|
||||||
name: form.value.name.trim(),
|
|
||||||
description: form.value.description.trim() || null,
|
|
||||||
price: Number(form.value.price),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formMode.value === 'create') {
|
|
||||||
await $fetch(`${config.public.apiBase}/plans`, {
|
|
||||||
method: 'POST',
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
toast.success('Plan berhasil ditambahkan')
|
|
||||||
} else {
|
|
||||||
await $fetch(`${config.public.apiBase}/plans/${selectedPlan.value!.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
toast.success('Plan berhasil diperbarui')
|
|
||||||
}
|
|
||||||
|
|
||||||
formOpen.value = false
|
|
||||||
await refresh()
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(e?.data?.message ?? 'Gagal menyimpan plan')
|
|
||||||
} finally {
|
|
||||||
formLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleConfirmDelete() {
|
|
||||||
if (!selectedPlan.value) return
|
|
||||||
|
|
||||||
deleteLoading.value = true
|
|
||||||
try {
|
|
||||||
await $fetch(`${config.public.apiBase}/plans/${selectedPlan.value.id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
toast.success('Plan berhasil dihapus')
|
|
||||||
deleteOpen.value = false
|
|
||||||
selectedPlan.value = null
|
|
||||||
await refresh()
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error(e?.data?.message ?? 'Gagal menghapus plan')
|
|
||||||
} finally {
|
|
||||||
deleteLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Filters ---
|
|
||||||
function getStatusFilter(table: any) {
|
|
||||||
const val = table.getColumn('is_active')?.getFilterValue()
|
|
||||||
if (val === true || val === 'true' || val === 1) return 'true'
|
|
||||||
if (val === false || val === 'false' || val === 0) return 'false'
|
|
||||||
return 'all'
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasActiveFilter(table: any) {
|
|
||||||
const search = table.getColumn('name_search')?.getFilterValue()
|
|
||||||
const status = table.getColumn('is_active')?.getFilterValue()
|
|
||||||
return (search && search !== '') || (status !== '' && status !== undefined && status !== null)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFilters(table: any) {
|
|
||||||
table.getColumn('name_search')?.setFilterValue('')
|
|
||||||
table.getColumn('is_active')?.setFilterValue('')
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Reset on dialog close ---
|
|
||||||
watch(formOpen, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
form.value = { name: '', description: '', price: '' }
|
|
||||||
selectedPlan.value = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(deleteOpen, (val) => {
|
|
||||||
if (!val) selectedPlan.value = null
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<SidebarProvider :style="{
|
|
||||||
'--sidebar-width': 'calc(var(--spacing) * 72)',
|
|
||||||
'--header-height': 'calc(var(--spacing) * 12)',
|
|
||||||
}">
|
|
||||||
<AppSidebar variant="inset" />
|
|
||||||
<SidebarInset>
|
|
||||||
<SiteHeader />
|
|
||||||
<div class="flex flex-1 flex-col">
|
|
||||||
<div class="@container/main flex flex-1 flex-col gap-2">
|
|
||||||
<div class="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
|
||||||
<div class="px-4 lg:px-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h1 class="text-2xl font-bold">Plans</h1>
|
|
||||||
<Button size="sm" @click="openCreateForm">
|
|
||||||
<Plus class="h-4 w-4 mr-1" />
|
|
||||||
Tambah
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-4 lg:px-6">
|
|
||||||
<DataTable ref="tableRef" :columns="columns" :data="plans ?? []" :loading="status === 'pending'">
|
|
||||||
<template #filters="{ table }">
|
|
||||||
<div class="flex items-center gap-2 py-4">
|
|
||||||
<Input class="max-w-sm" placeholder="Cari nama..."
|
|
||||||
:model-value="(table.getColumn('name_search')?.getFilterValue() as string) ?? ''"
|
|
||||||
@update:model-value="table.getColumn('name_search')?.setFilterValue($event)" />
|
|
||||||
<Select :model-value="getStatusFilter(table)"
|
|
||||||
@update:model-value="table.getColumn('is_active')?.setFilterValue($event === 'all' ? '' : $event)">
|
|
||||||
<SelectTrigger class="hidden h-8 w-auto md:flex">
|
|
||||||
<SelectValue placeholder="Semua Status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Semua Status</SelectItem>
|
|
||||||
<SelectItem value="true">Aktif</SelectItem>
|
|
||||||
<SelectItem value="false">Nonaktif</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button v-if="hasActiveFilter(table)" variant="ghost" size="sm" class="h-8"
|
|
||||||
@click="resetFilters(table)">
|
|
||||||
<X class="h-4 w-4 mr-1" />
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger as-child>
|
|
||||||
<Button variant="outline" class="ml-auto md:hidden">
|
|
||||||
Filter
|
|
||||||
<ChevronDown class="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
|
||||||
<div class="px-2 pb-2">
|
|
||||||
<Select :model-value="getStatusFilter(table)"
|
|
||||||
@update:model-value="table.getColumn('is_active')?.setFilterValue($event === 'all' ? '' : $event)">
|
|
||||||
<SelectTrigger class="h-8 w-37.5">
|
|
||||||
<SelectValue placeholder="Semua Status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Semua Status</SelectItem>
|
|
||||||
<SelectItem value="true">Aktif</SelectItem>
|
|
||||||
<SelectItem value="false">Nonaktif</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem :disabled="!hasActiveFilter(table)" @click="resetFilters(table)">
|
|
||||||
Reset Filter
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</DataTable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarInset>
|
|
||||||
|
|
||||||
<!-- ==================== FORM DIALOG (CREATE / EDIT) ==================== -->
|
|
||||||
<Dialog v-model:open="formOpen">
|
|
||||||
<DialogContent class="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{{ formTitle }}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<form id="planForm" @submit.prevent="handleFormSubmit">
|
|
||||||
<FieldGroup class="gap-4">
|
|
||||||
<Field>
|
|
||||||
<FieldLabel for="form-name">Nama <span class="text-red-500">*</span></FieldLabel>
|
|
||||||
<Input id="form-name" v-model="form.name" placeholder="Masukkan nama" :disabled="formLoading" />
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel for="form-price">Harga <span class="text-red-500">*</span></FieldLabel>
|
|
||||||
<PriceDisplay v-model="form.price" placeholder="0" :disabled="formLoading" />
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel for="form-description">Deskripsi</FieldLabel>
|
|
||||||
<textarea id="form-description" v-model="form.description" placeholder="Masukkan deskripsi (opsional)"
|
|
||||||
:disabled="formLoading" rows="3"
|
|
||||||
class="flex w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50" />
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose as-child>
|
|
||||||
<Button variant="outline" :disabled="formLoading">Batal</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button type="submit" form="planForm" :disabled="formLoading">
|
|
||||||
<Loader2 v-if="formLoading" class="h-4 w-4 mr-1 animate-spin" />
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<!-- ==================== DELETE DIALOG ==================== -->
|
|
||||||
<DeleteDialog v-model:open="deleteOpen" title="Hapus Plan" :item-name="selectedPlan?.name" :loading="deleteLoading"
|
|
||||||
@confirm="handleConfirmDelete" />
|
|
||||||
</SidebarProvider>
|
|
||||||
</template>
|
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
# Hapus Module Plan dan Jenis Bisnis
|
||||||
|
|
||||||
|
**Tanggal:** 2026-07-25
|
||||||
|
**Status:** Selesai
|
||||||
|
|
||||||
|
## Tujuan
|
||||||
|
Menghapus modul plans dan business-types, serta menghapus properti plan_id yang tidak digunakan dari komponen OnboardingForm.
|
||||||
|
|
||||||
|
## Yang Dikerjakan
|
||||||
|
|
||||||
|
### 1. Hapus Module Plans
|
||||||
|
- Menghapus direktori `app/pages/admin/plans/` yang berisi:
|
||||||
|
- `app/pages/admin/plans/columns.ts` - file yang mendefinisikan kolom untuk tabel plans
|
||||||
|
- `app/pages/admin/plans/index.vue` - halaman daftar plans
|
||||||
|
|
||||||
|
### 2. Hapus Module Business-Types
|
||||||
|
- Menghapus direktori `app/pages/admin/business-types/` yang berisi:
|
||||||
|
- `app/pages/admin/business-types/columns.ts` - file yang mendefinisikan kolom untuk tabel business-types
|
||||||
|
- `app/pages/admin/business-types/index.vue` - halaman daftar business-types
|
||||||
|
|
||||||
|
### 3. Perbarui AppSidebar
|
||||||
|
- Menghapus item menu "Plans" dari sidebar admin
|
||||||
|
- Sekarang menampilkan: Dashboard (existing), Jenis Bisnis (existing)
|
||||||
|
|
||||||
|
### 4. Perbarui OnboardingForm
|
||||||
|
- Menghapus tipe data Plan dari interface declarations
|
||||||
|
- Menghapus tipe data Plan yang tidak terpakai dari useFetch
|
||||||
|
- Menghapus properti plan_id dari handleSubmit (peninggalan setelah penghapusan)
|
||||||
|
- Menghapus pengiriman atau pemilihan plan di UI
|
||||||
|
- Mengurangi steps dari 3 menjadi 2:
|
||||||
|
- Step 0: Pilih Jenis Bisnis
|
||||||
|
- Step 1: Input Info Bisnis
|
||||||
|
- Memperbarui label stepper menjadi ['Bisnis', 'Info']
|
||||||
|
|
||||||
|
## File yang Diubah
|
||||||
|
|
||||||
|
| File | Aksi | Detail |
|
||||||
|
|------|------|--------|
|
||||||
|
| app/components/AppSidebar.vue | Diubah | Menghapus item menu "Plans" |
|
||||||
|
| app/components/OnboardingForm.vue | Diubah | Menghapus pemrosesan plan dan format 3-step |
|
||||||
|
| app/pages/admin/business-types/columns.ts | Dihapus | File kolom untuk business-types |
|
||||||
|
| app/pages/admin/business-types/index.vue | Dihapus | Halaman daftar business-types |
|
||||||
|
| app/pages/admin/plans/columns.ts | Dihapus | File kolom untuk plans |
|
||||||
|
| app/pages/admin/plans/index.vue | Dihapus | Halaman daftar plans |
|
||||||
Loading…
Reference in New Issue
Block a user