web/docs/history/2026-07-24-data-table-jenis-bisnis.md
Yoga Pangestu 2f79c83a17 feat: update sidebar button styles and add value updater utility function
- 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.
2026-07-24 10:45:35 +07:00

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.ts moved from app/components/business-types/columns.tsapp/pages/admin/business-types/columns.ts (co-located with page)
  • index.vue imports 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 @click handlers on Edit/Delete action buttons in columns.ts
  • Backend: ubah is_active default dari TrueNone supaya admin page bisa lihat semua data
  • Missing type import: index.vue uses useFetch<BusinessType[]> tapi BusinessType belum di-import (perlu import { 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

  • useFetch runs server-side (SSR), so API calls won't appear in browser Network tab
  • TanStack Table row.index is global index (not page-relative) after pagination
  • Filter values stored as strings ('true'/'false'/''), mapped to/from boolean via getStatusFilter()
  • name_search is virtual column with accessorFn: row => ${row.name} ${row.code} for combined search
  • app/components/DataTable.vue is legacy/unused — do not confuse with app/components/data-table/DataTable.vue