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.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user