fix(audit-wave-9): add mobile cardRender to remaining admin lists

Five DataTable consumers were rendering as horizontally-scrolling
desktop tables on mobile because they had no cardRender prop. Now they
collapse to a vertical card list below the lg: breakpoint with the
same actions inline:

- admin/tags/tag-list
- admin/roles/role-list
- admin/ports/port-list (also: Active/Inactive badge -> StatusPill)
- admin/document-templates/template-list (also: Active/Inactive badge
  -> StatusPill)
- admin/custom-fields/custom-fields-manager

All five now share the user-list / berth-list pattern: row-card with
title, secondary meta, and trailing action buttons; same TanStack
table instance powers both the desktop table and the mobile cards.

Closes ui/ux H2 + extends M2 (status-pill coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 12:00:35 +02:00
parent 153f6ac797
commit 0df761f4ad
5 changed files with 277 additions and 10 deletions

View File

@@ -173,6 +173,67 @@ export function RoleList() {
data={roles}
isLoading={loading}
getRowId={(row) => row.id}
cardRender={({ original }) => (
<div className="rounded-xl border border-border bg-card p-4 shadow-sm">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="truncate text-sm font-semibold text-foreground">
{formatRole(original.name)}
</p>
{original.isSystem ? (
<Badge variant="outline" className="text-xs">
<Lock className="mr-1 h-3 w-3" aria-hidden />
System
</Badge>
) : null}
</div>
{original.description ? (
<p className="mt-1 text-xs text-muted-foreground">{original.description}</p>
) : null}
<button
type="button"
onClick={() => setViewingPermissions(original)}
className="mt-2 inline-flex"
title="View permission breakdown"
>
<Badge variant="secondary" className="cursor-pointer hover:bg-secondary/80">
{countPermissions(original.permissions)} permissions
</Badge>
</button>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditRole(original)}
aria-label="Edit role"
>
<Pencil className="h-4 w-4" />
</Button>
{!original.isSystem ? (
<ConfirmationDialog
trigger={
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
aria-label="Delete role"
>
<Trash2 className="h-4 w-4" />
</Button>
}
title="Delete Role"
description={`Delete "${original.name}"? Users assigned to this role must be reassigned first.`}
confirmLabel="Delete"
onConfirm={() => deleteMutation.mutate(original.id)}
loading={deleteMutation.isPending && deleteMutation.variables === original.id}
/>
) : null}
</div>
</div>
</div>
)}
emptyState={
<div className="text-center py-8">
<p className="text-muted-foreground">No roles defined.</p>