Add ID and Created columns to all admin pages with sortable columns

- Add ID column to all admin list pages for easy record identification
- Add Created column (created_at/date_joined) to pages missing it
- Implement client-side column sorting in DataTable component
- Fix sorting implementation to work without getSortingRowModel export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-09 17:14:08 -06:00
parent 80c6b58361
commit 68a4c28d68
23 changed files with 200 additions and 7 deletions

View File

@@ -37,6 +37,15 @@ const roleColors: Record<string, 'default' | 'secondary' | 'destructive'> = {
};
const columns: ColumnDef<ManagedAdminUser>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<span className="font-mono text-sm text-muted-foreground">
{row.original.id}
</span>
),
},
{
accessorKey: 'email',
header: 'Email',

View File

@@ -170,6 +170,7 @@ export default function AppleSocialAuthPage() {
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>User</TableHead>
<TableHead>Apple ID</TableHead>
<TableHead>Apple Email</TableHead>
@@ -202,6 +203,7 @@ export default function AppleSocialAuthPage() {
}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{entry.id}</TableCell>
<TableCell>
<Link
href={`/admin/users/${entry.user_id}`}

View File

@@ -170,6 +170,7 @@ export default function AuthTokensPage() {
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>User</TableHead>
<TableHead>Email</TableHead>
<TableHead>Token</TableHead>
@@ -201,6 +202,7 @@ export default function AuthTokensPage() {
}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{token.user_id}</TableCell>
<TableCell>
<Link
href={`/admin/users/${token.user_id}`}

View File

@@ -170,6 +170,7 @@ export default function CompletionImagesPage() {
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>Preview</TableHead>
<TableHead>Task</TableHead>
<TableHead>Caption</TableHead>
@@ -202,6 +203,7 @@ export default function CompletionImagesPage() {
}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{img.id}</TableCell>
<TableCell>
{img.image_url ? (
<a href={img.image_url} target="_blank" rel="noopener noreferrer">

View File

@@ -179,12 +179,14 @@ export default function CompletionsPage() {
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>Task</TableHead>
<TableHead>Residence</TableHead>
<TableHead>Completed By</TableHead>
<TableHead>Completed At</TableHead>
<TableHead>Cost</TableHead>
<TableHead>Photo</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-24">Actions</TableHead>
</TableRow>
</TableHeader>
@@ -212,6 +214,7 @@ export default function CompletionsPage() {
}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{completion.id}</TableCell>
<TableCell>
<Link
href={`/completions/${completion.id}`}
@@ -295,6 +298,9 @@ export default function CompletionsPage() {
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
{new Date(completion.created_at).toLocaleDateString()}
</TableCell>
<TableCell>
<AlertDialog>
<AlertDialogTrigger asChild>

View File

@@ -175,6 +175,7 @@ export default function ConfirmationCodesPage() {
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>User</TableHead>
<TableHead>Email</TableHead>
<TableHead>Code</TableHead>
@@ -208,6 +209,7 @@ export default function ConfirmationCodesPage() {
}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{code.id}</TableCell>
<TableCell>
<Link
href={`/admin/users/${code.user_id}`}

View File

@@ -31,6 +31,15 @@ import {
import { toast } from 'sonner';
const columns: ColumnDef<Contractor>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<span className="font-mono text-sm text-muted-foreground">
{row.original.id}
</span>
),
},
{
accessorKey: 'name',
header: 'Name',
@@ -82,6 +91,11 @@ const columns: ColumnDef<Contractor>[] = [
</Badge>
),
},
{
accessorKey: 'created_at',
header: 'Created',
cell: ({ row }) => new Date(row.original.created_at).toLocaleDateString(),
},
{
id: 'actions',
cell: ({ row }) => {

View File

@@ -269,6 +269,7 @@ export default function DevicesPage() {
}}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>User</TableHead>
<TableHead>Device ID</TableHead>
@@ -306,6 +307,7 @@ export default function DevicesPage() {
}}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{device.id}</TableCell>
<TableCell className="font-medium">{device.name || 'Unknown'}</TableCell>
<TableCell>
{device.user_id ? (

View File

@@ -170,6 +170,7 @@ export default function DocumentImagesPage() {
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>Preview</TableHead>
<TableHead>Document</TableHead>
<TableHead>Residence</TableHead>
@@ -202,6 +203,7 @@ export default function DocumentImagesPage() {
}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{img.id}</TableCell>
<TableCell>
{img.image_url ? (
<a href={img.image_url} target="_blank" rel="noopener noreferrer">

View File

@@ -40,6 +40,15 @@ const documentTypeColors: Record<string, 'default' | 'secondary' | 'destructive'
};
const columns: ColumnDef<Document>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<span className="font-mono text-sm text-muted-foreground">
{row.original.id}
</span>
),
},
{
accessorKey: 'title',
header: 'Title',

View File

@@ -122,11 +122,13 @@ export default function FeatureBenefitsPage() {
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Order</TableHead>
<TableHead>Feature</TableHead>
<TableHead>Free Tier</TableHead>
<TableHead>Pro Tier</TableHead>
<TableHead>Active</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-24">Actions</TableHead>
</TableRow>
</TableHeader>
@@ -138,6 +140,7 @@ export default function FeatureBenefitsPage() {
) : (
data?.data?.map((benefit) => (
<TableRow key={benefit.id}>
<TableCell className="font-mono text-sm text-muted-foreground">{benefit.id}</TableCell>
<TableCell>{benefit.display_order}</TableCell>
<TableCell className="font-medium">{benefit.feature_name}</TableCell>
<TableCell>{benefit.free_tier_text}</TableCell>
@@ -147,6 +150,9 @@ export default function FeatureBenefitsPage() {
{benefit.is_active ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
{new Date(benefit.created_at).toLocaleDateString()}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button variant="ghost" size="icon" onClick={() => handleEdit(benefit)}>

View File

@@ -88,6 +88,15 @@ export default function NotificationPrefsPage() {
};
const columns: ColumnDef<NotificationPreference>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<span className="font-mono text-sm text-muted-foreground">
{row.original.id}
</span>
),
},
{
accessorKey: 'username',
header: 'User',
@@ -226,6 +235,11 @@ export default function NotificationPrefsPage() {
);
},
},
{
accessorKey: 'created_at',
header: 'Created',
cell: ({ row }) => new Date(row.original.created_at).toLocaleDateString(),
},
{
id: 'actions',
cell: ({ row }) => {

View File

@@ -20,6 +20,15 @@ import {
} from '@/components/ui/dropdown-menu';
const columns: ColumnDef<Notification>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<span className="font-mono text-sm text-muted-foreground">
{row.original.id}
</span>
),
},
{
accessorKey: 'title',
header: 'Title',

View File

@@ -207,6 +207,7 @@ export default function PasswordResetCodesPage() {
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>User</TableHead>
<TableHead>Email</TableHead>
<TableHead>Token</TableHead>
@@ -241,6 +242,7 @@ export default function PasswordResetCodesPage() {
}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{code.id}</TableCell>
<TableCell>
<Link
href={`/admin/users/${code.user_id}`}

View File

@@ -143,11 +143,13 @@ export default function PromotionsPage() {
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>ID (DB)</TableHead>
<TableHead>Promotion ID</TableHead>
<TableHead>Title</TableHead>
<TableHead>Target</TableHead>
<TableHead>Date Range</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-24">Actions</TableHead>
</TableRow>
</TableHeader>
@@ -159,6 +161,7 @@ export default function PromotionsPage() {
) : (
data?.data?.map((promo) => (
<TableRow key={promo.id}>
<TableCell className="font-mono text-sm text-muted-foreground">{promo.id}</TableCell>
<TableCell><code className="text-xs bg-muted px-2 py-1 rounded">{promo.promotion_id}</code></TableCell>
<TableCell className="font-medium">{promo.title}</TableCell>
<TableCell>
@@ -178,6 +181,9 @@ export default function PromotionsPage() {
<Badge variant="destructive">Inactive</Badge>
)}
</TableCell>
<TableCell>
{new Date(promo.created_at).toLocaleDateString()}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button variant="ghost" size="icon" onClick={() => handleEdit(promo)}>

View File

@@ -32,6 +32,15 @@ import {
import { toast } from 'sonner';
const columns: ColumnDef<Residence>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<span className="font-mono text-sm text-muted-foreground">
{row.original.id}
</span>
),
},
{
accessorKey: 'name',
header: 'Name',

View File

@@ -190,6 +190,7 @@ export default function ShareCodesPage() {
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>Code</TableHead>
<TableHead>Residence</TableHead>
<TableHead>Created By</TableHead>
@@ -223,6 +224,7 @@ export default function ShareCodesPage() {
}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{code.id}</TableCell>
<TableCell>
<code className="text-sm bg-muted px-2 py-1 rounded font-mono">
{code.code}

View File

@@ -26,6 +26,15 @@ const tierColors: Record<string, 'default' | 'secondary' | 'outline'> = {
};
const columns: ColumnDef<Subscription>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<span className="font-mono text-sm text-muted-foreground">
{row.original.id}
</span>
),
},
{
accessorKey: 'username',
header: 'User',

View File

@@ -239,12 +239,14 @@ export default function TaskTemplatesPage() {
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead className="w-16">Order</TableHead>
<TableHead>Title</TableHead>
<TableHead>Category</TableHead>
<TableHead>Frequency</TableHead>
<TableHead>iOS Icon</TableHead>
<TableHead>Active</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-32">Actions</TableHead>
</TableRow>
</TableHeader>
@@ -256,6 +258,7 @@ export default function TaskTemplatesPage() {
) : (
data?.data?.map((template) => (
<TableRow key={template.id}>
<TableCell className="font-mono text-sm text-muted-foreground">{template.id}</TableCell>
<TableCell className="text-muted-foreground">{template.display_order}</TableCell>
<TableCell className="font-medium max-w-xs">
<div className="truncate">{template.title}</div>
@@ -275,6 +278,9 @@ export default function TaskTemplatesPage() {
{template.is_active ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
{new Date(template.created_at).toLocaleDateString()}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button

View File

@@ -31,6 +31,15 @@ import {
import { toast } from 'sonner';
const columns: ColumnDef<Task>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<span className="font-mono text-sm text-muted-foreground">
{row.original.id}
</span>
),
},
{
accessorKey: 'title',
header: 'Title',
@@ -81,6 +90,11 @@ const columns: ColumnDef<Task>[] = [
return <Badge variant="default">Active</Badge>;
},
},
{
accessorKey: 'created_at',
header: 'Created',
cell: ({ row }) => new Date(row.original.created_at).toLocaleDateString(),
},
{
id: 'actions',
cell: ({ row }) => {

View File

@@ -170,6 +170,7 @@ export default function UserProfilesPage() {
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>ID</TableHead>
<TableHead>User</TableHead>
<TableHead>Email</TableHead>
<TableHead>Phone</TableHead>
@@ -202,6 +203,7 @@ export default function UserProfilesPage() {
}
/>
</TableCell>
<TableCell className="font-mono text-sm text-muted-foreground">{profile.id}</TableCell>
<TableCell>
<Link
href={`/admin/users/${profile.user_id}`}

View File

@@ -32,6 +32,15 @@ import {
import { toast } from 'sonner';
const columns: ColumnDef<User>[] = [
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<span className="font-mono text-sm text-muted-foreground">
{row.original.id}
</span>
),
},
{
accessorKey: 'username',
header: 'Username',

View File

@@ -1,12 +1,15 @@
'use client';
import * as React from 'react';
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
RowSelectionState,
SortingState,
} from '@tanstack/react-table';
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
import {
Table,
@@ -17,6 +20,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import { DataTablePagination } from './data-table-pagination';
import { DataTableToolbar } from './data-table-toolbar';
@@ -52,6 +56,37 @@ export function DataTable<TData, TValue>({
onRowSelectionChange,
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const [sorting, setSorting] = React.useState<SortingState>([]);
// Sort data client-side based on sorting state
const sortedData = React.useMemo(() => {
if (sorting.length === 0) return data;
const [sort] = sorting;
const { id, desc } = sort;
return [...data].sort((a, b) => {
const aValue = (a as Record<string, unknown>)[id];
const bValue = (b as Record<string, unknown>)[id];
// Handle null/undefined
if (aValue == null && bValue == null) return 0;
if (aValue == null) return desc ? -1 : 1;
if (bValue == null) return desc ? 1 : -1;
// Compare values
if (typeof aValue === 'number' && typeof bValue === 'number') {
return desc ? bValue - aValue : aValue - bValue;
}
// String comparison
const aStr = String(aValue).toLowerCase();
const bStr = String(bValue).toLowerCase();
if (aStr < bStr) return desc ? 1 : -1;
if (aStr > bStr) return desc ? -1 : 1;
return 0;
});
}, [data, sorting]);
// Add selection column if enabled
const tableColumns = React.useMemo(() => {
@@ -84,19 +119,22 @@ export function DataTable<TData, TValue>({
}, [columns, enableRowSelection]);
const table = useReactTable({
data,
data: sortedData,
columns: tableColumns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
manualSorting: true,
pageCount: Math.ceil(totalCount / pageSize),
state: {
rowSelection,
sorting,
pagination: {
pageIndex: page - 1,
pageSize,
},
},
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
enableRowSelection,
});
@@ -127,12 +165,31 @@ export function DataTable<TData, TValue>({
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
{header.isPlaceholder ? null : header.column.getCanSort() ? (
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent"
onClick={() => header.column.toggleSorting()}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getIsSorted() === 'asc' ? (
<ArrowUp className="ml-2 h-4 w-4" />
) : header.column.getIsSorted() === 'desc' ? (
<ArrowDown className="ml-2 h-4 w-4" />
) : (
<ArrowUpDown className="ml-2 h-4 w-4 opacity-50" />
)}
</Button>
) : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
</TableHead>
))}
</TableRow>
@@ -188,5 +245,3 @@ export function DataTable<TData, TValue>({
</div>
);
}
import * as React from 'react';