diff --git a/app/components/AppSidebar.vue b/app/components/AppSidebar.vue index 702e816..457b74b 100644 --- a/app/components/AppSidebar.vue +++ b/app/components/AppSidebar.vue @@ -1,5 +1,6 @@ diff --git a/app/components/PriceDisplay.vue b/app/components/PriceDisplay.vue new file mode 100644 index 0000000..5a044ff --- /dev/null +++ b/app/components/PriceDisplay.vue @@ -0,0 +1,66 @@ + + + diff --git a/app/pages/admin/plans/columns.ts b/app/pages/admin/plans/columns.ts new file mode 100644 index 0000000..763a241 --- /dev/null +++ b/app/pages/admin/plans/columns.ts @@ -0,0 +1,145 @@ +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 | null + is_active: boolean | number +} + +const nameFilter: FilterFn = (row, _columnId, filterValue) => { + const search = (filterValue as string).toLowerCase() + const name = (row.original.name as string).toLowerCase() + return name.includes(search) +} + +const booleanFilter: FilterFn = (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[] { + 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'), + ]), + ]) + ) + }, + }, + ] +} diff --git a/app/pages/admin/plans/index.vue b/app/pages/admin/plans/index.vue new file mode 100644 index 0000000..77a4ce6 --- /dev/null +++ b/app/pages/admin/plans/index.vue @@ -0,0 +1,333 @@ + + +