Files
pn-new-crm/src/components/admin/admin-sections-browser.tsx
Matt 31ba72f344 chore(launch-prep): hide unfinished report/import surfaces, defer big builds
Ship-what's-done prep ahead of the prod cutover (launch ~today):

- Hide Financial + Marketing report cards from the reports landing
  (both were "Builder in development" placeholders gated on unbuilt
  data sources). Sales/Operational/Custom + templates/scheduling/
  exports remain live.
- Trim the Custom-report card copy to match the shipped basic builder
  (no group-by/filters yet; the builder page header was already honest).
- Hide the Bulk Import mockup from search-nav-catalog + the admin
  sections browser; /admin/import is now unreachable from the UI.
- Correct client-facing doc over-claims (waiting-list "next-in-line
  notification", Import) in features-list.md + new-system-feature-summary.md.
- Un-stale BACKLOG.md (Documenso phases 2-7 confirmed shipped).
- Log decisions + deferred work (full importer, full custom-builder,
  waiting-list, maintenance-log, paper-upload bug) to launch-readiness.md.

Deferred-importer design spec added at
docs/superpowers/specs/2026-06-01-bulk-import-design.md.

Verified: tsc --noEmit clean, eslint clean on changed files,
1512/1519 vitest pass (7 failures are Redis-down, unrelated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:39:51 +02:00

565 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useMemo, useState } from 'react';
import Link from 'next/link';
import {
Activity,
AlertCircle,
Anchor,
BellRing,
BookMarked,
BookOpen,
ClipboardList,
CopyCheck,
DatabaseBackup,
FilePen,
FileSignature,
FileText,
GitBranch,
Home,
Inbox,
ListChecks,
Mail,
Paintbrush,
ScrollText,
Search,
Send,
Server,
Settings,
Shield,
Ship,
SlidersHorizontal,
Sparkles,
Tag,
TrendingUp,
Users,
Webhook,
X,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface AdminSection {
href: string;
label: string;
description: string;
icon: LucideIcon;
/**
* Free-text aliases the search input also matches against. Use this to
* surface the inner setting keys exposed *inside* a section card so
* typing "client portal" finds the System Settings card that hosts
* `client_portal_enabled`, not just labels that literally contain the
* phrase.
*/
keywords?: string[];
}
interface AdminGroup {
title: string;
description: string;
sections: AdminSection[];
}
// Catalog lives inside the client component so React Server Components don't
// need to serialize the lucide icon factories (functions / $$typeof refs)
// across the RSC boundary - that crash was the error the rep hit when the
// catalog lived in the server-component `page.tsx`. Adding a new admin
// surface? Append it to the matching domain below.
//
// 7-domain IA regrouped 2026-05-22 per docs/admin-ia-proposal.md:
// 1. Brand & Communication - how outbound looks
// 2. Sales workflow - pipeline behaviour + templates
// 3. Catalog - tenant-defined data shapes
// 4. Identity & access - who can use the system
// 5. Inbox & data quality - admin queues + cleanup
// 6. Integrations - external systems + providers
// 7. System & observability - infra + diagnostics + escape hatches
//
// Pages deleted in the regroup (redirect-shimmed): /admin/ocr (dup of /ai),
// /admin/invitations (merged into /users 2026-05-21), /admin/reports
// (duplicated dashboard widgets). See docs/admin-ia-proposal.md §8.
const GROUPS: AdminGroup[] = [
{
title: 'Brand & Communication',
description: 'How outbound looks and which channels it ships on.',
sections: [
{
href: 'branding',
label: 'Branding',
description: 'App name, logo, primary color, and email header/footer HTML.',
icon: Paintbrush,
keywords: ['logo', 'colors', 'email shell', 'preview', 'test email'],
},
{
href: 'email',
label: 'Email Settings',
description:
'SMTP credentials (noreply + sales), per-template tester, routing rules, and the connectivity probe.',
icon: Mail,
keywords: ['smtp', 'noreply', 'sales mailbox', 'test send', 'routing'],
},
{
href: 'email-templates',
label: 'Email Templates',
description:
'Subject-line + body overrides for transactional emails (portal, inquiry, invite).',
icon: FilePen,
},
],
},
{
title: 'Sales workflow',
description: 'How the pipeline behaves - triggers, scoring, document + form templates.',
sections: [
{
href: 'pipeline-rules',
label: 'Pipeline rules',
description:
'Berth-rules engine triggers + per-event auto-advance (EOI signed, deposit received, contract signed).',
icon: GitBranch,
keywords: ['pipeline', 'auto-advance', 'stage rules', 'aggressive', 'conservative'],
},
{
href: 'pulse',
label: 'Deal Pulse',
description:
'Configure the chip on every interest header - master toggle, per-signal toggles, and tier-label overrides.',
icon: Activity,
keywords: ['pulse', 'deal-health', 'health chip', 'hot warm cold'],
},
{
href: 'reminders',
label: 'Reminders',
description: 'Default reminder behaviour and the daily-digest delivery window.',
icon: BellRing,
},
{
href: 'qualification-criteria',
label: 'Qualification criteria',
description:
'Checklist reps complete to qualify an enquiry. Enable/disable/reorder per port; affects the "ready to qualify" hint on the interest detail.',
icon: ListChecks,
keywords: ['qualification', 'criteria', 'checklist', 'qualify', 'enquiry', 'sales gate'],
},
{
href: 'residential-stages',
label: 'Residential pipeline stages',
description:
'Configure stages residential interests flow through. Removing a stage with active interests prompts for reassignment.',
icon: Home,
keywords: ['stages', 'pipeline', 'residential funnel', 'reassign'],
},
{
href: 'forms',
label: 'Forms',
description: 'Form templates used by client-facing inquiry and intake flows.',
icon: ClipboardList,
},
{
href: 'templates',
label: 'Document Templates',
description:
'PDF + email templates with merge-field placeholders - EOI, reservation, contract.',
icon: FileText,
},
],
},
{
title: 'Catalog',
description: 'Tenant-defined data shapes that attach to records.',
sections: [
{
href: 'vocabularies',
label: 'Vocabularies',
description:
'Per-port pick lists used across the CRM: interest temperatures, status reasons, tenure types, expense categories, document types.',
icon: BookOpen,
},
{
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 tenancies.',
icon: SlidersHorizontal,
},
{
href: 'brochures',
label: 'Brochures',
description: 'Per-port versioned brochure PDFs distributed through the public site + reps.',
icon: BookMarked,
},
],
},
{
title: 'Identity & access',
description: 'Who can sign in and what they can do once they do.',
sections: [
{
href: 'users',
label: 'Users',
description:
'Active CRM accounts + pending invitations (merged tabs), role assignments, residential access toggles.',
icon: Users,
keywords: [
'accounts',
'staff',
'team',
'invitations',
'invite',
'disable user',
'reset password',
'residential access',
],
},
{
href: 'roles',
label: 'Roles & Permissions',
description: 'Default permission sets and per-port role overrides.',
icon: Shield,
},
{
href: 'ports',
label: 'Ports',
description: 'Manage the marinas/ports this installation serves (super-admin only).',
icon: Ship,
},
],
},
{
title: 'Inbox & data quality',
description: 'Admin queues for inbound submissions + cleanup tooling.',
sections: [
{
href: 'inquiries',
label: 'Inquiry Inbox',
description:
'Submissions captured from the public marketing site (berth, residence, contact).',
icon: Inbox,
},
{
href: 'sends',
label: 'Send Log',
description: 'Brochure and per-berth PDF sends, with delivery failures surfaced for retry.',
icon: Send,
},
{
href: 'duplicates',
label: 'Duplicates',
description: 'Review queue of suspected duplicate clients flagged by the dedup engine.',
icon: CopyCheck,
},
{
href: 'berths',
label: 'Berths admin',
description:
'Bulk-add new berths in one wizard + reconcile berths missing required fields.',
icon: Anchor,
keywords: ['bulk add', 'reconcile', 'berth pdf', 'mooring', 'bulk berths'],
},
],
},
{
title: 'Integrations',
description: 'External systems + provider configuration.',
sections: [
{
href: 'documenso',
label: 'Signing service (Documenso)',
description:
'API credentials, signer identities, templates, and signing-behaviour for every document the CRM puts out for signature.',
icon: FileSignature,
keywords: ['documenso', 'eoi', 'signing', 'envelope', 'signer', 'embedded'],
},
{
href: 'webhooks',
label: 'Webhooks',
description: 'Outgoing webhook subscriptions, secrets, and delivery log.',
icon: Webhook,
},
{
href: 'website-analytics',
label: 'Website analytics (Umami)',
description: 'Per-port Umami URL, API token, and Website ID.',
icon: TrendingUp,
keywords: ['umami', 'analytics', 'traffic', 'visitors', 'marketing', 'pageviews'],
},
{
href: 'ai',
label: 'AI configuration',
description:
'One panel for every AI feature: master switch, provider credentials, OCR settings, interest scoring, email-drafts, recommender controls.',
icon: Sparkles,
keywords: [
'openai',
'anthropic',
'gpt',
'claude',
'llm',
'api key',
'embeddings',
'receipt',
'scan',
'tesseract',
'ocr',
'expense scanner',
'interest scoring',
'email drafts',
],
},
],
},
{
title: 'System & observability',
description: 'Infra, observability, escape hatches.',
sections: [
{
href: 'audit',
label: 'Audit Log',
description: 'Searchable log of every authenticated mutation in the system.',
icon: ScrollText,
},
{
href: 'monitoring',
label: 'Queue Monitoring',
description: 'BullMQ queue health, throughput, and retry diagnostics.',
icon: Activity,
},
{
href: 'errors',
label: 'Errors',
description:
'Recent server-side error events with request-id drill-down, plus the error-code catalog.',
icon: AlertCircle,
keywords: ['exceptions', 'request id', 'stack trace', 'error code'],
},
{
href: 'backup',
label: 'Backup & Restore',
description: 'Backup posture + retention policy (read-only).',
icon: DatabaseBackup,
},
{
href: 'storage',
label: 'Storage Backend',
description:
'Choose between S3-compatible object store or local filesystem; migrate between them.',
icon: Server,
},
{
href: 'settings',
label: 'System Settings',
description: 'Generic key/value configuration store for advanced flags (escape hatch).',
icon: Settings,
// Mirrors KNOWN_SETTINGS in settings-manager.tsx so a search for any
// individual flag jumps straight to this card. Keep in sync when
// adding new keys there.
keywords: [
'client portal',
'client portal enabled',
'tenancies',
'tenancies module',
'tenancy',
'tenancy tracker',
'lease',
'lease windows',
'renewals',
'transfers',
'ai',
'ai interest scoring',
'ai email drafts',
'invoice net10 discount',
'net-10',
'pipeline weights',
'pipeline stage weights',
'forecast',
'berth rules',
'berth status rules',
'inquiry contact email',
'inquiry notification recipients',
'residential notification recipients',
'eoi signers',
'developer',
'approver',
'countersign',
'recommender max oversize',
'recommender top n',
'recommender default count',
'fallthrough policy',
'fallthrough cooldown',
'heat weight recency',
'heat weight furthest stage',
'heat weight interest count',
'heat weight eoi count',
'tier ladder',
'hide late stage',
'documents show expired tab',
'expired tab',
'berths default currency',
'default currency',
],
},
{
href: 'onboarding',
label: 'Onboarding checklist',
description:
'Step-by-step setup checklist for fresh ports - auto-detects what youve configured and lets you mark manual steps complete.',
icon: ListChecks,
},
],
},
];
interface AdminSectionsBrowserProps {
portSlug: string;
}
/**
* Searchable index of admin settings cards. The unfiltered view renders the
* grouped grid (Access / Configuration / Content / …); typing in the search
* input collapses every section into a flat result list of matching cards.
*
* Match is substring against label + description + group title so a search
* for "tax" finds Document Templates (description mentions tax-id mergefield)
* as well as the literal "Tax ID" field.
*/
export function AdminSectionsBrowser({ portSlug }: AdminSectionsBrowserProps) {
const [query, setQuery] = useState('');
const q = query.trim().toLowerCase();
const filteredMatches = useMemo(() => {
if (!q) return null;
const matches: Array<AdminSection & { groupTitle: string }> = [];
for (const g of GROUPS) {
for (const s of g.sections) {
const hay = [s.label, s.description, g.title, ...(s.keywords ?? [])]
.join(' ')
.toLowerCase();
if (hay.includes(q)) matches.push({ ...s, groupTitle: g.title });
}
}
return matches;
}, [q]);
return (
<div className="space-y-6">
<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"
aria-hidden
/>
<Input
type="search"
inputMode="search"
placeholder="Search settings…"
aria-label="Search admin settings"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="h-9 pl-9 pr-9"
/>
{query ? (
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setQuery('')}
className="absolute right-1 top-1 h-7 w-7"
aria-label="Clear search"
>
<X className="h-3.5 w-3.5" />
</Button>
) : null}
</div>
{filteredMatches ? (
filteredMatches.length === 0 ? (
<p className="rounded-md border border-dashed py-8 text-center text-sm text-muted-foreground">
No settings match &quot;{query}&quot;.
</p>
) : (
<div className="space-y-3">
<p className="text-xs uppercase tracking-wider text-muted-foreground">
{filteredMatches.length} match{filteredMatches.length === 1 ? '' : 'es'}
</p>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{filteredMatches.map((s) => (
<SectionCard
key={`${s.href}-${s.groupTitle}`}
portSlug={portSlug}
section={s}
groupTitle={s.groupTitle}
/>
))}
</div>
</div>
)
) : (
GROUPS.map((group) => (
<section key={group.title} className="space-y-3">
<div>
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{group.title}
</h2>
<p className="text-xs text-muted-foreground/80">{group.description}</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{group.sections.map((s) => (
<SectionCard key={s.href} portSlug={portSlug} section={s} />
))}
</div>
</section>
))
)}
</div>
);
}
function SectionCard({
portSlug,
section,
groupTitle,
}: {
portSlug: string;
section: AdminSection;
groupTitle?: string;
}) {
const Icon = section.icon;
return (
<Link
// eslint-disable-next-line @typescript-eslint/no-explicit-any
href={`/${portSlug}/admin/${section.href}` as any}
className="block group"
>
<Card
className={cn(
'h-full transition-colors group-hover:border-primary/50 group-hover:bg-muted/30',
)}
>
<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"
aria-hidden
/>
<div className="flex-1">
<CardTitle className="text-base">{section.label}</CardTitle>
{groupTitle ? (
<p className="text-xs uppercase tracking-wider text-muted-foreground">{groupTitle}</p>
) : null}
</div>
</CardHeader>
<CardContent>
<CardDescription>{section.description}</CardDescription>
</CardContent>
</Card>
</Link>
);
}