feat(berths): inline spec-PDF preview, manual-pin badge, maintenance module toggle, under-offer popover
Post-cutover UAT batch #3: - #62 Spec tab renders the current berth spec PDF inline (lazy PdfViewer, toggleable, default-open) + explicit download. Interest Documents tab already previews/downloads linked deal docs inline (verified). - #57 Surface berths.status_override_mode through the interest-berths API; linked-berth rows show an amber "Pin overrides pitch" badge + corrected consequence copy when a berth is specifically-pitched but manually pinned (the soft-pin wins on the public map). - #63 New maintenance-module gate (maintenance_module_enabled, default on): registry + admin Settings toggle, maintenance-module.service, port-provider useMaintenanceModuleEnabled, layout wiring, buildBerthTabs hides the Maintenance tab when off, and both maintenance log routes assert the gate. - #66 BerthOccupancyChip: >1 competing interest opens a popover listing every deal (name + stage + in-EOI/primary + link); single stays a direct link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -143,6 +143,10 @@ export interface InterestBerthWithDetails extends InterestBerth {
|
||||
mooringNumber: string | null;
|
||||
area: string | null;
|
||||
status: string | null;
|
||||
/** Soft-pin marker: when 'manual', the berth's pinned status wins over
|
||||
* this link's is_specific_interest signal on the public map, so the UI
|
||||
* warns the rep that "Specifically pitching" won't surface Under Offer. */
|
||||
statusOverrideMode: string | null;
|
||||
lengthFt: string | null;
|
||||
widthFt: string | null;
|
||||
draftFt: string | null;
|
||||
@@ -169,6 +173,7 @@ export async function listBerthsForInterest(
|
||||
mooringNumber: berths.mooringNumber,
|
||||
area: berths.area,
|
||||
status: berths.status,
|
||||
statusOverrideMode: berths.statusOverrideMode,
|
||||
lengthFt: berths.lengthFt,
|
||||
widthFt: berths.widthFt,
|
||||
draftFt: berths.draftFt,
|
||||
|
||||
59
src/lib/services/maintenance-module.service.ts
Normal file
59
src/lib/services/maintenance-module.service.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Maintenance module gate. Port-scoped on/off switch for the per-berth
|
||||
* maintenance surface (the berth "Maintenance" tab, its log API, and the
|
||||
* maintenance-reminder notification preference).
|
||||
*
|
||||
* Defaults to ENABLED so existing ports keep the feature on deploy.
|
||||
* When an admin turns it off:
|
||||
* - the Maintenance tab disappears from the berth detail page (gated via
|
||||
* the port-resolved maintenanceModuleByPort prop on the layout →
|
||||
* port-provider → `useMaintenanceModuleEnabled()`)
|
||||
* - the maintenance log routes reject reads/writes at the boundary
|
||||
* (`assertMaintenanceModuleEnabled` → 404)
|
||||
* - previously-recorded maintenance logs are preserved (no destructive
|
||||
* cleanup) so re-enabling restores everything
|
||||
*/
|
||||
|
||||
import { and, eq, isNull, or } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { systemSettings } from '@/lib/db/schema/system';
|
||||
import { NotFoundError } from '@/lib/errors';
|
||||
|
||||
/**
|
||||
* Resolve whether the Maintenance module is currently active for the given
|
||||
* port. Reads from `system_settings.maintenance_module_enabled` (port-
|
||||
* scoped row first, then global row, then registry default = true).
|
||||
*
|
||||
* Defaulting to enabled mirrors how the feature behaved before the toggle
|
||||
* existed: deploying this change to a port that has never configured the
|
||||
* setting leaves the feature visible.
|
||||
*/
|
||||
export async function isMaintenanceModuleEnabled(portId: string): Promise<boolean> {
|
||||
const settingRow = await db
|
||||
.select({ value: systemSettings.value })
|
||||
.from(systemSettings)
|
||||
.where(
|
||||
and(
|
||||
eq(systemSettings.key, 'maintenance_module_enabled'),
|
||||
or(eq(systemSettings.portId, portId), isNull(systemSettings.portId)),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
// Stored JSONB shape is the raw boolean; only an explicit `false` turns
|
||||
// the module off — missing row / true / unrecognized shape => enabled.
|
||||
if (settingRow[0]?.value === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw `NotFoundError` (→ 404) when the Maintenance module is disabled.
|
||||
* Defense-in-depth for the maintenance log routes; the UI independently
|
||||
* hides the tab so a rep never sees a link that would 404.
|
||||
*/
|
||||
export async function assertMaintenanceModuleEnabled(portId: string): Promise<void> {
|
||||
const enabled = await isMaintenanceModuleEnabled(portId);
|
||||
if (!enabled) {
|
||||
throw new NotFoundError('Maintenance module is not enabled for this port.');
|
||||
}
|
||||
}
|
||||
@@ -640,6 +640,23 @@ export const REGISTRY: SettingEntry[] = [
|
||||
defaultValue: true,
|
||||
},
|
||||
|
||||
// ─── Operations - Maintenance module ──────────────────────────────────────
|
||||
// Port-scoped gate for the per-berth Maintenance surface (the berth
|
||||
// "Maintenance" tab + its log API + the maintenance-reminder notification
|
||||
// preference). Defaults to enabled so existing ports keep the feature on
|
||||
// deploy. Disabling hides the Maintenance tab and blocks the maintenance
|
||||
// log routes; previously-recorded maintenance logs are preserved.
|
||||
{
|
||||
key: 'maintenance_module_enabled',
|
||||
section: 'operations.maintenance',
|
||||
label: 'Berth maintenance module',
|
||||
description:
|
||||
'When enabled, reps can log maintenance work against individual berths (the "Maintenance" tab on each berth) and receive maintenance reminders. Turning this off hides the Maintenance tab everywhere and blocks its routes. Disabling does not delete previously-recorded maintenance logs — they reappear on re-enable.',
|
||||
type: 'boolean',
|
||||
scope: 'port',
|
||||
defaultValue: true,
|
||||
},
|
||||
|
||||
// ─── Operations - Invoices module ─────────────────────────────────────────
|
||||
// Port-scoped gate for the standalone `/invoices` flow. Audit conclusion
|
||||
// (2026-05-27, Initiative 1c): the schema is rich (invoices + invoice_line_items
|
||||
|
||||
Reference in New Issue
Block a user