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:
2026-05-21 19:56:00 +02:00
parent 3999d4bbea
commit 292a8b5e4a
7 changed files with 261 additions and 21 deletions

View File

@@ -3,7 +3,7 @@
import { useEffect } from 'react';
import Link from 'next/link';
import { useRouter, useParams } from 'next/navigation';
import { Anchor, Plus } from 'lucide-react';
import { Anchor, Plus, Rows3, Rows4 } from 'lucide-react';
import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider';
import { DataTable } from '@/components/shared/data-table';
@@ -66,8 +66,13 @@ export function BerthList() {
'berth:statusChanged': [['berths']],
});
// Persisted column visibility — same pattern as ClientList / InterestList.
const { hidden, setHidden } = useTablePreferences('berths', BERTH_DEFAULT_HIDDEN);
// Persisted column visibility + row density — same pattern as
// ClientList / InterestList; density is new and falls back to
// 'comfortable' for users who haven't picked yet.
const { hidden, setHidden, density, setDensity } = useTablePreferences(
'berths',
BERTH_DEFAULT_HIDDEN,
);
const columnVisibility = Object.fromEntries(hidden.map((id) => [id, false]));
return (
@@ -112,6 +117,24 @@ export function BerthList() {
setAllFilters(savedFilters);
}}
/>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => setDensity(density === 'compact' ? 'comfortable' : 'compact')}
aria-label={
density === 'compact'
? 'Switch to comfortable row spacing'
: 'Switch to compact row spacing'
}
title={density === 'compact' ? 'Comfortable rows' : 'Compact rows'}
>
{density === 'compact' ? (
<Rows3 className="h-4 w-4" aria-hidden />
) : (
<Rows4 className="h-4 w-4" aria-hidden />
)}
</Button>
<ColumnPicker columns={BERTH_COLUMN_OPTIONS} hidden={hidden} onChange={setHidden} />
{canBulkAdd && (
<Button asChild size="sm" variant="default">
@@ -127,6 +150,7 @@ export function BerthList() {
<DataTable<BerthRow>
columns={berthColumns}
columnVisibility={columnVisibility}
density={density}
data={data}
isLoading={isLoading}
pagination={{