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>
2026-05-06 14:58:17 +02:00
'use client' ;
2026-05-13 11:50:07 +02:00
import { useMemo , useState } from 'react' ;
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>
2026-05-06 14:58:17 +02:00
import { useQuery , useQueryClient } from '@tanstack/react-query' ;
import { RotateCcw , Save } from 'lucide-react' ;
import { PageHeader } from '@/components/shared/page-header' ;
import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card' ;
import { Button } from '@/components/ui/button' ;
import { Input } from '@/components/ui/input' ;
import { Badge } from '@/components/ui/badge' ;
import { apiFetch } from '@/lib/api/client' ;
interface TemplateRow {
key : string ;
label : string ;
description : string ;
mergeTokens : string [ ] ;
defaultSubject : string ;
subjectOverride : string | null ;
effectiveSubject : string ;
}
export function EmailTemplatesAdmin() {
const qc = useQueryClient ( ) ;
const { data , isLoading , error } = useQuery ( {
queryKey : [ 'admin-email-templates' ] ,
queryFn : ( ) = > apiFetch < { data : TemplateRow [ ] } > ( '/api/v1/admin/email-templates' ) ,
} ) ;
2026-05-13 11:50:07 +02:00
// Key-based remount: re-mount the body when the server-loaded row
// signature changes so its useState seeds from fresh server data.
// Replaces the prior useEffect(setDrafts, [rows]) sync.
const sig = data ? . data
? data . data . map ( ( r ) = > ` ${ r . key } : ${ r . subjectOverride ? ? r . defaultSubject } ` ) . join ( '|' )
: 'loading' ;
return (
< EmailTemplatesAdminBody key = { sig } data = { data } isLoading = { isLoading } error = { error } qc = { qc } / >
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>
2026-05-06 14:58:17 +02:00
) ;
2026-05-13 11:50:07 +02:00
}
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>
2026-05-06 14:58:17 +02:00
2026-05-13 11:50:07 +02:00
function EmailTemplatesAdminBody ( {
data ,
isLoading ,
error ,
qc ,
} : {
data : { data : TemplateRow [ ] } | undefined ;
isLoading : boolean ;
error : unknown ;
qc : ReturnType < typeof useQueryClient > ;
} ) {
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>
2026-05-06 14:58:17 +02:00
const rows = useMemo ( ( ) = > data ? . data ? ? [ ] , [ data ] ) ;
2026-05-13 11:50:07 +02:00
const [ drafts , setDrafts ] = useState < Record < string , string > > ( ( ) = > {
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>
2026-05-06 14:58:17 +02:00
const next : Record < string , string > = { } ;
for ( const row of rows ) {
next [ row . key ] = row . subjectOverride ? ? row . defaultSubject ;
}
2026-05-13 11:50:07 +02:00
return next ;
} ) ;
const [ savingKey , setSavingKey ] = useState < string | null > ( null ) ;
const [ message , setMessage ] = useState < { key : string ; kind : 'ok' | 'err' ; text : string } | null > (
null ,
) ;
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>
2026-05-06 14:58:17 +02:00
async function save ( row : TemplateRow , mode : 'save' | 'reset' ) {
setSavingKey ( row . key ) ;
setMessage ( null ) ;
try {
const subject = mode === 'reset' ? null : ( drafts [ row . key ] ? ? '' ) ;
await apiFetch ( '/api/v1/admin/email-templates' , {
method : 'PUT' ,
body : { key : row.key , subject } ,
} ) ;
await qc . invalidateQueries ( { queryKey : [ 'admin-email-templates' ] } ) ;
setMessage ( {
key : row.key ,
kind : 'ok' ,
text : mode === 'reset' ? 'Reset to default' : 'Saved' ,
} ) ;
} catch ( err ) {
setMessage ( {
key : row.key ,
kind : 'err' ,
text : err instanceof Error ? err . message : 'Failed' ,
} ) ;
} finally {
setSavingKey ( null ) ;
}
}
return (
< div >
< PageHeader
title = "Email templates"
description = "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."
/ >
< div className = "mt-6 space-y-4" >
{ isLoading ? (
< p className = "text-sm text-muted-foreground py-6" > Loading … < / p >
) : error ? (
< p className = "text-sm text-red-600 py-6" >
Failed to load templates : { error instanceof Error ? error . message : 'unknown error' }
< / p >
) : (
rows . map ( ( row ) = > {
const draft = drafts [ row . key ] ? ? row . defaultSubject ;
const dirty =
draft !== ( row . subjectOverride ? ? row . defaultSubject ) ||
( row . subjectOverride !== null && draft === row . defaultSubject ) ;
const overridden = row . subjectOverride !== null ;
return (
< Card key = { row . key } >
< CardHeader className = "pb-2" >
< div className = "flex items-center gap-2 flex-wrap" >
< CardTitle className = "text-base font-medium" > { row . label } < / CardTitle >
{ overridden ? (
< Badge className = "bg-blue-100 text-blue-800" > Overridden < / Badge >
) : (
< Badge variant = "secondary" > Default < / Badge >
) }
< / div >
< CardDescription > { row . description } < / CardDescription >
< / CardHeader >
< CardContent className = "space-y-3" >
< div >
< label className = "text-xs uppercase tracking-wide text-muted-foreground" >
Subject
< / label >
< Input
value = { draft }
onChange = { ( e ) = >
setDrafts ( ( prev ) = > ( { . . . prev , [ row . key ] : e . target . value } ) )
}
className = "mt-1 font-mono text-sm"
/ >
< / div >
< div className = "text-xs text-muted-foreground" >
Default : < code className = "font-mono" > { row . defaultSubject } < / code >
< / div >
< div className = "text-xs text-muted-foreground" >
Available tokens : { ' ' }
{ row . mergeTokens . map ( ( t ) = > (
< code key = { t } className = "mr-1 font-mono" > { ` {{ ${ t } }} ` } < / code >
) ) }
< / div >
< div className = "flex items-center gap-2" >
< Button
size = "sm"
onClick = { ( ) = > save ( row , 'save' ) }
disabled = { savingKey === row . key || ! dirty }
>
< Save className = "h-3.5 w-3.5 mr-1.5" / > Save
< / Button >
{ overridden ? (
< Button
size = "sm"
variant = "outline"
onClick = { ( ) = > save ( row , 'reset' ) }
disabled = { savingKey === row . key }
>
< RotateCcw className = "h-3.5 w-3.5 mr-1.5" / > Reset to default
< / Button >
) : null }
{ message ? . key === row . key ? (
< span
className = {
message . kind === 'ok' ? 'text-sm text-green-600' : 'text-sm text-red-600'
}
>
{ message . text }
< / span >
) : null }
< / div >
< / CardContent >
< / Card >
) ;
} )
) }
< / div >
< / div >
) ;
}