Files
pn-new-crm/src/components/companies/company-owned-yachts-tab.tsx
Matt c8ea9ec0a0 fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:

- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/

The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.

Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.

Test suite stays at 1315/1315 vitest. typescript clean.

Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00

157 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { Loader2 } from 'lucide-react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { EmptyState } from '@/components/shared/empty-state';
import { apiFetch } from '@/lib/api/client';
interface OwnedYachtRow {
id: string;
name: string;
hullNumber: string | null;
lengthFt: string | null;
widthFt: string | null;
lengthM: string | null;
widthM: string | null;
status: string;
}
interface YachtListResponse {
data: OwnedYachtRow[];
}
interface CompanyOwnedYachtsTabProps {
companyId: string;
portSlug: string;
}
const STATUS_COLORS: Record<string, string> = {
active: 'bg-green-100 text-green-800 border-green-300',
retired: 'bg-gray-100 text-gray-800 border-gray-300',
sold_away: 'bg-amber-100 text-amber-800 border-amber-300',
};
const STATUS_LABELS: Record<string, string> = {
active: 'Active',
retired: 'Retired',
sold_away: 'Sold Away',
};
function formatDimensions(y: OwnedYachtRow): string | null {
if (y.lengthFt || y.widthFt) {
const length = y.lengthFt ?? '-';
const width = y.widthFt ?? '-';
return `${length} × ${width} ft`;
}
if (y.lengthM || y.widthM) {
const length = y.lengthM ?? '-';
const width = y.widthM ?? '-';
return `${length} × ${width} m`;
}
return null;
}
export function CompanyOwnedYachtsTab({ companyId, portSlug }: CompanyOwnedYachtsTabProps) {
const { data, isLoading } = useQuery<OwnedYachtRow[]>({
queryKey: ['companies', companyId, 'owned-yachts'],
queryFn: async () => {
const params = new URLSearchParams({
ownerType: 'company',
ownerId: companyId,
page: '1',
limit: '50',
includeArchived: 'false',
order: 'desc',
});
const res = await apiFetch<YachtListResponse>(`/api/v1/yachts?${params.toString()}`);
return res.data;
},
});
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" aria-hidden />
</div>
);
}
const yachts = data ?? [];
if (yachts.length === 0) {
return (
<EmptyState
title="No yachts owned"
description="Yachts owned by this company will appear here."
/>
);
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Dimensions</TableHead>
<TableHead>Hull Number</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{yachts.map((y) => {
const dims = formatDimensions(y);
const statusLabel = STATUS_LABELS[y.status] ?? y.status;
const statusColor =
STATUS_COLORS[y.status] ?? 'bg-muted text-muted-foreground border-muted';
return (
<TableRow key={y.id}>
<TableCell>
<Link
// eslint-disable-next-line @typescript-eslint/no-explicit-any
href={`/${portSlug}/yachts/${y.id}` as any}
className="font-medium text-primary hover:underline"
>
{y.name}
</Link>
</TableCell>
<TableCell>
{dims ? (
<span className="text-sm">{dims}</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
{y.hullNumber ? (
<span className="text-sm">{y.hullNumber}</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
<span
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${statusColor}`}
>
{statusLabel}
</span>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}