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 @@ + + + + + + Rp + + + + 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 @@ + + + + + + + + + + + + + Plans + + + Tambah + + + + + + Memuat data... + + + + + + + + + + + Semua Status + Aktif + Nonaktif + + + + + Reset + + + + + Filter + + + + + Status + + + + + + + Semua Status + Aktif + Nonaktif + + + + + + Reset Filter + + + + + + + + + + + + + + + + + {{ formTitle }} + + + + + Nama * + + + + Harga * + + + + Deskripsi + + + + + + + Batal + + + + Simpan + + + + + + + + + diff --git a/docs/history/2026-07-24-component-price-display.md b/docs/history/2026-07-24-component-price-display.md new file mode 100644 index 0000000..9b9ca6b --- /dev/null +++ b/docs/history/2026-07-24-component-price-display.md @@ -0,0 +1,49 @@ +# Component PriceDisplay + +**Tanggal:** 2026-07-24 +**Status:** Selesai + +## Tujuan +Membuat component reusable untuk menampilkan dan menginput format harga Rupiah. + +## Yang Dikerjakan + +### 1. Component PriceDisplay +- Menggunakan `InputGroup` dengan prefix `Rp` +- Support 2 mode: display (readonly) dan input (editable) +- Format angka dengan pemisah titik (1.000.000) +- Input type `text` dengan `inputmode="numeric"` agar format titik berfungsi + +### 2. Integrasi +- Tabel plans: menampilkan harga dengan format Rp 50.000 +- Form plans: input harga dengan format Rp + +## File yang Diubah + +| File | Aksi | Detail | +|------|------|--------| +| `web2/app/components/PriceDisplay.vue` | Dibuat | Component PriceDisplay | +| `web2/app/pages/admin/plans/columns.ts` | Diubah | Format harga di tabel | +| `web2/app/pages/admin/plans/index.vue` | Diubah | Input harga pakai PriceDisplay | + +## Props + +| Prop | Tipe | Default | Deskripsi | +|------|------|---------|-----------| +| `value` | `number` | - | Nilai harga (display mode) | +| `modelValue` | `string` | - | v-model (input mode) | +| `placeholder` | `string` | `'0'` | Placeholder input | +| `readonly` | `boolean` | `false` | Readonly mode | +| `disabled` | `boolean` | `false` | Disabled mode | + +## Contoh Penggunaan + +```vue + + + + + + + +``` diff --git a/docs/history/2026-07-24-halaman-plans.md b/docs/history/2026-07-24-halaman-plans.md new file mode 100644 index 0000000..c170081 --- /dev/null +++ b/docs/history/2026-07-24-halaman-plans.md @@ -0,0 +1,29 @@ +# Halaman Plans + +**Tanggal:** 2026-07-24 +**Status:** Selesai + +## Tujuan +Menambahkan halaman Plans pada menu Master dengan style coding yang sama seperti halaman Business Types. + +## Yang Dikerjakan + +### 1. Halaman Plans (shadcn-vue) +- Membuat `/admin/plans/index.vue` dengan DataTable, dialog create/edit, delete dialog +- Membuat `columns.ts` dengan TanStack Table columns (nama, kode, harga, deskripsi, status, aksi) +- Menggunakan shadcn-vue components (Dialog, Field, Input, Select, Badge, Button) +- Form fields: code, name, price (number), description +- Formatting harga ke Rupiah (IDR) +- API: GET/POST/PUT/DELETE ke `/v1/plans` + +### 2. Sidebar Navigation +- Menambahkan menu "Plans" di navMain dengan icon `CreditCard` +- Link ke `/admin/plans` + +## File yang Diubah + +| File | Aksi | Detail | +|------|------|--------| +| `web2/app/pages/admin/plans/index.vue` | Dibuat | Halaman Plans | +| `web2/app/pages/admin/plans/columns.ts` | Dibuat | Column definitions | +| `web2/app/components/AppSidebar.vue` | Diubah | Tambah menu Plans di sidebar | diff --git a/docs/history/2026-07-24-hapus-kode-plan.md b/docs/history/2026-07-24-hapus-kode-plan.md new file mode 100644 index 0000000..8854f2c --- /dev/null +++ b/docs/history/2026-07-24-hapus-kode-plan.md @@ -0,0 +1,33 @@ +# Hapus Kolom Code dari Plan (Frontend) + +**Tanggal:** 2026-07-24 +**Status:** Selesai + +## Tujuan +Menghapus kolom `code` dari tampilan dan form data Plans di admin panel. + +## Yang Dikerjakan + +### 1. DataTable Columns +- Hapus kolom `code` dari tabel +- Hapus `code` dari `Plan` interface + +### 2. Form Dialog +- Hapus field `code` dari form create/edit +- Hapus `code` dari interface `FormData` +- Sebelum: form fields = Kode (wajib), Nama (wajib), Harga (wajib), Deskripsi +- Sesudah: form fields = Nama (wajib), Harga (wajib), Deskripsi + +### 3. CRUD Operations +- Hapus `code` dari body POST (create) dan PUT (update) +- Validasi diubah dari "Kode wajib diisi" → hanya "Nama wajib diisi" + +## File yang Diubah + +| File | Aksi | Detail | +|------|------|--------| +| `web2/app/pages/admin/plans/columns.ts` | Diubah | Hapus kolom code, hapus code dari interface | +| `web2/app/pages/admin/plans/index.vue` | Diubah | Hapus code dari form, body, validasi, FormData | + +## Notes +- Tidak ada perubahan pada toggle status atau delete dialog diff --git a/docs/history/2026-07-24-toggle-status-plan.md b/docs/history/2026-07-24-toggle-status-plan.md new file mode 100644 index 0000000..27f15eb --- /dev/null +++ b/docs/history/2026-07-24-toggle-status-plan.md @@ -0,0 +1,24 @@ +# Toggle Status Plan (Frontend) + +**Tanggal:** 2026-07-24 +**Status:** Selesai + +## Tujuan +Menambahkan fitur toggle status aktif/nonaktif pada halaman Plans di admin panel. + +## Yang Dikerjakan + +### 1. DataTable Columns +- Kolom Status sekarang ada Switch toggle (mirip Business Types) +- Badge tetap ditampilkan di samping Switch + +### 2. CRUD Operations +- Tambah `handleToggleStatus()` yang call `PATCH /v1/plans/{id}/active` +- Update data lokal setelah toggle berhasil + +## File yang Diubah + +| File | Aksi | Detail | +|------|------|--------| +| `web2/app/pages/admin/plans/columns.ts` | Diubah | Tambah Switch di kolom Status | +| `web2/app/pages/admin/plans/index.vue` | Diubah | Tambah `handleToggleStatus()` |
Memuat data...