feat(berths): bulk price update + per-berth price API

Two new endpoints lift price editing out of the full berth-update form:

- `PATCH /api/v1/berths/[id]/price` — single-berth price edit triggered
  inline from the berth list / detail (no need to open the heavy edit
  modal just to retag a price).
- `POST /api/v1/berths/bulk-update-prices` — multi-row update from a
  selection in the berth list; transactional, audit-logged per row.

Berth list column gets an inline price-edit affordance backed by the
single-berth endpoint; the bulk action lives in the row-selection
toolbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 15:54:27 +02:00
parent b4bf9cca3f
commit 8c669e2918
5 changed files with 330 additions and 2 deletions

View File

@@ -68,6 +68,9 @@ export type BerthRow = {
/** Most-advanced pipeline stage among the berth's active interests. Null
* when no active interest is linked. Read-only; computed server-side. */
latestInterestStage?: string | null;
/** Count of non-terminal, non-archived interests linked to this berth.
* Drives the "Active interests" column + the demand sort. */
activeInterestCount?: number;
/** #67: source of the last status write. 'manual' when a human set it
* via the API; 'automated' when a berth-rule fired; null on rows that
* haven't been touched since seed. The reconciliation surface treats
@@ -85,6 +88,7 @@ export const BERTH_COLUMN_OPTIONS: Array<{ id: string; label: string }> = [
{ id: 'area', label: 'Area' },
{ id: 'status', label: 'Status' },
{ id: 'latestInterestStage', label: 'Latest deal stage' },
{ id: 'activeInterestCount', label: 'Active interests' },
{ id: 'sidePontoon', label: 'Side / Pontoon' },
{ id: 'dimensions', label: 'Dimensions' },
{ id: 'nominalBoatSize', label: 'Nominal boat size' },
@@ -281,6 +285,16 @@ export const berthColumns: ColumnDef<BerthRow, unknown>[] = [
);
},
},
{
id: 'activeInterestCount',
accessorKey: 'activeInterestCount',
header: 'Active interests',
cell: ({ row }) => {
const n = row.original.activeInterestCount ?? 0;
if (n === 0) return <span className="text-muted-foreground"></span>;
return <span className="font-medium tabular-nums">{n}</span>;
},
},
{
id: 'sidePontoon',
header: 'Side / Pontoon',