feat(berths): active-interests popover + row-density toggle on berth list
Two complementary UX upgrades on the berth list: 1. Active-interests popover — replaces the plain "Active interests" count cell with a click-to-expand popover. Each row shows the linked deal's client name, pipeline stage (with stage-badge tint), and a primary-star icon. Lazy-loads on first open (30s stale), capped at 20 entries server-side, sorted most-recently-updated first. Backed by `GET /api/v1/berths/[id]/active-interests`. 2. Row-density toggle — DataTable gains a `density: 'comfortable' | 'compact'` prop. Compact drops cell vertical padding from py-3 to py-1.5 so reps can scan many more berths per viewport on the high-density admin lists. Persisted alongside hidden-columns in `user_profiles.preferences. tablePreferences[entityType].density`. Hook returns `density + setDensity`; defaults to 'comfortable' for users who haven't chosen. The setter shares the same debounced PATCH with setHidden so toggling both doesn't multiply the network round-trips. Toolbar adds a Rows3/Rows4 icon button between the saved-views dropdown and the ColumnPicker. tooltip + aria-label flip to communicate the next state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
src/app/api/v1/berths/[id]/active-interests/route.ts
Normal file
70
src/app/api/v1/berths/[id]/active-interests/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { and, eq, isNull, desc } from 'drizzle-orm';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { errorResponse, NotFoundError } from '@/lib/errors';
|
||||
import { db } from '@/lib/db';
|
||||
import { berths } from '@/lib/db/schema/berths';
|
||||
import { interestBerths, interests } from '@/lib/db/schema/interests';
|
||||
import { clients } from '@/lib/db/schema/clients';
|
||||
|
||||
/**
|
||||
* GET /api/v1/berths/[id]/active-interests
|
||||
*
|
||||
* Lightweight read for the berth-list popover: every non-archived
|
||||
* non-terminal interest currently linked to this berth, plus the link's
|
||||
* flags (primary, in-EOI-bundle). Sorted most-recently-updated first so
|
||||
* the popover surfaces the hottest deals at the top.
|
||||
*
|
||||
* Tenancy: the berth row must belong to the caller's port; the inner
|
||||
* join to interests carries an implicit port filter via the interest.
|
||||
* Throws NotFoundError when the berth doesn't exist or is cross-port
|
||||
* (same enumeration-prevention as the other berth routes).
|
||||
*/
|
||||
export const GET = withAuth(
|
||||
withPermission('berths', 'view', async (_req, ctx, params) => {
|
||||
try {
|
||||
const berthId = params.id!;
|
||||
const berth = await db.query.berths.findFirst({
|
||||
where: and(eq(berths.id, berthId), eq(berths.portId, ctx.portId)),
|
||||
columns: { id: true },
|
||||
});
|
||||
if (!berth) throw new NotFoundError('Berth');
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
interestId: interests.id,
|
||||
clientName: clients.fullName,
|
||||
pipelineStage: interests.pipelineStage,
|
||||
isPrimary: interestBerths.isPrimary,
|
||||
isInEoiBundle: interestBerths.isInEoiBundle,
|
||||
updatedAt: interests.updatedAt,
|
||||
})
|
||||
.from(interestBerths)
|
||||
.innerJoin(interests, eq(interests.id, interestBerths.interestId))
|
||||
.innerJoin(clients, eq(clients.id, interests.clientId))
|
||||
.where(
|
||||
and(
|
||||
eq(interestBerths.berthId, berthId),
|
||||
eq(interests.portId, ctx.portId),
|
||||
isNull(interests.archivedAt),
|
||||
isNull(interests.outcome),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(interests.updatedAt))
|
||||
.limit(20);
|
||||
|
||||
return NextResponse.json({
|
||||
data: rows.map((r) => ({
|
||||
interestId: r.interestId,
|
||||
clientName: r.clientName,
|
||||
pipelineStage: r.pipelineStage,
|
||||
isPrimary: r.isPrimary,
|
||||
isInEoiBundle: r.isInEoiBundle,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user