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>
This commit is contained in:
@@ -377,7 +377,10 @@ export function AdminSectionsBrowser({ portSlug }: AdminSectionsBrowserProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="relative max-w-md">
|
<div className="relative max-w-md">
|
||||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
<Search
|
||||||
|
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
inputMode="search"
|
inputMode="search"
|
||||||
@@ -466,7 +469,10 @@ function SectionCard({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CardHeader className="flex flex-row items-start gap-3 space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-start gap-3 space-y-0 pb-2">
|
||||||
<Icon className="h-5 w-5 mt-0.5 text-muted-foreground group-hover:text-primary" />
|
<Icon
|
||||||
|
className="h-5 w-5 mt-0.5 text-muted-foreground group-hover:text-primary"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CardTitle className="text-base">{section.label}</CardTitle>
|
<CardTitle className="text-base">{section.label}</CardTitle>
|
||||||
{groupTitle ? (
|
{groupTitle ? (
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ function AiBudgetCardBody({
|
|||||||
<CardTitle>AI cost guardrails</CardTitle>
|
<CardTitle>AI cost guardrails</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center gap-2 text-sm text-muted-foreground">
|
<CardContent className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading…
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden /> Loading…
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -198,7 +198,9 @@ function AiBudgetCardBody({
|
|||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={() => save.mutate()} disabled={save.isPending}>
|
<Button onClick={() => save.mutate()} disabled={save.isPending}>
|
||||||
{save.isPending ? <Loader2 className="mr-1.5 h-3 w-3 animate-spin" /> : null}
|
{save.isPending ? (
|
||||||
|
<Loader2 className="mr-1.5 h-3 w-3 animate-spin" aria-hidden />
|
||||||
|
) : null}
|
||||||
Save guardrails
|
Save guardrails
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ const ACTION_BADGE_COLORS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function ActionIcon({ action }: { action: string }) {
|
function ActionIcon({ action }: { action: string }) {
|
||||||
if (action === 'create') return <Plus className="h-5 w-5" />;
|
if (action === 'create') return <Plus className="h-5 w-5" aria-hidden />;
|
||||||
if (action === 'update') return <Pencil className="h-5 w-5" />;
|
if (action === 'update') return <Pencil className="h-5 w-5" aria-hidden />;
|
||||||
if (action === 'delete') return <Trash2 className="h-5 w-5" />;
|
if (action === 'delete') return <Trash2 className="h-5 w-5" aria-hidden />;
|
||||||
if (action === 'viewed') return <Eye className="h-5 w-5" />;
|
if (action === 'viewed') return <Eye className="h-5 w-5" aria-hidden />;
|
||||||
return <Activity className="h-5 w-5" />;
|
return <Activity className="h-5 w-5" aria-hidden />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionVerb(action: string): string {
|
function actionVerb(action: string): string {
|
||||||
@@ -114,7 +114,7 @@ export function AuditLogCard({ entry }: AuditLogCardProps) {
|
|||||||
|
|
||||||
{/* Timestamp + IP meta line */}
|
{/* Timestamp + IP meta line */}
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
||||||
<ListCardMeta icon={<Clock className="h-3 w-3" />}>
|
<ListCardMeta icon={<Clock className="h-3 w-3" aria-hidden />}>
|
||||||
{formatDistanceToNow(new Date(entry.createdAt), { addSuffix: true })}
|
{formatDistanceToNow(new Date(entry.createdAt), { addSuffix: true })}
|
||||||
</ListCardMeta>
|
</ListCardMeta>
|
||||||
{entry.ipAddress ? (
|
{entry.ipAddress ? (
|
||||||
|
|||||||
@@ -351,7 +351,10 @@ export function AuditLogList() {
|
|||||||
Search
|
Search
|
||||||
</Label>
|
</Label>
|
||||||
<div className="relative w-72">
|
<div className="relative w-72">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search
|
||||||
|
className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
id="audit-search"
|
id="audit-search"
|
||||||
className="pl-9 h-9"
|
className="pl-9 h-9"
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export function BackupAdminPanel() {
|
|||||||
<CardHeader className="flex flex-row items-start justify-between gap-3 space-y-0">
|
<CardHeader className="flex flex-row items-start justify-between gap-3 space-y-0">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base flex items-center gap-2">
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
<Database className="h-4 w-4" />
|
<Database className="h-4 w-4" aria-hidden />
|
||||||
Run a backup now
|
Run a backup now
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -112,11 +112,11 @@ export function BackupAdminPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="ghost" size="sm" onClick={load} disabled={loading || running}>
|
<Button variant="ghost" size="sm" onClick={load} disabled={loading || running}>
|
||||||
<RefreshCw className="mr-1.5 h-3.5 w-3.5" />
|
<RefreshCw className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setConfirmOpen(true)} disabled={running}>
|
<Button onClick={() => setConfirmOpen(true)} disabled={running}>
|
||||||
{running && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
|
{running && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" aria-hidden />}
|
||||||
{running ? 'Running…' : 'Run backup'}
|
{running ? 'Running…' : 'Run backup'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,7 +136,7 @@ export function BackupAdminPanel() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading…
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden /> Loading…
|
||||||
</div>
|
</div>
|
||||||
) : jobs.length === 0 ? (
|
) : jobs.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">No backups yet.</p>
|
<p className="text-sm text-muted-foreground">No backups yet.</p>
|
||||||
@@ -168,7 +168,7 @@ export function BackupAdminPanel() {
|
|||||||
</span>
|
</span>
|
||||||
{j.status === 'completed' && (
|
{j.status === 'completed' && (
|
||||||
<Button size="sm" variant="outline" onClick={() => download(j.id)}>
|
<Button size="sm" variant="outline" onClick={() => download(j.id)}>
|
||||||
<Download className="mr-1.5 h-3.5 w-3.5" />
|
<Download className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -184,7 +184,7 @@ export function BackupAdminPanel() {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<AlertTriangle className="h-5 w-5 text-amber-600" />
|
<AlertTriangle className="h-5 w-5 text-amber-600" aria-hidden />
|
||||||
Run a fresh backup now?
|
Run a fresh backup now?
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export function PdfLogoUploader() {
|
|||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading current logo…
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden /> Loading current logo…
|
||||||
</div>
|
</div>
|
||||||
) : current ? (
|
) : current ? (
|
||||||
<div className="flex items-start gap-4 rounded-md border p-4">
|
<div className="flex items-start gap-4 rounded-md border p-4">
|
||||||
@@ -232,7 +232,7 @@ export function PdfLogoUploader() {
|
|||||||
Test with sample PDF
|
Test with sample PDF
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" onClick={clear} disabled={working}>
|
<Button variant="outline" size="sm" onClick={clear} disabled={working}>
|
||||||
<Trash2 className="mr-1 h-3 w-3" /> Remove
|
<Trash2 className="mr-1 h-3 w-3" aria-hidden /> Remove
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,9 +292,9 @@ export function PdfLogoUploader() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={upload} disabled={working}>
|
<Button onClick={upload} disabled={working}>
|
||||||
{working ? (
|
{working ? (
|
||||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
<Loader2 className="mr-1 h-3 w-3 animate-spin" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<Upload className="mr-1 h-3 w-3" />
|
<Upload className="mr-1 h-3 w-3" aria-hidden />
|
||||||
)}
|
)}
|
||||||
Save logo
|
Save logo
|
||||||
</Button>
|
</Button>
|
||||||
@@ -316,7 +316,7 @@ export function PdfLogoUploader() {
|
|||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={working}
|
disabled={working}
|
||||||
>
|
>
|
||||||
<RefreshCw className="mr-1 h-3 w-3" /> Choose a different file
|
<RefreshCw className="mr-1 h-3 w-3" aria-hidden /> Choose a different file
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,13 +69,13 @@ export function BrochuresAdminPanel() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button onClick={() => setCreateOpen(true)}>
|
<Button onClick={() => setCreateOpen(true)}>
|
||||||
<Plus className="mr-2 h-4 w-4" /> New brochure
|
<Plus className="mr-2 h-4 w-4" aria-hidden /> New brochure
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{brochuresQuery.isLoading && (
|
{brochuresQuery.isLoading && (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading…
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden /> Loading…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -179,10 +179,10 @@ function BrochureCard({ brochure, onChange }: { brochure: BrochureRow; onChange:
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center justify-between text-base">
|
<CardTitle className="flex items-center justify-between text-base">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<FileText className="h-4 w-4" /> {brochure.label}
|
<FileText className="h-4 w-4" aria-hidden /> {brochure.label}
|
||||||
{brochure.isDefault && (
|
{brochure.isDefault && (
|
||||||
<span className="flex items-center gap-1 rounded bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
<span className="flex items-center gap-1 rounded bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||||
<Star className="h-3 w-3" /> default
|
<Star className="h-3 w-3" aria-hidden /> default
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{brochure.archivedAt && (
|
{brochure.archivedAt && (
|
||||||
@@ -222,9 +222,9 @@ function BrochureCard({ brochure, onChange }: { brochure: BrochureRow; onChange:
|
|||||||
<Button asChild variant="outline" size="sm" disabled={uploading}>
|
<Button asChild variant="outline" size="sm" disabled={uploading}>
|
||||||
<span>
|
<span>
|
||||||
{uploading ? (
|
{uploading ? (
|
||||||
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
|
<Loader2 className="mr-2 h-3 w-3 animate-spin" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<Upload className="mr-2 h-3 w-3" />
|
<Upload className="mr-2 h-3 w-3" aria-hidden />
|
||||||
)}
|
)}
|
||||||
Upload version
|
Upload version
|
||||||
</span>
|
</span>
|
||||||
@@ -237,7 +237,7 @@ function BrochureCard({ brochure, onChange }: { brochure: BrochureRow; onChange:
|
|||||||
onClick={() => setDefaultMutation.mutate()}
|
onClick={() => setDefaultMutation.mutate()}
|
||||||
disabled={setDefaultMutation.isPending}
|
disabled={setDefaultMutation.isPending}
|
||||||
>
|
>
|
||||||
<Star className="mr-2 h-3 w-3" /> Mark default
|
<Star className="mr-2 h-3 w-3" aria-hidden /> Mark default
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -246,7 +246,7 @@ function BrochureCard({ brochure, onChange }: { brochure: BrochureRow; onChange:
|
|||||||
onClick={() => archiveMutation.mutate()}
|
onClick={() => archiveMutation.mutate()}
|
||||||
disabled={archiveMutation.isPending}
|
disabled={archiveMutation.isPending}
|
||||||
>
|
>
|
||||||
<Archive className="mr-2 h-3 w-3" /> Archive
|
<Archive className="mr-2 h-3 w-3" aria-hidden /> Archive
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -331,7 +331,9 @@ function CreateBrochureDialog({
|
|||||||
disabled={!label.trim() || createMutation.isPending}
|
disabled={!label.trim() || createMutation.isPending}
|
||||||
onClick={() => createMutation.mutate()}
|
onClick={() => createMutation.mutate()}
|
||||||
>
|
>
|
||||||
{createMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{createMutation.isPending && (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
|
)}
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ function CustomFieldFormBody({ open, onOpenChange, field, onSuccess }: CustomFie
|
|||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={addOption}>
|
<Button type="button" variant="outline" size="sm" onClick={addOption}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{selectOptions.length > 0 && (
|
{selectOptions.length > 0 && (
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function CustomFieldsManager() {
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(row.original)}>
|
<Button variant="ghost" size="sm" onClick={() => handleEdit(row.original)}>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Edit</span>
|
<span className="sr-only">Edit</span>
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
@@ -135,7 +135,7 @@ export function CustomFieldsManager() {
|
|||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
disabled={deletingId === row.original.id}
|
disabled={deletingId === row.original.id}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Delete</span>
|
<span className="sr-only">Delete</span>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ export function CustomFieldsManager() {
|
|||||||
description="Define custom fields for clients and records"
|
description="Define custom fields for clients and records"
|
||||||
actions={
|
actions={
|
||||||
<Button onClick={handleCreate}>
|
<Button onClick={handleCreate}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
New Field
|
New Field
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -228,7 +228,7 @@ export function CustomFieldsManager() {
|
|||||||
onClick={() => handleEdit(original)}
|
onClick={() => handleEdit(original)}
|
||||||
aria-label="Edit field"
|
aria-label="Edit field"
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
trigger={
|
trigger={
|
||||||
@@ -239,7 +239,7 @@ export function CustomFieldsManager() {
|
|||||||
disabled={deletingId === original.id}
|
disabled={deletingId === original.id}
|
||||||
aria-label="Delete field"
|
aria-label="Delete field"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="Delete Custom Field"
|
title="Delete Custom Field"
|
||||||
|
|||||||
@@ -47,17 +47,17 @@ export function DocumensoTestButton() {
|
|||||||
{result &&
|
{result &&
|
||||||
(result.ok ? (
|
(result.ok ? (
|
||||||
<span className="flex items-center text-xs text-green-600">
|
<span className="flex items-center text-xs text-green-600">
|
||||||
<CheckCircle2 className="mr-1 h-3.5 w-3.5" />
|
<CheckCircle2 className="mr-1 h-3.5 w-3.5" aria-hidden />
|
||||||
HTTP {result.status}
|
HTTP {result.status}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center text-xs text-destructive">
|
<span className="flex items-center text-xs text-destructive">
|
||||||
<XCircle className="mr-1 h-3.5 w-3.5" />
|
<XCircle className="mr-1 h-3.5 w-3.5" aria-hidden />
|
||||||
{result.error ?? `HTTP ${result.status}`}
|
{result.error ?? `HTTP ${result.status}`}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
<Button variant="outline" onClick={runTest} disabled={pending}>
|
<Button variant="outline" onClick={runTest} disabled={pending}>
|
||||||
{pending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{pending && <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />}
|
||||||
Test connection
|
Test connection
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function TemplateList() {
|
|||||||
header: 'Name',
|
header: 'Name',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
||||||
<span className="font-medium">{row.original.name}</span>
|
<span className="font-medium">{row.original.name}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -147,7 +147,7 @@ export function TemplateList() {
|
|||||||
title="Version history"
|
title="Version history"
|
||||||
onClick={() => handleViewHistory(row.original)}
|
onClick={() => handleViewHistory(row.original)}
|
||||||
>
|
>
|
||||||
<History className="h-4 w-4" />
|
<History className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -155,7 +155,7 @@ export function TemplateList() {
|
|||||||
title="Edit template"
|
title="Edit template"
|
||||||
onClick={() => handleEditTemplate(row.original)}
|
onClick={() => handleEditTemplate(row.original)}
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -168,7 +168,7 @@ export function TemplateList() {
|
|||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="ghost" size="icon" title="Delete template">
|
<Button variant="ghost" size="icon" title="Delete template">
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="Delete Template"
|
title="Delete Template"
|
||||||
@@ -189,7 +189,7 @@ export function TemplateList() {
|
|||||||
description="Manage reusable document templates with TipTap content and PDF generation"
|
description="Manage reusable document templates with TipTap content and PDF generation"
|
||||||
actions={
|
actions={
|
||||||
<Button onClick={handleNewTemplate}>
|
<Button onClick={handleNewTemplate}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" aria-hidden />
|
||||||
New Template
|
New Template
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -237,7 +237,7 @@ export function TemplateList() {
|
|||||||
title="Version history"
|
title="Version history"
|
||||||
onClick={() => handleViewHistory(original)}
|
onClick={() => handleViewHistory(original)}
|
||||||
>
|
>
|
||||||
<History className="h-4 w-4" />
|
<History className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -245,12 +245,12 @@ export function TemplateList() {
|
|||||||
title="Edit template"
|
title="Edit template"
|
||||||
onClick={() => handleEditTemplate(original)}
|
onClick={() => handleEditTemplate(original)}
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="ghost" size="icon" title="Delete template">
|
<Button variant="ghost" size="icon" title="Delete template">
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="Delete Template"
|
title="Delete Template"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function TemplatePreview({ content, templateName }: TemplatePreviewProps)
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" size="sm" onClick={handlePreview}>
|
<Button variant="outline" size="sm" onClick={handlePreview}>
|
||||||
<Eye className="mr-1.5 h-3.5 w-3.5" />
|
<Eye className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Preview PDF
|
Preview PDF
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ export function TemplatePreview({ content, templateName }: TemplatePreviewProps)
|
|||||||
<DialogTitle>Preview - {templateName}</DialogTitle>
|
<DialogTitle>Preview - {templateName}</DialogTitle>
|
||||||
{pdfBase64 && (
|
{pdfBase64 && (
|
||||||
<Button variant="ghost" size="sm" onClick={handleOpenInNewTab} className="mr-6">
|
<Button variant="ghost" size="sm" onClick={handleOpenInNewTab} className="mr-6">
|
||||||
<ExternalLink className="mr-1.5 h-3.5 w-3.5" />
|
<ExternalLink className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Open in new tab
|
Open in new tab
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function TemplateVersionHistory({
|
|||||||
if (versions.length === 0) {
|
if (versions.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-2 rounded-md border border-dashed p-6 text-center">
|
<div className="flex flex-col items-center gap-2 rounded-md border border-dashed p-6 text-center">
|
||||||
<Clock className="h-6 w-6 text-muted-foreground" />
|
<Clock className="h-6 w-6 text-muted-foreground" aria-hidden />
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
No previous versions found. Versions are saved whenever you update the template content.
|
No previous versions found. Versions are saved whenever you update the template content.
|
||||||
</p>
|
</p>
|
||||||
@@ -129,7 +129,7 @@ export function TemplateVersionHistory({
|
|||||||
onClick={() => handleRollback(v.version)}
|
onClick={() => handleRollback(v.version)}
|
||||||
disabled={rollingBack === v.version}
|
disabled={rollingBack === v.version}
|
||||||
>
|
>
|
||||||
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
<RotateCcw className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
{rollingBack === v.version ? 'Restoring…' : 'Restore'}
|
{rollingBack === v.version ? 'Restoring…' : 'Restore'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ function EmailTemplatesAdminBody({
|
|||||||
onClick={() => save(row, 'save')}
|
onClick={() => save(row, 'save')}
|
||||||
disabled={savingKey === row.key || !dirty}
|
disabled={savingKey === row.key || !dirty}
|
||||||
>
|
>
|
||||||
<Save className="h-3.5 w-3.5 mr-1.5" /> Save
|
<Save className="h-3.5 w-3.5 mr-1.5" aria-hidden /> Save
|
||||||
</Button>
|
</Button>
|
||||||
{overridden ? (
|
{overridden ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -159,7 +159,7 @@ function EmailTemplatesAdminBody({
|
|||||||
onClick={() => save(row, 'reset')}
|
onClick={() => save(row, 'reset')}
|
||||||
disabled={savingKey === row.key}
|
disabled={savingKey === row.key}
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-3.5 w-3.5 mr-1.5" /> Reset to default
|
<RotateCcw className="h-3.5 w-3.5 mr-1.5" aria-hidden /> Reset to default
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{message?.key === row.key ? (
|
{message?.key === row.key ? (
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ function FormTemplateFormBody({ open, onOpenChange, template, onSaved }: Props)
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-sm font-medium">Fields</Label>
|
<Label className="text-sm font-medium">Fields</Label>
|
||||||
<Button variant="outline" size="sm" onClick={addField}>
|
<Button variant="outline" size="sm" onClick={addField}>
|
||||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
<Plus className="h-3.5 w-3.5 mr-1" aria-hidden />
|
||||||
Add field
|
Add field
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +154,7 @@ function FormTemplateFormBody({ open, onOpenChange, template, onSaved }: Props)
|
|||||||
className="text-destructive h-7 w-7"
|
className="text-destructive h-7 w-7"
|
||||||
onClick={() => removeField(i)}
|
onClick={() => removeField(i)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function FormTemplateList() {
|
|||||||
setFormOpen(true);
|
setFormOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-1.5" />
|
<Plus className="h-4 w-4 mr-1.5" aria-hidden />
|
||||||
New template
|
New template
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -95,12 +95,12 @@ export function FormTemplateList() {
|
|||||||
setFormOpen(true);
|
setFormOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="ghost" size="icon" className="text-destructive">
|
<Button variant="ghost" size="icon" className="text-destructive">
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="Delete form template"
|
title="Delete form template"
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function InvitationsManager() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setSheetOpen(true)}>
|
<Button onClick={() => setSheetOpen(true)}>
|
||||||
<Plus className="h-4 w-4 mr-1.5" />
|
<Plus className="h-4 w-4 mr-1.5" aria-hidden />
|
||||||
Send invite
|
Send invite
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +98,7 @@ export function InvitationsManager() {
|
|||||||
<p className="text-sm text-muted-foreground">Loading…</p>
|
<p className="text-sm text-muted-foreground">Loading…</p>
|
||||||
) : invites.length === 0 ? (
|
) : invites.length === 0 ? (
|
||||||
<div className="rounded-lg border border-dashed p-8 text-center text-muted-foreground">
|
<div className="rounded-lg border border-dashed p-8 text-center text-muted-foreground">
|
||||||
<Mail className="mx-auto h-6 w-6 mb-2" />
|
<Mail className="mx-auto h-6 w-6 mb-2" aria-hidden />
|
||||||
<p className="text-sm">No invitations issued yet.</p>
|
<p className="text-sm">No invitations issued yet.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -142,7 +142,7 @@ export function InvitationsManager() {
|
|||||||
disabled={resendMutation.isPending || !!i.usedAt}
|
disabled={resendMutation.isPending || !!i.usedAt}
|
||||||
title="Resend invite"
|
title="Resend invite"
|
||||||
>
|
>
|
||||||
<RotateCw className="h-3.5 w-3.5" />
|
<RotateCw className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
{i.status === 'pending' && (
|
{i.status === 'pending' && (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
@@ -153,7 +153,7 @@ export function InvitationsManager() {
|
|||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
title="Revoke"
|
title="Revoke"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="Revoke invitation?"
|
title="Revoke invitation?"
|
||||||
@@ -230,7 +230,9 @@ export function InvitationsManager() {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={!email.trim() || createMutation.isPending}>
|
<Button type="submit" disabled={!email.trim() || createMutation.isPending}>
|
||||||
{createMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{createMutation.isPending && (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
|
)}
|
||||||
Send invite
|
Send invite
|
||||||
</Button>
|
</Button>
|
||||||
</SheetFooter>
|
</SheetFooter>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ function SettingsBlockBody({
|
|||||||
<CardTitle>{title}</CardTitle>
|
<CardTitle>{title}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex items-center gap-2 text-sm text-muted-foreground">
|
<CardContent className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading…
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden /> Loading…
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -250,7 +250,11 @@ function SettingsBlockBody({
|
|||||||
onClick={() => setShowKey((v) => !v)}
|
onClick={() => setShowKey((v) => !v)}
|
||||||
aria-label={showKey ? 'Hide key' : 'Show key'}
|
aria-label={showKey ? 'Hide key' : 'Show key'}
|
||||||
>
|
>
|
||||||
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
{showKey ? (
|
||||||
|
<EyeOff className="h-4 w-4" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" aria-hidden />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@@ -264,7 +268,9 @@ function SettingsBlockBody({
|
|||||||
disabled={save.isPending}
|
disabled={save.isPending}
|
||||||
data-testid={`save-${scope}`}
|
data-testid={`save-${scope}`}
|
||||||
>
|
>
|
||||||
{save.isPending ? <Loader2 className="mr-1.5 h-3 w-3 animate-spin" /> : null}
|
{save.isPending ? (
|
||||||
|
<Loader2 className="mr-1.5 h-3 w-3 animate-spin" aria-hidden />
|
||||||
|
) : null}
|
||||||
Save settings
|
Save settings
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -273,7 +279,9 @@ function SettingsBlockBody({
|
|||||||
onClick={() => test.mutate()}
|
onClick={() => test.mutate()}
|
||||||
disabled={test.isPending || apiKey.length === 0}
|
disabled={test.isPending || apiKey.length === 0}
|
||||||
>
|
>
|
||||||
{test.isPending ? <Loader2 className="mr-1.5 h-3 w-3 animate-spin" /> : null}
|
{test.isPending ? (
|
||||||
|
<Loader2 className="mr-1.5 h-3 w-3 animate-spin" aria-hidden />
|
||||||
|
) : null}
|
||||||
Test connection
|
Test connection
|
||||||
</Button>
|
</Button>
|
||||||
{hasKey ? (
|
{hasKey ? (
|
||||||
@@ -290,13 +298,13 @@ function SettingsBlockBody({
|
|||||||
|
|
||||||
{testStatus?.ok ? (
|
{testStatus?.ok ? (
|
||||||
<span className="inline-flex items-center gap-1 text-sm text-green-700">
|
<span className="inline-flex items-center gap-1 text-sm text-green-700">
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
<CheckCircle2 className="h-4 w-4" aria-hidden />
|
||||||
Connection OK
|
Connection OK
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{testStatus && !testStatus.ok ? (
|
{testStatus && !testStatus.ok ? (
|
||||||
<span className="inline-flex items-center gap-1 text-sm text-destructive">
|
<span className="inline-flex items-center gap-1 text-sm text-destructive">
|
||||||
<XCircle className="h-4 w-4" />
|
<XCircle className="h-4 w-4" aria-hidden />
|
||||||
{testStatus.reason}
|
{testStatus.reason}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -228,11 +228,11 @@ export function OnboardingChecklist() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{done ? (
|
{done ? (
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" aria-hidden />
|
||||||
) : loading ? (
|
) : loading ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<Circle className="h-4 w-4" />
|
<Circle className="h-4 w-4" aria-hidden />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -243,7 +243,7 @@ export function OnboardingChecklist() {
|
|||||||
className="text-sm font-medium hover:underline inline-flex items-center gap-1"
|
className="text-sm font-medium hover:underline inline-flex items-center gap-1"
|
||||||
>
|
>
|
||||||
{idx + 1}. {step.label}
|
{idx + 1}. {step.label}
|
||||||
<ExternalLink className="h-3 w-3 opacity-50" />
|
<ExternalLink className="h-3 w-3 opacity-50" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">{step.description}</p>
|
<p className="mt-0.5 text-xs text-muted-foreground">{step.description}</p>
|
||||||
{auto && (
|
{auto && (
|
||||||
@@ -265,7 +265,7 @@ export function OnboardingChecklist() {
|
|||||||
onClick={() => toggleManual(step.id)}
|
onClick={() => toggleManual(step.id)}
|
||||||
>
|
>
|
||||||
{saving === step.id ? (
|
{saving === step.id ? (
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||||
) : manual ? (
|
) : manual ? (
|
||||||
'Mark incomplete'
|
'Mark incomplete'
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function PortList() {
|
|||||||
header: '',
|
header: '',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleEditPort(row.original)}>
|
<Button variant="ghost" size="sm" onClick={() => handleEditPort(row.original)}>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Edit</span>
|
<span className="sr-only">Edit</span>
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
@@ -111,7 +111,7 @@ export function PortList() {
|
|||||||
description="Manage marina ports and their configuration"
|
description="Manage marina ports and their configuration"
|
||||||
actions={
|
actions={
|
||||||
<Button onClick={handleNewPort}>
|
<Button onClick={handleNewPort}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
New Port
|
New Port
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@ export function PortList() {
|
|||||||
aria-label={`Edit ${original.name}`}
|
aria-label={`Edit ${original.name}`}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export function QueueDetailTable({ queueName }: QueueDetailTableProps) {
|
|||||||
disabled={retryMutation.isPending}
|
disabled={retryMutation.isPending}
|
||||||
onClick={() => retryMutation.mutate(job.id)}
|
onClick={() => retryMutation.mutate(job.id)}
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-3.5 w-3.5" />
|
<RotateCcw className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -170,7 +170,7 @@ export function QueueDetailTable({ queueName }: QueueDetailTableProps) {
|
|||||||
disabled={deleteMutation.isPending}
|
disabled={deleteMutation.isPending}
|
||||||
onClick={() => deleteMutation.mutate(job.id)}
|
onClick={() => deleteMutation.mutate(job.id)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export function ResidentialStagesAdmin() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading stages…
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden /> Loading stages…
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ export function ResidentialStagesAdmin() {
|
|||||||
key={`${stage.id}-${idx}`}
|
key={`${stage.id}-${idx}`}
|
||||||
className="flex items-center gap-3 rounded-md border p-3"
|
className="flex items-center gap-3 rounded-md border p-3"
|
||||||
>
|
>
|
||||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
<GripVertical className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||||||
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
<Label className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
||||||
@@ -223,13 +223,13 @@ export function ResidentialStagesAdmin() {
|
|||||||
onClick={() => remove(idx)}
|
onClick={() => remove(idx)}
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Button type="button" variant="outline" size="sm" onClick={add}>
|
<Button type="button" variant="outline" size="sm" onClick={add}>
|
||||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
<Plus className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Add stage
|
Add stage
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -245,9 +245,9 @@ export function ResidentialStagesAdmin() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button onClick={() => attemptSave(false)} disabled={saving}>
|
<Button onClick={() => attemptSave(false)} disabled={saving}>
|
||||||
{saving ? (
|
{saving ? (
|
||||||
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<Save className="mr-1.5 h-4 w-4" />
|
<Save className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
)}
|
)}
|
||||||
Save stages
|
Save stages
|
||||||
</Button>
|
</Button>
|
||||||
@@ -318,7 +318,7 @@ export function ResidentialStagesAdmin() {
|
|||||||
}}
|
}}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
>
|
>
|
||||||
{saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
|
{saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" aria-hidden />}
|
||||||
Reassign + save
|
Reassign + save
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function RoleList() {
|
|||||||
<span className="font-medium">{formatRole(row.original.name)}</span>
|
<span className="font-medium">{formatRole(row.original.name)}</span>
|
||||||
{row.original.isSystem && (
|
{row.original.isSystem && (
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
<Lock className="mr-1 h-3 w-3" />
|
<Lock className="mr-1 h-3 w-3" aria-hidden />
|
||||||
System
|
System
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -126,7 +126,7 @@ export function RoleList() {
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleEditRole(row.original)}>
|
<Button variant="ghost" size="sm" onClick={() => handleEditRole(row.original)}>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Edit</span>
|
<span className="sr-only">Edit</span>
|
||||||
</Button>
|
</Button>
|
||||||
{!row.original.isSystem && (
|
{!row.original.isSystem && (
|
||||||
@@ -137,7 +137,7 @@ export function RoleList() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Delete</span>
|
<span className="sr-only">Delete</span>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ export function RoleList() {
|
|||||||
description="Manage roles and their permissions"
|
description="Manage roles and their permissions"
|
||||||
actions={
|
actions={
|
||||||
<Button onClick={handleNewRole}>
|
<Button onClick={handleNewRole}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
New Role
|
New Role
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -209,7 +209,7 @@ export function RoleList() {
|
|||||||
onClick={() => handleEditRole(original)}
|
onClick={() => handleEditRole(original)}
|
||||||
aria-label="Edit role"
|
aria-label="Edit role"
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
{!original.isSystem ? (
|
{!original.isSystem ? (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
@@ -220,7 +220,7 @@ export function RoleList() {
|
|||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
aria-label="Delete role"
|
aria-label="Delete role"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="Delete Role"
|
title="Delete Role"
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export function SalesEmailConfigCard() {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex items-center gap-2 py-6 text-sm text-muted-foreground">
|
<CardContent className="flex items-center gap-2 py-6 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading sales email config…
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden /> Loading sales email config…
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -387,7 +387,7 @@ export function SalesEmailConfigCard() {
|
|||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button onClick={handleSave} disabled={saving}>
|
<Button onClick={handleSave} disabled={saving}>
|
||||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />}
|
||||||
Save sales email settings
|
Save sales email settings
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ export function SettingsManager() {
|
|||||||
saveSetting(setting.key, values[setting.key] ?? setting.defaultValue)
|
saveSetting(setting.key, values[setting.key] ?? setting.defaultValue)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Save className="h-3.5 w-3.5" />
|
<Save className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -454,7 +454,7 @@ export function SettingsManager() {
|
|||||||
saveSetting(setting.key, values[setting.key] ?? setting.defaultValue)
|
saveSetting(setting.key, values[setting.key] ?? setting.defaultValue)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Save className="h-3.5 w-3.5" />
|
<Save className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -542,7 +542,7 @@ export function SettingsManager() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="Delete Setting"
|
title="Delete Setting"
|
||||||
@@ -569,7 +569,7 @@ export function SettingsManager() {
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button variant="outline" onClick={handleAddCustom} disabled={!customKey.trim()}>
|
<Button variant="outline" onClick={handleAddCustom} disabled={!customKey.trim()}>
|
||||||
<Plus className="mr-1 h-4 w-4" />
|
<Plus className="mr-1 h-4 w-4" aria-hidden />
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export function SettingsFormCard({ title, description, fields, extra }: Settings
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center text-sm text-muted-foreground">
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
Loading…
|
Loading…
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -176,7 +176,7 @@ export function SettingsFormCard({ title, description, fields, extra }: Settings
|
|||||||
<div className="flex items-center justify-end gap-2 pt-2 border-t">
|
<div className="flex items-center justify-end gap-2 pt-2 border-t">
|
||||||
{extra}
|
{extra}
|
||||||
<Button onClick={handleSave} disabled={saving || !dirty}>
|
<Button onClick={handleSave} disabled={saving || !dirty}>
|
||||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />}
|
||||||
Save changes
|
Save changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export function StorageAdminPanel() {
|
|||||||
if (status.isLoading) {
|
if (status.isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading storage status…
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden /> Loading storage status…
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -216,18 +216,22 @@ export function StorageAdminPanel() {
|
|||||||
onClick={() => testMutation.mutate()}
|
onClick={() => testMutation.mutate()}
|
||||||
disabled={testMutation.isPending}
|
disabled={testMutation.isPending}
|
||||||
>
|
>
|
||||||
{testMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{testMutation.isPending && (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
|
)}
|
||||||
Test S3 connection
|
Test S3 connection
|
||||||
</Button>
|
</Button>
|
||||||
{testResult && (
|
{testResult && (
|
||||||
<div className="rounded-md border p-3 text-sm">
|
<div className="rounded-md border p-3 text-sm">
|
||||||
{testResult.ok ? (
|
{testResult.ok ? (
|
||||||
<div className="flex items-center gap-2 text-emerald-600">
|
<div className="flex items-center gap-2 text-emerald-600">
|
||||||
<CheckCircle2 className="h-4 w-4" /> Connection OK — round-trip succeeded.
|
<CheckCircle2 className="h-4 w-4" aria-hidden /> Connection OK — round-trip
|
||||||
|
succeeded.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2 text-destructive">
|
<div className="flex items-center gap-2 text-destructive">
|
||||||
<XCircle className="h-4 w-4" /> {testResult.error ?? 'Connection failed'}
|
<XCircle className="h-4 w-4" aria-hidden />{' '}
|
||||||
|
{testResult.error ?? 'Connection failed'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -248,9 +252,9 @@ export function StorageAdminPanel() {
|
|||||||
<Card className="lg:col-span-2">
|
<Card className="lg:col-span-2">
|
||||||
<CardHeader className="flex flex-row items-start gap-3 space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-start gap-3 space-y-0 pb-2">
|
||||||
{s.backend === 's3' ? (
|
{s.backend === 's3' ? (
|
||||||
<ServerCog className="mt-0.5 h-5 w-5 text-muted-foreground" />
|
<ServerCog className="mt-0.5 h-5 w-5 text-muted-foreground" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<HardDrive className="mt-0.5 h-5 w-5 text-muted-foreground" />
|
<HardDrive className="mt-0.5 h-5 w-5 text-muted-foreground" aria-hidden />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base">Active backend: {s.backend}</CardTitle>
|
<CardTitle className="text-base">Active backend: {s.backend}</CardTitle>
|
||||||
@@ -283,7 +287,9 @@ export function StorageAdminPanel() {
|
|||||||
disabled={dryRunMutation.isPending}
|
disabled={dryRunMutation.isPending}
|
||||||
onClick={() => openConfirm('switch-and-migrate')}
|
onClick={() => openConfirm('switch-and-migrate')}
|
||||||
>
|
>
|
||||||
{dryRunMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{dryRunMutation.isPending && (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
|
)}
|
||||||
Switch + migrate existing files to {otherBackend}
|
Switch + migrate existing files to {otherBackend}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={() => openConfirm('switch-only')}>
|
<Button variant="outline" onClick={() => openConfirm('switch-only')}>
|
||||||
@@ -294,7 +300,7 @@ export function StorageAdminPanel() {
|
|||||||
onClick={() => status.refetch()}
|
onClick={() => status.refetch()}
|
||||||
disabled={status.isFetching}
|
disabled={status.isFetching}
|
||||||
>
|
>
|
||||||
<RefreshCw className="mr-2 h-4 w-4" /> Refresh
|
<RefreshCw className="mr-2 h-4 w-4" aria-hidden /> Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@@ -337,7 +343,7 @@ export function StorageAdminPanel() {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<AlertTriangle className="h-5 w-5 text-amber-600" />
|
<AlertTriangle className="h-5 w-5 text-amber-600" aria-hidden />
|
||||||
{confirmMode === 'switch-and-migrate'
|
{confirmMode === 'switch-and-migrate'
|
||||||
? `Switch + migrate to ${otherBackend}?`
|
? `Switch + migrate to ${otherBackend}?`
|
||||||
: `Switch active backend to ${otherBackend}?`}
|
: `Switch active backend to ${otherBackend}?`}
|
||||||
@@ -382,7 +388,9 @@ export function StorageAdminPanel() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{migrateMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{migrateMutation.isPending && (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
|
)}
|
||||||
{confirmMode === 'switch-and-migrate' ? 'Migrate now' : 'Switch now'}
|
{confirmMode === 'switch-and-migrate' ? 'Migrate now' : 'Switch now'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ export function SystemMonitoringDashboard() {
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
{health.overall === 'healthy' ? (
|
{health.overall === 'healthy' ? (
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 text-green-500" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
<AlertTriangle className="h-4 w-4 text-yellow-500" aria-hidden />
|
||||||
)}
|
)}
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
All services checked at {new Date(health.checkedAt).toLocaleTimeString()}
|
All services checked at {new Date(health.checkedAt).toLocaleTimeString()}
|
||||||
@@ -88,7 +88,7 @@ export function SystemMonitoringDashboard() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-xs text-muted-foreground font-medium uppercase tracking-wide flex items-center gap-1.5">
|
<CardTitle className="text-xs text-muted-foreground font-medium uppercase tracking-wide flex items-center gap-1.5">
|
||||||
<Wifi className="h-3.5 w-3.5" />
|
<Wifi className="h-3.5 w-3.5" aria-hidden />
|
||||||
Active Connections
|
Active Connections
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -100,7 +100,7 @@ export function SystemMonitoringDashboard() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-xs text-muted-foreground font-medium uppercase tracking-wide flex items-center gap-1.5">
|
<CardTitle className="text-xs text-muted-foreground font-medium uppercase tracking-wide flex items-center gap-1.5">
|
||||||
<AlertTriangle className="h-3.5 w-3.5" />
|
<AlertTriangle className="h-3.5 w-3.5" aria-hidden />
|
||||||
Total Failed Jobs
|
Total Failed Jobs
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -114,7 +114,7 @@ export function SystemMonitoringDashboard() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-xs text-muted-foreground font-medium uppercase tracking-wide flex items-center gap-1.5">
|
<CardTitle className="text-xs text-muted-foreground font-medium uppercase tracking-wide flex items-center gap-1.5">
|
||||||
<Activity className="h-3.5 w-3.5" />
|
<Activity className="h-3.5 w-3.5" aria-hidden />
|
||||||
Active Jobs
|
Active Jobs
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -87,13 +87,13 @@ export function TagList() {
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleEditTag(row.original)}>
|
<Button variant="ghost" size="sm" onClick={() => handleEditTag(row.original)}>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Edit</span>
|
<span className="sr-only">Edit</span>
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive">
|
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive">
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Delete</span>
|
<span className="sr-only">Delete</span>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ export function TagList() {
|
|||||||
description="Manage tags used across clients and records"
|
description="Manage tags used across clients and records"
|
||||||
actions={
|
actions={
|
||||||
<Button onClick={handleNewTag}>
|
<Button onClick={handleNewTag}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
New Tag
|
New Tag
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ export function TagList() {
|
|||||||
onClick={() => handleEditTag(original)}
|
onClick={() => handleEditTag(original)}
|
||||||
aria-label={`Edit ${original.name}`}
|
aria-label={`Edit ${original.name}`}
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
trigger={
|
trigger={
|
||||||
@@ -162,7 +162,7 @@ export function TagList() {
|
|||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
aria-label={`Delete ${original.name}`}
|
aria-label={`Delete ${original.name}`}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="Delete Tag"
|
title="Delete Tag"
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function UserCard({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={`Actions for ${user.displayName}`}
|
aria-label={`Actions for ${user.displayName}`}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
@@ -83,7 +83,7 @@ export function UserCard({
|
|||||||
onEdit(user);
|
onEdit(user);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
@@ -91,12 +91,12 @@ export function UserCard({
|
|||||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()} disabled={isToggling}>
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()} disabled={isToggling}>
|
||||||
{user.isActive ? (
|
{user.isActive ? (
|
||||||
<>
|
<>
|
||||||
<PowerOff className="mr-2 h-3.5 w-3.5" />
|
<PowerOff className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Disable sign-in
|
Disable sign-in
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Power className="mr-2 h-3.5 w-3.5 text-emerald-600" />
|
<Power className="mr-2 h-3.5 w-3.5 text-emerald-600" aria-hidden />
|
||||||
Enable sign-in
|
Enable sign-in
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -115,7 +115,7 @@ export function UserCard({
|
|||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
trigger={
|
trigger={
|
||||||
<DropdownMenuItem className="text-destructive" onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuItem className="text-destructive" onSelect={(e) => e.preventDefault()}>
|
||||||
<Trash2 className="mr-2 h-3.5 w-3.5" />
|
<Trash2 className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Remove
|
Remove
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
}
|
}
|
||||||
@@ -155,16 +155,18 @@ export function UserCard({
|
|||||||
|
|
||||||
{/* Role + last login meta */}
|
{/* Role + last login meta */}
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
||||||
<ListCardMeta icon={<Shield className="h-3 w-3" />}>
|
<ListCardMeta icon={<Shield className="h-3 w-3" aria-hidden />}>
|
||||||
{formatRole(user.role.name)}
|
{formatRole(user.role.name)}
|
||||||
</ListCardMeta>
|
</ListCardMeta>
|
||||||
|
|
||||||
{user.lastLoginAt ? (
|
{user.lastLoginAt ? (
|
||||||
<ListCardMeta icon={<Clock className="h-3 w-3" />}>
|
<ListCardMeta icon={<Clock className="h-3 w-3" aria-hidden />}>
|
||||||
{formatDistanceToNow(new Date(user.lastLoginAt), { addSuffix: true })}
|
{formatDistanceToNow(new Date(user.lastLoginAt), { addSuffix: true })}
|
||||||
</ListCardMeta>
|
</ListCardMeta>
|
||||||
) : (
|
) : (
|
||||||
<ListCardMeta icon={<Clock className="h-3 w-3" />}>Never logged in</ListCardMeta>
|
<ListCardMeta icon={<Clock className="h-3 w-3" aria-hidden />}>
|
||||||
|
Never logged in
|
||||||
|
</ListCardMeta>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function UserList() {
|
|||||||
onClick={() => handleEditUser(row.original)}
|
onClick={() => handleEditUser(row.original)}
|
||||||
title="Edit user"
|
title="Edit user"
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Edit</span>
|
<span className="sr-only">Edit</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
@@ -149,9 +149,9 @@ export function UserList() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{row.original.isActive ? (
|
{row.original.isActive ? (
|
||||||
<PowerOff className="h-4 w-4" />
|
<PowerOff className="h-4 w-4" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<Power className="h-4 w-4" />
|
<Power className="h-4 w-4" aria-hidden />
|
||||||
)}
|
)}
|
||||||
<span className="sr-only">{row.original.isActive ? 'Disable' : 'Enable'}</span>
|
<span className="sr-only">{row.original.isActive ? 'Disable' : 'Enable'}</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -176,7 +176,7 @@ export function UserList() {
|
|||||||
title="Remove from port"
|
title="Remove from port"
|
||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Remove</span>
|
<span className="sr-only">Remove</span>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -201,7 +201,7 @@ export function UserList() {
|
|||||||
description="Manage users and their roles for this port"
|
description="Manage users and their roles for this port"
|
||||||
actions={
|
actions={
|
||||||
<Button onClick={handleNewUser}>
|
<Button onClick={handleNewUser}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
New User
|
New User
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export function VocabulariesManager() {
|
|||||||
<div>
|
<div>
|
||||||
<PageHeader title="Vocabularies" description="Per-port pick lists used across the CRM." />
|
<PageHeader title="Vocabularies" description="Per-port pick lists used across the CRM." />
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Loading…
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden /> Loading…
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -208,7 +208,7 @@ export function VocabulariesManager() {
|
|||||||
disabled={saving === def.key}
|
disabled={saving === def.key}
|
||||||
title="Restore the shipped defaults"
|
title="Restore the shipped defaults"
|
||||||
>
|
>
|
||||||
<RotateCcw className="mr-1 h-3.5 w-3.5" />
|
<RotateCcw className="mr-1 h-3.5 w-3.5" aria-hidden />
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -217,9 +217,9 @@ export function VocabulariesManager() {
|
|||||||
disabled={!v.dirty || saving === def.key}
|
disabled={!v.dirty || saving === def.key}
|
||||||
>
|
>
|
||||||
{saving === def.key ? (
|
{saving === def.key ? (
|
||||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<Save className="mr-1 h-3.5 w-3.5" />
|
<Save className="mr-1 h-3.5 w-3.5" aria-hidden />
|
||||||
)}
|
)}
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
@@ -244,7 +244,7 @@ export function VocabulariesManager() {
|
|||||||
onClick={() => moveEntry(def.key, index, -1)}
|
onClick={() => moveEntry(def.key, index, -1)}
|
||||||
disabled={index === 0}
|
disabled={index === 0}
|
||||||
>
|
>
|
||||||
<GripVertical className="h-3.5 w-3.5 rotate-90" />
|
<GripVertical className="h-3.5 w-3.5 rotate-90" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
@@ -259,7 +259,7 @@ export function VocabulariesManager() {
|
|||||||
onClick={() => removeEntry(def.key, index)}
|
onClick={() => removeEntry(def.key, index)}
|
||||||
aria-label={`Remove ${entry}`}
|
aria-label={`Remove ${entry}`}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -285,7 +285,7 @@ export function VocabulariesManager() {
|
|||||||
onClick={() => addEntry(def.key)}
|
onClick={() => addEntry(def.key)}
|
||||||
disabled={!v.newEntry.trim()}
|
disabled={!v.newEntry.trim()}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3.5 w-3.5" /> Add
|
<Plus className="mr-1 h-3.5 w-3.5" aria-hidden /> Add
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -50,17 +50,17 @@ export function UmamiTestButton() {
|
|||||||
{result &&
|
{result &&
|
||||||
(result.ok ? (
|
(result.ok ? (
|
||||||
<span className="flex items-center text-xs text-green-600">
|
<span className="flex items-center text-xs text-green-600">
|
||||||
<CheckCircle2 className="mr-1 h-3.5 w-3.5" />
|
<CheckCircle2 className="mr-1 h-3.5 w-3.5" aria-hidden />
|
||||||
Connected ({result.visitors ?? 0} active)
|
Connected ({result.visitors ?? 0} active)
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center text-xs text-destructive">
|
<span className="flex items-center text-xs text-destructive">
|
||||||
<XCircle className="mr-1 h-3.5 w-3.5" />
|
<XCircle className="mr-1 h-3.5 w-3.5" aria-hidden />
|
||||||
{result.error ?? 'Failed'}
|
{result.error ?? 'Failed'}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
<Button type="button" size="sm" variant="outline" onClick={runTest} disabled={pending}>
|
<Button type="button" size="sm" variant="outline" onClick={runTest} disabled={pending}>
|
||||||
{pending ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : null}
|
{pending ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" aria-hidden /> : null}
|
||||||
Test connection
|
Test connection
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export function BerthCard({ berth }: BerthCardProps) {
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={`Actions for berth ${berth.mooringNumber}`}
|
aria-label={`Actions for berth ${berth.mooringNumber}`}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
@@ -117,7 +117,7 @@ export function BerthCard({ berth }: BerthCardProps) {
|
|||||||
router.push(`/${portSlug}/berths/${berth.id}`);
|
router.push(`/${portSlug}/berths/${berth.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Activity className="mr-2 h-3.5 w-3.5" />
|
<Activity className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
View details
|
View details
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -126,7 +126,7 @@ export function BerthCard({ berth }: BerthCardProps) {
|
|||||||
router.push(`/${portSlug}/berths/${berth.id}?edit=true`);
|
router.push(`/${portSlug}/berths/${berth.id}?edit=true`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function ActionsCell({ row }: { row: { original: BerthRow } }) {
|
|||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -158,7 +158,7 @@ function ActionsCell({ row }: { row: { original: BerthRow } }) {
|
|||||||
router.push(`/${params.portSlug}/berths/${berth.id}`);
|
router.push(`/${params.portSlug}/berths/${berth.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Activity className="mr-2 h-4 w-4" />
|
<Activity className="mr-2 h-4 w-4" aria-hidden />
|
||||||
View details
|
View details
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -167,7 +167,7 @@ function ActionsCell({ row }: { row: { original: BerthRow } }) {
|
|||||||
router.push(`/${params.portSlug}/berths/${berth.id}?edit=true`);
|
router.push(`/${params.portSlug}/berths/${berth.id}?edit=true`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="mr-2 h-4 w-4" />
|
<Pencil className="mr-2 h-4 w-4" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function BerthDealDocumentsTab({ berthId }: { berthId: string }) {
|
|||||||
className="flex flex-wrap items-center justify-between gap-2 py-2.5 text-sm"
|
className="flex flex-wrap items-center justify-between gap-2 py-2.5 text-sm"
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
||||||
<span className="truncate font-medium">{doc.title}</span>
|
<span className="truncate font-medium">{doc.title}</span>
|
||||||
<span className="text-xs text-muted-foreground">{doc.documentType}</span>
|
<span className="text-xs text-muted-foreground">{doc.documentType}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +76,7 @@ export function BerthDealDocumentsTab({ berthId }: { berthId: string }) {
|
|||||||
href={`/${portSlug}/interests/${doc.interestId}` as any}
|
href={`/${portSlug}/interests/${doc.interestId}` as any}
|
||||||
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
||||||
>
|
>
|
||||||
Open <ExternalLink className="h-3 w-3" />
|
Open <ExternalLink className="h-3 w-3" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -290,11 +290,11 @@ export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) {
|
|||||||
<div className="flex flex-wrap items-center gap-2 sm:shrink-0">
|
<div className="flex flex-wrap items-center gap-2 sm:shrink-0">
|
||||||
<PermissionGate resource="berths" action="edit">
|
<PermissionGate resource="berths" action="edit">
|
||||||
<Button variant="outline" size="sm" onClick={() => setStatusOpen(true)}>
|
<Button variant="outline" size="sm" onClick={() => setStatusOpen(true)}>
|
||||||
<RefreshCw className="mr-1.5 h-4 w-4" />
|
<RefreshCw className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Change Status
|
Change Status
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" onClick={() => setEditOpen(true)}>
|
<Button size="sm" onClick={() => setEditOpen(true)}>
|
||||||
<Pencil className="mr-1.5 h-4 w-4" />
|
<Pencil className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
@@ -369,7 +369,7 @@ function InterestLinkPicker({
|
|||||||
) : (
|
) : (
|
||||||
'— No interest —'
|
'— No interest —'
|
||||||
)}
|
)}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-(--radix-popper-anchor-width) min-w-[320px] p-0" align="start">
|
<PopoverContent className="w-(--radix-popper-anchor-width) min-w-[320px] p-0" align="start">
|
||||||
@@ -420,7 +420,7 @@ function InterestLinkPicker({
|
|||||||
{stageLabel(opt.pipelineStage)}
|
{stageLabel(opt.pipelineStage)}
|
||||||
</span>
|
</span>
|
||||||
{value === opt.id ? (
|
{value === opt.id ? (
|
||||||
<Check className="h-3.5 w-3.5 text-muted-foreground" />
|
<Check className="h-3.5 w-3.5 text-muted-foreground" aria-hidden />
|
||||||
) : null}
|
) : null}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function BerthInterestPulse({ berthId }: { berthId: string }) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="flex items-center gap-1.5 text-sm font-medium">
|
<CardTitle className="flex items-center gap-1.5 text-sm font-medium">
|
||||||
<Users className="size-3.5" />
|
<Users className="size-3.5" aria-hidden />
|
||||||
Interested parties
|
Interested parties
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -99,7 +99,7 @@ export function BerthInterestPulse({ berthId }: { berthId: string }) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-3 space-y-0">
|
<CardHeader className="flex flex-row items-center justify-between pb-3 space-y-0">
|
||||||
<CardTitle className="flex items-center gap-1.5 text-sm font-medium">
|
<CardTitle className="flex items-center gap-1.5 text-sm font-medium">
|
||||||
<Users className="size-3.5" />
|
<Users className="size-3.5" aria-hidden />
|
||||||
Interested parties
|
Interested parties
|
||||||
<span className="ml-1 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
<span className="ml-1 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
|
||||||
{active.length}
|
{active.length}
|
||||||
@@ -161,7 +161,10 @@ export function BerthInterestPulse({ berthId }: { berthId: string }) {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5" />
|
<ChevronRight
|
||||||
|
className="size-4 shrink-0 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function BerthReservationsTab({ berthId }: BerthReservationsTabProps) {
|
|||||||
<h3 className="text-lg font-semibold">Reservations</h3>
|
<h3 className="text-lg font-semibold">Reservations</h3>
|
||||||
<PermissionGate resource="reservations" action="create">
|
<PermissionGate resource="reservations" action="create">
|
||||||
<Button size="sm" onClick={() => setReserveOpen(true)}>
|
<Button size="sm" onClick={() => setReserveOpen(true)}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Reserve this berth
|
Reserve this berth
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function BerthStatusSuggestionDialog({
|
|||||||
<Badge variant="outline" className="text-base px-4 py-1.5">
|
<Badge variant="outline" className="text-base px-4 py-1.5">
|
||||||
{currentStatus.replace(/_/g, ' ')}
|
{currentStatus.replace(/_/g, ' ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<ArrowRight className="h-5 w-5 text-muted-foreground" />
|
<ArrowRight className="h-5 w-5 text-muted-foreground" aria-hidden />
|
||||||
<Badge variant="default" className="text-base px-4 py-1.5">
|
<Badge variant="default" className="text-base px-4 py-1.5">
|
||||||
{suggestedStatus.replace(/_/g, ' ')}
|
{suggestedStatus.replace(/_/g, ' ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -73,7 +73,9 @@ export function BerthStatusSuggestionDialog({
|
|||||||
Dismiss
|
Dismiss
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => applyMutation.mutate()} disabled={applyMutation.isPending}>
|
<Button onClick={() => applyMutation.mutate()} disabled={applyMutation.isPending}>
|
||||||
{applyMutation.isPending && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
|
{applyMutation.isPending && (
|
||||||
|
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" aria-hidden />
|
||||||
|
)}
|
||||||
Apply Change
|
Apply Change
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function SortableEntry({
|
|||||||
{...listeners}
|
{...listeners}
|
||||||
className="cursor-grab active:cursor-grabbing text-muted-foreground"
|
className="cursor-grab active:cursor-grabbing text-muted-foreground"
|
||||||
>
|
>
|
||||||
<GripVertical className="h-4 w-4" />
|
<GripVertical className="h-4 w-4" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="text-sm font-mono w-6 text-center text-muted-foreground">
|
<span className="text-sm font-mono w-6 text-center text-muted-foreground">
|
||||||
@@ -88,7 +88,7 @@ function SortableEntry({
|
|||||||
onClick={() => onRemove(entry.id)}
|
onClick={() => onRemove(entry.id)}
|
||||||
className="text-muted-foreground hover:text-destructive transition-colors"
|
className="text-muted-foreground hover:text-destructive transition-colors"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -189,7 +189,7 @@ export function WaitingListManager({ berthId }: WaitingListManagerProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">Waiting List ({entries.length})</span>
|
<span className="text-sm font-medium">Waiting List ({entries.length})</span>
|
||||||
<Button size="sm" variant="outline" onClick={() => setShowAddForm((v) => !v)}>
|
<Button size="sm" variant="outline" onClick={() => setShowAddForm((v) => !v)}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,7 +217,9 @@ export function WaitingListManager({ berthId }: WaitingListManagerProps) {
|
|||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button size="sm" onClick={handleAdd} disabled={addMutation.isPending}>
|
<Button size="sm" onClick={handleAdd} disabled={addMutation.isPending}>
|
||||||
{addMutation.isPending && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
|
{addMutation.isPending && (
|
||||||
|
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" aria-hidden />
|
||||||
|
)}
|
||||||
Add to List
|
Add to List
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="ghost" onClick={() => setShowAddForm(false)}>
|
<Button size="sm" variant="ghost" onClick={() => setShowAddForm(false)}>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
|
|||||||
|
|
||||||
{preflight.isLoading ? (
|
{preflight.isLoading ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />
|
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" aria-hidden />
|
||||||
Checking each client…
|
Checking each client…
|
||||||
</div>
|
</div>
|
||||||
) : preflight.error ? (
|
) : preflight.error ? (
|
||||||
@@ -156,7 +156,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
|
|||||||
{blocked.length > 0 && (
|
{blocked.length > 0 && (
|
||||||
<div className="rounded-md border border-red-300 bg-red-50 p-3 text-xs text-red-900 space-y-1">
|
<div className="rounded-md border border-red-300 bg-red-50 p-3 text-xs text-red-900 space-y-1">
|
||||||
<div className="font-medium flex items-center gap-1.5">
|
<div className="font-medium flex items-center gap-1.5">
|
||||||
<AlertTriangle className="h-4 w-4" /> Blocked
|
<AlertTriangle className="h-4 w-4" aria-hidden /> Blocked
|
||||||
</div>
|
</div>
|
||||||
{blocked.slice(0, 5).map((b) => (
|
{blocked.slice(0, 5).map((b) => (
|
||||||
<div key={b.clientId}>
|
<div key={b.clientId}>
|
||||||
@@ -236,7 +236,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
|
|||||||
{stage === 'confirm' && (
|
{stage === 'confirm' && (
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="rounded-md border border-emerald-300 bg-emerald-50 p-3 text-emerald-900 flex items-start gap-2">
|
<div className="rounded-md border border-emerald-300 bg-emerald-50 p-3 text-emerald-900 flex items-start gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 mt-0.5" />
|
<CheckCircle2 className="h-4 w-4 mt-0.5" aria-hidden />
|
||||||
<div>
|
<div>
|
||||||
Ready to archive <strong>{archivable.length}</strong> client
|
Ready to archive <strong>{archivable.length}</strong> client
|
||||||
{archivable.length === 1 ? '' : 's'}
|
{archivable.length === 1 ? '' : 's'}
|
||||||
@@ -268,7 +268,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Continue <ArrowRight className="h-4 w-4 ml-1" />
|
Continue <ArrowRight className="h-4 w-4 ml-1" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -279,18 +279,18 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
|
|||||||
disabled={carouselIndex === 0}
|
disabled={carouselIndex === 0}
|
||||||
onClick={() => setCarouselIndex((i) => Math.max(0, i - 1))}
|
onClick={() => setCarouselIndex((i) => Math.max(0, i - 1))}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4 mr-1" /> Back
|
<ArrowLeft className="h-4 w-4 mr-1" aria-hidden /> Back
|
||||||
</Button>
|
</Button>
|
||||||
{carouselIndex < highStakes.length - 1 ? (
|
{carouselIndex < highStakes.length - 1 ? (
|
||||||
<Button
|
<Button
|
||||||
disabled={(reasons[currentHighStakes?.clientId ?? '']?.trim().length ?? 0) < 5}
|
disabled={(reasons[currentHighStakes?.clientId ?? '']?.trim().length ?? 0) < 5}
|
||||||
onClick={() => setCarouselIndex((i) => i + 1)}
|
onClick={() => setCarouselIndex((i) => i + 1)}
|
||||||
>
|
>
|
||||||
Next <ArrowRight className="h-4 w-4 ml-1" />
|
Next <ArrowRight className="h-4 w-4 ml-1" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button disabled={!allHighStakesReasoned} onClick={() => setStage('confirm')}>
|
<Button disabled={!allHighStakesReasoned} onClick={() => setStage('confirm')}>
|
||||||
Review <ArrowRight className="h-4 w-4 ml-1" />
|
Review <ArrowRight className="h-4 w-4 ml-1" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -304,7 +304,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
|
|||||||
>
|
>
|
||||||
{archiveMutation.isPending ? (
|
{archiveMutation.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-1.5" /> Archiving…
|
<Loader2 className="h-4 w-4 animate-spin mr-1.5" aria-hidden /> Archiving…
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
`Archive ${archivable.length}`
|
`Archive ${archivable.length}`
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ function BulkHardDeleteDialogBody({ onOpenChange, clientIds, onDeleted }: Props)
|
|||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2 text-destructive">
|
<DialogTitle className="flex items-center gap-2 text-destructive">
|
||||||
<AlertTriangle className="h-5 w-5" />
|
<AlertTriangle className="h-5 w-5" aria-hidden />
|
||||||
Permanently delete {clientIds.length} client{clientIds.length === 1 ? '' : 's'}
|
Permanently delete {clientIds.length} client{clientIds.length === 1 ? '' : 's'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -136,7 +136,7 @@ function BulkHardDeleteDialogBody({ onOpenChange, clientIds, onDeleted }: Props)
|
|||||||
{stage === 'confirm' && (
|
{stage === 'confirm' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-start gap-2 rounded-md border border-blue-300 bg-blue-50 p-3 text-xs text-blue-900">
|
<div className="flex items-start gap-2 rounded-md border border-blue-300 bg-blue-50 p-3 text-xs text-blue-900">
|
||||||
<Mail className="h-4 w-4 shrink-0 mt-0.5" />
|
<Mail className="h-4 w-4 shrink-0 mt-0.5" aria-hidden />
|
||||||
<div>
|
<div>
|
||||||
Code sent to <span className="font-mono">{maskedEmail}</span>. Enter both fields
|
Code sent to <span className="font-mono">{maskedEmail}</span>. Enter both fields
|
||||||
below.
|
below.
|
||||||
@@ -214,7 +214,7 @@ function BulkHardDeleteDialogBody({ onOpenChange, clientIds, onDeleted }: Props)
|
|||||||
>
|
>
|
||||||
{requestCode.isPending ? (
|
{requestCode.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-1.5" /> Sending…
|
<Loader2 className="h-4 w-4 animate-spin mr-1.5" aria-hidden /> Sending…
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Send confirmation code'
|
'Send confirmation code'
|
||||||
@@ -229,7 +229,7 @@ function BulkHardDeleteDialogBody({ onOpenChange, clientIds, onDeleted }: Props)
|
|||||||
>
|
>
|
||||||
{bulkDelete.isPending ? (
|
{bulkDelete.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-1.5" /> Deleting…
|
<Loader2 className="h-4 w-4 animate-spin mr-1.5" aria-hidden /> Deleting…
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
`Permanently delete ${clientIds.length}`
|
`Permanently delete ${clientIds.length}`
|
||||||
|
|||||||
@@ -61,16 +61,16 @@ export function ClientCard({ client, portSlug, onEdit, onArchive }: ClientCardPr
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={`Actions for ${client.fullName}`}
|
aria-label={`Actions for ${client.fullName}`}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => onEdit(client)}>
|
<DropdownMenuItem onClick={() => onEdit(client)}>
|
||||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(client)}>
|
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(client)}>
|
||||||
<Archive className="mr-2 h-3.5 w-3.5" />
|
<Archive className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Archive
|
Archive
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function getClientColumns({
|
|||||||
className="inline-flex items-center gap-1.5 text-sm text-foreground hover:text-primary hover:underline"
|
className="inline-flex items-center gap-1.5 text-sm text-foreground hover:text-primary hover:underline"
|
||||||
title={`Email ${value}`}
|
title={`Email ${value}`}
|
||||||
>
|
>
|
||||||
<Mail className="h-3 w-3 shrink-0 text-muted-foreground" />
|
<Mail className="h-3 w-3 shrink-0 text-muted-foreground" aria-hidden />
|
||||||
<span className="truncate">{value}</span>
|
<span className="truncate">{value}</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@@ -147,7 +147,7 @@ export function getClientColumns({
|
|||||||
className="inline-flex items-center gap-1.5 text-foreground hover:text-primary hover:underline"
|
className="inline-flex items-center gap-1.5 text-foreground hover:text-primary hover:underline"
|
||||||
title={`Call ${value}`}
|
title={`Call ${value}`}
|
||||||
>
|
>
|
||||||
<Phone className="h-3 w-3 shrink-0 text-muted-foreground" />
|
<Phone className="h-3 w-3 shrink-0 text-muted-foreground" aria-hidden />
|
||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
</a>
|
</a>
|
||||||
{waDigits && (
|
{waDigits && (
|
||||||
@@ -160,7 +160,7 @@ export function getClientColumns({
|
|||||||
title={`WhatsApp ${value}`}
|
title={`WhatsApp ${value}`}
|
||||||
aria-label={`WhatsApp ${value}`}
|
aria-label={`WhatsApp ${value}`}
|
||||||
>
|
>
|
||||||
<MessageCircle className="h-3.5 w-3.5" />
|
<MessageCircle className="h-3.5 w-3.5" aria-hidden />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -303,16 +303,16 @@ export function getClientColumns({
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
||||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(row.original)}>
|
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(row.original)}>
|
||||||
<Archive className="mr-2 h-3.5 w-3.5" />
|
<Archive className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Archive
|
Archive
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function ClientDetailHeader({ client }: ClientDetailHeaderProps) {
|
|||||||
title="Permanently delete client"
|
title="Permanently delete client"
|
||||||
className="shrink-0 rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-destructive/10 hover:text-destructive"
|
className="shrink-0 rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
)}
|
)}
|
||||||
@@ -182,7 +182,11 @@ export function ClientDetailHeader({ client }: ClientDetailHeaderProps) {
|
|||||||
isArchived ? 'hover:text-emerald-600' : 'hover:text-destructive',
|
isArchived ? 'hover:text-emerald-600' : 'hover:text-destructive',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isArchived ? <RotateCcw className="size-4" /> : <Archive className="size-4" />}
|
{isArchived ? (
|
||||||
|
<RotateCcw className="size-4" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<Archive className="size-4" aria-hidden />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export function ClientForm({ open, onOpenChange, client, onUseExistingClient }:
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => append({ channel: 'email', value: '', isPrimary: false })}
|
onClick={() => append({ channel: 'email', value: '', isPrimary: false })}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3.5 w-3.5" />
|
<Plus className="mr-1 h-3.5 w-3.5" aria-hidden />
|
||||||
Add Contact
|
Add Contact
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -333,7 +333,7 @@ export function ClientForm({ open, onOpenChange, client, onUseExistingClient }:
|
|||||||
className="h-8 text-destructive hover:text-destructive"
|
className="h-8 text-destructive hover:text-destructive"
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-1 h-3.5 w-3.5" />
|
<Trash2 className="mr-1 h-3.5 w-3.5" aria-hidden />
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -419,7 +419,7 @@ export function ClientForm({ open, onOpenChange, client, onUseExistingClient }:
|
|||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isSubmitting || mutation.isPending}>
|
<Button type="submit" disabled={isSubmitting || mutation.isPending}>
|
||||||
{(isSubmitting || mutation.isPending) && (
|
{(isSubmitting || mutation.isPending) && (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
)}
|
)}
|
||||||
{isEdit ? 'Save changes' : 'Create Client'}
|
{isEdit ? 'Save changes' : 'Create Client'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -77,7 +77,10 @@ function InterestRowItem({
|
|||||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">{yachtLabel}</p>
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">{yachtLabel}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
<ChevronRight
|
||||||
|
className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
@@ -364,9 +367,9 @@ function InterestPreviewSheet({
|
|||||||
function InterestSkeleton() {
|
function InterestSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border bg-card p-4 shadow-sm">
|
<div className="rounded-xl border border-border bg-card p-4 shadow-sm">
|
||||||
<Skeleton className="h-4 w-32" />
|
<Skeleton className="h-4 w-32" aria-hidden />
|
||||||
<Skeleton className="mt-2 h-3 w-24" />
|
<Skeleton className="mt-2 h-3 w-24" aria-hidden />
|
||||||
<Skeleton className="mt-3 h-2 w-48" />
|
<Skeleton className="mt-3 h-2 w-48" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -421,7 +424,7 @@ export function ClientInterestsTab({ clientId }: ClientInterestsTabProps) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
||||||
<Plus className="mr-1.5 size-3.5" />
|
<Plus className="mr-1.5 size-3.5" aria-hidden />
|
||||||
Add interest
|
Add interest
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function ClientList() {
|
|||||||
far-right edge, which is where reps look first. */}
|
far-right edge, which is where reps look first. */}
|
||||||
<PermissionGate resource="clients" action="create">
|
<PermissionGate resource="clients" action="create">
|
||||||
<Button size="sm" className="ml-auto" onClick={() => setCreateOpen(true)}>
|
<Button size="sm" className="ml-auto" onClick={() => setCreateOpen(true)}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
New Client
|
New Client
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ function HeroVariant({ clientId, portSlug }: { clientId: string; portSlug: strin
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-4 w-40" />
|
<Skeleton className="h-4 w-40" aria-hidden />
|
||||||
<Skeleton className="h-2 w-48" />
|
<Skeleton className="h-2 w-48" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ function HeroVariant({ clientId, portSlug }: { clientId: string; portSlug: strin
|
|||||||
href={`/${portSlug}/interests/new` as Route}
|
href={`/${portSlug}/interests/new` as Route}
|
||||||
className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
|
className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
|
||||||
>
|
>
|
||||||
Start interest <ArrowRight className="size-3" />
|
Start interest <ArrowRight className="size-3" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -182,7 +182,10 @@ function HeroVariant({ clientId, portSlug }: { clientId: string; portSlug: strin
|
|||||||
>
|
>
|
||||||
{STAGE_LABELS[stage]}
|
{STAGE_LABELS[stage]}
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
<ChevronRight
|
||||||
|
className="size-3.5 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1.5">
|
<div className="mt-1.5">
|
||||||
<StageStepper current={stage} size="xs" />
|
<StageStepper current={stage} size="xs" />
|
||||||
@@ -214,8 +217,8 @@ function PanelVariant({ clientId, portSlug }: { clientId: string; portSlug: stri
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-4 w-40" />
|
<Skeleton className="h-4 w-40" aria-hidden />
|
||||||
<Skeleton className="h-2 w-48" />
|
<Skeleton className="h-2 w-48" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -233,7 +236,7 @@ function PanelVariant({ clientId, portSlug }: { clientId: string; portSlug: stri
|
|||||||
href={`/${portSlug}/interests/new` as Route}
|
href={`/${portSlug}/interests/new` as Route}
|
||||||
className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
|
className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
|
||||||
>
|
>
|
||||||
Start interest <ArrowRight className="size-3" />
|
Start interest <ArrowRight className="size-3" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -289,7 +292,10 @@ function PanelVariant({ clientId, portSlug }: { clientId: string; portSlug: stri
|
|||||||
<StageStepper current={stage} size="xs" />
|
<StageStepper current={stage} size="xs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
<ChevronRight
|
||||||
|
className="size-3.5 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function ClientYachtsTab({ clientId, yachts }: ClientYachtsTabProps) {
|
|||||||
<h3 className="text-sm font-medium">Client-owned yachts</h3>
|
<h3 className="text-sm font-medium">Client-owned yachts</h3>
|
||||||
<PermissionGate resource="yachts" action="create">
|
<PermissionGate resource="yachts" action="create">
|
||||||
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Add yacht
|
Add yacht
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export function ContactsEditor({ clientId, contacts }: { clientId: string; conta
|
|||||||
onClick={() => setAdding(true)}
|
onClick={() => setAdding(true)}
|
||||||
className="w-full justify-center"
|
className="w-full justify-center"
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
<Plus className="h-3.5 w-3.5 mr-1.5" aria-hidden />
|
||||||
Add contact
|
Add contact
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -201,7 +201,7 @@ function ContactRow({
|
|||||||
{/* Top / left: channel + value */}
|
{/* Top / left: channel + value */}
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
<ChannelPicker value={contact.channel} onChange={changeChannel}>
|
<ChannelPicker value={contact.channel} onChange={changeChannel}>
|
||||||
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
|
<Icon className="h-3.5 w-3.5 text-muted-foreground" aria-hidden />
|
||||||
</ChannelPicker>
|
</ChannelPicker>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{contact.channel === 'phone' || contact.channel === 'whatsapp' ? (
|
{contact.channel === 'phone' || contact.channel === 'whatsapp' ? (
|
||||||
@@ -282,7 +282,7 @@ function ContactRow({
|
|||||||
title="Remove"
|
title="Remove"
|
||||||
className="rounded p-1 text-muted-foreground/50 transition-all hover:bg-background/60 hover:text-destructive sm:opacity-0 sm:group-hover:opacity-100"
|
className="rounded p-1 text-muted-foreground/50 transition-all hover:bg-background/60 hover:text-destructive sm:opacity-0 sm:group-hover:opacity-100"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -439,7 +439,7 @@ function NewContactForm({
|
|||||||
|
|
||||||
<div className="ml-auto flex gap-2">
|
<div className="ml-auto flex gap-2">
|
||||||
<Button type="button" size="sm" onClick={submit} disabled={submitDisabled}>
|
<Button type="button" size="sm" onClick={submit} disabled={submitDisabled}>
|
||||||
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : 'Save'}
|
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden /> : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" size="sm" variant="ghost" onClick={onCancel} disabled={saving}>
|
<Button type="button" size="sm" variant="ghost" onClick={onCancel} disabled={saving}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export function GdprExportButton({ clientId }: { clientId: string }) {
|
|||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="h-8">
|
<Button variant="outline" size="sm" className="h-8">
|
||||||
<FileDown className="mr-1.5 h-3.5 w-3.5" />
|
<FileDown className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
GDPR export
|
GDPR export
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -154,9 +154,9 @@ export function GdprExportButton({ clientId }: { clientId: string }) {
|
|||||||
|
|
||||||
<Button onClick={() => request.mutate()} disabled={request.isPending}>
|
<Button onClick={() => request.mutate()} disabled={request.isPending}>
|
||||||
{request.isPending ? (
|
{request.isPending ? (
|
||||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<FileDown className="mr-1.5 h-3.5 w-3.5" />
|
<FileDown className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
)}
|
)}
|
||||||
Queue export
|
Queue export
|
||||||
</Button>
|
</Button>
|
||||||
@@ -180,7 +180,7 @@ export function GdprExportButton({ clientId }: { clientId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
{r.sentTo ? (
|
{r.sentTo ? (
|
||||||
<div className="text-xs text-muted-foreground inline-flex items-center gap-1">
|
<div className="text-xs text-muted-foreground inline-flex items-center gap-1">
|
||||||
<Mail className="h-3 w-3" />
|
<Mail className="h-3 w-3" aria-hidden />
|
||||||
Sent to {r.sentTo}
|
Sent to {r.sentTo}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -195,7 +195,7 @@ export function GdprExportButton({ clientId }: { clientId: string }) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => downloadById(r.id)}
|
onClick={() => downloadById(r.id)}
|
||||||
>
|
>
|
||||||
<Download className="h-3.5 w-3.5" />
|
<Download className="h-3.5 w-3.5" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function HardDeleteDialogBody({ onOpenChange, clientId, clientName, onDeleted }:
|
|||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2 text-destructive">
|
<DialogTitle className="flex items-center gap-2 text-destructive">
|
||||||
<AlertTriangle className="h-5 w-5" />
|
<AlertTriangle className="h-5 w-5" aria-hidden />
|
||||||
Permanently delete {clientName}
|
Permanently delete {clientName}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -126,7 +126,7 @@ function HardDeleteDialogBody({ onOpenChange, clientId, clientName, onDeleted }:
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-start gap-2 rounded-md border border-blue-300 bg-blue-50 p-3 text-xs text-blue-900">
|
<div className="flex items-start gap-2 rounded-md border border-blue-300 bg-blue-50 p-3 text-xs text-blue-900">
|
||||||
<Mail className="h-4 w-4 shrink-0 mt-0.5" />
|
<Mail className="h-4 w-4 shrink-0 mt-0.5" aria-hidden />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div>
|
<div>
|
||||||
Code sent to <span className="font-mono">{maskedEmail}</span>. It expires in 10
|
Code sent to <span className="font-mono">{maskedEmail}</span>. It expires in 10
|
||||||
@@ -185,7 +185,7 @@ function HardDeleteDialogBody({ onOpenChange, clientId, clientName, onDeleted }:
|
|||||||
>
|
>
|
||||||
{requestCode.isPending ? (
|
{requestCode.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-1.5" /> Sending…
|
<Loader2 className="h-4 w-4 animate-spin mr-1.5" aria-hidden /> Sending…
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Send confirmation code'
|
'Send confirmation code'
|
||||||
@@ -199,7 +199,7 @@ function HardDeleteDialogBody({ onOpenChange, clientId, clientName, onDeleted }:
|
|||||||
>
|
>
|
||||||
{hardDelete.isPending ? (
|
{hardDelete.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-1.5" /> Deleting…
|
<Loader2 className="h-4 w-4 animate-spin mr-1.5" aria-hidden /> Deleting…
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Permanently delete'
|
'Permanently delete'
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function PortalInviteButton({
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UserPlus className="mr-1.5 h-3.5 w-3.5" />
|
<UserPlus className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Invite to portal
|
Invite to portal
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export function PortalInviteButton({
|
|||||||
|
|
||||||
{success ? (
|
{success ? (
|
||||||
<div className="py-4 flex items-center gap-3 text-sm text-green-700">
|
<div className="py-4 flex items-center gap-3 text-sm text-green-700">
|
||||||
<Check className="h-5 w-5" />
|
<Check className="h-5 w-5" aria-hidden />
|
||||||
Activation email sent to <strong>{email}</strong>.
|
Activation email sent to <strong>{email}</strong>.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -138,7 +138,7 @@ export function PortalInviteButton({
|
|||||||
<Button onClick={submit} disabled={loading || !email}>
|
<Button onClick={submit} disabled={loading || !email}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" aria-hidden />
|
||||||
Sending…
|
Sending…
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -82,11 +82,11 @@ export function SendDocumentsDialog({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 flex items-center gap-2 text-sm font-semibold">
|
<h3 className="mb-2 flex items-center gap-2 text-sm font-semibold">
|
||||||
<Mail className="h-4 w-4" /> Brochures
|
<Mail className="h-4 w-4" aria-hidden /> Brochures
|
||||||
</h3>
|
</h3>
|
||||||
{brochuresQuery.isLoading && (
|
{brochuresQuery.isLoading && (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Loading brochures…
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden /> Loading brochures…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!brochuresQuery.isLoading && usableBrochures.length === 0 && (
|
{!brochuresQuery.isLoading && usableBrochures.length === 0 && (
|
||||||
@@ -103,7 +103,7 @@ export function SendDocumentsDialog({
|
|||||||
onClick={() => setActiveSend({ kind: 'brochure', brochureId: b.id })}
|
onClick={() => setActiveSend({ kind: 'brochure', brochureId: b.id })}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<FileText className="h-4 w-4" />
|
<FileText className="h-4 w-4" aria-hidden />
|
||||||
{b.label}
|
{b.label}
|
||||||
{b.isDefault && (
|
{b.isDefault && (
|
||||||
<span className="rounded bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
<span className="rounded bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ function SmartArchiveDialogBody({
|
|||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />
|
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" aria-hidden />
|
||||||
Loading dossier…
|
Loading dossier…
|
||||||
</div>
|
</div>
|
||||||
) : error || !dossier ? (
|
) : error || !dossier ? (
|
||||||
@@ -296,7 +296,7 @@ function SmartArchiveDialogBody({
|
|||||||
<Card className="border-red-300 bg-red-50">
|
<Card className="border-red-300 bg-red-50">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-red-800 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-red-800 flex items-center gap-2">
|
||||||
<AlertTriangle className="h-4 w-4" /> Cannot archive
|
<AlertTriangle className="h-4 w-4" aria-hidden /> Cannot archive
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-sm text-red-700 space-y-1">
|
<CardContent className="text-sm text-red-700 space-y-1">
|
||||||
@@ -311,7 +311,7 @@ function SmartArchiveDialogBody({
|
|||||||
<Card className="border-amber-300 bg-amber-50">
|
<Card className="border-amber-300 bg-amber-50">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-amber-900 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-amber-900 flex items-center gap-2">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" aria-hidden />
|
||||||
Late-stage deal — confirmation required
|
Late-stage deal — confirmation required
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -328,7 +328,8 @@ function SmartArchiveDialogBody({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<FileText className="h-4 w-4" /> Pipeline interests ({dossier.interests.length})
|
<FileText className="h-4 w-4" aria-hidden /> Pipeline interests (
|
||||||
|
{dossier.interests.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-xs space-y-1">
|
<CardContent className="text-xs space-y-1">
|
||||||
@@ -371,7 +372,7 @@ function SmartArchiveDialogBody({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<Anchor className="h-4 w-4" /> Berths ({dossier.berths.length})
|
<Anchor className="h-4 w-4" aria-hidden /> Berths ({dossier.berths.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
@@ -425,7 +426,7 @@ function SmartArchiveDialogBody({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<Ship className="h-4 w-4" /> Yachts owned ({dossier.yachts.length})
|
<Ship className="h-4 w-4" aria-hidden /> Yachts owned ({dossier.yachts.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
@@ -459,7 +460,7 @@ function SmartArchiveDialogBody({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<Anchor className="h-4 w-4" /> Active reservations (
|
<Anchor className="h-4 w-4" aria-hidden /> Active reservations (
|
||||||
{dossier.reservations.length})
|
{dossier.reservations.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -493,7 +494,8 @@ function SmartArchiveDialogBody({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<Receipt className="h-4 w-4" /> Outstanding invoices ({dossier.invoices.length})
|
<Receipt className="h-4 w-4" aria-hidden /> Outstanding invoices (
|
||||||
|
{dossier.invoices.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
@@ -527,7 +529,7 @@ function SmartArchiveDialogBody({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<FileText className="h-4 w-4" /> In-flight signing requests
|
<FileText className="h-4 w-4" aria-hidden /> In-flight signing requests
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
@@ -559,7 +561,7 @@ function SmartArchiveDialogBody({
|
|||||||
<Card className="border-muted">
|
<Card className="border-muted">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<Users className="h-4 w-4" /> Automatically handled
|
<Users className="h-4 w-4" aria-hidden /> Automatically handled
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-xs text-muted-foreground space-y-1">
|
<CardContent className="text-xs text-muted-foreground space-y-1">
|
||||||
@@ -600,7 +602,7 @@ function SmartArchiveDialogBody({
|
|||||||
onClick={() => archiveMutation.mutate()}
|
onClick={() => archiveMutation.mutate()}
|
||||||
>
|
>
|
||||||
{archiveMutation.isPending ? (
|
{archiveMutation.isPending ? (
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
|
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" aria-hidden />
|
||||||
) : null}
|
) : null}
|
||||||
Archive
|
Archive
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function iconFor(kind: string) {
|
function iconFor(kind: string) {
|
||||||
if (kind.startsWith('berth_')) return <Anchor className="h-3 w-3" />;
|
if (kind.startsWith('berth_')) return <Anchor className="h-3 w-3" aria-hidden />;
|
||||||
if (kind.startsWith('yacht_')) return <Ship className="h-3 w-3" />;
|
if (kind.startsWith('yacht_')) return <Ship className="h-3 w-3" aria-hidden />;
|
||||||
if (kind.startsWith('documenso_')) return <FileText className="h-3 w-3" />;
|
if (kind.startsWith('documenso_')) return <FileText className="h-3 w-3" aria-hidden />;
|
||||||
return <Wrench className="h-3 w-3" />;
|
return <Wrench className="h-3 w-3" aria-hidden />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SmartRestoreDialog(props: Props) {
|
export function SmartRestoreDialog(props: Props) {
|
||||||
@@ -135,7 +135,7 @@ function SmartRestoreDialogBody({ open, onOpenChange, clientId, clientName, onSu
|
|||||||
|
|
||||||
{dossierQuery.isLoading ? (
|
{dossierQuery.isLoading ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />
|
<Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" aria-hidden />
|
||||||
Loading restore dossier…
|
Loading restore dossier…
|
||||||
</div>
|
</div>
|
||||||
) : dossierQuery.error || !dossier ? (
|
) : dossierQuery.error || !dossier ? (
|
||||||
@@ -153,7 +153,7 @@ function SmartRestoreDialogBody({ open, onOpenChange, clientId, clientName, onSu
|
|||||||
<Card className="border-emerald-300 bg-emerald-50">
|
<Card className="border-emerald-300 bg-emerald-50">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-emerald-900 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-emerald-900 flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4" /> Auto-reversed (
|
<CheckCircle2 className="h-4 w-4" aria-hidden /> Auto-reversed (
|
||||||
{dossier.autoReversible.length})
|
{dossier.autoReversible.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -174,7 +174,7 @@ function SmartRestoreDialogBody({ open, onOpenChange, clientId, clientName, onSu
|
|||||||
<Card className="border-amber-300 bg-amber-50">
|
<Card className="border-amber-300 bg-amber-50">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-amber-900 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-amber-900 flex items-center gap-2">
|
||||||
<AlertTriangle className="h-4 w-4" /> Opt-in to undo (
|
<AlertTriangle className="h-4 w-4" aria-hidden /> Opt-in to undo (
|
||||||
{dossier.reversibleWithPrompt.length})
|
{dossier.reversibleWithPrompt.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -207,7 +207,8 @@ function SmartRestoreDialogBody({ open, onOpenChange, clientId, clientName, onSu
|
|||||||
<Card className="border-slate-300 bg-slate-50">
|
<Card className="border-slate-300 bg-slate-50">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-slate-900 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-slate-900 flex items-center gap-2">
|
||||||
<Lock className="h-4 w-4" /> Cannot be undone ({dossier.locked.length})
|
<Lock className="h-4 w-4" aria-hidden /> Cannot be undone (
|
||||||
|
{dossier.locked.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-xs space-y-1.5 text-slate-700">
|
<CardContent className="text-xs space-y-1.5 text-slate-700">
|
||||||
@@ -236,7 +237,7 @@ function SmartRestoreDialogBody({ open, onOpenChange, clientId, clientName, onSu
|
|||||||
>
|
>
|
||||||
{restoreMutation.isPending ? (
|
{restoreMutation.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-1.5" /> Restoring…
|
<Loader2 className="h-4 w-4 animate-spin mr-1.5" aria-hidden /> Restoring…
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Restore client'
|
'Restore client'
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export function AddMembershipDialog({ open, onOpenChange, companyId }: AddMember
|
|||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isSubmitting || mutation.isPending}>
|
<Button type="submit" disabled={isSubmitting || mutation.isPending}>
|
||||||
{(isSubmitting || mutation.isPending) && (
|
{(isSubmitting || mutation.isPending) && (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
)}
|
)}
|
||||||
Add member
|
Add member
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -66,22 +66,22 @@ export function CompanyCard({ company, portSlug, onEdit, onArchive }: CompanyCar
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={`Actions for ${company.name}`}
|
aria-label={`Actions for ${company.name}`}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href={`/${portSlug}/companies/${company.id}`}>
|
<Link href={`/${portSlug}/companies/${company.id}`}>
|
||||||
<Eye className="mr-2 h-3.5 w-3.5" />
|
<Eye className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
View
|
View
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onEdit(company)}>
|
<DropdownMenuItem onClick={() => onEdit(company)}>
|
||||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(company)}>
|
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(company)}>
|
||||||
<Archive className="mr-2 h-3.5 w-3.5" />
|
<Archive className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Archive
|
Archive
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -89,7 +89,7 @@ export function CompanyCard({ company, portSlug, onEdit, onArchive }: CompanyCar
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<ListCardAvatar icon={<Building2 className="h-5 w-5" />} />
|
<ListCardAvatar icon={<Building2 className="h-5 w-5" aria-hidden />} />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{/* Title row + spacer for actions button */}
|
{/* Title row + spacer for actions button */}
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@@ -108,10 +108,14 @@ export function CompanyCard({ company, portSlug, onEdit, onArchive }: CompanyCar
|
|||||||
{country || company.taxId ? (
|
{country || company.taxId ? (
|
||||||
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
||||||
{country ? (
|
{country ? (
|
||||||
<ListCardMeta icon={<MapPin className="h-3 w-3" />}>{country}</ListCardMeta>
|
<ListCardMeta icon={<MapPin className="h-3 w-3" aria-hidden />}>
|
||||||
|
{country}
|
||||||
|
</ListCardMeta>
|
||||||
) : null}
|
) : null}
|
||||||
{company.taxId ? (
|
{company.taxId ? (
|
||||||
<ListCardMeta icon={<Hash className="h-3 w-3" />}>{company.taxId}</ListCardMeta>
|
<ListCardMeta icon={<Hash className="h-3 w-3" aria-hidden />}>
|
||||||
|
{company.taxId}
|
||||||
|
</ListCardMeta>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export function getCompanyColumns({
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
@@ -166,16 +166,16 @@ export function getCompanyColumns({
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
href={`/${portSlug}/companies/${row.original.id}` as any}
|
href={`/${portSlug}/companies/${row.original.id}` as any}
|
||||||
>
|
>
|
||||||
<Eye className="mr-2 h-3.5 w-3.5" />
|
<Eye className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
View
|
View
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
||||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(row.original)}>
|
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(row.original)}>
|
||||||
<Archive className="mr-2 h-3.5 w-3.5" />
|
<Archive className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Archive
|
Archive
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function CompanyDetailHeader({ company }: CompanyDetailHeaderProps) {
|
|||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<PermissionGate resource="companies" action="edit">
|
<PermissionGate resource="companies" action="edit">
|
||||||
<Button variant="outline" size="sm" onClick={() => setEditOpen(true)}>
|
<Button variant="outline" size="sm" onClick={() => setEditOpen(true)}>
|
||||||
<Pencil className="mr-1.5 h-3.5 w-3.5" />
|
<Pencil className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
@@ -119,7 +119,7 @@ export function CompanyDetailHeader({ company }: CompanyDetailHeaderProps) {
|
|||||||
onClick={() => setArchiveOpen(true)}
|
onClick={() => setArchiveOpen(true)}
|
||||||
disabled={isArchived}
|
disabled={isArchived}
|
||||||
>
|
>
|
||||||
<Archive className="mr-1.5 h-3.5 w-3.5" />
|
<Archive className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Archive
|
Archive
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ export function CompanyForm({ open, onOpenChange, company }: CompanyFormProps) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setClientFormOpen(true)}
|
onClick={() => setClientFormOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3.5 w-3.5" /> New client
|
<Plus className="mr-1 h-3.5 w-3.5" aria-hidden /> New client
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -420,7 +420,7 @@ export function CompanyForm({ open, onOpenChange, company }: CompanyFormProps) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setYachtFormOpen(true)}
|
onClick={() => setYachtFormOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3.5 w-3.5" /> New yacht
|
<Plus className="mr-1 h-3.5 w-3.5" aria-hidden /> New yacht
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -459,7 +459,7 @@ export function CompanyForm({ open, onOpenChange, company }: CompanyFormProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isSubmitting || mutation.isPending}>
|
<Button type="submit" disabled={isSubmitting || mutation.isPending}>
|
||||||
{(isSubmitting || mutation.isPending) && (
|
{(isSubmitting || mutation.isPending) && (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
)}
|
)}
|
||||||
{isEdit ? 'Save changes' : 'Create Company'}
|
{isEdit ? 'Save changes' : 'Create Company'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -669,7 +669,7 @@ function EntityMultiPicker({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{placeholder}
|
{placeholder}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-(--radix-popper-anchor-width) min-w-[280px] p-0" align="start">
|
<PopoverContent className="w-(--radix-popper-anchor-width) min-w-[280px] p-0" align="start">
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function CompanyList() {
|
|||||||
actions={
|
actions={
|
||||||
<PermissionGate resource="companies" action="create">
|
<PermissionGate resource="companies" action="create">
|
||||||
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
New Company
|
New Company
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export function CompanyMembersTab({ companyId, portSlug }: CompanyMembersTabProp
|
|||||||
|
|
||||||
<PermissionGate resource="memberships" action="manage">
|
<PermissionGate resource="memberships" action="manage">
|
||||||
<Button size="sm" onClick={() => setAddOpen(true)}>
|
<Button size="sm" onClick={() => setAddOpen(true)}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Add Member
|
Add Member
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
@@ -167,7 +167,7 @@ export function CompanyMembersTab({ companyId, portSlug }: CompanyMembersTabProp
|
|||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
) : members.length === 0 ? (
|
) : members.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -226,7 +226,7 @@ export function CompanyMembersTab({ companyId, portSlug }: CompanyMembersTabProp
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7">
|
<Button variant="ghost" size="icon" className="h-7 w-7">
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
@@ -235,7 +235,7 @@ export function CompanyMembersTab({ companyId, portSlug }: CompanyMembersTabProp
|
|||||||
onClick={() => setPrimaryMutation.mutate(m.id)}
|
onClick={() => setPrimaryMutation.mutate(m.id)}
|
||||||
disabled={setPrimaryMutation.isPending}
|
disabled={setPrimaryMutation.isPending}
|
||||||
>
|
>
|
||||||
<Star className="mr-2 h-3.5 w-3.5" />
|
<Star className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Set Primary
|
Set Primary
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
@@ -245,7 +245,7 @@ export function CompanyMembersTab({ companyId, portSlug }: CompanyMembersTabProp
|
|||||||
onClick={() => endMutation.mutate(m.id)}
|
onClick={() => endMutation.mutate(m.id)}
|
||||||
disabled={endMutation.isPending}
|
disabled={endMutation.isPending}
|
||||||
>
|
>
|
||||||
<XCircle className="mr-2 h-3.5 w-3.5" />
|
<XCircle className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Remove from company
|
Remove from company
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function CompanyOwnedYachtsTab({ companyId, portSlug }: CompanyOwnedYacht
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function CompanyPicker({
|
|||||||
className={cn('w-full justify-between', !value && 'text-muted-foreground')}
|
className={cn('w-full justify-between', !value && 'text-muted-foreground')}
|
||||||
>
|
>
|
||||||
<span className="truncate">{selectedLabel}</span>
|
<span className="truncate">{selectedLabel}</span>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[320px] p-0" align="start">
|
<PopoverContent className="w-[320px] p-0" align="start">
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function ActiveDealsTile() {
|
|||||||
Active deals
|
Active deals
|
||||||
</p>
|
</p>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Skeleton className="mt-1 h-7 w-12" />
|
<Skeleton className="mt-1 h-7 w-12" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<p className="text-2xl font-bold leading-tight text-foreground">
|
<p className="text-2xl font-bold leading-tight text-foreground">
|
||||||
{data?.activeInterests ?? 0}
|
{data?.activeInterests ?? 0}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function BerthStatusChart() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Skeleton className="h-[240px] w-full" />
|
<Skeleton className="h-[240px] w-full" aria-hidden />
|
||||||
) : chartData.length === 0 ? (
|
) : chartData.length === 0 ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">No berths yet.</p>
|
<p className="py-8 text-center text-sm text-muted-foreground">No berths yet.</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -114,18 +114,18 @@ export function ChartCard({
|
|||||||
aria-label="Chart options"
|
aria-label="Chart options"
|
||||||
data-testid="chart-menu"
|
data-testid="chart-menu"
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-44">
|
<DropdownMenuContent align="end" className="w-44">
|
||||||
{toCsv ? (
|
{toCsv ? (
|
||||||
<DropdownMenuItem onSelect={onDownloadCsv}>
|
<DropdownMenuItem onSelect={onDownloadCsv}>
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" aria-hidden />
|
||||||
Download CSV
|
Download CSV
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
<DropdownMenuItem onSelect={onDownloadPng}>
|
<DropdownMenuItem onSelect={onDownloadPng}>
|
||||||
<ImageIcon className="mr-2 h-4 w-4" />
|
<ImageIcon className="mr-2 h-4 w-4" aria-hidden />
|
||||||
Download PNG
|
Download PNG
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function CustomizeWidgetsMenu() {
|
|||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="gap-1.5">
|
<Button variant="outline" size="sm" className="gap-1.5">
|
||||||
<LayoutGrid className="h-4 w-4" />
|
<LayoutGrid className="h-4 w-4" aria-hidden />
|
||||||
Customize
|
Customize
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ export function HotDealsCard() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Skeleton className="h-12 w-full" />
|
<Skeleton className="h-12 w-full" aria-hidden />
|
||||||
<Skeleton className="h-12 w-full" />
|
<Skeleton className="h-12 w-full" aria-hidden />
|
||||||
<Skeleton className="h-12 w-full" />
|
<Skeleton className="h-12 w-full" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
) : deals.length === 0 ? (
|
) : deals.length === 0 ? (
|
||||||
<p className="py-4 text-center text-sm text-muted-foreground">
|
<p className="py-4 text-center text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ function KpiTileSkeleton() {
|
|||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden rounded-xl border border-border bg-card p-3 shadow-sm sm:p-5">
|
<div className="relative overflow-hidden rounded-xl border border-border bg-card p-3 shadow-sm sm:p-5">
|
||||||
<div className="absolute inset-x-0 top-0 h-1 bg-muted" aria-hidden />
|
<div className="absolute inset-x-0 top-0 h-1 bg-muted" aria-hidden />
|
||||||
<Skeleton className="h-3 w-20" />
|
<Skeleton className="h-3 w-20" aria-hidden />
|
||||||
<Skeleton className="mt-2 h-6 w-24 sm:mt-3 sm:h-7" />
|
<Skeleton className="mt-2 h-6 w-24 sm:mt-3 sm:h-7" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function MyRemindersRail() {
|
|||||||
<CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0 pb-3">
|
<CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0 pb-3">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<CardTitle className="flex items-center gap-1.5 text-base">
|
<CardTitle className="flex items-center gap-1.5 text-base">
|
||||||
<AlarmClock className="size-4" />
|
<AlarmClock className="size-4" aria-hidden />
|
||||||
Reminders
|
Reminders
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{overdueCount > 0 ? (
|
{overdueCount > 0 ? (
|
||||||
@@ -150,7 +150,10 @@ export function MyRemindersRail() {
|
|||||||
? 'in ' + formatDistanceToNowStrict(due)
|
? 'in ' + formatDistanceToNowStrict(due)
|
||||||
: 'now'}
|
: 'now'}
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5" />
|
<ChevronRight
|
||||||
|
className="size-3.5 shrink-0 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function PipelineValueTile() {
|
|||||||
Pipeline value
|
Pipeline value
|
||||||
</p>
|
</p>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Skeleton className="mt-1 h-7 w-24" />
|
<Skeleton className="mt-1 h-7 w-24" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<p
|
<p
|
||||||
className="truncate text-2xl font-bold leading-tight text-foreground"
|
className="truncate text-2xl font-bold leading-tight text-foreground"
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ export function SourceConversionChart() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Skeleton className="h-8 w-full" />
|
<Skeleton className="h-8 w-full" aria-hidden />
|
||||||
<Skeleton className="h-8 w-full" />
|
<Skeleton className="h-8 w-full" aria-hidden />
|
||||||
<Skeleton className="h-8 w-full" />
|
<Skeleton className="h-8 w-full" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<p className="py-4 text-center text-sm text-muted-foreground">
|
<p className="py-4 text-center text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function TimezoneDriftBanner() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-amber-300 bg-amber-50 px-4 py-2.5 text-sm text-amber-900 sm:flex-nowrap">
|
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-amber-300 bg-amber-50 px-4 py-2.5 text-sm text-amber-900 sm:flex-nowrap">
|
||||||
<div className="flex items-start gap-2 min-w-0">
|
<div className="flex items-start gap-2 min-w-0">
|
||||||
<Clock className="mt-0.5 h-4 w-4 shrink-0" />
|
<Clock className="mt-0.5 h-4 w-4 shrink-0" aria-hidden />
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="font-medium leading-tight">
|
<p className="font-medium leading-tight">
|
||||||
You appear to be in{' '}
|
You appear to be in{' '}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function WebsiteGlanceTile() {
|
|||||||
Website today
|
Website today
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Skeleton className="mt-2 h-7 w-20" />
|
<Skeleton className="mt-2 h-7 w-20" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-1 flex items-baseline gap-2 text-lg font-semibold tabular-nums sm:mt-2 sm:text-2xl">
|
<div className="mt-1 flex items-baseline gap-2 text-lg font-semibold tabular-nums sm:mt-2 sm:text-2xl">
|
||||||
{today.toLocaleString()}
|
{today.toLocaleString()}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function AggregatedSection<K extends AggregatedItemKind['kind']>({
|
|||||||
<h3 className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
<h3 className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||||
{icon}
|
{icon}
|
||||||
{title}
|
{title}
|
||||||
<Loader2 className="ml-1 h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
<Loader2 className="ml-1 h-3.5 w-3.5 animate-spin text-muted-foreground" aria-hidden />
|
||||||
</h3>
|
</h3>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -108,7 +108,7 @@ function GroupBlock<K extends AggregatedItemKind['kind']>({
|
|||||||
// The server always sets exactly one of `files` / `workflows` per group;
|
// The server always sets exactly one of `files` / `workflows` per group;
|
||||||
// unify them into a single list for rendering. The discriminated-union
|
// unify them into a single list for rendering. The discriminated-union
|
||||||
// generic on `AggregatedSection` keeps the row type correct upstream.
|
// generic on `AggregatedSection` keeps the row type correct upstream.
|
||||||
const items = ((group.files ?? group.workflows ?? []) as unknown) as ItemOfKind<K>[];
|
const items = (group.files ?? group.workflows ?? []) as unknown as ItemOfKind<K>[];
|
||||||
return (
|
return (
|
||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
<header className="mb-1 text-[0.7rem] font-medium uppercase tracking-wide text-muted-foreground">
|
<header className="mb-1 text-[0.7rem] font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
|||||||
actions={
|
actions={
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={`/${portSlug}/documents`}>
|
<Link href={`/${portSlug}/documents`}>
|
||||||
<ArrowLeft className="mr-1.5 h-4 w-4" /> Back
|
<ArrowLeft className="mr-1.5 h-4 w-4" aria-hidden /> Back
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -360,7 +360,7 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
|||||||
Signers
|
Signers
|
||||||
</h2>
|
</h2>
|
||||||
<Button size="sm" variant="outline" onClick={addSigner}>
|
<Button size="sm" variant="outline" onClick={addSigner}>
|
||||||
<Plus className="mr-1.5 h-3.5 w-3.5" /> Add signer
|
<Plus className="mr-1.5 h-3.5 w-3.5" aria-hidden /> Add signer
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
@@ -405,7 +405,7 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
|||||||
onClick={() => removeSigner(idx)}
|
onClick={() => removeSigner(idx)}
|
||||||
className="text-muted-foreground hover:text-destructive"
|
className="text-muted-foreground hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
|
|||||||
actions={
|
actions={
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={`/${portSlug}/documents`}>
|
<Link href={`/${portSlug}/documents`}>
|
||||||
<ArrowLeft className="mr-1.5 h-4 w-4" />
|
<ArrowLeft className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Back to documents
|
Back to documents
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -225,18 +225,18 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button asChild variant="outline" size="sm">
|
<Button asChild variant="outline" size="sm">
|
||||||
<Link href={`/${portSlug}/documents`}>
|
<Link href={`/${portSlug}/documents`}>
|
||||||
<ArrowLeft className="mr-1.5 h-4 w-4" /> Back
|
<ArrowLeft className="mr-1.5 h-4 w-4" aria-hidden /> Back
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
{isComplete && doc.signedFileId ? (
|
{isComplete && doc.signedFileId ? (
|
||||||
<>
|
<>
|
||||||
<Button asChild size="sm">
|
<Button asChild size="sm">
|
||||||
<Link href={`/api/v1/files/${doc.signedFileId}/download`}>
|
<Link href={`/api/v1/files/${doc.signedFileId}/download`}>
|
||||||
<Download className="mr-1.5 h-4 w-4" /> Download signed PDF
|
<Download className="mr-1.5 h-4 w-4" aria-hidden /> Download signed PDF
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="outline" onClick={handleEmailSignedPdf}>
|
<Button size="sm" variant="outline" onClick={handleEmailSignedPdf}>
|
||||||
<Mail className="mr-1.5 h-4 w-4" /> Email signatories
|
<Mail className="mr-1.5 h-4 w-4" aria-hidden /> Email signatories
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -299,7 +299,7 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleRemind(signer.id)}
|
onClick={() => handleRemind(signer.id)}
|
||||||
>
|
>
|
||||||
<Bell className="mr-1.5 h-3 w-3" /> Remind
|
<Bell className="mr-1.5 h-3 w-3" aria-hidden /> Remind
|
||||||
</Button>
|
</Button>
|
||||||
{signer.signingUrl ? (
|
{signer.signingUrl ? (
|
||||||
<button
|
<button
|
||||||
@@ -370,7 +370,7 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
|
|||||||
}}
|
}}
|
||||||
className="text-muted-foreground hover:text-destructive"
|
className="text-muted-foreground hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function DocRow({ doc, onDelete, onSend }: DocRowProps) {
|
|||||||
)}
|
)}
|
||||||
<PermissionGate resource="documents" action="manage_folders">
|
<PermissionGate resource="documents" action="manage_folders">
|
||||||
<DropdownMenuItem onSelect={() => setMoveOpen(true)}>
|
<DropdownMenuItem onSelect={() => setMoveOpen(true)}>
|
||||||
<FolderInput className="mr-2 h-4 w-4" />
|
<FolderInput className="mr-2 h-4 w-4" aria-hidden />
|
||||||
Move to folder…
|
Move to folder…
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function DocumentTemplatePicker({
|
|||||||
className={cn('w-full justify-between', !value && 'text-muted-foreground')}
|
className={cn('w-full justify-between', !value && 'text-muted-foreground')}
|
||||||
>
|
>
|
||||||
<span className="truncate">{selectedLabel}</span>
|
<span className="truncate">{selectedLabel}</span>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[320px] p-0" align="start">
|
<PopoverContent className="w-[320px] p-0" align="start">
|
||||||
|
|||||||
@@ -311,7 +311,11 @@ function FlatFolderListing({ portSlug, folderId }: FlatFolderListingProps) {
|
|||||||
onClick={() => setExpandedDocId(expanded ? null : doc.id)}
|
onClick={() => setExpandedDocId(expanded ? null : doc.id)}
|
||||||
className="flex min-h-[44px] min-w-[44px] items-center justify-center text-muted-foreground transition-transform"
|
className="flex min-h-[44px] min-w-[44px] items-center justify-center text-muted-foreground transition-transform"
|
||||||
>
|
>
|
||||||
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
{expanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" aria-hidden />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href={`/${portSlug}/documents/${doc.id}`}
|
href={`/${portSlug}/documents/${doc.id}`}
|
||||||
@@ -409,18 +413,18 @@ function FlatFolderListing({ portSlug, folderId }: FlatFolderListingProps) {
|
|||||||
</ul>
|
</ul>
|
||||||
) : documents.length === 0 ? (
|
) : documents.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={<FileText className="h-7 w-7" />}
|
icon={<FileText className="h-7 w-7" aria-hidden />}
|
||||||
title="No documents in this folder"
|
title="No documents in this folder"
|
||||||
body="Upload a file, generate a signing flow, or move existing documents here."
|
body="Upload a file, generate a signing flow, or move existing documents here."
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => setUploadOpen(true)}>
|
<Button onClick={() => setUploadOpen(true)}>
|
||||||
<Upload className="mr-1.5 h-4 w-4" />
|
<Upload className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Upload file
|
Upload file
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={`/${portSlug}/documents/new`}>
|
<Link href={`/${portSlug}/documents/new`}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Generate for signing
|
Generate for signing
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -560,7 +564,7 @@ function FolderDropZone({ folderId, entityType, entityId, children }: FolderDrop
|
|||||||
{(dragActive || uploading) && (
|
{(dragActive || uploading) && (
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-md border-2 border-dashed border-primary bg-primary/5 backdrop-blur-[1px] z-10">
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-md border-2 border-dashed border-primary bg-primary/5 backdrop-blur-[1px] z-10">
|
||||||
<div className="flex flex-col items-center gap-2 text-sm font-medium text-primary">
|
<div className="flex flex-col items-center gap-2 text-sm font-medium text-primary">
|
||||||
<Upload className="h-8 w-8" />
|
<Upload className="h-8 w-8" aria-hidden />
|
||||||
{uploading ? 'Uploading…' : 'Drop to upload to this folder'}
|
{uploading ? 'Uploading…' : 'Drop to upload to this folder'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<AggregatedSection<'workflows'>
|
<AggregatedSection<'workflows'>
|
||||||
title="Signing in progress"
|
title="Signing in progress"
|
||||||
icon={<ClipboardSignature className="h-4 w-4 text-muted-foreground" />}
|
icon={<ClipboardSignature className="h-4 w-4 text-muted-foreground" aria-hidden />}
|
||||||
groups={workflowGroups}
|
groups={workflowGroups}
|
||||||
loading={workflowsLoading}
|
loading={workflowsLoading}
|
||||||
emptyMessage="No workflows in flight for this entity."
|
emptyMessage="No workflows in flight for this entity."
|
||||||
@@ -68,7 +68,7 @@ export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
|
|||||||
|
|
||||||
<AggregatedSection<'files'>
|
<AggregatedSection<'files'>
|
||||||
title="Files"
|
title="Files"
|
||||||
icon={<FileText className="h-4 w-4 text-muted-foreground" />}
|
icon={<FileText className="h-4 w-4 text-muted-foreground" aria-hidden />}
|
||||||
groups={fileGroups}
|
groups={fileGroups}
|
||||||
loading={filesLoading}
|
loading={filesLoading}
|
||||||
emptyMessage="No files for this entity yet."
|
emptyMessage="No files for this entity yet."
|
||||||
@@ -86,7 +86,7 @@ export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
|
|||||||
className="min-h-[44px] gap-1 px-2 text-xs text-brand"
|
className="min-h-[44px] gap-1 px-2 text-xs text-brand"
|
||||||
onClick={() => setDetailsId(signedFromDocumentId)}
|
onClick={() => setDetailsId(signedFromDocumentId)}
|
||||||
>
|
>
|
||||||
<Eye className="h-3 w-3" />
|
<Eye className="h-3 w-3" aria-hidden />
|
||||||
View signing details
|
View signing details
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export function EoiGenerateDialog({
|
|||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<FileSignature className="size-4" />
|
<FileSignature className="size-4" aria-hidden />
|
||||||
Generate Expression of Interest
|
Generate Expression of Interest
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -290,9 +290,9 @@ export function EoiGenerateDialog({
|
|||||||
|
|
||||||
{ctxLoading ? (
|
{ctxLoading ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-4 w-full" />
|
<Skeleton className="h-4 w-full" aria-hidden />
|
||||||
<Skeleton className="h-4 w-3/4" />
|
<Skeleton className="h-4 w-3/4" aria-hidden />
|
||||||
<Skeleton className="h-4 w-2/3" />
|
<Skeleton className="h-4 w-2/3" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
) : ctx ? (
|
) : ctx ? (
|
||||||
<div className="space-y-3 rounded-md border bg-muted/20 p-3">
|
<div className="space-y-3 rounded-md border bg-muted/20 p-3">
|
||||||
@@ -339,9 +339,9 @@ export function EoiGenerateDialog({
|
|||||||
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
<Pencil className="size-3" />
|
<Pencil className="size-3" aria-hidden />
|
||||||
Edit client details
|
Edit client details
|
||||||
<ExternalLink className="size-3" />
|
<ExternalLink className="size-3" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -349,9 +349,9 @@ export function EoiGenerateDialog({
|
|||||||
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
<Pencil className="size-3" />
|
<Pencil className="size-3" aria-hidden />
|
||||||
Manage linked berths
|
Manage linked berths
|
||||||
<ExternalLink className="size-3" />
|
<ExternalLink className="size-3" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -365,7 +365,7 @@ export function EoiGenerateDialog({
|
|||||||
|
|
||||||
{!ctxLoading && ctx && !requiredMet && (
|
{!ctxLoading && ctx && !requiredMet && (
|
||||||
<p className="flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
<p className="flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
||||||
<AlertTriangle className="size-3.5 shrink-0 mt-0.5" />
|
<AlertTriangle className="size-3.5 shrink-0 mt-0.5" aria-hidden />
|
||||||
Add the missing required details on the client's record before generating the
|
Add the missing required details on the client's record before generating the
|
||||||
EOI.
|
EOI.
|
||||||
</p>
|
</p>
|
||||||
@@ -481,7 +481,7 @@ function PreviewRow({
|
|||||||
className="ml-1 inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-muted/60 hover:text-foreground"
|
className="ml-1 inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-muted/60 hover:text-foreground"
|
||||||
aria-label={`Edit ${label}`}
|
aria-label={`Edit ${label}`}
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ export function FolderActionsMenu({ selectedFolderId, onAfterDelete }: FolderAct
|
|||||||
setCreateOpen(true);
|
setCreateOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FolderPlus className="mr-2 h-4 w-4" />
|
<FolderPlus className="mr-2 h-4 w-4" aria-hidden />
|
||||||
New folder {isFolderSelected ? 'inside this' : 'at root'}
|
New folder {isFolderSelected ? 'inside this' : 'at root'}
|
||||||
</Button>
|
</Button>
|
||||||
{isFolderSelected ? (
|
{isFolderSelected ? (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
<span className="sr-only">More folder actions</span>
|
<span className="sr-only">More folder actions</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -111,7 +111,7 @@ export function FolderActionsMenu({ selectedFolderId, onAfterDelete }: FolderAct
|
|||||||
setRenameOpen(true);
|
setRenameOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="mr-2 h-4 w-4" />
|
<Pencil className="mr-2 h-4 w-4" aria-hidden />
|
||||||
Rename
|
Rename
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</span>
|
</span>
|
||||||
@@ -130,7 +130,7 @@ export function FolderActionsMenu({ selectedFolderId, onAfterDelete }: FolderAct
|
|||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" aria-hidden />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function FolderBreadcrumb({ selectedFolderId, onSelect }: FolderBreadcrum
|
|||||||
onClick={() => onSelect(undefined)}
|
onClick={() => onSelect(undefined)}
|
||||||
className="flex min-h-[44px] items-center gap-1 py-2 hover:text-foreground"
|
className="flex min-h-[44px] items-center gap-1 py-2 hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Home className="h-3.5 w-3.5" />
|
<Home className="h-3.5 w-3.5" aria-hidden />
|
||||||
<span>All</span>
|
<span>All</span>
|
||||||
</button>
|
</button>
|
||||||
{path.length === 0 && selectedFolderId === null ? (
|
{path.length === 0 && selectedFolderId === null ? (
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function FolderTreeSidebar({ selectedFolderId, onSelect, footer }: Folder
|
|||||||
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="min-h-[44px]">
|
<Button variant="outline" size="sm" className="min-h-[44px]">
|
||||||
<FolderTree className="mr-2 h-4 w-4" />
|
<FolderTree className="mr-2 h-4 w-4" aria-hidden />
|
||||||
Show folders
|
Show folders
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
@@ -130,7 +130,7 @@ function PseudoRow({
|
|||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<Icon className="mr-2 h-4 w-4" />
|
<Icon className="mr-2 h-4 w-4" aria-hidden />
|
||||||
{label}
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -183,9 +183,9 @@ function FolderRow({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{open && hasChildren ? (
|
{open && hasChildren ? (
|
||||||
<FolderOpen className="h-4 w-4 shrink-0" />
|
<FolderOpen className="h-4 w-4 shrink-0" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<Folder className="h-4 w-4 shrink-0" />
|
<Folder className="h-4 w-4 shrink-0" aria-hidden />
|
||||||
)}
|
)}
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{node.name}
|
{node.name}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function HubRootView({ portSlug }: Props) {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<section className="rounded-md border bg-white">
|
<section className="rounded-md border bg-white">
|
||||||
<h3 className="flex items-center gap-2 border-b px-3 py-2 text-sm font-semibold">
|
<h3 className="flex items-center gap-2 border-b px-3 py-2 text-sm font-semibold">
|
||||||
<ClipboardSignature className="h-4 w-4 text-muted-foreground" />
|
<ClipboardSignature className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||||||
Signing in progress
|
Signing in progress
|
||||||
</h3>
|
</h3>
|
||||||
{workflowsLoading ? (
|
{workflowsLoading ? (
|
||||||
@@ -76,7 +76,7 @@ export function HubRootView({ portSlug }: Props) {
|
|||||||
|
|
||||||
<section className="rounded-md border bg-white">
|
<section className="rounded-md border bg-white">
|
||||||
<h3 className="flex items-center gap-2 border-b px-3 py-2 text-sm font-semibold">
|
<h3 className="flex items-center gap-2 border-b px-3 py-2 text-sm font-semibold">
|
||||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
<FileText className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||||||
Recent files
|
Recent files
|
||||||
</h3>
|
</h3>
|
||||||
{filesLoading ? (
|
{filesLoading ? (
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ function DialogBody({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FolderInput className="mr-1.5 h-4 w-4" />
|
<FolderInput className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Move
|
Move
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ export function NewDocumentMenu({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button size={size}>
|
<Button size={size}>
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
New document
|
New document
|
||||||
<ChevronDown className="ml-1.5 h-4 w-4 opacity-80" />
|
<ChevronDown className="ml-1.5 h-4 w-4 opacity-80" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-64">
|
<DropdownMenuContent align="end" className="w-64">
|
||||||
<DropdownMenuItem onSelect={() => setUploadOpen(true)} className="gap-2 py-2.5">
|
<DropdownMenuItem onSelect={() => setUploadOpen(true)} className="gap-2 py-2.5">
|
||||||
<Upload className="h-4 w-4" />
|
<Upload className="h-4 w-4" aria-hidden />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>Upload file</span>
|
<span>Upload file</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
@@ -79,7 +79,7 @@ export function NewDocumentMenu({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild className="gap-2 py-2.5">
|
<DropdownMenuItem asChild className="gap-2 py-2.5">
|
||||||
<Link href={`/${portSlug}/documents/new`}>
|
<Link href={`/${portSlug}/documents/new`}>
|
||||||
<FileSignature className="h-4 w-4" />
|
<FileSignature className="h-4 w-4" aria-hidden />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>Generate document for signing</span>
|
<span>Generate document for signing</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function SigningDetailsDialog({ documentId, open, onOpenChange }: Props)
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isLoading || !data ? (
|
{isLoading || !data ? (
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function EmailAccountsList() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setSheetOpen(true)}>
|
<Button onClick={() => setSheetOpen(true)}>
|
||||||
<Plus className="h-4 w-4 mr-1.5" />
|
<Plus className="h-4 w-4 mr-1.5" aria-hidden />
|
||||||
Add account
|
Add account
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,14 +126,14 @@ export function EmailAccountsList() {
|
|||||||
<p className="text-sm text-muted-foreground">Loading…</p>
|
<p className="text-sm text-muted-foreground">Loading…</p>
|
||||||
) : accounts.length === 0 ? (
|
) : accounts.length === 0 ? (
|
||||||
<div className="rounded-lg border border-dashed p-8 text-center text-muted-foreground">
|
<div className="rounded-lg border border-dashed p-8 text-center text-muted-foreground">
|
||||||
<Mail className="mx-auto h-6 w-6 mb-2" />
|
<Mail className="mx-auto h-6 w-6 mb-2" aria-hidden />
|
||||||
<p className="text-sm">No email accounts connected.</p>
|
<p className="text-sm">No email accounts connected.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border divide-y">
|
<div className="rounded-lg border divide-y">
|
||||||
{accounts.map((a) => (
|
{accounts.map((a) => (
|
||||||
<div key={a.id} className="flex items-center gap-3 p-3">
|
<div key={a.id} className="flex items-center gap-3 p-3">
|
||||||
<Mail className="h-5 w-5 text-muted-foreground" />
|
<Mail className="h-5 w-5 text-muted-foreground" aria-hidden />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm font-medium truncate">{a.emailAddress}</div>
|
<div className="text-sm font-medium truncate">{a.emailAddress}</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
@@ -152,12 +152,12 @@ export function EmailAccountsList() {
|
|||||||
disabled={syncMutation.isPending}
|
disabled={syncMutation.isPending}
|
||||||
title="Sync now"
|
title="Sync now"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="ghost" size="icon" className="text-destructive">
|
<Button variant="ghost" size="icon" className="text-destructive">
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="Remove account"
|
title="Remove account"
|
||||||
|
|||||||
@@ -101,12 +101,12 @@ export function EmailDraftButton({
|
|||||||
<Button variant="outline" size="sm" onClick={handleGenerateDraft} disabled={isLoading}>
|
<Button variant="outline" size="sm" onClick={handleGenerateDraft} disabled={isLoading}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||||
Generating...
|
Generating...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Mail className="mr-1.5 h-3.5 w-3.5" />
|
<Mail className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Draft Email
|
Draft Email
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -156,12 +156,12 @@ export function EmailDraftButton({
|
|||||||
<Button variant="outline" size="sm" onClick={handleCopy}>
|
<Button variant="outline" size="sm" onClick={handleCopy}>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<>
|
<>
|
||||||
<Check className="mr-1.5 h-3.5 w-3.5 text-green-600" />
|
<Check className="mr-1.5 h-3.5 w-3.5 text-green-600" aria-hidden />
|
||||||
Copied
|
Copied
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Copy className="mr-1.5 h-3.5 w-3.5" />
|
<Copy className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
Copy to clipboard
|
Copy to clipboard
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -174,7 +174,7 @@ export function EmailDraftButton({
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||||
Regenerating...
|
Regenerating...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ export function EmailThreadsList() {
|
|||||||
{Array.from({ length: 4 }).map((_, i) => (
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
<div key={i} className="p-3 space-y-2">
|
<div key={i} className="p-3 space-y-2">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Skeleton className="h-4 w-1/3" />
|
<Skeleton className="h-4 w-1/3" aria-hidden />
|
||||||
<Skeleton className="h-3 w-16" />
|
<Skeleton className="h-3 w-16" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-3 w-1/2" />
|
<Skeleton className="h-3 w-1/2" aria-hidden />
|
||||||
<Skeleton className="h-3 w-2/3" />
|
<Skeleton className="h-3 w-2/3" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,22 +108,22 @@ export function ExpenseCard({ expense, portSlug, onEdit, onArchive }: ExpenseCar
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
aria-label={`Actions for expense: ${title}`}
|
aria-label={`Actions for expense: ${title}`}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href={`/${portSlug}/expenses/${expense.id}`}>
|
<Link href={`/${portSlug}/expenses/${expense.id}`}>
|
||||||
<Eye className="mr-2 h-3.5 w-3.5" />
|
<Eye className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
View
|
View
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onEdit(expense)}>
|
<DropdownMenuItem onClick={() => onEdit(expense)}>
|
||||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(expense)}>
|
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(expense)}>
|
||||||
<Archive className="mr-2 h-3.5 w-3.5" />
|
<Archive className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Archive
|
Archive
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -131,7 +131,7 @@ export function ExpenseCard({ expense, portSlug, onEdit, onArchive }: ExpenseCar
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<ListCardAvatar icon={<Receipt className="h-5 w-5" />} />
|
<ListCardAvatar icon={<Receipt className="h-5 w-5" aria-hidden />} />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{/* Title row + spacer for actions button */}
|
{/* Title row + spacer for actions button */}
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@@ -159,7 +159,9 @@ export function ExpenseCard({ expense, portSlug, onEdit, onArchive }: ExpenseCar
|
|||||||
{/* Date meta */}
|
{/* Date meta */}
|
||||||
{dateFormatted ? (
|
{dateFormatted ? (
|
||||||
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground">
|
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground">
|
||||||
<ListCardMeta icon={<Calendar className="h-3 w-3" />}>{dateFormatted}</ListCardMeta>
|
<ListCardMeta icon={<Calendar className="h-3 w-3" aria-hidden />}>
|
||||||
|
{dateFormatted}
|
||||||
|
</ListCardMeta>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -166,22 +166,22 @@ export function getExpenseColumns({
|
|||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href={`/${portSlug}/expenses/${row.original.id}`}>
|
<Link href={`/${portSlug}/expenses/${row.original.id}`}>
|
||||||
<Eye className="mr-2 h-3.5 w-3.5" />
|
<Eye className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
View
|
View
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
||||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(row.original)}>
|
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(row.original)}>
|
||||||
<Archive className="mr-2 h-3.5 w-3.5" />
|
<Archive className="mr-2 h-3.5 w-3.5" aria-hidden />
|
||||||
Archive
|
Archive
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function ReceiptThumbnail({ fileId }: { fileId: string }) {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-32 items-center justify-center rounded border bg-muted/40 text-xs text-muted-foreground">
|
<div className="flex h-32 items-center justify-center rounded border bg-muted/40 text-xs text-muted-foreground">
|
||||||
<Loader2 className="mr-2 h-3 w-3 animate-spin" /> Loading preview…
|
<Loader2 className="mr-2 h-3 w-3 animate-spin" aria-hidden /> Loading preview…
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ function ReceiptThumbnail({ fileId }: { fileId: string }) {
|
|||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-32 items-center justify-center text-muted-foreground">
|
<div className="flex h-32 items-center justify-center text-muted-foreground">
|
||||||
<FileText className="h-8 w-8" />
|
<FileText className="h-8 w-8" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 flex items-center justify-between text-xs text-muted-foreground">
|
<div className="mt-2 flex items-center justify-between text-xs text-muted-foreground">
|
||||||
@@ -78,7 +78,7 @@ function ReceiptThumbnail({ fileId }: { fileId: string }) {
|
|||||||
href={`/api/v1/files/${fileId}/download`}
|
href={`/api/v1/files/${fileId}/download`}
|
||||||
className="inline-flex items-center gap-1 text-primary hover:underline"
|
className="inline-flex items-center gap-1 text-primary hover:underline"
|
||||||
>
|
>
|
||||||
<Download className="h-3 w-3" /> Download
|
<Download className="h-3 w-3" aria-hidden /> Download
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +131,7 @@ export function ExpenseDetail({ expenseId, onEdit, onArchived }: ExpenseDetailPr
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center p-12">
|
<div className="flex items-center justify-center p-12">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ export function ExpenseDetail({ expenseId, onEdit, onArchived }: ExpenseDetailPr
|
|||||||
{onEdit && (
|
{onEdit && (
|
||||||
<PermissionGate resource="expenses" action="edit">
|
<PermissionGate resource="expenses" action="edit">
|
||||||
<Button variant="outline" size="sm" onClick={onEdit}>
|
<Button variant="outline" size="sm" onClick={onEdit}>
|
||||||
<Edit className="mr-1.5 h-4 w-4" />
|
<Edit className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
@@ -174,7 +174,7 @@ export function ExpenseDetail({ expenseId, onEdit, onArchived }: ExpenseDetailPr
|
|||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
onClick={() => setArchiveOpen(true)}
|
onClick={() => setArchiveOpen(true)}
|
||||||
>
|
>
|
||||||
<Archive className="mr-1.5 h-4 w-4" />
|
<Archive className="mr-1.5 h-4 w-4" aria-hidden />
|
||||||
Archive
|
Archive
|
||||||
</Button>
|
</Button>
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
@@ -259,7 +259,7 @@ export function ExpenseDetail({ expenseId, onEdit, onArchived }: ExpenseDetailPr
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
<Receipt className="h-4 w-4" />
|
<Receipt className="h-4 w-4" aria-hidden />
|
||||||
Receipts ({expense.receiptFileIds.length})
|
Receipts ({expense.receiptFileIds.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function ExpenseDuplicateBanner({ expense }: Props) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-start gap-2">
|
<div className="flex min-w-0 items-start gap-2">
|
||||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" aria-hidden />
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="font-medium">Looks like a duplicate</p>
|
<p className="font-medium">Looks like a duplicate</p>
|
||||||
<p className="mt-0.5 text-xs text-amber-800">
|
<p className="mt-0.5 text-xs text-amber-800">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user