fix(ux): pass-2 audit fixes — admin grouping, Duplicates entry, header tooltips
Three small but high-leverage fixes from the second audit pass on main:
Admin index (src/app/(dashboard)/[portSlug]/admin/page.tsx):
- Grouped 21 sections into 7 categories: Access, Configuration, Content,
Data Quality, Operations, Tenancy, Integrations. Each group has a
one-line description so first-time admins can orient themselves
without reading every card.
- Added the missing Duplicates entry (links to /admin/duplicates from
the dedup-migration work) under Data Quality.
More sheet (mobile bottom-drawer nav):
- "Email" -> "Inbox". The page that opens is an email-inbox surface
(Inbox + Accounts tabs), not a generic email composer. The previous
label was ambiguous.
Interest detail header (Won / Lost outcome buttons):
- Added title="Mark as won" / "Close as lost" so the icon-only buttons
on mobile have a tooltip on long-press / desktop hover.
- Tightened mobile padding (px-2 vs px-2.5) so the full-text desktop
labels still fit on sm+ without re-introducing a regression where a
visible mobile "Won"/"Lost" inline label crowded the right cluster
enough to push Email/Call/WhatsApp action chips into a vertical
stack.
Verification: 0 tsc errors, 926/926 vitest passing, lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Upload,
|
Upload,
|
||||||
Users,
|
Users,
|
||||||
|
UsersRound,
|
||||||
Webhook,
|
Webhook,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
@@ -29,132 +30,186 @@ interface AdminSection {
|
|||||||
icon: typeof Settings;
|
icon: typeof Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SECTIONS: AdminSection[] = [
|
interface AdminGroup {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
sections: AdminSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const GROUPS: AdminGroup[] = [
|
||||||
{
|
{
|
||||||
href: 'users',
|
title: 'Access',
|
||||||
label: 'Users',
|
description: 'Who can sign in and what they can do once they do.',
|
||||||
description: 'CRM accounts, role assignments, and per-user residential access toggles.',
|
sections: [
|
||||||
icon: Users,
|
{
|
||||||
|
href: 'users',
|
||||||
|
label: 'Users',
|
||||||
|
description: 'CRM accounts, role assignments, and per-user residential access toggles.',
|
||||||
|
icon: Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'invitations',
|
||||||
|
label: 'Invitations',
|
||||||
|
description: 'Send invitations, track pending invites, and resend or revoke them.',
|
||||||
|
icon: Mail,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'roles',
|
||||||
|
label: 'Roles & Permissions',
|
||||||
|
description: 'Default permission sets and per-port role overrides.',
|
||||||
|
icon: Shield,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'invitations',
|
title: 'Configuration',
|
||||||
label: 'Invitations',
|
description: 'Branding, integrations, and per-port settings.',
|
||||||
description: 'Send invitations, track pending invites, and resend or revoke them.',
|
sections: [
|
||||||
icon: Mail,
|
{
|
||||||
|
href: 'email',
|
||||||
|
label: 'Email Settings',
|
||||||
|
description: 'From address, signatures, and per-port SMTP overrides.',
|
||||||
|
icon: Mail,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'documenso',
|
||||||
|
label: 'Documenso & EOI',
|
||||||
|
description: 'API credentials, EOI template, and default in-app vs Documenso pathway.',
|
||||||
|
icon: FileText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'reminders',
|
||||||
|
label: 'Reminders',
|
||||||
|
description: 'Default reminder behaviour and the daily-digest delivery window.',
|
||||||
|
icon: Bell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'branding',
|
||||||
|
label: 'Branding',
|
||||||
|
description: 'App name, logo, primary color, and email header/footer HTML.',
|
||||||
|
icon: Palette,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'settings',
|
||||||
|
label: 'System Settings',
|
||||||
|
description: 'Generic key/value configuration store for advanced flags.',
|
||||||
|
icon: Settings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'webhooks',
|
||||||
|
label: 'Webhooks',
|
||||||
|
description: 'Outgoing webhook subscriptions, secrets, and delivery log.',
|
||||||
|
icon: Webhook,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'roles',
|
title: 'Content',
|
||||||
label: 'Roles & Permissions',
|
description: 'Forms, templates, and labels that users see.',
|
||||||
description: 'Default permission sets and per-port role overrides.',
|
sections: [
|
||||||
icon: Shield,
|
{
|
||||||
|
href: 'forms',
|
||||||
|
label: 'Forms',
|
||||||
|
description: 'Form templates used by client-facing inquiry and intake flows.',
|
||||||
|
icon: Sliders,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'templates',
|
||||||
|
label: 'Document Templates',
|
||||||
|
description: 'PDF + email templates with merge-field placeholders.',
|
||||||
|
icon: FileText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'tags',
|
||||||
|
label: 'Tags',
|
||||||
|
description: 'Color-coded tags applied to clients, yachts, companies, and interests.',
|
||||||
|
icon: Tag,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'custom-fields',
|
||||||
|
label: 'Custom Fields',
|
||||||
|
description: 'Tenant-defined fields for clients, yachts, and reservations.',
|
||||||
|
icon: Key,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'audit',
|
title: 'Data Quality',
|
||||||
label: 'Audit Log',
|
description: 'Cleanup, imports, and the audit trail.',
|
||||||
description: 'Searchable log of every authenticated mutation in the system.',
|
sections: [
|
||||||
icon: ScrollText,
|
{
|
||||||
|
href: 'duplicates',
|
||||||
|
label: 'Duplicates',
|
||||||
|
description: 'Review queue of suspected duplicate clients flagged by the dedup engine.',
|
||||||
|
icon: UsersRound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'import',
|
||||||
|
label: 'Bulk Import',
|
||||||
|
description: 'CSV-driven imports for clients, yachts, and reservations.',
|
||||||
|
icon: Upload,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'audit',
|
||||||
|
label: 'Audit Log',
|
||||||
|
description: 'Searchable log of every authenticated mutation in the system.',
|
||||||
|
icon: ScrollText,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'email',
|
title: 'Operations',
|
||||||
label: 'Email Settings',
|
description: 'Health checks and disaster recovery.',
|
||||||
description: 'From address, signatures, and per-port SMTP overrides.',
|
sections: [
|
||||||
icon: Mail,
|
{
|
||||||
|
href: 'reports',
|
||||||
|
label: 'Reports',
|
||||||
|
description: 'Saved analytics views and ad-hoc query results.',
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'monitoring',
|
||||||
|
label: 'Queue Monitoring',
|
||||||
|
description: 'BullMQ queue health, throughput, and retry diagnostics.',
|
||||||
|
icon: Database,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'backup',
|
||||||
|
label: 'Backup & Restore',
|
||||||
|
description: 'Database snapshots and on-demand exports.',
|
||||||
|
icon: HardDrive,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'documenso',
|
title: 'Tenancy',
|
||||||
label: 'Documenso & EOI',
|
description: 'Multi-port and multi-install scaffolding.',
|
||||||
description: 'API credentials, EOI template, and default in-app vs Documenso pathway.',
|
sections: [
|
||||||
icon: FileText,
|
{
|
||||||
|
href: 'ports',
|
||||||
|
label: 'Ports',
|
||||||
|
description: 'Manage the marinas/ports this installation serves.',
|
||||||
|
icon: Briefcase,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'onboarding',
|
||||||
|
label: 'Onboarding',
|
||||||
|
description: 'Initial-setup wizard for fresh ports.',
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'reminders',
|
title: 'Integrations',
|
||||||
label: 'Reminders',
|
description: 'Third-party providers wired into the app.',
|
||||||
description: 'Default reminder behaviour and the daily-digest delivery window.',
|
sections: [
|
||||||
icon: Bell,
|
{
|
||||||
},
|
href: 'ocr',
|
||||||
{
|
label: 'Receipt OCR',
|
||||||
href: 'branding',
|
description: 'Configure the AI provider used by the mobile receipt scanner.',
|
||||||
label: 'Branding',
|
icon: ScrollText,
|
||||||
description: 'App name, logo, primary color, and email header/footer HTML.',
|
},
|
||||||
icon: Palette,
|
],
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'settings',
|
|
||||||
label: 'System Settings',
|
|
||||||
description: 'Generic key/value configuration store for advanced flags.',
|
|
||||||
icon: Settings,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'webhooks',
|
|
||||||
label: 'Webhooks',
|
|
||||||
description: 'Outgoing webhook subscriptions, secrets, and delivery log.',
|
|
||||||
icon: Webhook,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'forms',
|
|
||||||
label: 'Forms',
|
|
||||||
description: 'Form templates used by client-facing inquiry and intake flows.',
|
|
||||||
icon: Sliders,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'templates',
|
|
||||||
label: 'Document Templates',
|
|
||||||
description: 'PDF + email templates with merge-field placeholders.',
|
|
||||||
icon: FileText,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'tags',
|
|
||||||
label: 'Tags',
|
|
||||||
description: 'Color-coded tags applied to clients, yachts, companies, and interests.',
|
|
||||||
icon: Tag,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'custom-fields',
|
|
||||||
label: 'Custom Fields',
|
|
||||||
description: 'Tenant-defined fields for clients, yachts, and reservations.',
|
|
||||||
icon: Key,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'reports',
|
|
||||||
label: 'Reports',
|
|
||||||
description: 'Saved analytics views and ad-hoc query results.',
|
|
||||||
icon: LayoutDashboard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'monitoring',
|
|
||||||
label: 'Queue Monitoring',
|
|
||||||
description: 'BullMQ queue health, throughput, and retry diagnostics.',
|
|
||||||
icon: Database,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'import',
|
|
||||||
label: 'Bulk Import',
|
|
||||||
description: 'CSV-driven imports for clients, yachts, and reservations.',
|
|
||||||
icon: Upload,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'backup',
|
|
||||||
label: 'Backup & Restore',
|
|
||||||
description: 'Database snapshots and on-demand exports.',
|
|
||||||
icon: HardDrive,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'ports',
|
|
||||||
label: 'Ports',
|
|
||||||
description: 'Manage the marinas/ports this installation serves.',
|
|
||||||
icon: Briefcase,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'onboarding',
|
|
||||||
label: 'Onboarding',
|
|
||||||
description: 'Initial-setup wizard for fresh ports.',
|
|
||||||
icon: LayoutDashboard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'ocr',
|
|
||||||
label: 'Receipt OCR',
|
|
||||||
description: 'Configure the AI provider used by the mobile receipt scanner.',
|
|
||||||
icon: ScrollText,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -165,36 +220,46 @@ export default async function AdminLandingPage({
|
|||||||
}) {
|
}) {
|
||||||
const { portSlug } = await params;
|
const { portSlug } = await params;
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Administration"
|
title="Administration"
|
||||||
description="Per-port configuration and system administration. Each card below opens a dedicated settings page."
|
description="Per-port configuration and system administration. Each card below opens a dedicated settings page."
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
{GROUPS.map((group) => (
|
||||||
{SECTIONS.map((s) => {
|
<section key={group.title} className="space-y-3">
|
||||||
const Icon = s.icon;
|
<div>
|
||||||
return (
|
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
<Link
|
{group.title}
|
||||||
key={s.href}
|
</h2>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
<p className="text-xs text-muted-foreground/80">{group.description}</p>
|
||||||
href={`/${portSlug}/admin/${s.href}` as any}
|
</div>
|
||||||
className="block group"
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
>
|
{group.sections.map((s) => {
|
||||||
<Card className="h-full transition-colors group-hover:border-primary/50 group-hover:bg-muted/30">
|
const Icon = s.icon;
|
||||||
<CardHeader className="flex flex-row items-start gap-3 space-y-0 pb-2">
|
return (
|
||||||
<Icon className="h-5 w-5 mt-0.5 text-muted-foreground group-hover:text-primary" />
|
<Link
|
||||||
<div className="flex-1">
|
key={s.href}
|
||||||
<CardTitle className="text-base">{s.label}</CardTitle>
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
</div>
|
href={`/${portSlug}/admin/${s.href}` as any}
|
||||||
</CardHeader>
|
className="block group"
|
||||||
<CardContent>
|
>
|
||||||
<CardDescription>{s.description}</CardDescription>
|
<Card className="h-full transition-colors group-hover:border-primary/50 group-hover:bg-muted/30">
|
||||||
</CardContent>
|
<CardHeader className="flex flex-row items-start gap-3 space-y-0 pb-2">
|
||||||
</Card>
|
<Icon className="h-5 w-5 mt-0.5 text-muted-foreground group-hover:text-primary" />
|
||||||
</Link>
|
<div className="flex-1">
|
||||||
);
|
<CardTitle className="text-base">{s.label}</CardTitle>
|
||||||
})}
|
</div>
|
||||||
</div>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<CardDescription>{s.description}</CardDescription>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,12 +339,19 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* Mobile: icon-only with title tooltip + colored fill carries
|
||||||
|
the won/lost meaning (green vs rose). Adding a "Won" /
|
||||||
|
"Lost" text label inline blew out the cluster width and
|
||||||
|
forced the Email/Call/WhatsApp action-chip row above to
|
||||||
|
stack vertically — bad trade. From sm up, the full
|
||||||
|
"Mark won" / "Close as lost" labels read clearly. */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOutcomeDialog('won')}
|
onClick={() => setOutcomeDialog('won')}
|
||||||
aria-label="Mark as won"
|
aria-label="Mark as won"
|
||||||
|
title="Mark as won"
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
|
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-2.5',
|
||||||
'border border-emerald-200 bg-emerald-50 text-emerald-700',
|
'border border-emerald-200 bg-emerald-50 text-emerald-700',
|
||||||
'hover:bg-emerald-100',
|
'hover:bg-emerald-100',
|
||||||
)}
|
)}
|
||||||
@@ -356,8 +363,9 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOutcomeDialog('lost')}
|
onClick={() => setOutcomeDialog('lost')}
|
||||||
aria-label="Close as lost"
|
aria-label="Close as lost"
|
||||||
|
title="Close as lost"
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
|
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-2.5',
|
||||||
'border border-rose-200 text-rose-700',
|
'border border-rose-200 text-rose-700',
|
||||||
'hover:bg-rose-50',
|
'hover:bg-rose-50',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const MORE_ITEMS: MoreItem[] = [
|
|||||||
{ label: 'Companies', icon: Building2, segment: 'companies' },
|
{ label: 'Companies', icon: Building2, segment: 'companies' },
|
||||||
{ label: 'Invoices', icon: FileText, segment: 'invoices' },
|
{ label: 'Invoices', icon: FileText, segment: 'invoices' },
|
||||||
{ label: 'Expenses', icon: Receipt, segment: 'expenses' },
|
{ label: 'Expenses', icon: Receipt, segment: 'expenses' },
|
||||||
{ label: 'Email', icon: Mail, segment: 'email' },
|
{ label: 'Inbox', icon: Mail, segment: 'email' },
|
||||||
{ label: 'Alerts', icon: ShieldAlert, segment: 'alerts' },
|
{ label: 'Alerts', icon: ShieldAlert, segment: 'alerts' },
|
||||||
{ label: 'Reports', icon: BarChart3, segment: 'reports' },
|
{ label: 'Reports', icon: BarChart3, segment: 'reports' },
|
||||||
{ label: 'Reminders', icon: Bell, segment: 'reminders' },
|
{ label: 'Reminders', icon: Bell, segment: 'reminders' },
|
||||||
|
|||||||
Reference in New Issue
Block a user