'use client'; import { useMemo, useState } from 'react'; import Link from 'next/link'; import { Bell, BookOpen, Briefcase, Database, FileText, Globe, HardDrive, Inbox, Key, LayoutDashboard, Mail, Palette, ScrollText, Search, Settings, Shield, Sliders, Tag, Upload, Users, UsersRound, 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 group below. const GROUPS: AdminGroup[] = [ { title: 'Access', description: 'Who can sign in and what they can do once they do.', sections: [ { href: 'users', label: 'Users', description: 'CRM accounts, role assignments, and per-user residential access toggles.', icon: Users, keywords: [ 'accounts', 'staff', 'team', 'disable user', 'reset password', 'residential access', ], }, { 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, }, ], }, { title: 'Configuration', description: 'Branding, integrations, and per-port settings.', sections: [ { href: 'email', label: 'Email Settings', description: 'From address, signatures, and per-port SMTP overrides.', icon: Mail, }, { href: 'documenso', label: 'EOI signing service', description: 'API credentials, EOI template, and default in-app vs external signing 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, // 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', '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: 'webhooks', label: 'Webhooks', description: 'Outgoing webhook subscriptions, secrets, and delivery log.', icon: Webhook, }, ], }, { title: 'Content', description: 'Forms, templates, and labels that users see.', sections: [ { 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: 'email-templates', label: 'Email Templates', description: 'Customize subject lines for transactional emails (portal, inquiry, invite).', icon: Mail, }, { href: 'tags', label: 'Tags', description: 'Color-coded tags applied to clients, yachts, companies, and interests.', icon: Tag, }, { 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: 'custom-fields', label: 'Custom Fields', description: 'Tenant-defined fields for clients, yachts, and reservations.', icon: Key, }, ], }, { title: 'Data Quality', description: 'Cleanup, imports, and the audit trail.', 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: Mail, }, { 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, }, ], }, { title: 'Operations', description: 'Health checks and disaster recovery.', sections: [ { 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: 'Backup posture + retention policy (read-only).', icon: HardDrive, }, { href: 'storage', label: 'Storage Backend', description: 'Choose between S3-compatible object store or local filesystem; migrate between them.', icon: HardDrive, }, ], }, { title: 'Tenancy', description: 'Multi-port and multi-install scaffolding.', sections: [ { href: 'ports', label: 'Ports', description: 'Manage the marinas/ports this installation serves.', icon: Briefcase, }, { href: 'onboarding', label: 'Onboarding checklist', description: 'Step-by-step setup checklist for fresh ports — auto-detects what you’ve configured and lets you mark manual steps complete.', icon: LayoutDashboard, }, ], }, { title: 'Integrations', description: 'Third-party providers wired into the app.', sections: [ { href: 'ai', label: 'AI configuration', description: 'Master switch + provider credentials shared by every AI surface (OCR, berth-PDF parser, future recommender embeddings).', icon: ScrollText, keywords: ['openai', 'anthropic', 'gpt', 'claude', 'llm', 'api key', 'embeddings'], }, { href: 'ocr', label: 'Receipt OCR (per-feature)', description: 'Provider, model, and confidence thresholds for the receipt scanner.', icon: ScrollText, keywords: ['receipt', 'scan', 'tesseract', 'expense scanner', 'confidence'], }, { href: 'website-analytics', label: 'Website analytics (Umami)', description: 'Per-port Umami URL, API token, and Website ID.', icon: Globe, keywords: ['umami', 'analytics', 'traffic', 'visitors', 'marketing', 'pageviews'], }, { href: 'residential-stages', label: 'Residential pipeline stages', description: 'Configure stages residential interests flow through. Removing a stage with active interests prompts for reassignment.', icon: ScrollText, keywords: ['stages', 'pipeline', 'residential funnel', 'reassign'], }, ], }, ]; 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 = []; 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 (
setQuery(e.target.value)} className="h-9 pl-9 pr-9" /> {query ? ( ) : null}
{filteredMatches ? ( filteredMatches.length === 0 ? (

No settings match "{query}".

) : (

{filteredMatches.length} match{filteredMatches.length === 1 ? '' : 'es'}

{filteredMatches.map((s) => ( ))}
) ) : ( GROUPS.map((group) => (

{group.title}

{group.description}

{group.sections.map((s) => ( ))}
)) )}
); } function SectionCard({ portSlug, section, groupTitle, }: { portSlug: string; section: AdminSection; groupTitle?: string; }) { const Icon = section.icon; return (
{section.label} {groupTitle ? (

{groupTitle}

) : null}
{section.description}
); }