- Adjusted sidebar menu button styles for improved icon size and padding. - Increased default button height and text size for better accessibility. - Introduced a new utility function `valueUpdater` to simplify state updates in components.
5.6 KiB
2026-07-24 — DataTable Admin Jenis Bisnis
Goal
Build reusable DataTable with pagination, sorting, filtering, and actions for admin "Jenis Bisnis" (Business Types) page using Nuxt 4 + shadcn-vue.
Installed Components
@shadcn-vue/select,@shadcn-vue/tooltip,@shadcn-vue/badge
Current Files
| File | Path | Role |
|---|---|---|
| columns.ts | app/pages/admin/business-types/columns.ts |
BusinessType interface, column defs, filter functions, action buttons (Edit/Delete) — co-located with page |
| index.vue | app/pages/admin/business-types/index.vue |
Page: fetches data, passes columns to DataTable, filter UI via #filters slot |
| DataTable.vue | app/components/data-table/DataTable.vue |
Reusable generic DataTable — accepts columns + data props, auto-prepends # row-number column, filters slot, includes pagination |
| DataTablePagination.vue | app/components/data-table/DataTablePagination.vue |
Pagination sub-component (page size selector, prev/next/first/last buttons) |
| DataTable.vue (old) | app/components/DataTable.vue |
Legacy DataTable with hardcoded columns, drag-and-drop, tabs — NOT used by business-types |
Refactoring Done
columns.tsmoved fromapp/components/business-types/columns.ts→app/pages/admin/business-types/columns.ts(co-located with page)index.vueimports columns via relative path./columns- Actions column simplified: removed Copy (ID) and Eye (Lihat) buttons → now only Edit (Pencil, yellow) and Delete (Trash2, destructive) with Tooltip — no click handlers wired yet
Bugs Fixed
1. Numbering salah di page 2 (page 1: 1-10, page 2: 21-30)
Root cause: Formula pageIndex * pageSize + row.index + 1 double-counts. TanStack Table's getPaginationRowModel does NOT reset row.index — it stays as the global index from sorted/filtered model. So row.index is already 10-19 on page 2, and 1*10 + 10 = 20 gives 21-30.
Fix: Use row.index + 1 directly in DataTable.vue:
cell: ({ row }) => {
return h('div', { class: 'text-center' }, `${row.index + 1}`)
}
2. Search filter tidak berfungsi
Root cause: nameAndCodeFilter menggunakan row.getValue('name') dan row.getValue('code'), tapi column ID-nya name_search (virtual column dengan accessorFn), jadi row.getValue('name') return undefined.
Fix: Gunakan row.original.name dan row.original.code di columns.ts:
const nameAndCodeFilter: FilterFn<BusinessType> = (row, _columnId, filterValue) => {
const search = (filterValue as string).toLowerCase()
const name = (row.original.name as string).toLowerCase()
const code = (row.original.code as string).toLowerCase()
return name.includes(search) || code.includes(search)
}
3. Data inactive (is_active: 0) tidak tampil / semua tampil "Aktif"
Root cause: API return is_active sebagai number 0/1, tapi cell dan filter pakai !!row.getValue('is_active'). Operator !! pada string "0" menghasilkan true (non-empty string).
Fix: Explicit check untuk number/string/boolean:
// Cell display
const raw = row.getValue('is_active')
const isActive = raw === true || raw === 1 || raw === '1'
// booleanFilter
const raw = row.original[columnId as keyof BusinessType]
const cellValue = raw === true || raw === 1 || raw === '1'
4. Select status tidak reflect selection
Root cause: getStatusFilter() hanya handle boolean dan number, tidak handle string 'true'/'false' dari Select value.
Fix:
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'
}
5. Model order salah
Root cause: getPaginationRowModel di-register SEBELUM getFilteredRowModel, jadi pagination jalan duluan sebelum filter.
Fix: Reorder di DataTable.vue:
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getExpandedRowModel: getExpandedRowModel(),
Pending
- Wire
@clickhandlers on Edit/Delete action buttons incolumns.ts - Backend: ubah
is_activedefault dariTrue→Nonesupaya admin page bisa lihat semua data - Missing type import:
index.vueusesuseFetch<BusinessType[]>tapiBusinessTypebelum di-import (perluimport { type BusinessType } from './columns')
Backend Note
API GET /v1/business-types default is_active=True — hanya return data aktif. Perlu ubah backend:
def list_business_types(is_active: bool = None, db: Session = Depends(get_db)):
return get_business_type_list(db, is_active=is_active)
def get_business_type_list(db: Session, is_active: bool = None):
query = db.query(BusinessType).filter(BusinessType.deleted_at.is_(None))
if is_active is not None:
query = query.filter(BusinessType.is_active == is_active)
return query.all()
Admin page perlu lihat semua data (active + inactive), frontend TanStack Table handle filtering.
Tech Details
useFetchruns server-side (SSR), so API calls won't appear in browser Network tab- TanStack Table
row.indexis global index (not page-relative) after pagination - Filter values stored as strings (
'true'/'false'/''), mapped to/from boolean viagetStatusFilter() name_searchis virtual column withaccessorFn: row => ${row.name} ${row.code}for combined searchapp/components/DataTable.vueis legacy/unused — do not confuse withapp/components/data-table/DataTable.vue