feat(admin): inquiry inbox, send log, email-template overrides, reports dashboard, recommender keys, role-editor coverage; replace placeholder pages

Closes the bulk of audit-pass-#1 admin gaps in one batch.

New admin pages:
- /admin/inquiries reads website_submissions with filter chips for
  berth/residence/contact + payload viewer per row.
- /admin/sends reads document_sends with sent/failed filter chips and
  expandable body markdown; failures surface errorReason and any
  fallback-to-link reason from the SMTP retry.
- /admin/email-templates lets per-port admins override the subject of
  each transactional template (8 templates catalogued in
  template-catalog.ts). Body editing is a follow-on; portal_activation
  + portal_reset are wired to honor the override via loadSubjectOverride.
- /admin/reports replaces the "Coming in Layer 3" placeholder with a
  KPI dashboard: 4 KPI tiles, pipeline funnel bars, berth occupancy
  donut-bars, conversion %, refresh every 60s.
- backup/import/onboarding admin pages replace placeholders with
  actionable guidance: backup posture + planned features, available CLI
  imports + planned UI, ordered onboarding checklist linking to admin
  pages.

Existing pages widened:
- settings-manager exposes the 9 berth-recommender tunables that were
  previously code-only (recommender_*, heat_weight_*, fallthrough_*,
  tier_ladder_hide_late_stage).
- role-form covers all 19 RolePermissions schema groups; previously
  missing yachts/companies/memberships/reservations + missing
  documents.edit + files.edit checkboxes. snake_case residential
  labels replaced with friendly text.

portal-auth.service.ts now also writes audit_log rows for portal
invite, resend, activate, password-reset request, and reset (closes one
more audit-pass-#2 gap while we were touching the file).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-06 14:58:17 +02:00
parent 8cdee99310
commit c90876abad
22 changed files with 1703 additions and 54 deletions

View File

@@ -17,7 +17,8 @@ import {
} from '@/components/ui/accordion';
import { apiFetch } from '@/lib/api/client';
/** Default permissions structure matching RolePermissions type */
/** Default permissions structure matching RolePermissions type in
* src/lib/db/schema/users.ts. Keep this in sync when actions are added. */
const DEFAULT_PERMISSIONS: Record<string, Record<string, boolean>> = {
clients: { view: false, create: false, edit: false, delete: false, merge: false, export: false },
interests: {
@@ -33,6 +34,7 @@ const DEFAULT_PERMISSIONS: Record<string, Record<string, boolean>> = {
documents: {
view: false,
create: false,
edit: false,
send_for_signing: false,
upload_signed: false,
delete: false,
@@ -54,7 +56,7 @@ const DEFAULT_PERMISSIONS: Record<string, Record<string, boolean>> = {
record_payment: false,
export: false,
},
files: { view: false, upload: false, delete: false, manage_folders: false },
files: { view: false, upload: false, edit: false, delete: false, manage_folders: false },
email: { view: false, send: false, configure_account: false },
reminders: {
view_own: false,
@@ -67,6 +69,10 @@ const DEFAULT_PERMISSIONS: Record<string, Record<string, boolean>> = {
calendar: { connect: false, view_events: false },
reports: { view_dashboard: false, view_analytics: false, export: false },
document_templates: { view: false, generate: false, manage: false },
yachts: { view: false, create: false, edit: false, delete: false, transfer: false },
companies: { view: false, create: false, edit: false, delete: false },
memberships: { view: false, manage: false },
reservations: { view: false, create: false, activate: false, cancel: false },
admin: {
manage_users: false,
view_audit_log: false,
@@ -101,7 +107,13 @@ const GROUP_LABELS: Record<string, string> = {
calendar: 'Calendar',
reports: 'Reports',
document_templates: 'Document Templates',
yachts: 'Yachts',
companies: 'Companies',
memberships: 'Company Memberships',
reservations: 'Reservations',
admin: 'Administration',
residential_clients: 'Residential Clients',
residential_interests: 'Residential Interests',
};
function formatAction(action: string): string {