- 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.
112 lines
5.6 KiB
Markdown
112 lines
5.6 KiB
Markdown
# 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.ts` → `app/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`:
|
|
```ts
|
|
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`:
|
|
```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:
|
|
```ts
|
|
// 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:**
|
|
```ts
|
|
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`:
|
|
```ts
|
|
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 `True` → `None` 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:
|
|
```python
|
|
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`
|