fix(uat): batch — timeline overshoot, name-sync, reset-password, dashboard cleanup, queue/seed hygiene + alpha UAT findings doc
UAT findings landed across the last few Playwright + React Grab passes; single grouped commit so the index doesn't fragment into 30 one-liners. User & auth: - `user-settings`: name now updates the avatar + topbar menu after save (was reading stale session). - `me/password-reset`: 3 bugs (token validation, error response shape, redirect chain). - Admin user permission-overrides route honours the same envelope as the rest of the admin surface. Dashboard: - Removed obsolete `revenue-breakdown-chart` + `dashboard-widgets-card` (replaced by the customisable widget grid). - Strip `revenue_breakdown` from analytics route + use-analytics + service + integration test so nothing renders an empty card. - Activity log timeline overshoot fix (`interest-timeline` + `entity-activity-feed`). - Tightened tiles: active-deals, berth-heat-widget, pipeline-value, kpi-tile. - `dev-mode-banner`: derive dismissed state synchronously instead of via an effect (set-state-in-effect lint rule). Forms & lists (assorted polish): - client / company / yacht / interest / reminder forms — validation + empty-state copy + tab transitions. - companies/yachts list tweaks; berth recommender panel; qualification checklist; supplemental info request button. Infra & misc: - Queue workers (ai / email / notifications) — log shape + per-job timeout consistency. - Auth / brochures / users schema small adjustments; seeds reflect permissions matrix changes. - Scan shell + scanner manifest + AI admin page small fixes. - `next.config.transpilePackages` adds `echarts`/`zrender`/`echarts-for-react` (recommended config from echarts-for-react inside Next). Docs: - `docs/superpowers/audits/alpha-uat-master.md` — single rolling cross-cutting UAT findings doc (per CLAUDE.md convention). - `docs/BACKLOG.md`: dashboard stats cards (§I) + activity-log normalization (§J). - 2026-05-18 audit log updated with this batch. - `CLAUDE.md` — small manual UAT scaffold notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,14 @@ interface BerthRecommenderPanelProps {
|
||||
* Falls back to 'ft' when missing.
|
||||
*/
|
||||
desiredUnit?: 'ft' | 'm' | null;
|
||||
/**
|
||||
* Number of berths already linked to the interest. When ≥ 1 the panel
|
||||
* defaults to collapsed (header-only) so the LinkedBerthsList card above
|
||||
* dominates the rep's attention. They can expand to browse more options
|
||||
* (multi-berth deals, swap recommendations). Zero / undefined keeps the
|
||||
* panel expanded so reps see options immediately.
|
||||
*/
|
||||
linkedBerthCount?: number;
|
||||
}
|
||||
|
||||
const TIER_LABELS: Record<Tier, { label: string; tone: string }> = {
|
||||
@@ -358,6 +366,7 @@ export function BerthRecommenderPanel({
|
||||
desiredWidthFt,
|
||||
desiredDraftFt,
|
||||
desiredUnit,
|
||||
linkedBerthCount,
|
||||
}: BerthRecommenderPanelProps) {
|
||||
const params = useParams<{ portSlug: string }>();
|
||||
const portSlug = params?.portSlug ?? '';
|
||||
@@ -370,6 +379,11 @@ export function BerthRecommenderPanel({
|
||||
// single pier (e.g. "show me only A-row matches"). Client-side over
|
||||
// the already-fetched result set; no service change required.
|
||||
const [selectedAreas, setSelectedAreas] = useState<string[]>([]);
|
||||
// Collapse state — defaults to collapsed when the deal already has at
|
||||
// least one linked berth (recommender becomes a "browse more options"
|
||||
// tool rather than the primary surface). Reps can manually expand any
|
||||
// time. Header click toggles.
|
||||
const [collapsed, setCollapsed] = useState<boolean>((linkedBerthCount ?? 0) > 0);
|
||||
|
||||
const hasDimensions = desiredLengthFt !== null;
|
||||
|
||||
@@ -380,7 +394,9 @@ export function BerthRecommenderPanel({
|
||||
|
||||
const { data, isFetching, refetch } = useQuery({
|
||||
queryKey,
|
||||
enabled: hasDimensions,
|
||||
// Skip the network call when collapsed — no point fetching options
|
||||
// the rep won't see. Re-fires automatically on expand.
|
||||
enabled: hasDimensions && !collapsed,
|
||||
queryFn: () =>
|
||||
apiFetch<{ data: Recommendation[] }>(`/api/v1/interests/${interestId}/recommend-berths`, {
|
||||
method: 'POST',
|
||||
@@ -441,32 +457,56 @@ export function BerthRecommenderPanel({
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{!collapsed ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setFiltersOpen((v) => !v)}
|
||||
disabled={!hasDimensions}
|
||||
>
|
||||
<Filter className="mr-1.5 size-3.5" aria-hidden />
|
||||
{filtersOpen ? 'Hide filters' : 'Add filters'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => refetch()}
|
||||
disabled={!hasDimensions || isFetching}
|
||||
>
|
||||
<RefreshCw className={cn('mr-1.5 size-3.5', isFetching && 'animate-spin')} />
|
||||
Refresh
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setFiltersOpen((v) => !v)}
|
||||
disabled={!hasDimensions}
|
||||
variant="ghost"
|
||||
onClick={() => setCollapsed((v) => !v)}
|
||||
aria-expanded={!collapsed}
|
||||
aria-controls={`recommender-body-${interestId}`}
|
||||
>
|
||||
<Filter className="mr-1.5 size-3.5" aria-hidden />
|
||||
{filtersOpen ? 'Hide filters' : 'Add filters'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => refetch()}
|
||||
disabled={!hasDimensions || isFetching}
|
||||
>
|
||||
<RefreshCw className={cn('mr-1.5 size-3.5', isFetching && 'animate-spin')} />
|
||||
Refresh
|
||||
{collapsed ? (
|
||||
<>
|
||||
<ChevronDown className="mr-1.5 size-3.5" aria-hidden />
|
||||
Show recommendations
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronUp className="mr-1.5 size-3.5" aria-hidden />
|
||||
Hide
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{filtersOpen && hasDimensions ? (
|
||||
{!collapsed && filtersOpen && hasDimensions ? (
|
||||
<AmenityFilterForm filters={amenityFilters} onChange={setAmenityFilters} />
|
||||
) : null}
|
||||
{hasDimensions && areaChips.length > 1 ? (
|
||||
{!collapsed && hasDimensions && areaChips.length > 1 ? (
|
||||
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
||||
<span className="text-xs font-medium text-muted-foreground">Area:</span>
|
||||
{areaChips.map((letter) => {
|
||||
@@ -504,55 +544,57 @@ export function BerthRecommenderPanel({
|
||||
</div>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{!hasDimensions ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Once length, width, and draft are set on this interest, the recommender will surface
|
||||
berths that fit. Edit the desired dimensions on the{' '}
|
||||
<Link href="?tab=overview" className="text-primary underline">
|
||||
Overview tab
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
) : isFetching && recommendations.length === 0 ? (
|
||||
<div className="space-y-2">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="h-16 animate-pulse rounded-lg bg-muted" />
|
||||
))}
|
||||
</div>
|
||||
) : recommendations.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground space-y-2">
|
||||
<p>
|
||||
{showAll
|
||||
? 'No berths in the port match these dimensions and filters.'
|
||||
: 'No berths fit inside the strict oversize tolerance.'}
|
||||
{collapsed ? null : (
|
||||
<CardContent className="space-y-3" id={`recommender-body-${interestId}`}>
|
||||
{!hasDimensions ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Once length, width, and draft are set on this interest, the recommender will surface
|
||||
berths that fit. Edit the desired dimensions on the{' '}
|
||||
<Link href="?tab=overview" className="text-primary underline">
|
||||
Overview tab
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
{!showAll && (
|
||||
<Button type="button" size="sm" variant="outline" onClick={() => setShowAll(true)}>
|
||||
Show oversized matches too
|
||||
) : isFetching && recommendations.length === 0 ? (
|
||||
<div className="space-y-2">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="h-16 animate-pulse rounded-lg bg-muted" />
|
||||
))}
|
||||
</div>
|
||||
) : recommendations.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground space-y-2">
|
||||
<p>
|
||||
{showAll
|
||||
? 'No berths in the port match these dimensions and filters.'
|
||||
: 'No berths fit inside the strict oversize tolerance.'}
|
||||
</p>
|
||||
{!showAll && (
|
||||
<Button type="button" size="sm" variant="outline" onClick={() => setShowAll(true)}>
|
||||
Show oversized matches too
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{recommendations.map((rec) => (
|
||||
<RecommendationCard
|
||||
key={rec.berthId}
|
||||
rec={rec}
|
||||
portSlug={portSlug}
|
||||
onAdd={setPendingBerth}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{hasDimensions && recommendations.length > 0 ? (
|
||||
<div className="flex justify-center pt-1">
|
||||
<Button type="button" size="sm" variant="ghost" onClick={() => setShowAll((v) => !v)}>
|
||||
{showAll ? 'Show top in-tolerance only' : 'Show oversized matches too'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{recommendations.map((rec) => (
|
||||
<RecommendationCard
|
||||
key={rec.berthId}
|
||||
rec={rec}
|
||||
portSlug={portSlug}
|
||||
onAdd={setPendingBerth}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{hasDimensions && recommendations.length > 0 ? (
|
||||
<div className="flex justify-center pt-1">
|
||||
<Button type="button" size="sm" variant="ghost" onClick={() => setShowAll((v) => !v)}>
|
||||
{showAll ? 'Show top in-tolerance only' : 'Show oversized matches too'}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
{pendingBerth ? (
|
||||
<AddBerthToInterestDialog
|
||||
|
||||
Reference in New Issue
Block a user