---
title: "Record Table"
description: "Kayıt listelemenin standart yolu: arama ve filtreler, toplu aksiyonlu seçim, yükleme iskeletleri, boş durum ve sayfalama tek kartta."
---

<Callout type="info" title="Ne zaman kullanılır">
Sipariş, ürün, müşteri ve log listeleri. Tablo verisi için kart grid'i kullanmayın.
</Callout>

## Kurulum

```bash
npx shadcn@latest add @ikas/record-table
```

Registry tanımı yoksa önce [Kurulum](https://builders.ikas.com/tr/docs/app-development/ui-kit/installation) adımlarını tamamlayın.

## Örnekler

### Siparişler

Arama, durum filtresi, toplu aksiyonlu satır seçimi, satır tıklama, sayfalama.

**Örnek: Siparişler** (`record-table/orders`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/record-table/orders?align=stretch)

```tsx
import * as React from "react"
import { InboxIcon, SearchIcon } from "lucide-react"
import { toast } from "sonner"

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { EmptyState } from "@/components/ikas/empty-state"
import { RecordTable, type RecordTableColumn } from "@/components/ikas/record-table"
import { channelSyncBadge, formatDate, formatMoney, orderStatusBadge, orders, type Order } from "@/demos/_data"

const PAGE_SIZE = 5

const columns: RecordTableColumn<Order>[] = [
  {
    id: "number",
    header: "Sipariş",
    cell: (order) => <span className="font-medium text-foreground">{order.number}</span>,
  },
  { id: "date", header: "Tarih", cell: (order) => formatDate(order.date), hideOnMobile: true },
  { id: "customer", header: "Müşteri" },
  {
    id: "status",
    header: "Durum",
    cell: (order) => (
      <Badge status={orderStatusBadge[order.status].status} dot>
        {orderStatusBadge[order.status].label}
      </Badge>
    ),
  },
  {
    id: "sync",
    header: "Pazaryeri",
    hideOnMobile: true,
    cell: (order) =>
      order.sync === "none" ? (
        <span className="text-muted-foreground">Web siparişi</span>
      ) : (
        <Badge variant="surface" status={channelSyncBadge[order.sync].status}>
          {channelSyncBadge[order.sync].label}
        </Badge>
      ),
  },
  { id: "total", header: "Toplam", align: "end", cell: (order) => formatMoney(order.total) },
]

export default function RecordTableOrders() {
  const [query, setQuery] = React.useState("")
  const [status, setStatus] = React.useState("all")
  const [page, setPage] = React.useState(1)
  const [selectedIds, setSelectedIds] = React.useState<string[]>([])
  const [loading, setLoading] = React.useState(false)

  const filtered = orders.filter(
    (order) =>
      (status === "all" || order.status === status) &&
      (query === "" || `${order.number} ${order.customer}`.toLowerCase().includes(query.toLowerCase()))
  )
  const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
  const rows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
  const first = filtered.length === 0 ? 0 : (page - 1) * PAGE_SIZE + 1

  function reload() {
    setLoading(true)
    setTimeout(() => setLoading(false), 1200)
  }

  return (
    <div className="flex w-full flex-col gap-3">
      <RecordTable
        label="Siparişler"
        columns={columns}
        rows={rows}
        getRowId={(order) => order.id}
        loading={loading}
        selectedIds={selectedIds}
        onSelectedIdsChange={setSelectedIds}
        onRowClick={(order) => toast(`${order.number} açılıyor`)}
        bulkActions={(ids) => (
          <>
            <Button size="sm" variant="outline" onClick={() => toast.success(`${ids.length} sipariş kargoya verildi`)}>
              Kargoya verildi olarak işaretle
            </Button>
            <Button size="sm" variant="outline" onClick={() => toast(`${ids.length} irsaliye yazdırılıyor`)}>
              İrsaliyeleri yazdır
            </Button>
          </>
        )}
        toolbar={
          <>
            <InputGroup className="w-full sm:max-w-64">
              <InputGroupAddon>
                <SearchIcon />
              </InputGroupAddon>
              <InputGroupInput
                aria-label="Siparişlerde ara"
                placeholder="Siparişlerde ara"
                value={query}
                onChange={(event) => {
                  setQuery(event.target.value)
                  setPage(1)
                }}
              />
            </InputGroup>
            <Select
              value={status}
              onValueChange={(value) => {
                setStatus(value)
                setPage(1)
              }}
            >
              <SelectTrigger aria-label="Sipariş durumu" className="w-40">
                <SelectValue />
              </SelectTrigger>
              <SelectContent position="popper">
                <SelectItem value="all">Tüm durumlar</SelectItem>
                {Object.entries(orderStatusBadge).map(([value, badge]) => (
                  <SelectItem key={value} value={value}>
                    {badge.label}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </>
        }
        emptyState={
          <EmptyState
            media={<InboxIcon />}
            title="Sipariş bulunamadı"
            description="Aramayı ya da durum filtresini değiştirmeyi deneyin."
            actions={
              <Button
                variant="outline"
                onClick={() => {
                  setQuery("")
                  setStatus("all")
                }}
              >
                Filtreleri temizle
              </Button>
            }
          />
        }
        pagination={{
          page,
          pageCount,
          onPageChange: setPage,
          summary: `${first}–${Math.min(page * PAGE_SIZE, filtered.length)} / ${filtered.length}`,
        }}
      />
      <div>
        <Button variant="ghost" size="sm" onClick={reload}>
          Yüklenmeyi simüle et
        </Button>
      </div>
    </div>
  )
}
```

### Yükleniyor ve boş

Yüklenirken iskelet satırlar. Gösterilecek bir şey yoksa EmptyState.

**Örnek: Yükleniyor ve boş** (`record-table/states`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/record-table/states?align=stretch)

```tsx
import { PackageIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { EmptyState } from "@/components/ikas/empty-state"
import { RecordTable, type RecordTableColumn } from "@/components/ikas/record-table"

type Product = { id: string; name: string; stock: number }

const columns: RecordTableColumn<Product>[] = [
  { id: "name", header: "Ürün" },
  { id: "stock", header: "Stok", align: "end" },
]

export default function RecordTableStates() {
  return (
    <div className="grid w-full gap-6 md:grid-cols-2">
      <RecordTable label="Ürünler (yükleniyor)" columns={columns} rows={[]} getRowId={(p) => p.id} loading />
      <RecordTable
        label="Ürünler (boş)"
        columns={columns}
        rows={[]}
        getRowId={(p) => p.id}
        emptyState={
          <EmptyState
            size="inline"
            media={<PackageIcon />}
            title="Henüz eşitlenen ürün yok"
            actions={<Button size="sm">Eşitlemeyi başlat</Button>}
          />
        }
      />
    </div>
  )
}
```

### Hata

Satırlar yüklenemezse `error` verin: satırların yerine danger tonlu bir EmptyState gelir. `onRetry` "Tekrar dene" butonunu ekler; yeniden denerken `loading` verin, iskelet satırlar hatanın önüne geçer. `error={true}` varsayılan metni kullanır, metin verirseniz açıklama olur.

**Örnek: Hata** (`record-table/error`) · [Canlı önizleme](https://builders.ikas.com/ui-preview/demo/record-table/error?align=stretch)

```tsx
import * as React from "react"

import { RecordTable, type RecordTableColumn } from "@/components/ikas/record-table"

type Order = { id: string; customer: string; total: string }

const columns: RecordTableColumn<Order>[] = [
  { id: "id", header: "Sipariş" },
  { id: "customer", header: "Müşteri" },
  { id: "total", header: "Tutar", align: "end" },
]

const rows: Order[] = [
  { id: "#1042", customer: "Ayşe Yılmaz", total: "₺1.240,00" },
  { id: "#1041", customer: "Mehmet Kaya", total: "₺389,90" },
]

/** error replaces the rows with a danger EmptyState; onRetry adds the retry button. */
export default function RecordTableError() {
  const [state, setState] = React.useState<"error" | "loading" | "ready">("error")

  return (
    <RecordTable
      className="w-full"
      label="Siparişler"
      columns={columns}
      rows={state === "ready" ? rows : []}
      getRowId={(o) => o.id}
      loading={state === "loading"}
      error={state === "error"}
      onRetry={() => {
        setState("loading")
        setTimeout(() => setState("ready"), 1200)
      }}
    />
  )
}
```

## Kullanım ilkeleri

**Yapın:**

- İlk sütunu tanımlayıcı yapın (sipariş numarası, ürün adı).
- Tutarları ve adetleri sağa hizalayın.
- Durumları Badge status değerleriyle gösterin.

**Yapmayın:**

- 6–7 sütundan fazlasını göstermeyin; ikincil olanları hideOnMobile ile gizleyin.
- Yüklenirken sayfanın ortasında spinner kullanmayın.
- Yükleme hatasında tabloyu boş bırakıp yalnızca toast göstermeyin; error ve onRetry verin.

## API

### `RecordTable<T>`

| Prop | Tip | Varsayılan | Açıklama |
| --- | --- | --- | --- |
| `columns` (zorunlu) | `RecordTableColumn<T>[]` | - | { id, header, cell?, align?, hideOnMobile?, className? } |
| `rows` (zorunlu) | `T[]` | - | Geçerli sayfanın satırları. |
| `getRowId` (zorunlu) | `(row: T) => string` | - | Key ve seçim için kullanılan sabit id. |
| `label` (zorunlu) | `string` | - | Tablonun erişilebilir adı. |
| `selectedIds` | `string[]` | - | Checkbox'ları açmak için ikisini birlikte verin. |
| `onSelectedIdsChange` | `(ids: string[]) => void` | - | Checkbox'ları açmak için ikisini birlikte verin. |
| `bulkActions` | `(selectedIds: string[]) => React.ReactNode` | - | Satırlar seçiliyken başlık satırının yerini alır. |
| `onRowClick` | `(row: T) => void` | - | Satırları tıklanabilir yapar (örn. detayı aç). |
| `loading` | `boolean` | `false` | 5 iskelet satır gösterir. |
| `emptyState` | `React.ReactNode` | - | rows boşken gösterilir. |
| `error` | `React.ReactNode` | - | Satırlar yüklenemedi: satırların yerine danger EmptyState çizilir. true varsayılan metni, metin açıklamayı verir. loading öncelik alır. |
| `onRetry` | `() => void` | - | Hata durumuna "Tekrar dene" butonunu ekler. Yeniden denerken loading verin. |
| `toolbar` | `React.ReactNode` | - | Tablonun üstünde arama, filtreler, sekmeler. |
| `pagination` | `{ page: number pageCount: number onPageChange: (page: number) => void /** e.g. "1–20 of 134" */ summary?: React.ReactNode }` | - | Önceki/sonraki içeren alt bilgi. |
| `className` | `string` | - |  |
