Files
pn-new-crm/docs/audit-missing-features-2026-05-06.md
Matt a0e68eb060 docs: comprehensive audits + Documenso build plan + admin UX backlog
Six audit documents capture the 2026-05-06 review pass (comprehensive,
frontend, missing-features, permissions, reliability) along with the
Documenso integration audit + locked build plan that drove the bulk
of subsequent feature work.

Adds `docs/admin-ux-backlog.md` as a living tracker for the autonomous
push — every item marked DONE or REMAINING with file pointers and
scope estimates so future sessions can pick up where this one stopped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:57:53 +02:00

16 KiB

Missing-Features Audit — 2026-05-06

Focused pass on features that look done in the UI but aren't fully wired through the service layer, plus admin settings exposed to users that no code reads. Companion to docs/audit-comprehensive-2026-05-06.md — the three "coming soon" stubs already documented there (client Files tab, client reservations history, berth tabs), the import-worker stub, the two interest-form TODOs, and the EOI "Price: TBD" finding are NOT re-flagged here.

Hard cap: 12 findings. Severity tiers below.


VISIBLE-BROKEN (admin sees a control, click is a no-op or wrong)

V1. 6 of 8 admin-editable email subject overrides are silently ignored at send time

Files:

  • src/components/admin/email-templates-admin.tsx:24-72 (UI)
  • src/lib/email/template-catalog.ts:16-25 (catalog of 8 keys)
  • src/lib/services/portal-auth.service.ts:120-127, 332-339 (the only consumers of loadSubjectOverride)

The /admin/email-templates page lets an admin override the subject line on eight transactional templates: portal_activation, portal_reset, portal_invite_resend, crm_invite, inquiry_client_confirmation, inquiry_sales_notification, residential_inquiry_client_confirmation, residential_inquiry_sales_alert. The save endpoint persists each one to system_settings (email_template_<key>_subject).

Only two of those eight are ever read at send time — portal_activation and portal_reset in portal-auth.service.ts. A repo-wide search for loadSubjectOverride / settingKeyForSubject returns no other consumers. The other six templates use their hardcoded subject regardless of the admin override.

Impact: sales/ops teams will customize an inquiry confirmation subject, hit Save, see the "Overridden" badge, and silently ship the default subject to every prospect.

Fix: small per template — call loadSubjectOverride(portId, key) in each sender (crm-invite.service.ts, the inquiry sender, the residential inquiry sender, the portal-invite-resend path) and pass the result through as the email subject.

Scope: small (5 callsites + tests).


Files:

  • src/app/(dashboard)/[portSlug]/admin/branding/page.tsx:7-46 — UI with five fields.
  • src/lib/services/port-config.ts:240-272getPortBrandingConfig() resolves the five branding_* settings into a typed config.
  • Repo-wide: getPortBrandingConfig has zero callers outside its declaration. The five SETTING_KEYS.branding* constants are only read inside getPortBrandingConfig itself.

The admin panel is functional end-to-end (write hits the settings API, "Reset to default" works), and the email-templates module hardcodes s3.portnimara.com/... for the logo URL plus a fixed table layout. None of the email-rendering helpers (renderEmail, the template modules in src/lib/email/templates/) call getPortBrandingConfig, and the <BrandedAuthShell> component sources its logo + colors from constants too.

Impact: every multi-tenant assumption made about branding is broken. A second port wired into this CRM will see Port Nimara's logo

  • colors in every transactional email and on the auth pages, even after their admin "configures branding" successfully.

Fix: plumb getPortBrandingConfig(portId) through the email renderer (header/footer HTML + primary button color), and through <BrandedAuthShell> via a server-fetched prop.

Scope: medium (touches every transactional email + auth shell).


V3. Reminder admin page configures defaults that no service applies

Files:

  • src/app/(dashboard)/[portSlug]/admin/reminders/page.tsx:7-50 — UI for default-enabled, default-days, digest-enabled, digest-time, digest-timezone.
  • src/lib/services/port-config.ts:284-306getPortReminderConfig() defines the schema.
  • Repo-wide: the keys (reminder_default_*, reminder_digest_*) and getPortReminderConfig have zero callers.

Same pattern as V2. The admin sets "enable reminders by default on new interests" → toggles to true → save succeeds → newly-created interests still default to reminderEnabled=false. The digest-time + timezone fields go nowhere — there is no scheduler that batches pending reminders into a daily digest.

Impact: the entire reminder UX is decorative. Sales reps think they configured a daily digest at 09:00 Europe/Warsaw, get fire-as-they-hit notifications instead.

Fix: wire getPortReminderConfig into (a) the interest-create service (defaults), (b) the maintenance/notifications worker that fires reminders (digest batching + delivery window). The digest behavior didn't exist before this audit — needs a new scheduled job.

Scope: medium (defaults are small, digest job is new code).


Files:

  • src/app/(portal)/portal/dashboard/page.tsx:58-63<PortalCard title="My Memberships" ... icon={Building2} /> — note no href prop.
  • src/components/portal/portal-nav.tsx:8-15 — six nav entries, no memberships.
  • Filesystem: src/app/(portal)/portal/memberships/ does not exist.

The dashboard shows a count of "memberships" (companies the portal user belongs to) but the tile is non-clickable and there is no /portal/memberships route. A user with 3 memberships sees the tile, clicks → nothing happens.

Impact: dead-end on the portal home for any client tied to a company (the residential and yacht-ownership use-cases).

Fix: ship /portal/memberships/page.tsx listing the companies returned by the existing companyMemberships query (already aggregated in getPortalDashboard), and add it to PortalNav. Or pull the tile if memberships isn't a portal feature.

Scope: small.


V5. Company detail page Documents tab is a "Coming soon" stub

File: src/components/companies/company-tabs.tsx:230-234

{
  id: 'documents',
  label: 'Documents',
  content: <EmptyState title="Documents" description="Coming soon" />,
},

Visible alongside the working Notes / Activity / Addresses / Members tabs on every company detail page. NOT covered by the existing audit doc's H7 (which lists clients, client reservations, and berths).

Impact: the same UX problem H7 calls out for clients.

Fix: mirror what client-Files-tab needs — query documents joined to a polymorphic billing-entity = company link, render a list, ship a download button. Or hide the tab.

Scope: small to medium.


HALF-WIRED (the page works but the surrounding promise overstates it)

V6. "Onboarding" admin page is a static checklist, not the wizard the page itself promises

File: src/app/(dashboard)/[portSlug]/admin/onboarding/page.tsx

The page renders 8 stepwise links and explicitly says (lines 71-72, 98-110): "The future onboarding wizard will track progress per port…", "What this page will become", "The wizard will record completion per port in system_settings, gate the public marketing-site cutover…".

The admin landing card describes it as the "Initial-setup wizard for fresh ports" — admins clicking through expect a wizard, get a static table of contents.

Impact: the only "fresh port" workflow doesn't exist; cutover gating logic mentioned in the page body is also unimplemented.

Fix: either (a) build the wizard with progress in system_settings

  • banner integration, or (b) re-label both this page and the admin landing card to "Setup checklist" so expectations match reality.

Scope: large for the wizard; tiny for the relabel.


V7. Backup & Restore admin page is informational only — admin landing card promises actions

Files:

  • src/app/(dashboard)/[portSlug]/admin/backup/page.tsx
  • src/app/(dashboard)/[portSlug]/admin/page.tsx:148 — landing card description: "Database snapshots and on-demand exports."

The landing card sells "on-demand exports". The actual page renders a two-card explainer: "Current backup posture" (read-only) and "What this page will become" (the entire interactive surface — list snapshots, "Take backup now" button, per-port logical export, restore preview, GDPR per-client export). None of those exist.

Impact: the "Backup & Restore" tile is functionally a docs page. Compliance officers / users expecting a self-serve GDPR export button have to file a support ticket.

Fix: match the language on the landing card to the page reality ("Backup posture" → docs only) until the snapshot/export buttons ship. The maintenance worker already runs database-backup (per docs/audit-comprehensive-2026-05-06.md C1 — though that worker isn't imported), so wiring "Take backup now" against the existing job is small once C1 is fixed.

Scope: small (doc tweak) or medium (button + per-port export endpoint).


V8. Inquiry inbox is read-only — no "Convert to Client" / "Mark resolved" / "Assign" actions

File: src/components/admin/inquiry-inbox.tsx (entire file, 207 lines, ends at the View payload toggle)

The inbox lists website-form submissions (berth_inquiry, residence_inquiry, contact_form) with filter chips and a "View payload" expand. There is no action to:

  • create a client/interest from the submission,
  • assign the inquiry to a sales rep,
  • mark it resolved / triaged,
  • reply directly,
  • archive or trash the row,
  • export.

The website_submissions table appears to be permanent — every inquiry ever received remains in the inbox forever, with no triage state. Sales has to manually copy the email into a new client form and back-reference the original submission.

Impact: the inquiry-to-pipeline conversion step isn't supported in the CRM. The marketing-site cutover (per the user's project_email_ownership_at_cutover.md memory) will increase volume on this surface and make the missing triage UX painful.

Fix: add a per-submission "Convert" action that prefills the client + interest forms with the payload, plus a triage_state column (open / converted / dismissed) and a default filter that hides non-open rows.

Scope: medium.


MOBILE PARITY

V9. Mobile More-sheet is missing several real top-nav destinations

File: src/components/layout/mobile/more-sheet.tsx:38-50

MORE_ITEMS lists 11 entries. The dashboard route directory has at least these top-level segments not represented anywhere in the mobile bottom-tabs OR more-sheet:

  • residential — exists at /[portSlug]/residential/...
  • notifications — exists at /[portSlug]/notifications/...
  • berth-reservations — exists at /[portSlug]/berth-reservations/...
  • documents — exists as a top-level page (separate from the bottom tab documents, which IS in mobile-bottom-tabs)
  • website-analytics — exists at /[portSlug]/website-analytics/...

A mobile-only user has no path to any of them. The Documents bottom tab does cover the doc list, but residential is an entire feature domain (per the (dashboard)/.../residential directory) with no mobile entry point.

Impact: anyone using the mobile chrome to triage on the go can't reach residential clients/interests, alerts (alerts IS in the sheet), or notifications.

Fix: add the missing segments to MORE_ITEMS. If the grid feels too dense, reorganize into sections.

Scope: small.


V10. Portal has no "Profile" / "Change password" surface

Files:

  • src/components/portal/portal-nav.tsx:8-15 — six tabs, no profile.
  • Filesystem: no src/app/(portal)/portal/profile/ directory.

A portal user who wants to change their email, phone, mailing address, or password has no UI. The portal sign-in flow goes through the better-auth session but the app exposes zero account-management controls. The "Need assistance?" card on the dashboard tells the user to contact the port team — which is the explicit answer for data edits, but does not cover password changes (a security expectation, not a per-port-staff burden).

Impact: every portal user who forgets their password (after already activating) has to use /portal/forgot-password even if they remember the old one. There's no proactive password rotation. A user who changes their phone number has to email the port to update it.

Fix: ship /portal/profile with at minimum: read-only PII view + "Change password" form (re-uses the existing reset-password endpoint or a new change-password endpoint that takes the current pw). Phone/address editing is a longer fix because of the audit-trail implications.

Scope: small for password; medium with PII edits.


V11. Portal invoices page lists invoices but offers no view/download — even though documents do

File: src/app/(portal)/portal/invoices/page.tsx:53-99

Each invoice row shows number, status, due/paid dates, amount, and a small payment-status caption. There is no link, no PDF view, no download. By contrast, the portal Documents page (peer route) ends each row with a <DocumentDownloadButton documentId={doc.id} /> that fetches a signed S3 URL.

Compare to admin/CRM where invoices have a full PDF render flow (invoice service generates the PDF + signed URL).

Impact: a portal user can see they owe money and cannot retrieve the actual invoice document. They have to email the port to ask for a PDF copy.

Fix: add an invoice-PDF endpoint under /api/portal/invoices/[id]/ download mirroring the documents one, and a download button on each row. The invoice PDF generator already exists (src/lib/services/ invoices.ts).

Scope: small.


DEV-NOTES (legitimately staged-for-later, calling out so they're not forgotten)

V12. Email-templates admin only edits subject lines — body editing is a documented "next iteration"

Files:

  • src/components/admin/email-templates-admin.tsx:78-79 — "Customize the subject line of transactional emails per port. Body editing is the next iteration; for now the layout and HTML stay locked to the default template."
  • src/lib/email/template-catalog.ts:5-9 — same statement in the catalog header.

The page is honest about the limitation, so this isn't a "broken" finding. But it's a notable shipped-without-the-killer-feature gap: the multi-tenant promise of per-port email customization can't deliver the body changes that ports actually want (logo placement, signature, language). Combined with V2 (branding HTML fragments aren't read at all), there is currently NO way for a non-super-admin per-port admin to customize the email body in any way.

Impact: confined to admin expectations — most ports will assume "Email templates" = "edit the email", click in, see only a subject field, and request the missing body editor.

Fix: scope a body-editing flow that reuses the merge_fields.ts token catalog (the validator already exists for document templates) for safety. Until that's built, V2 + this finding together mean a "rebrand the emails" task is single-tenant only.

Scope: large (HTML editor + token validator + per-port override storage + render-side composition).


Summary

12 findings, four severity tiers:

  • Visible-broken (V1-V5): five admin/portal controls produce no effect. V1 (email overrides) and V2 (branding) are the highest impact — both silently break the multi-tenant promise.
  • Half-wired (V6-V8): three pages where the surrounding wrapper oversells what's there. V8 (inquiry inbox) is the largest scope.
  • Mobile parity (V9-V11): mobile users can't reach several real features; portal users have no profile/password surface and can't download invoices.
  • Dev-notes (V12): documented limitations called out for the roadmap.

The two highest-leverage quick wins are V1 (wire 6 missing template subject overrides — a few hours) and V11 (portal invoice download — small, fixes a real customer pain point).