feat(bulk-berths): 2-step wizard for new-port setup

Step 5 per PRE-DEPLOY-PLAN § 1.4.13.

Service: bulkAddBerths(portId, inputs, meta) — input-level dedup
catches in-batch duplicates, then a single SELECT against existing
port rows rejects with ConflictError on first collision. All inserts
in one round-trip; audit log + realtime alert.

Validator: bulkAddBerthsSchema with min(1) max(500) per call.

Route: POST /api/v1/berths/bulk-add gated on berths.create.

Wizard UI (/[portSlug]/admin/berths/bulk-add):
  Step 1 — dock letter A-E, range start+end mooring numbers, tenure
    default. Generates N empty rows.
  Step 2 — editable table with per-row dimensions / pontoon / pricing.
    "Apply to all" inputs in the header row copy a value down every
    row at once (covers the "every row is 40ft × 15ft at €125k" case
    in two clicks). Per-row remove button.

Drag-fill deferred. Server-side mooring uniqueness check is canonical;
client-side dedup is a pre-flight courtesy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 15:45:06 +02:00
parent 4182652d49
commit 709ef350ff
5 changed files with 525 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import { PageHeader } from '@/components/shared/page-header';
import { BulkAddBerthsWizard } from '@/components/admin/bulk-add-berths-wizard';
export default function BulkAddBerthsPage() {
return (
<div className="space-y-6">
<PageHeader
title="Bulk add berths"
description="Create many berths at once. Pick a dock letter + range to generate the rows, then fill in per-row dimensions / pricing / pontoon. Standard fields (tenure, status) apply to every row; everything else is per-row."
/>
<BulkAddBerthsWizard />
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { NextResponse } from 'next/server';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { parseBody } from '@/lib/api/route-helpers';
import { errorResponse } from '@/lib/errors';
import { bulkAddBerths } from '@/lib/services/berths.service';
import { bulkAddBerthsSchema } from '@/lib/validators/berths';
/**
* POST /api/v1/berths/bulk-add
*
* Bulk-insert berths for new-port setup. Cap of 500 rows per call;
* the service rejects with ConflictError on first duplicate (within
* the input array or against existing port rows). Wizard UI lives at
* /[portSlug]/admin/berths/bulk-add.
*/
export const POST = withAuth(
withPermission('berths', 'create', async (req, ctx) => {
try {
const input = await parseBody(req, bulkAddBerthsSchema);
const result = await bulkAddBerths(ctx.portId, input.berths, {
userId: ctx.userId,
portId: ctx.portId,
ipAddress: ctx.ipAddress,
userAgent: ctx.userAgent,
});
return NextResponse.json({ data: result });
} catch (error) {
return errorResponse(error);
}
}),
);