Komponen / Form & input
FilterBar
Baris pencarian + filter + jumlah hasil di atas daftar dokumen (PO, invoice, BAST).
Daftar PO
Tampilan tersimpan, cari, dan "Simpan tampilan" di baris atas; chip Status/Customer/Kategori/Periode/Pemilik, "Hapus semua filter", dan jumlah hasil di bawah.
2 dari 7 PO
| No. PO | Customer | Kategori | Status |
|---|---|---|---|
| PO-2026-0144 | PT Samudra Farma | Lisensi | Menunggu BAST/DO |
| PO-2026-0143 | PT Graha Konstruksi Utama | Lisensi | Menunggu BAST/DO |
import { useMemo, useState } from 'react';
import {
Button,
Checkbox,
DataTable,
FilterBar,
FilterChip,
Popover,
Radio,
RadioGroup,
Select,
StateLabel,
type DataTableColumn,
type StateLabelTone,
} from '@divistant/circca-react';
// Figma BizOps for IT Services › Template · Daftar (29:982): tampilan tersimpan + cari + "Simpan tampilan",
// lalu chip filter, "Hapus semua filter", dan jumlah hasil di kanan. Penyaringan di sini lokal; di aplikasi nyata
// kirim sebagai parameter ke server.
type Po = { no: string; customer: string; kategori: string; status: string; tone: StateLabelTone };
const SEMUA: Po[] = [
{ no: 'PO-2026-0148', customer: 'CV Sinar Logistik', kategori: 'Lisensi + jasa', status: 'Menunggu approval', tone: 'warning' },
{ no: 'PO-2026-0147', customer: 'PT Arunika Data', kategori: 'Lisensi', status: 'Diproses', tone: 'info' },
{ no: 'PO-2026-0146', customer: 'PT Mitra Energi Persada', kategori: 'Jasa', status: 'Draft', tone: 'neutral' },
{ no: 'PO-2026-0145', customer: 'PT Kawan Retail Indonesia', kategori: 'Lisensi + jasa', status: 'PO terbit', tone: 'accent' },
{ no: 'PO-2026-0144', customer: 'PT Samudra Farma', kategori: 'Lisensi', status: 'Menunggu BAST/DO', tone: 'warning' },
{ no: 'PO-2026-0143', customer: 'PT Graha Konstruksi Utama', kategori: 'Lisensi', status: 'Menunggu BAST/DO', tone: 'warning' },
{ no: 'PO-2026-0142', customer: 'PT Pelanggan Contoh A', kategori: 'Lisensi + jasa', status: 'Ditagih', tone: 'success' },
];
const STATUS = ['Draft', 'Menunggu approval', 'Diproses', 'PO terbit', 'Menunggu BAST/DO', 'Ditagih'];
const KATEGORI = ['Lisensi', 'Jasa', 'Lisensi + jasa'];
const columns: DataTableColumn<Po>[] = [
{ key: 'no', header: 'No. PO', rowHeader: true, width: 140 },
{ key: 'customer', header: 'Customer', width: 220 },
{ key: 'kategori', header: 'Kategori', width: 130 },
{
key: 'status',
header: 'Status',
cell: r => (
<StateLabel size="sm" tone={r.tone}>
{r.status}
</StateLabel>
),
},
];
export default function Example() {
const [q, setQ] = useState('');
const [status, setStatus] = useState<string[]>(['Menunggu BAST/DO']);
const [draf, setDraf] = useState<string[]>(status);
const [kategori, setKategori] = useState<string>();
const [kategoriOpen, setKategoriOpen] = useState(false);
const rows = useMemo(() => {
const kata = q.trim().toLowerCase();
return SEMUA.filter(
r =>
(!kata || r.no.toLowerCase().includes(kata) || r.customer.toLowerCase().includes(kata)) &&
(status.length === 0 || status.includes(r.status)) &&
(!kategori || r.kategori === kategori),
);
}, [q, status, kategori]);
const adaFilter = status.length > 0 || kategori != null;
return (
<div className="grid w-full gap-3">
<FilterBar
leading={
<Select
aria-label="Tampilan tersimpan"
defaultValue="perlu"
options={[
{ value: 'perlu', label: 'Tampilan: Perlu tindakan' },
{ value: 'semua', label: 'Tampilan: Semua PO' },
{ value: 'saya', label: 'Tampilan: PO saya' },
]}
/>
}
search={{ 'aria-label': 'Cari PO', placeholder: 'Cari no. PO, customer', value: q, onValueChange: setQ }}
actions={<Button variant="ghost">Simpan tampilan</Button>}
onClearAll={
adaFilter
? () => {
setStatus([]);
setKategori(undefined);
}
: undefined
}
resultCount={`${rows.length} dari ${SEMUA.length} PO`}
>
<FilterChip
field="Status"
value={status.length === 0 ? undefined : status.length === 1 ? status[0] : `${status.length} status`}
onRemove={() => setStatus([])}
onOpenChange={o => o && setDraf(status)}
content={
<div className="grid gap-2">
{STATUS.map(s => (
<Checkbox key={s} checked={draf.includes(s)} onCheckedChange={on => setDraf(d => (on ? [...d, s] : d.filter(x => x !== s)))}>
{s}
</Checkbox>
))}
</div>
}
popoverFooter={
<>
<Button size="sm" onClick={() => setDraf([])}>
Atur ulang
</Button>
<Popover.Close asChild>
<Button size="sm" variant="primary" onClick={() => setStatus(draf)}>
Terapkan
</Button>
</Popover.Close>
</>
}
/>
<FilterChip field="Customer" content={<p className="text-muted">Cari dan pilih customer…</p>} />
<FilterChip
field="Kategori"
value={kategori}
open={kategoriOpen}
onOpenChange={setKategoriOpen}
onRemove={() => setKategori(undefined)}
content={
<RadioGroup
aria-label="Kategori"
value={kategori ?? ''}
onValueChange={v => {
setKategori(v);
setKategoriOpen(false);
}}
>
{KATEGORI.map(k => (
<Radio key={k} value={k}>
{k}
</Radio>
))}
</RadioGroup>
}
/>
<FilterChip field="Periode" content={<p className="text-muted">Pilih rentang tanggal PO…</p>} />
<FilterChip field="Pemilik" content={<p className="text-muted">Pilih pemilik PO…</p>} />
</FilterBar>
<DataTable framed caption="PO customer" captionHidden minWidth={640} columns={columns} rows={rows} getRowId={r => r.no} />
</div>
);
}Sederhana dengan aksi
Tanpa tampilan tersimpan; IconButton ekspor di kanan. Persempit layar untuk melihat baris membungkus.
18 dari 86 invoice
import { useState } from 'react';
import { FilterBar, FilterChip, IconButton } from '@divistant/circca-react';
import { DownloadSimple } from '@phosphor-icons/react';
// Tanpa tampilan tersimpan: cari + aksi ekspor di baris atas, chip filter + jumlah hasil di bawah.
// "Hapus semua filter" hanya tampil saat ada filter aktif. Persempit layar untuk melihat baris membungkus.
export default function Example() {
const [q, setQ] = useState('');
const [status, setStatus] = useState<string | undefined>('Belum lunas');
const [periode, setPeriode] = useState<string | undefined>('Sep 2026');
const total = 86;
const hasil = (status ? 24 : 61) - (periode ? 6 : 0) - (q ? 12 : 0);
return (
<FilterBar
className="max-w-3xl"
search={{ 'aria-label': 'Cari invoice', placeholder: 'Cari no. invoice, customer', value: q, onValueChange: setQ }}
actions={<IconButton variant="ghost" icon={DownloadSimple} label="Ekspor CSV" />}
onClearAll={
status || periode
? () => {
setStatus(undefined);
setPeriode(undefined);
}
: undefined
}
resultCount={`${Math.max(hasil, 0)} dari ${total} invoice`}
>
<FilterChip field="Status" value={status} onRemove={() => setStatus(undefined)} onClick={() => setStatus('Belum lunas')} />
<FilterChip field="Periode" value={periode} onRemove={() => setPeriode(undefined)} onClick={() => setPeriode('Sep 2026')} />
<FilterChip field="Customer" />
</FilterBar>
);
}