feat(notifications): include berth-range suffix in stage-change titles

Stage-change notification titles previously read "Acme Corp moved to
Reservation" with no context on which berths the deal covers. For
multi-berth deals the rep had to drill into the interest to see what
moved. With multiple deals in flight per client the bell tray became
ambiguous.

Switch the title-build path from `getPrimaryBerth` (single-row) to
`listBerthsForInterest` (full set) and append a compact suffix via
`formatBerthRange()`:

    Acme Corp moved to Reservation [A1-A3, B5]

Falls back to plain "<subject> moved to <stage>" when the interest
has no linked berths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 19:07:00 +02:00
parent bb7a371d1f
commit e52b3a6d38

View File

@@ -24,10 +24,12 @@ import { logger } from '@/lib/logger';
import {
getPrimaryBerth,
getPrimaryBerthsForInterests,
listBerthsForInterest,
removeInterestBerth,
upsertInterestBerth,
upsertInterestBerthTx,
} from '@/lib/services/interest-berths.service';
import { formatBerthRange } from '@/lib/templates/berth-range';
import { buildListQuery } from '@/lib/db/query-builder';
import { diffEntity } from '@/lib/entity-diff';
import { softDelete, restore, withTransaction } from '@/lib/db/utils';
@@ -1041,17 +1043,22 @@ export async function changeInterestStage(
// canonical STAGE_LABELS dictionary so "deposit_10pct" reads as
// "10% Deposit" everywhere.
void (async () => {
const [{ createNotification }, clientRow, primaryBerth] = await Promise.all([
const [{ createNotification }, clientRow, allBerths] = await Promise.all([
import('@/lib/services/notifications.service'),
db.query.clients.findFirst({
where: eq(clients.id, existing.clientId),
columns: { fullName: true },
}),
getPrimaryBerth(id).catch(() => null),
listBerthsForInterest(id).catch(
() => [] as Awaited<ReturnType<typeof listBerthsForInterest>>,
),
]);
const primaryBerth = allBerths[0] ?? null;
const moorings = allBerths.map((b) => b.mooringNumber).filter((m): m is string => Boolean(m));
const berthSuffix = moorings.length > 0 ? ` [${formatBerthRange(moorings)}]` : '';
const subject =
clientRow?.fullName ??
(primaryBerth ? `Berth ${primaryBerth.mooringNumber}` : 'this interest');
(primaryBerth?.mooringNumber ? `Berth ${primaryBerth.mooringNumber}` : 'this interest');
const fromLabel = oldStage
? (STAGE_LABELS[oldStage as PipelineStage] ?? oldStage.replace(/_/g, ' '))
: 'unknown';
@@ -1061,7 +1068,7 @@ export async function changeInterestStage(
portId,
userId: meta.userId,
type: 'interest_stage_changed',
title: `${subject} moved to ${toLabel}`,
title: `${subject} moved to ${toLabel}${berthSuffix}`,
description: `Stage changed from ${fromLabel} to ${toLabel}.`,
link: `/interests/${id}`,
entityType: 'interest',