partial (source-backed; Tasks/Contacts migrations and runtime evidence remain open)

Spec: F66 — TanStack Table (Declarative Data Tables)

Status: partial (source-backed; Tasks/Contacts migrations and runtime evidence remain open)

1. Problem & CEO Job

Decide fast. CEO-facing pages are data lists, but each hand-rolls sorting, filtering, pagination, and selection. The result is inconsistent behavior, missed edge cases, and slow delivery. A headless table library removes that boilerplate.

2. User Stories & Acceptance Criteria

  • As a CEO, when I view any data table, I want to sort by clicking column headers, so that I find what I need quickly.

    • Given a table with sortable columns, clicking a header sorts ascending, clicking again sorts descending (DataTable.tsx handleSort)
    • Given an active sort, the header shows a sort indicator (arrow icon) (ChevronUp/Down icons, lines 146–149)
  • As a CEO, when I view a table with many rows, I want client-side pagination, so that I see 25 rows at a time without scrolling endlessly.

    • Given >25 items, the table shows pagination controls (DataTable.tsx pagination bar, line 283)
    • Given paginated data, clicking “Next” shows the next page (page state + paged slice)
  • As a CEO, when I check off multiple items, I want row selection with bulk actions, so that I can act on several items at once.

    • Given a table with checkboxes, selecting N rows reveals a bulk action bar (DataTable selectable prop; consumers wire bulk bar — e.g., DealsPage BulkActionBar)
    • Given selected rows, clicking “Xóa” deletes all selected (consumer-provided action via onSelectionChange + external bulk handler)
  • As a developer, I want a single <DataTable> component that accepts columns and data, so that a new list page is ~20 lines of markup.

3. Scope Boundaries

  • In: Installation (@tanstack/react-table), shared <DataTable> component with sort/filter/pagination/selection, <TableSkeleton> and <TableEmpty> states, migration of existing list pages one-by-one
  • Out: Server-side pagination/sorting (all current lists fetch fully client-side), inline editing, row reordering, column visibility toggles, export-to-CSV

4. Data Model

n/a — purely frontend.

5. API Contract

n/a — tables consume existing TanStack Query data.

6. UI / UX

  • Component: frontend/src/components/table/DataTable.tsx — wraps shadcn’s <Table>, <TableHeader>, <TableBody>, <TableRow>, <TableCell> with sort/select/pagination built in
  • Sorting: <DataTable> accepts sortable: boolean per column (default true for text/date/number columns)
  • Selection: <DataTable selection={true}> shows checkbox column, exposes selectedRows via render prop
  • Pagination: <DataTable pagination={true} pageSize={25}> shows “Hiển thị 1–25 / 80”
  • Bulk actions: Pass bulkActions={[{ label: 'Xóa', onClick: (rows) => ... }]} — renders as a floating action bar when rows selected
  • States:
    • Loading: <TableSkeleton /> — N rows of <Skeleton> matching column count
    • Empty: <TableEmpty message={t('empty')} /> — centered text + optional CTA button
    • Error: Toast from TanStack Query isError — table shows empty state with retry button
    • Populated: Full table with sort/select/pagination
  • i18n: common namespace keys — common.no_data, common.rows_per_page, common.of, common.selected, common.sort_asc, common.sort_desc, common.clear_sort
  • Mobile: Tables collapse to card list on <768px — each row becomes a <Card> with label:value pairs. useMediaQuery from shadcn hooks to detect breakpoint.

7. Test Plan

#CaseTypeWhere
1DataTable renders with data and columnsfrontendfrontend/src/test/DataTable.test.tsx
2DataTable sorts by column on header clickfrontendfrontend/src/test/DataTable.test.tsx
3DataTable paginates correctlyfrontendfrontend/src/test/DataTable.test.tsx
4DataTable row selection worksfrontendfrontend/src/test/DataTable.test.tsx
5DataTable shows skeleton on loadingfrontendfrontend/src/test/DataTable.test.tsx
6DataTable shows empty statefrontendfrontend/src/test/DataTable.test.tsx
7DataTable collapses to cards on mobilefrontendfrontend/src/test/DataTable.test.tsx
8Each migrated page — sorting workse2eExisting describe blocks
9Deals table view renders on /deals after Table togglee2efrontend/tests/e2e/crm.spec.ts — F66 — TanStack Table (DataTable)
10Column headers exist and sort click leaves table visiblee2efrontend/tests/e2e/crm.spec.ts — deals table view renders sortable columns
11Table has more than 2 column headerse2efrontend/tests/e2e/crm.spec.ts — deals table view has multiple column headers

8. Current Behavior

The shared <DataTable> primitive uses TanStack Table v8 and is reused by CRM, project-task, commitment, subscription, HQ, and settings surfaces. The top-level Tasks and Contacts list surfaces still use custom list/virtual-list UIs rather than the shared table. Browser and frontend runner evidence remain open.

9. Error States

  • Fetch error: TanStack Query error → table shows <TableEmpty> with “Không thể tải dữ liệu” + retry button
  • Empty data: <TableEmpty> with “Chưa có dữ liệu” + CTA to create
  • Selection + bulk action error: Toast “Không thể thực hiện” + selection persists so user can retry

10. Diagrams & Docs Touched

  • TASTE.md — add table rule (use DataTable, don’t hand-roll)
  • frontend/AGENTS.md — add DataTable convention section

11. Implementation Audit

#ClaimStatusEvidence
1TanStack Table v8 installed✅ matchpackage.json
2frontend/src/pages/crm/TableView.tsx — sortable, column-togglable deals table with DealHealthBadge✅ matchTableView.tsx
3Shared <DataTable> wrapper component established✅ source matchfrontend/src/components/table/DataTable.tsx; tests in frontend/src/test/_shared/DataTable.test.tsx
4Tasks + contacts table surfaces migrated⚠️ gapTasksPage.tsx still uses the drag/drop card list and ContactsPage.tsx still uses VirtualList; shared rollout elsewhere is source-backed

2026-07-16 — Contract Graph audit

The shared table primitive and current rollout are now contract-owned. Tasks and Contacts remain explicit implementation gaps; no browser or production evidence is inferred.

12. Open Questions / Decisions

  • TanStack Table v8 (headless) vs AG Grid (batteries-included)? Decision: TanStack Table v8. AG Grid adds 500KB+ and its configuration model fights React patterns. TanStack Table is headless, zero-bundle visual, and pairs naturally with shadcn Table components.

13. Bug / Change Log

2026-07-01 — Correct F66 rollout truth without over-claiming the remaining gap

Type: Documentation truth fix

Summary:

  • Updated F66 to reflect the real shipped DataTable rollout across the repo instead of implying CRM was the only live consumer.
  • Kept the genuine remaining gap open: the top-level Tasks and Contacts pages still have not migrated to the shared table surface.

2026-06-13 — E2E tests added for deals table view (T3-E r11)

Type: Test

Summary:

  • Added F66 — TanStack Table (DataTable) E2E describe block in frontend/tests/e2e/crm.spec.ts.
  • Tests: table view renders after toggle, sortable column headers visible, multiple column count assertion.
  • Unskipped can switch to table view test in F50 block.

Impact:

  • E2E gate for F66 DataTable now active in CI.