Files
pn-new-crm/src/lib/services/search-nav-catalog.ts

510 lines
15 KiB
TypeScript
Raw Normal View History

/**
* Static catalog of navigation destinations the global search bar can jump
* to: settings pages, admin panels, and top-level dashboards.
*
* Each entry has an `href` template (run through `resolveHref` with the
* current portSlug), a human label, and a list of keyword aliases. The
* search service substring-matches the query against the label + every
* keyword, so `smtp` lands on the email settings page even though the
* label reads "Email accounts".
*
* Why hardcoded vs introspecting routes? The catalog is curated - only
* pages worth jumping to from a global search appear here, and each
* entry has hand-picked keyword synonyms that route inference can't
* derive. Adding a route to the catalog is cheap; misfiring routes are
* expensive.
*/
import type { RolePermissions } from '@/lib/db/schema/users';
export type NavCatalogCategory = 'settings' | 'admin' | 'dashboard';
export interface NavCatalogEntry {
/** Path template - `:portSlug` is substituted at lookup time. */
href: string;
label: string;
category: NavCatalogCategory;
/** Lowercase aliases - query is matched against label + these. */
keywords: string[];
/**
* Permission gate; only shown to users whose `RolePermissions` resolves
* truthy at the given dot-path (e.g. `'admin.manage_users'`). Super
* admins bypass the gate.
*/
requires?: string;
/** When set, only super admins see the entry. */
superAdminOnly?: boolean;
}
export const NAV_CATALOG: NavCatalogEntry[] = [
// ─── Dashboards ─────────────────────────────────────────────────────────
{
href: '/:portSlug/dashboard',
label: 'Dashboard',
category: 'dashboard',
keywords: ['home', 'overview', 'kpis', 'metrics'],
},
{
href: '/:portSlug/website-analytics',
label: 'Website analytics',
category: 'dashboard',
keywords: ['umami', 'traffic', 'visitors', 'pageviews', 'marketing'],
},
// ─── Settings ───────────────────────────────────────────────────────────
{
href: '/:portSlug/settings',
label: 'Settings',
category: 'settings',
keywords: ['preferences', 'configuration', 'config'],
},
// The granular settings cards below redirect to the `/admin/<x>` routes
// that actually exist - the catalog previously listed `/settings/<x>`
// paths that have never had route folders. We keep the keyword aliases
// so the cmd-K search still finds them under the right destination.
{
href: '/:portSlug/admin/email',
label: 'Email accounts (SMTP / IMAP)',
category: 'settings',
keywords: [
'smtp',
'imap',
'mail',
'mail server',
'email credentials',
'send-from',
'inbox',
'bounces',
],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/branding',
label: 'Branding (per-port logo, colors, copy)',
category: 'settings',
keywords: ['logo', 'theme', 'colors', 'tenant brand', 'white-label'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/templates',
label: 'Document templates',
category: 'settings',
fix(uat-batch-1): wave-1 blocker bugs — supplemental gate, file FK, downloads, search dedup, notes stale, expense form, vocab Surgical fixes for the 7 UAT blockers that prevent productive forward testing. Each item has a corresponding entry in alpha-uat-master.md. - supplemental-info route relocated out of (portal) so it bypasses the isPortalDisabledGlobally() kill-switch. URL unchanged. - file upload service derives client_id/company_id/yacht_id from (entityType, entityId) when not explicitly passed, so interest-tab uploads no longer land with client_id=NULL and stay visible in the Attachments list. - triggerBlobDownload / triggerUrlDownload helpers in src/lib/utils attach the anchor to the DOM before click so Chromium honours the download attribute; 7 sites refactored, file-named downloads stop arriving as bare UUIDs. - search-nav-catalog dedupes by href at the result-collection layer so the same href can no longer surface twice in the command-K dropdown (kills the React duplicate-key warning); /admin/templates entries merged into a single richer-keyword variant. - NotesList gains a parentInvalidateKey prop, wired through all five callers (interest, client, yacht, company, residential client/ interest) so the Overview "Latest note" teaser refreshes when a note is added in the Notes tab. - expense-form-dialog: setValue('receiptFileIds') / setValue( 'noReceiptAcknowledged') on upload/clear/checkbox so the schema-level refine sees the field and Create stops silently no-op'ing on submit. - bulk-add-berths-wizard: side-pontoon dropdown now reads through useVocabulary('berth_side_pontoon_options') instead of a wrong local enum ('Port', 'Starboard', 'Bow', 'Stern') — wizard data now matches the rest of the platform + honours admin-editable per-port overrides. tsc clean. 1419/1419 vitest. lint clean on touched files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:50:58 +02:00
keywords: [
'eoi',
'documenso',
'pdf templates',
'email templates',
'template merge fields',
'merge fields',
'eoi template',
],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/storage',
label: 'File storage backend',
category: 'settings',
keywords: ['s3', 'minio', 'filesystem', 'storage'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/settings',
label: 'Berth recommender weights',
category: 'settings',
keywords: ['ranking', 'tier ladder', 'heat', 'fallthrough', 'recommend'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/tags',
label: 'Tags',
category: 'settings',
keywords: ['labels', 'categories', 'classification'],
},
{
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
href: '/:portSlug/settings',
label: 'Notification preferences',
category: 'settings',
keywords: ['alerts', 'email digest', 'in-app', 'push', 'reminders digest'],
},
feat(admin+search): user-mgmt polish, role labels, search keyword index Admin search now matches against per-card keyword lists so typing "client portal", "smtp", "tier ladder" lands on the System Settings card (which hosts those flags). The same keyword list extends the topbar global search (NAV_CATALOG) so any setting key resolves from the cmd-K input — settings results sort to the bottom of the dropdown beneath entity hits. User management: - Third action button (Power/PowerOff) enables/disables sign-in from the desktop list; mobile card dropdown gains the same item. Backed by the existing userProfiles.isActive flag — withAuth already refuses disabled sessions with 403. - UserForm collects first + last name (canonical) alongside displayName, with admin email-change behind a confirmation modal. On confirm we send the OLD address an automated "your admin changed your sign-in email" notice (new template at admin-email-change.ts) and rewrite the Better Auth user row. - Phone field swaps the bare tel input for the shared PhoneInput (country combobox + AsYouType formatting + E.164 storage). - "Manage permissions" link points to /admin/roles?focusUser=… as a stepping stone for the future fine-tuned-permissions UI. Role names normalize through a new ROLE_LABELS + formatRole() helper in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the prettifyRoleName in role-list; user-list and user-card now render "Sales Agent" instead of "sales_agent". Custom roles pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:14:12 +02:00
{
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
href: '/:portSlug/settings',
feat(admin+search): user-mgmt polish, role labels, search keyword index Admin search now matches against per-card keyword lists so typing "client portal", "smtp", "tier ladder" lands on the System Settings card (which hosts those flags). The same keyword list extends the topbar global search (NAV_CATALOG) so any setting key resolves from the cmd-K input — settings results sort to the bottom of the dropdown beneath entity hits. User management: - Third action button (Power/PowerOff) enables/disables sign-in from the desktop list; mobile card dropdown gains the same item. Backed by the existing userProfiles.isActive flag — withAuth already refuses disabled sessions with 403. - UserForm collects first + last name (canonical) alongside displayName, with admin email-change behind a confirmation modal. On confirm we send the OLD address an automated "your admin changed your sign-in email" notice (new template at admin-email-change.ts) and rewrite the Better Auth user row. - Phone field swaps the bare tel input for the shared PhoneInput (country combobox + AsYouType formatting + E.164 storage). - "Manage permissions" link points to /admin/roles?focusUser=… as a stepping stone for the future fine-tuned-permissions UI. Role names normalize through a new ROLE_LABELS + formatRole() helper in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the prettifyRoleName in role-list; user-list and user-card now render "Sales Agent" instead of "sales_agent". Custom roles pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:14:12 +02:00
label: 'My profile & preferences',
category: 'settings',
keywords: [
'profile',
'avatar',
'display name',
'full name',
'phone',
'timezone',
'locale',
'country',
'dark mode',
'theme',
'password',
'change email',
'security',
'account',
'me',
],
},
{
href: '/:portSlug/dashboard?customize=1',
label: 'Customize dashboard widgets',
category: 'settings',
keywords: ['widgets', 'tiles', 'dashboard layout', 'kpi', 'reorder widgets'],
},
// ─── Admin ──────────────────────────────────────────────────────────────
{
href: '/:portSlug/admin',
label: 'Administration',
category: 'admin',
keywords: ['admin'],
requires: 'admin.manage_users',
},
{
href: '/:portSlug/admin/users',
label: 'Users & roles',
category: 'admin',
feat(uat-batch): Groups F + G + H — DocsHub/signing + admin consolidation + email F27–F29, G30, G31, H32, H33 from the 2026-05-21 plan. Shipped now: F28 Past-milestones expandable history. The Past strip on the Interest overview becomes an <Accordion> — each row collapses to the same one-line summary as before, expands to render the full <MilestoneSection> (steps list, sub-status, inline doc actions). Reuses the existing MilestoneSection so no new per-milestone rendering needs to be maintained. F29 Watchers configurable at document creation time. The unified create-document wizard gets a Watchers section with a multi-select checkbox list backed by /api/v1/admin/users/picker. Selected user ids are sent in the `watchers` array on the POST (replacing the prior hardcoded `[]`). UI matches the post-creation WatchersCard so reps see the same identity rows regardless of entry point. G30 /admin/invitations merged into /admin/users. The Users page now wraps the existing UserList + InvitationsManager in a Tabs control (Active users / Invitations). The standalone /admin/invitations route returns a redirect to the merged page for bookmark back-compat. Removed nav catalog entry + admin-sections-browser tile; extended the Users catalog keywords with "invitations / pending invites / onboarding" so command-K search still lands on the right surface. G31 /admin/ai picks up the berth-PDF-parser section + a "planned AI surfaces" placeholder. Berth PDF parser remains env-configured today; the page now documents it so admins don't hunt for the controls. Closes the "where do I configure AI?" loop. H32 Email settings explainer panel above the SMTP cards. Spells out why noreply + sales have separate credentials and which workflows ship from each mailbox. Existing field titles gained the "(noreply)" suffix so the model maps cleanly. H33 Supplemental-info-request email rebuilt to use the shared branded shell (logo + blurred overhead background + max- width 600 table layout) instead of the prior plain-HTML page. Per-port branding (logo / primary color / background / header / footer) flows from getPortBrandingConfig. CTA button picks up the port's primary color. Already shipped (verified pre-shipped): F27 DocumentsHub root view already hides the breadcrumb via `selectedFolderId !== undefined` conditional. Verified: tsc clean, vitest 1454/1454. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:40:48 +02:00
keywords: [
'accounts',
'permissions',
'invites',
'invitations',
'pending invites',
'onboarding',
'team',
'staff',
'roles',
],
requires: 'admin.manage_users',
},
{
href: '/:portSlug/admin/audit',
label: 'Audit log',
category: 'admin',
keywords: ['activity', 'history', 'events', 'who did what', 'compliance'],
requires: 'admin.view_audit_log',
},
{
href: '/:portSlug/admin/berths',
label: 'Berths admin',
category: 'admin',
keywords: ['bulk add berths', 'reconcile berths', 'berth pdf', 'mooring', 'bulk'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/inquiries',
label: 'Website inquiries inbox',
category: 'admin',
keywords: ['enquiries', 'leads', 'contact form', 'eoi requests', 'website'],
},
{
href: '/:portSlug/admin/errors',
label: 'Platform errors',
category: 'admin',
keywords: ['errors', 'exceptions', 'incidents', 'failures'],
superAdminOnly: true,
},
feat(admin+search): user-mgmt polish, role labels, search keyword index Admin search now matches against per-card keyword lists so typing "client portal", "smtp", "tier ladder" lands on the System Settings card (which hosts those flags). The same keyword list extends the topbar global search (NAV_CATALOG) so any setting key resolves from the cmd-K input — settings results sort to the bottom of the dropdown beneath entity hits. User management: - Third action button (Power/PowerOff) enables/disables sign-in from the desktop list; mobile card dropdown gains the same item. Backed by the existing userProfiles.isActive flag — withAuth already refuses disabled sessions with 403. - UserForm collects first + last name (canonical) alongside displayName, with admin email-change behind a confirmation modal. On confirm we send the OLD address an automated "your admin changed your sign-in email" notice (new template at admin-email-change.ts) and rewrite the Better Auth user row. - Phone field swaps the bare tel input for the shared PhoneInput (country combobox + AsYouType formatting + E.164 storage). - "Manage permissions" link points to /admin/roles?focusUser=… as a stepping stone for the future fine-tuned-permissions UI. Role names normalize through a new ROLE_LABELS + formatRole() helper in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the prettifyRoleName in role-list; user-list and user-card now render "Sales Agent" instead of "sales_agent". Custom roles pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:14:12 +02:00
// ─── Admin → granular section cards (the AdminSectionsBrowser groups) ────
// These deep-link to specific admin sub-pages. Each one's `keywords`
// mirrors the corresponding entry in src/components/admin/
// admin-sections-browser.tsx - so typing a setting key in the topbar
feat(admin+search): user-mgmt polish, role labels, search keyword index Admin search now matches against per-card keyword lists so typing "client portal", "smtp", "tier ladder" lands on the System Settings card (which hosts those flags). The same keyword list extends the topbar global search (NAV_CATALOG) so any setting key resolves from the cmd-K input — settings results sort to the bottom of the dropdown beneath entity hits. User management: - Third action button (Power/PowerOff) enables/disables sign-in from the desktop list; mobile card dropdown gains the same item. Backed by the existing userProfiles.isActive flag — withAuth already refuses disabled sessions with 403. - UserForm collects first + last name (canonical) alongside displayName, with admin email-change behind a confirmation modal. On confirm we send the OLD address an automated "your admin changed your sign-in email" notice (new template at admin-email-change.ts) and rewrite the Better Auth user row. - Phone field swaps the bare tel input for the shared PhoneInput (country combobox + AsYouType formatting + E.164 storage). - "Manage permissions" link points to /admin/roles?focusUser=… as a stepping stone for the future fine-tuned-permissions UI. Role names normalize through a new ROLE_LABELS + formatRole() helper in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the prettifyRoleName in role-list; user-list and user-card now render "Sales Agent" instead of "sales_agent". Custom roles pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:14:12 +02:00
// global search finds the same card the in-admin search would.
{
href: '/:portSlug/admin/settings',
label: 'System Settings',
category: 'admin',
keywords: [
feat(launch-readiness-batch): UAT drains, navigation refactor, launch infra, trackers Bundles the rest of the in-flight work from this UAT round into one checkpoint. Each sub-area is independent; see the headings below. UAT polish (drained 11 findings from active-uat.md): - Dialog primitive default bumped sm:max-w-xl/lg:max-w-3xl → sm:max-w-2xl/lg:max-w-4xl so multi-field forms + PDF previews aren't cramped at 1440-1920px. - Notes tab badge aggregation: new countFor{Client,Yacht,Company} Aggregated helpers in notes.service mirror the listFor*Aggregated symmetric-reach joins. yacht-tabs + company-tabs render the badge; client-tabs already had badge support. - Supplemental-info form polish bundle: BrandedAuthShell gains a `width: 'sm' | 'md'` prop (md uses min-h-dvh scroll instead of fixed inset-0 pin so long forms scroll naturally). Form picks up port branding (logoUrl + backgroundUrl + appName) via loadByToken. Address fields completed (street + city + region + postal + country). Port name eyebrow + success-state copy added. - new-document-menu Upload-file landing toast: per-file completion emits toast.success with action link to the destination entity or folder. - interest-tabs OverviewTab "from client" pill on Email + Phone rows via new EditableRow `inheritedFrom` prop. - create-document-wizard subject picker → segmented button strip (5 types visible at once). Launch infra: - UTM column wiring (Init 1b step 4): migration 0089_website_submissions_utm.sql adds utm_source/medium/campaign/ term/content + composite index (port_id, utm_source, received_at) for per-campaign rollups. website-inquiries intake accepts the five fields. Residential intake intentionally untouched per audit scope. - Invoicing module gate (Init 1c spike): new invoices-module.service + invoices layout guard + registry entry invoices_module_enabled (default false). Audit conclusion in launch-readiness.md: payments table is canonical money path; /invoices flow is parallel infrastructure now hidden by default. Smart-back navigation refactor: - Replaced breadcrumb component with history-aware Back button. New route-labels.ts + use-smart-back hook + navigation-history-tracker so back falls through to the parent route when there's no prior page in history. - Sidebar / topbar / mobile-topbar adopt the new pattern; old breadcrumb-store kept for back-compat consumers but the breadcrumbs component is gone. - 6 detail pages (admin/errors per-id + codes, invoices/ upload-receipts, reports kind, tenancies detail, analytics metric, client detail) migrated. Trackers + docs: - docs/launch-readiness.md — master pre-launch tracker. Includes the reports gap audit (cross-cutting filter set, Marketing + Financial blockers, custom builder remaining entities, scheduled CSV/XLSX, template scope picker). - docs/superpowers/audits/active-uat.md — 15 findings flipped OPEN → SHIPPED locally with fix-applied notes; 4 OPEN remaining (each blocked on user input or cross-repo). - CLAUDE.md — minor session notes carried forward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:42:37 +02:00
'feature flags',
'feature flag',
feat(admin+search): user-mgmt polish, role labels, search keyword index Admin search now matches against per-card keyword lists so typing "client portal", "smtp", "tier ladder" lands on the System Settings card (which hosts those flags). The same keyword list extends the topbar global search (NAV_CATALOG) so any setting key resolves from the cmd-K input — settings results sort to the bottom of the dropdown beneath entity hits. User management: - Third action button (Power/PowerOff) enables/disables sign-in from the desktop list; mobile card dropdown gains the same item. Backed by the existing userProfiles.isActive flag — withAuth already refuses disabled sessions with 403. - UserForm collects first + last name (canonical) alongside displayName, with admin email-change behind a confirmation modal. On confirm we send the OLD address an automated "your admin changed your sign-in email" notice (new template at admin-email-change.ts) and rewrite the Better Auth user row. - Phone field swaps the bare tel input for the shared PhoneInput (country combobox + AsYouType formatting + E.164 storage). - "Manage permissions" link points to /admin/roles?focusUser=… as a stepping stone for the future fine-tuned-permissions UI. Role names normalize through a new ROLE_LABELS + formatRole() helper in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the prettifyRoleName in role-list; user-list and user-card now render "Sales Agent" instead of "sales_agent". Custom roles pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:14:12 +02:00
'client portal',
'client portal enabled',
feat(launch-readiness-batch): UAT drains, navigation refactor, launch infra, trackers Bundles the rest of the in-flight work from this UAT round into one checkpoint. Each sub-area is independent; see the headings below. UAT polish (drained 11 findings from active-uat.md): - Dialog primitive default bumped sm:max-w-xl/lg:max-w-3xl → sm:max-w-2xl/lg:max-w-4xl so multi-field forms + PDF previews aren't cramped at 1440-1920px. - Notes tab badge aggregation: new countFor{Client,Yacht,Company} Aggregated helpers in notes.service mirror the listFor*Aggregated symmetric-reach joins. yacht-tabs + company-tabs render the badge; client-tabs already had badge support. - Supplemental-info form polish bundle: BrandedAuthShell gains a `width: 'sm' | 'md'` prop (md uses min-h-dvh scroll instead of fixed inset-0 pin so long forms scroll naturally). Form picks up port branding (logoUrl + backgroundUrl + appName) via loadByToken. Address fields completed (street + city + region + postal + country). Port name eyebrow + success-state copy added. - new-document-menu Upload-file landing toast: per-file completion emits toast.success with action link to the destination entity or folder. - interest-tabs OverviewTab "from client" pill on Email + Phone rows via new EditableRow `inheritedFrom` prop. - create-document-wizard subject picker → segmented button strip (5 types visible at once). Launch infra: - UTM column wiring (Init 1b step 4): migration 0089_website_submissions_utm.sql adds utm_source/medium/campaign/ term/content + composite index (port_id, utm_source, received_at) for per-campaign rollups. website-inquiries intake accepts the five fields. Residential intake intentionally untouched per audit scope. - Invoicing module gate (Init 1c spike): new invoices-module.service + invoices layout guard + registry entry invoices_module_enabled (default false). Audit conclusion in launch-readiness.md: payments table is canonical money path; /invoices flow is parallel infrastructure now hidden by default. Smart-back navigation refactor: - Replaced breadcrumb component with history-aware Back button. New route-labels.ts + use-smart-back hook + navigation-history-tracker so back falls through to the parent route when there's no prior page in history. - Sidebar / topbar / mobile-topbar adopt the new pattern; old breadcrumb-store kept for back-compat consumers but the breadcrumbs component is gone. - 6 detail pages (admin/errors per-id + codes, invoices/ upload-receipts, reports kind, tenancies detail, analytics metric, client detail) migrated. Trackers + docs: - docs/launch-readiness.md — master pre-launch tracker. Includes the reports gap audit (cross-cutting filter set, Marketing + Financial blockers, custom builder remaining entities, scheduled CSV/XLSX, template scope picker). - docs/superpowers/audits/active-uat.md — 15 findings flipped OPEN → SHIPPED locally with fix-applied notes; 4 OPEN remaining (each blocked on user input or cross-repo). - CLAUDE.md — minor session notes carried forward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:42:37 +02:00
'tenancies',
'tenancies module',
'tenancy',
'tenancy tracker',
'lease',
'lease windows',
'renewals',
'transfers',
'expenses',
'expenses module',
'receipts',
'expense receipts',
'ai',
feat(admin+search): user-mgmt polish, role labels, search keyword index Admin search now matches against per-card keyword lists so typing "client portal", "smtp", "tier ladder" lands on the System Settings card (which hosts those flags). The same keyword list extends the topbar global search (NAV_CATALOG) so any setting key resolves from the cmd-K input — settings results sort to the bottom of the dropdown beneath entity hits. User management: - Third action button (Power/PowerOff) enables/disables sign-in from the desktop list; mobile card dropdown gains the same item. Backed by the existing userProfiles.isActive flag — withAuth already refuses disabled sessions with 403. - UserForm collects first + last name (canonical) alongside displayName, with admin email-change behind a confirmation modal. On confirm we send the OLD address an automated "your admin changed your sign-in email" notice (new template at admin-email-change.ts) and rewrite the Better Auth user row. - Phone field swaps the bare tel input for the shared PhoneInput (country combobox + AsYouType formatting + E.164 storage). - "Manage permissions" link points to /admin/roles?focusUser=… as a stepping stone for the future fine-tuned-permissions UI. Role names normalize through a new ROLE_LABELS + formatRole() helper in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the prettifyRoleName in role-list; user-list and user-card now render "Sales Agent" instead of "sales_agent". Custom roles pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:14:12 +02:00
'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',
],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/branding',
label: 'Branding',
category: 'admin',
keywords: ['logo', 'app name', 'theme', 'colors', 'email header', 'white-label'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/email',
label: 'Email settings',
category: 'admin',
keywords: ['smtp', 'imap', 'mail', 'from address', 'signature', 'mail server'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/documenso',
label: 'EOI signing service',
category: 'admin',
keywords: ['documenso', 'signing', 'eoi', 'api credentials', 'webhook secret'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/reminders',
label: 'Reminder settings',
category: 'admin',
keywords: ['reminders', 'daily digest', 'delivery window'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/webhooks',
label: 'Webhooks',
category: 'admin',
keywords: ['webhook', 'outgoing', 'callback', 'delivery log'],
requires: 'admin.manage_webhooks',
},
{
href: '/:portSlug/admin/forms',
label: 'Forms',
category: 'admin',
keywords: ['form templates', 'inquiry', 'intake', 'public form'],
requires: 'admin.manage_forms',
},
{
href: '/:portSlug/admin/email-templates',
label: 'Email templates',
category: 'admin',
keywords: ['transactional emails', 'subject lines', 'portal email', 'inquiry email'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/tags',
label: 'Tags',
category: 'admin',
keywords: ['labels', 'color-coded', 'classification'],
requires: 'admin.manage_tags',
},
{
href: '/:portSlug/admin/vocabularies',
label: 'Vocabularies',
category: 'admin',
keywords: [
'pick lists',
'interest temperatures',
'status reasons',
'tenure types',
'expense categories',
'document types',
],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/custom-fields',
label: 'Custom fields',
category: 'admin',
keywords: ['custom fields', 'tenant fields', 'extra fields'],
requires: 'admin.manage_custom_fields',
},
{
href: '/:portSlug/admin/duplicates',
label: 'Duplicates queue',
category: 'admin',
keywords: ['dedup', 'duplicate clients', 'merge', 'review queue'],
},
{
href: '/:portSlug/admin/import',
label: 'Bulk import',
category: 'admin',
keywords: ['csv', 'import', 'bulk upload'],
},
{
href: '/:portSlug/admin/sends',
label: 'Send log',
category: 'admin',
keywords: ['email sends', 'brochures', 'send failures', 'retries'],
},
{
href: '/:portSlug/admin/monitoring',
label: 'Queue monitoring',
category: 'admin',
keywords: ['bullmq', 'queue', 'jobs', 'throughput', 'retries'],
requires: 'admin.system_backup',
},
{
href: '/:portSlug/admin/backup',
label: 'Backup & restore',
category: 'admin',
keywords: ['backup', 'restore', 'retention', 'disaster recovery'],
requires: 'admin.system_backup',
},
{
href: '/:portSlug/admin/storage',
label: 'Storage backend',
category: 'admin',
keywords: ['s3', 'minio', 'filesystem', 'storage backend', 'object store'],
requires: 'admin.system_backup',
},
{
href: '/:portSlug/admin/ports',
label: 'Ports',
category: 'admin',
keywords: ['marinas', 'tenancy', 'port management', 'multi-port'],
superAdminOnly: true,
},
{
href: '/:portSlug/admin/ai',
label: 'AI configuration',
category: 'admin',
keywords: ['openai', 'anthropic', 'gpt', 'claude', 'llm', 'api key', 'embeddings'],
requires: 'admin.manage_settings',
},
// /admin/ocr collapsed into /admin/ai on 2026-05-22 (the OcrSettingsForm
// already lived on both pages). Keywords surfaced via the AI tile.
feat(admin+search): user-mgmt polish, role labels, search keyword index Admin search now matches against per-card keyword lists so typing "client portal", "smtp", "tier ladder" lands on the System Settings card (which hosts those flags). The same keyword list extends the topbar global search (NAV_CATALOG) so any setting key resolves from the cmd-K input — settings results sort to the bottom of the dropdown beneath entity hits. User management: - Third action button (Power/PowerOff) enables/disables sign-in from the desktop list; mobile card dropdown gains the same item. Backed by the existing userProfiles.isActive flag — withAuth already refuses disabled sessions with 403. - UserForm collects first + last name (canonical) alongside displayName, with admin email-change behind a confirmation modal. On confirm we send the OLD address an automated "your admin changed your sign-in email" notice (new template at admin-email-change.ts) and rewrite the Better Auth user row. - Phone field swaps the bare tel input for the shared PhoneInput (country combobox + AsYouType formatting + E.164 storage). - "Manage permissions" link points to /admin/roles?focusUser=… as a stepping stone for the future fine-tuned-permissions UI. Role names normalize through a new ROLE_LABELS + formatRole() helper in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the prettifyRoleName in role-list; user-list and user-card now render "Sales Agent" instead of "sales_agent". Custom roles pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:14:12 +02:00
{
href: '/:portSlug/admin/website-analytics',
label: 'Website analytics (Umami)',
category: 'admin',
keywords: ['umami', 'analytics', 'traffic', 'visitors', 'marketing', 'pageviews'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/residential-stages',
label: 'Residential pipeline stages',
category: 'admin',
keywords: ['residential stages', 'pipeline', 'residential funnel'],
requires: 'admin.manage_settings',
},
{
href: '/:portSlug/admin/roles',
label: 'Roles & permissions',
category: 'admin',
keywords: ['roles', 'permissions', 'access control', 'rbac'],
requires: 'admin.manage_users',
},
// /admin/invitations was merged into /admin/users on 2026-05-21 - the
feat(uat-batch): Groups F + G + H — DocsHub/signing + admin consolidation + email F27–F29, G30, G31, H32, H33 from the 2026-05-21 plan. Shipped now: F28 Past-milestones expandable history. The Past strip on the Interest overview becomes an <Accordion> — each row collapses to the same one-line summary as before, expands to render the full <MilestoneSection> (steps list, sub-status, inline doc actions). Reuses the existing MilestoneSection so no new per-milestone rendering needs to be maintained. F29 Watchers configurable at document creation time. The unified create-document wizard gets a Watchers section with a multi-select checkbox list backed by /api/v1/admin/users/picker. Selected user ids are sent in the `watchers` array on the POST (replacing the prior hardcoded `[]`). UI matches the post-creation WatchersCard so reps see the same identity rows regardless of entry point. G30 /admin/invitations merged into /admin/users. The Users page now wraps the existing UserList + InvitationsManager in a Tabs control (Active users / Invitations). The standalone /admin/invitations route returns a redirect to the merged page for bookmark back-compat. Removed nav catalog entry + admin-sections-browser tile; extended the Users catalog keywords with "invitations / pending invites / onboarding" so command-K search still lands on the right surface. G31 /admin/ai picks up the berth-PDF-parser section + a "planned AI surfaces" placeholder. Berth PDF parser remains env-configured today; the page now documents it so admins don't hunt for the controls. Closes the "where do I configure AI?" loop. H32 Email settings explainer panel above the SMTP cards. Spells out why noreply + sales have separate credentials and which workflows ship from each mailbox. Existing field titles gained the "(noreply)" suffix so the model maps cleanly. H33 Supplemental-info-request email rebuilt to use the shared branded shell (logo + blurred overhead background + max- width 600 table layout) instead of the prior plain-HTML page. Per-port branding (logo / primary color / background / header / footer) flows from getPortBrandingConfig. CTA button picks up the port's primary color. Already shipped (verified pre-shipped): F27 DocumentsHub root view already hides the breadcrumb via `selectedFolderId !== undefined` conditional. Verified: tsc clean, vitest 1454/1454. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:40:48 +02:00
// standalone catalog entry would route to the redirect stub. Reps
// searching for "invite" still land on the right surface via the
// /admin/users keyword list (extended below).
];
/** Substitute `:portSlug` placeholder for the current port. */
export function resolveHref(href: string, portSlug: string): string {
return href.replace(':portSlug', portSlug);
}
/**
* Returns nav catalog entries matching the query, filtered by what the
* current user is allowed to see. Match is a substring check against
* label + each keyword; ranking favors label hits over keyword hits and
* prefix hits over mid-string hits.
*
* Pure / sync - runs in-process. The catalog is ~15 entries today, so
* the linear scan is irrelevant cost-wise.
*/
export function searchNavCatalog(
query: string,
opts: { isSuperAdmin: boolean; permissions: RolePermissions | null; limit?: number },
): Array<NavCatalogEntry & { score: number }> {
const q = query.trim().toLowerCase();
if (q.length === 0) return [];
const limit = opts.limit ?? 5;
fix(uat-batch-1): wave-1 blocker bugs — supplemental gate, file FK, downloads, search dedup, notes stale, expense form, vocab Surgical fixes for the 7 UAT blockers that prevent productive forward testing. Each item has a corresponding entry in alpha-uat-master.md. - supplemental-info route relocated out of (portal) so it bypasses the isPortalDisabledGlobally() kill-switch. URL unchanged. - file upload service derives client_id/company_id/yacht_id from (entityType, entityId) when not explicitly passed, so interest-tab uploads no longer land with client_id=NULL and stay visible in the Attachments list. - triggerBlobDownload / triggerUrlDownload helpers in src/lib/utils attach the anchor to the DOM before click so Chromium honours the download attribute; 7 sites refactored, file-named downloads stop arriving as bare UUIDs. - search-nav-catalog dedupes by href at the result-collection layer so the same href can no longer surface twice in the command-K dropdown (kills the React duplicate-key warning); /admin/templates entries merged into a single richer-keyword variant. - NotesList gains a parentInvalidateKey prop, wired through all five callers (interest, client, yacht, company, residential client/ interest) so the Overview "Latest note" teaser refreshes when a note is added in the Notes tab. - expense-form-dialog: setValue('receiptFileIds') / setValue( 'noReceiptAcknowledged') on upload/clear/checkbox so the schema-level refine sees the field and Create stops silently no-op'ing on submit. - bulk-add-berths-wizard: side-pontoon dropdown now reads through useVocabulary('berth_side_pontoon_options') instead of a wrong local enum ('Port', 'Starboard', 'Bow', 'Stern') — wizard data now matches the rest of the platform + honours admin-editable per-port overrides. tsc clean. 1419/1419 vitest. lint clean on touched files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:50:58 +02:00
const byHref = new Map<string, NavCatalogEntry & { score: number }>();
for (const entry of NAV_CATALOG) {
if (entry.superAdminOnly && !opts.isSuperAdmin) continue;
if (entry.requires && !opts.isSuperAdmin && !hasPermission(opts.permissions, entry.requires)) {
continue;
}
const score = scoreEntry(q, entry);
fix(uat-batch-1): wave-1 blocker bugs — supplemental gate, file FK, downloads, search dedup, notes stale, expense form, vocab Surgical fixes for the 7 UAT blockers that prevent productive forward testing. Each item has a corresponding entry in alpha-uat-master.md. - supplemental-info route relocated out of (portal) so it bypasses the isPortalDisabledGlobally() kill-switch. URL unchanged. - file upload service derives client_id/company_id/yacht_id from (entityType, entityId) when not explicitly passed, so interest-tab uploads no longer land with client_id=NULL and stay visible in the Attachments list. - triggerBlobDownload / triggerUrlDownload helpers in src/lib/utils attach the anchor to the DOM before click so Chromium honours the download attribute; 7 sites refactored, file-named downloads stop arriving as bare UUIDs. - search-nav-catalog dedupes by href at the result-collection layer so the same href can no longer surface twice in the command-K dropdown (kills the React duplicate-key warning); /admin/templates entries merged into a single richer-keyword variant. - NotesList gains a parentInvalidateKey prop, wired through all five callers (interest, client, yacht, company, residential client/ interest) so the Overview "Latest note" teaser refreshes when a note is added in the Notes tab. - expense-form-dialog: setValue('receiptFileIds') / setValue( 'noReceiptAcknowledged') on upload/clear/checkbox so the schema-level refine sees the field and Create stops silently no-op'ing on submit. - bulk-add-berths-wizard: side-pontoon dropdown now reads through useVocabulary('berth_side_pontoon_options') instead of a wrong local enum ('Port', 'Starboard', 'Bow', 'Stern') — wizard data now matches the rest of the platform + honours admin-editable per-port overrides. tsc clean. 1419/1419 vitest. lint clean on touched files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:50:58 +02:00
if (score === 0) continue;
// Some hrefs intentionally appear in multiple catalog categories
// (e.g. /admin/templates lives under both 'settings' and 'admin').
// Keep the highest-scoring variant so the dropdown never renders
// two rows with the same `id` (href) - React would otherwise warn
fix(uat-batch-1): wave-1 blocker bugs — supplemental gate, file FK, downloads, search dedup, notes stale, expense form, vocab Surgical fixes for the 7 UAT blockers that prevent productive forward testing. Each item has a corresponding entry in alpha-uat-master.md. - supplemental-info route relocated out of (portal) so it bypasses the isPortalDisabledGlobally() kill-switch. URL unchanged. - file upload service derives client_id/company_id/yacht_id from (entityType, entityId) when not explicitly passed, so interest-tab uploads no longer land with client_id=NULL and stay visible in the Attachments list. - triggerBlobDownload / triggerUrlDownload helpers in src/lib/utils attach the anchor to the DOM before click so Chromium honours the download attribute; 7 sites refactored, file-named downloads stop arriving as bare UUIDs. - search-nav-catalog dedupes by href at the result-collection layer so the same href can no longer surface twice in the command-K dropdown (kills the React duplicate-key warning); /admin/templates entries merged into a single richer-keyword variant. - NotesList gains a parentInvalidateKey prop, wired through all five callers (interest, client, yacht, company, residential client/ interest) so the Overview "Latest note" teaser refreshes when a note is added in the Notes tab. - expense-form-dialog: setValue('receiptFileIds') / setValue( 'noReceiptAcknowledged') on upload/clear/checkbox so the schema-level refine sees the field and Create stops silently no-op'ing on submit. - bulk-add-berths-wizard: side-pontoon dropdown now reads through useVocabulary('berth_side_pontoon_options') instead of a wrong local enum ('Port', 'Starboard', 'Bow', 'Stern') — wizard data now matches the rest of the platform + honours admin-editable per-port overrides. tsc clean. 1419/1419 vitest. lint clean on touched files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:50:58 +02:00
// about duplicate keys.
const existing = byHref.get(entry.href);
if (!existing || score > existing.score) {
byHref.set(entry.href, { ...entry, score });
}
}
fix(uat-batch-1): wave-1 blocker bugs — supplemental gate, file FK, downloads, search dedup, notes stale, expense form, vocab Surgical fixes for the 7 UAT blockers that prevent productive forward testing. Each item has a corresponding entry in alpha-uat-master.md. - supplemental-info route relocated out of (portal) so it bypasses the isPortalDisabledGlobally() kill-switch. URL unchanged. - file upload service derives client_id/company_id/yacht_id from (entityType, entityId) when not explicitly passed, so interest-tab uploads no longer land with client_id=NULL and stay visible in the Attachments list. - triggerBlobDownload / triggerUrlDownload helpers in src/lib/utils attach the anchor to the DOM before click so Chromium honours the download attribute; 7 sites refactored, file-named downloads stop arriving as bare UUIDs. - search-nav-catalog dedupes by href at the result-collection layer so the same href can no longer surface twice in the command-K dropdown (kills the React duplicate-key warning); /admin/templates entries merged into a single richer-keyword variant. - NotesList gains a parentInvalidateKey prop, wired through all five callers (interest, client, yacht, company, residential client/ interest) so the Overview "Latest note" teaser refreshes when a note is added in the Notes tab. - expense-form-dialog: setValue('receiptFileIds') / setValue( 'noReceiptAcknowledged') on upload/clear/checkbox so the schema-level refine sees the field and Create stops silently no-op'ing on submit. - bulk-add-berths-wizard: side-pontoon dropdown now reads through useVocabulary('berth_side_pontoon_options') instead of a wrong local enum ('Port', 'Starboard', 'Bow', 'Stern') — wizard data now matches the rest of the platform + honours admin-editable per-port overrides. tsc clean. 1419/1419 vitest. lint clean on touched files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:50:58 +02:00
const out = Array.from(byHref.values());
out.sort((a, b) => b.score - a.score);
return out.slice(0, limit);
}
function scoreEntry(q: string, entry: NavCatalogEntry): number {
const label = entry.label.toLowerCase();
// Strongest signals first.
if (label === q) return 100;
if (label.startsWith(q)) return 80;
if (label.includes(q)) return 60;
// Keyword hits - strongest if the keyword exactly equals the query
// (e.g. user types "smtp"), then prefix, then substring.
for (const kw of entry.keywords) {
const k = kw.toLowerCase();
if (k === q) return 50;
if (k.startsWith(q)) return 35;
if (k.includes(q)) return 20;
}
return 0;
}
function hasPermission(perms: RolePermissions | null, dotPath: string): boolean {
if (!perms) return false;
const parts = dotPath.split('.');
let cur: unknown = perms;
for (const part of parts) {
if (typeof cur !== 'object' || cur === null) return false;
cur = (cur as Record<string, unknown>)[part];
}
return cur === true;
}