> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# DataTable Migration Guide

> This guide provides step-by-step instructions for migrating existing table implementations to the new @/platform/table DataTable framework.

**Created:** 2026-01-08\
**Status:** Complete\
**Task:** T17

## Overview

This guide provides step-by-step instructions for migrating existing table implementations to the new `@/platform/table` DataTable framework.

***

## Quick Start

### 1. Add Imports

```tsx theme={null}
import { 
  DataTable, 
  useDataTable, 
  TableFilters, 
  TablePagination,
  type Column 
} from '@/platform/table';
```

### 2. Define Columns

```tsx theme={null}
const columns: Column<YourEntityType>[] = [
  {
    key: 'name',
    header: 'Name',
    accessor: (row) => row.name,
    filterable: true,
  },
  {
    key: 'status',
    header: 'Status',
    accessor: (row) => row.status,
    render: (value) => <StatusBadge status={value as string} />,
  },
  {
    key: 'amount',
    header: 'Amount',
    accessor: (row) => row.amount,
    type: 'currency',
    align: 'right',
  },
];
```

### 3. Use the Hook

```tsx theme={null}
const { 
  processedData, 
  filteringProps, 
  paginationProps 
} = useDataTable({
  data: entities,
  columns,
  initialPageSize: 20,
});
```

### 4. Render Components

```tsx theme={null}
return (
  <div className="space-y-4">
    <TableFilters {...filteringProps} showGlobalSearch />
    <DataTable
      data={processedData}
      columns={columns}
      isLoading={isLoading}
      emptyState={{ title: 'No items found' }}
    />
    <TablePagination {...paginationProps} />
  </div>
);
```

***

## Migration Patterns

### Pattern 1: Basic Table with Loading/Empty States

**BEFORE:**

```tsx theme={null}
export function ItemsTable({ items, isLoading }: Props) {
  if (isLoading) {
    return <Skeleton className="h-[400px]" />;
  }

  if (!items || items.length === 0) {
    return (
      <div className="text-center py-8 text-muted-foreground">
        No items found
      </div>
    );
  }

  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Name</TableHead>
          <TableHead>Description</TableHead>
          <TableHead>Status</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {items.map((item) => (
          <TableRow key={item.id}>
            <TableCell>{item.name}</TableCell>
            <TableCell>{item.description}</TableCell>
            <TableCell>{item.status}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
```

**AFTER:**

```tsx theme={null}
import { DataTable, type Column } from '@/platform/table';

const columns: Column<Item>[] = [
  { key: 'name', header: 'Name', accessor: (row) => row.name },
  { key: 'description', header: 'Description', accessor: (row) => row.description },
  { key: 'status', header: 'Status', accessor: (row) => row.status },
];

export function ItemsTable({ items, isLoading }: Props) {
  return (
    <DataTable
      data={items}
      columns={columns}
      isLoading={isLoading}
      emptyState={{ title: 'No items found' }}
    />
  );
}
```

***

### Pattern 2: Table with Search and Filters

**BEFORE:**

```tsx theme={null}
export function VendorsTable({ vendors, isLoading }: Props) {
  const [searchTerm, setSearchTerm] = useState("");
  const [activeFilter, setActiveFilter] = useState<string>("all");
  const [is1099Filter, setIs1099Filter] = useState<string>("all");

  const filteredVendors = vendors.filter((vendor) => {
    const matchesSearch = vendor.name
      .toLowerCase()
      .includes(searchTerm.toLowerCase());
    const matchesActive =
      activeFilter === "all" ||
      (activeFilter === "active" ? vendor.is_active : !vendor.is_active);
    const matches1099 =
      is1099Filter === "all" ||
      (is1099Filter === "1099" ? vendor.is_1099 : !vendor.is_1099);
    return matchesSearch && matchesActive && matches1099;
  });

  return (
    <div className="space-y-4">
      <div className="flex gap-4">
        <Input
          placeholder="Search vendors..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
        <Select value={activeFilter} onValueChange={setActiveFilter}>
          <SelectTrigger><SelectValue /></SelectTrigger>
          <SelectContent>
            <SelectItem value="all">All</SelectItem>
            <SelectItem value="active">Active</SelectItem>
            <SelectItem value="inactive">Inactive</SelectItem>
          </SelectContent>
        </Select>
      </div>
      <Table>
        {/* ... table body with filteredVendors */}
      </Table>
    </div>
  );
}
```

**AFTER:**

```tsx theme={null}
import { DataTable, useDataTable, TableFilters, type Column } from '@/platform/table';

const columns: Column<Vendor>[] = [
  { 
    key: 'name', 
    header: 'Vendor Name', 
    accessor: (row) => row.name,
    filterable: true,
  },
  { 
    key: 'is_active', 
    header: 'Status', 
    accessor: (row) => row.is_active,
    type: 'boolean',
    render: (value) => (
      <Badge variant={value ? 'success' : 'secondary'}>
        {value ? 'Active' : 'Inactive'}
      </Badge>
    ),
  },
  { 
    key: 'is_1099', 
    header: '1099', 
    accessor: (row) => row.is_1099,
    type: 'boolean',
  },
];

export function VendorsTable({ vendors, isLoading }: Props) {
  const { processedData, filteringProps } = useDataTable({
    data: vendors,
    columns,
  });

  return (
    <div className="space-y-4">
      <TableFilters {...filteringProps} showGlobalSearch />
      <DataTable
        data={processedData}
        columns={columns}
        isLoading={isLoading}
        emptyState={{ title: 'No vendors found' }}
      />
    </div>
  );
}
```

***

### Pattern 3: Row Actions Menu

**BEFORE:**

```tsx theme={null}
<TableCell>
  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <Button variant="ghost" size="icon">
        <MoreHorizontal className="h-4 w-4" />
      </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent align="end">
      <DropdownMenuItem onClick={() => onView(item)}>
        <Eye className="h-4 w-4 mr-2" />
        View
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => onEdit(item)}>
        <Pencil className="h-4 w-4 mr-2" />
        Edit
      </DropdownMenuItem>
      <DropdownMenuSeparator />
      <DropdownMenuItem 
        onClick={() => onDelete(item)}
        className="text-destructive"
      >
        <Trash className="h-4 w-4 mr-2" />
        Delete
      </DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
</TableCell>
```

**AFTER:**

```tsx theme={null}
const columns: Column<Item>[] = [
  // ... other columns
  {
    key: 'actions',
    header: '',
    accessor: () => null,
    render: (_, row) => (
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button variant="ghost" size="icon">
            <MoreHorizontal className="h-4 w-4" />
          </Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuItem onClick={() => onView(row)}>
            <Eye className="h-4 w-4 mr-2" />
            View
          </DropdownMenuItem>
          <DropdownMenuItem onClick={() => onEdit(row)}>
            <Pencil className="h-4 w-4 mr-2" />
            Edit
          </DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem 
            onClick={() => onDelete(row)}
            className="text-destructive"
          >
            <Trash className="h-4 w-4 mr-2" />
            Delete
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    ),
  },
];
```

***

### Pattern 4: Status Badges

**BEFORE:**

```tsx theme={null}
function getStatusBadge(status: string) {
  const variants: Record<string, string> = {
    active: 'success',
    pending: 'warning',
    inactive: 'secondary',
    error: 'destructive',
  };
  return (
    <Badge variant={variants[status] || 'default'}>
      {status}
    </Badge>
  );
}

// In table cell
<TableCell>{getStatusBadge(item.status)}</TableCell>
```

**AFTER:**

```tsx theme={null}
const columns: Column<Item>[] = [
  {
    key: 'status',
    header: 'Status',
    accessor: (row) => row.status,
    render: (value) => {
      const variants: Record<string, string> = {
        active: 'success',
        pending: 'warning',
        inactive: 'secondary',
        error: 'destructive',
      };
      return (
        <Badge variant={variants[value as string] || 'default'}>
          {value}
        </Badge>
      );
    },
  },
];
```

***

### Pattern 5: Currency Formatting

**BEFORE:**

```tsx theme={null}
function formatCurrency(amount: number): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(amount);
}

// In table cell
<TableCell className="text-right font-mono">
  {formatCurrency(Number(item.amount))}
</TableCell>
```

**AFTER:**

```tsx theme={null}
const columns: Column<Item>[] = [
  {
    key: 'amount',
    header: 'Amount',
    accessor: (row) => row.amount,
    type: 'currency',
    align: 'right',
  },
];
```

***

### Pattern 6: Date Formatting

**BEFORE:**

```tsx theme={null}
import { format } from 'date-fns';

<TableCell>
  {item.created_at ? format(new Date(item.created_at), 'MMM d, yyyy') : '-'}
</TableCell>
```

**AFTER:**

```tsx theme={null}
const columns: Column<Item>[] = [
  {
    key: 'created_at',
    header: 'Created',
    accessor: (row) => row.created_at,
    type: 'date',
  },
];
```

***

### Pattern 7: Pagination

**BEFORE:**

```tsx theme={null}
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);

const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedData = data.slice(startIndex, endIndex);
const totalPages = Math.ceil(data.length / pageSize);

// Render pagination controls manually
```

**AFTER:**

```tsx theme={null}
const { processedData, paginationProps } = useDataTable({
  data,
  columns,
  initialPageSize: 20,
});

return (
  <>
    <DataTable data={processedData} columns={columns} />
    <TablePagination {...paginationProps} />
  </>
);
```

***

### Pattern 8: Row Selection with Bulk Actions

**BEFORE:**

```tsx theme={null}
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

const toggleSelection = (id: string) => {
  const newSet = new Set(selectedIds);
  if (newSet.has(id)) {
    newSet.delete(id);
  } else {
    newSet.add(id);
  }
  setSelectedIds(newSet);
};

const toggleAll = () => {
  if (selectedIds.size === data.length) {
    setSelectedIds(new Set());
  } else {
    setSelectedIds(new Set(data.map(d => d.id)));
  }
};
```

**AFTER:**

```tsx theme={null}
import { DataTable, BulkActionsBar, type BulkAction } from '@/platform/table';

const bulkActions: BulkAction<Item>[] = [
  {
    id: 'delete',
    label: 'Delete Selected',
    icon: <Trash className="h-4 w-4" />,
    variant: 'destructive',
    requireConfirmation: true,
    confirmationMessage: 'Delete selected items?',
    onExecute: async (items) => {
      await deleteItems(items.map(i => i.id));
    },
  },
];

export function ItemsTable({ items }: Props) {
  const [selectedRows, setSelectedRows] = useState<Item[]>([]);

  return (
    <>
      <BulkActionsBar
        selectedItems={selectedRows}
        actions={bulkActions}
        onClearSelection={() => setSelectedRows([])}
      />
      <DataTable
        data={items}
        columns={columns}
        selectable
        selectedRows={selectedRows}
        onSelectionChange={setSelectedRows}
      />
    </>
  );
}
```

***

## Common Gotchas

### 1. Column Key Must Be Unique

```tsx theme={null}
// ❌ WRONG - duplicate keys
const columns = [
  { key: 'name', ... },
  { key: 'name', ... }, // Error!
];

// ✅ CORRECT
const columns = [
  { key: 'first_name', ... },
  { key: 'last_name', ... },
];
```

### 2. Accessor Must Return Sortable Value

```tsx theme={null}
// ❌ WRONG - returns JSX
{ key: 'status', accessor: (row) => <Badge>{row.status}</Badge> }

// ✅ CORRECT - accessor returns value, render handles display
{ 
  key: 'status', 
  accessor: (row) => row.status,
  render: (value) => <Badge>{value}</Badge>,
}
```

### 3. Remember filterable: true for Searchable Columns

```tsx theme={null}
// ❌ WRONG - column won't appear in filters
{ key: 'name', header: 'Name', accessor: (row) => row.name }

// ✅ CORRECT
{ key: 'name', header: 'Name', accessor: (row) => row.name, filterable: true }
```

### 4. Use hideOnMobile for Non-Essential Columns

```tsx theme={null}
// Mobile optimization
const columns: Column<Item>[] = [
  { key: 'name', header: 'Name', accessor: (row) => row.name },
  { key: 'email', header: 'Email', accessor: (row) => row.email, hideOnMobile: true },
  { key: 'phone', header: 'Phone', accessor: (row) => row.phone, hideOnMobile: true },
];
```

### 5. Custom Render Must Handle Null/Undefined

```tsx theme={null}
// ❌ WRONG - may crash on null
{ render: (value) => value.toUpperCase() }

// ✅ CORRECT
{ render: (value) => value ? String(value).toUpperCase() : '-' }
```

***

## Step-by-Step Migration Checklist

For each table file:

### Preparation

* [ ] Review current implementation
* [ ] Identify all features (filters, sorting, pagination, actions)
* [ ] Note any custom rendering

### Migration

* [ ] Add imports from `@/platform/table`
* [ ] Define `Column<T>[]` array
* [ ] Map existing TableHead elements to columns
* [ ] Add `type` for currency/date/boolean columns
* [ ] Add `filterable: true` for searchable columns
* [ ] Add `hideOnMobile: true` for non-essential columns
* [ ] Move row actions to column render function
* [ ] Replace filter state with `useDataTable` hook
* [ ] Replace `<Table>` with `<DataTable>`
* [ ] Add `TableFilters` component if filters exist
* [ ] Add `TablePagination` component if pagination needed
* [ ] Add bulk actions if selection exists

### Testing

* [ ] Verify loading state shows skeleton
* [ ] Verify empty state displays correctly
* [ ] Verify filtering works (global + column)
* [ ] Verify sorting works
* [ ] Verify pagination works
* [ ] Verify row actions work
* [ ] Verify mobile responsiveness
* [ ] Verify accessibility (keyboard navigation)

### Cleanup

* [ ] Remove unused imports
* [ ] Remove duplicate utility functions
* [ ] Update component props if simplified

***

## Need Help?

* **DataTable API:** See `src/platform/table/README.md`
* **Column Types:** See `src/platform/table/types.ts`
* **Examples:** See existing platform tables
* **Issues:** Document in migration audit file
