feat(berths): inline spec-PDF preview, manual-pin badge, maintenance module toggle, under-offer popover
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m45s
Build & Push Docker Images / build-and-push (push) Successful in 8m11s

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:
2026-06-03 19:15:04 +02:00
parent 2a7f922a01
commit 1750e265e7
13 changed files with 301 additions and 59 deletions

View File

@@ -20,6 +20,7 @@ import { getPortBrandingConfig } from '@/lib/services/port-config';
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
import { isExpensesModuleEnabled } from '@/lib/services/expenses-module.service';
import { isResidentialModuleEnabled } from '@/lib/services/residential-module.service';
import { isMaintenanceModuleEnabled } from '@/lib/services/maintenance-module.service';
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const headerList = await headers();
@@ -127,12 +128,29 @@ export default async function DashboardLayout({ children }: { children: React.Re
const residentialModuleByPort: Record<string, boolean> =
Object.fromEntries(residentialModuleEntries);
// Per-port maintenance-module gate. Defaults to enabled (registry
// default) so existing ports keep the berth Maintenance tab on deploy.
// Resolved server-side so the tab SSRs in/out without flicker.
const maintenanceModuleEntries = await Promise.all(
ports.map(async (p) => {
try {
return [p.id, await isMaintenanceModuleEnabled(p.id)] as const;
} catch {
// Conservative default on lookup failure: keep the feature visible.
return [p.id, true] as const;
}
}),
);
const maintenanceModuleByPort: Record<string, boolean> =
Object.fromEntries(maintenanceModuleEntries);
return (
<QueryProvider>
<PortProvider
ports={ports}
defaultPortId={ports[0]?.id ?? null}
tenanciesModuleByPort={tenanciesModuleByPort}
maintenanceModuleByPort={maintenanceModuleByPort}
>
<PermissionsProvider>
<SocketProvider>

View File

@@ -4,12 +4,14 @@ import { withAuth, withPermission } from '@/lib/api/helpers';
import { parseBody } from '@/lib/api/route-helpers';
import { updateMaintenanceLogSchema } from '@/lib/validators/berths';
import { updateMaintenanceLog, deleteMaintenanceLog } from '@/lib/services/berths.service';
import { assertMaintenanceModuleEnabled } from '@/lib/services/maintenance-module.service';
import { errorResponse } from '@/lib/errors';
// PATCH /api/v1/berths/[id]/maintenance/[logId]
export const PATCH = withAuth(
withPermission('berths', 'edit', async (req, ctx, params) => {
try {
await assertMaintenanceModuleEnabled(ctx.portId);
const body = await parseBody(req, updateMaintenanceLogSchema);
const log = await updateMaintenanceLog(params.id!, params.logId!, ctx.portId, body, {
userId: ctx.userId,
@@ -28,6 +30,7 @@ export const PATCH = withAuth(
export const DELETE = withAuth(
withPermission('berths', 'edit', async (_req, ctx, params) => {
try {
await assertMaintenanceModuleEnabled(ctx.portId);
await deleteMaintenanceLog(params.id!, params.logId!, ctx.portId, {
userId: ctx.userId,
portId: ctx.portId,

View File

@@ -4,12 +4,14 @@ import { withAuth, withPermission } from '@/lib/api/helpers';
import { parseBody } from '@/lib/api/route-helpers';
import { addMaintenanceLogSchema } from '@/lib/validators/berths';
import { getMaintenanceLogs, addMaintenanceLog } from '@/lib/services/berths.service';
import { assertMaintenanceModuleEnabled } from '@/lib/services/maintenance-module.service';
import { errorResponse } from '@/lib/errors';
// GET /api/v1/berths/[id]/maintenance
export const GET = withAuth(
withPermission('berths', 'view', async (req, ctx, params) => {
try {
await assertMaintenanceModuleEnabled(ctx.portId);
const logs = await getMaintenanceLogs(params.id!, ctx.portId);
return NextResponse.json({ data: logs });
} catch (error) {
@@ -22,6 +24,7 @@ export const GET = withAuth(
export const POST = withAuth(
withPermission('berths', 'edit', async (req, ctx, params) => {
try {
await assertMaintenanceModuleEnabled(ctx.portId);
const body = await parseBody(req, addMaintenanceLogSchema);
const log = await addMaintenanceLog(params.id!, ctx.portId, body, {
userId: ctx.userId,