Files
pn-new-crm/src/components/berths/active-interests-popover.tsx
Matt f0dbefcac2 chore(copy): em-dash sweep across user-facing JSX text + bump lint to error
Replaced 174 em-dashes (—) with " - " (space-hyphen-space) across 49
files in src/components + src/app. The em-dash reads as a tell-tale
"AI-generated" marker per the user's design feedback; hyphens with
spaces preserve the connector semantics without the AI tint.

Touched only lines outside pure-comment context (// /* * */). Code
comments, JSDoc, audit-log strings, structured logging strings, and
templates outside the lint scope retain their em-dashes for now —
they're not user-visible.

Also captured two remaining cases that used the `—` HTML entity
instead of the literal character (system-monitoring-dashboard,
interest-stage-picker) — replaced with a plain hyphen.

Bumped the existing `no-restricted-syntax` rule from `warn` → `error`
in eslint.config.mjs scoped to src/components/**/*.tsx +
src/app/**/*.tsx. New code reintroducing em-dashes in JSX text now
fails the lint gate.

Verified: tsc clean, vitest 1448/1448, eslint 0 em-dash warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:02:58 +02:00

102 lines
3.9 KiB
TypeScript

'use client';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { Loader2, Star } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { apiFetch } from '@/lib/api/client';
import { stageBadgeClass, stageLabel } from '@/lib/constants';
interface ActiveInterestRow {
interestId: string;
clientName: string;
pipelineStage: string;
isPrimary: boolean;
isInEoiBundle: boolean;
}
interface Props {
berthId: string;
portSlug: string;
count: number;
}
/**
* Click-to-expand popover for the berth-list "Active interests" cell.
* Lazy-loads the linked-interest list when the rep opens it; cached
* for 30s. Each row links to the interest detail page and shows the
* pipeline-stage chip + the primary/EOI-bundle flags so the rep can
* judge urgency without leaving the berth list.
*/
export function ActiveInterestsPopover({ berthId, portSlug, count }: Props) {
// Lazy-load: only fetch when the popover opens. Pattern from the
// detail-label fallback queries elsewhere in the codebase — the
// `enabled` flag flips on first open.
const { data, isLoading, isError } = useQuery<{ data: ActiveInterestRow[] }>({
queryKey: ['berth', berthId, 'active-interests'],
queryFn: () =>
apiFetch<{ data: ActiveInterestRow[] }>(`/api/v1/berths/${berthId}/active-interests`),
staleTime: 30_000,
// The popover only renders when the trigger is clicked, so the
// enabled gate falls out naturally from React Query's behaviour
// inside the conditionally-rendered PopoverContent.
});
if (count === 0) return <span className="text-muted-foreground"> - </span>;
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="inline-flex items-center gap-1 rounded-sm px-1 font-medium tabular-nums hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40"
aria-label={`Show ${count} active ${count === 1 ? 'interest' : 'interests'}`}
>
{count}
</button>
</PopoverTrigger>
<PopoverContent side="right" align="start" className="w-72 p-2">
<div className="mb-1 px-1 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Active interests
</div>
{isLoading ? (
<div className="flex items-center gap-2 px-2 py-3 text-sm text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />
Loading
</div>
) : isError ? (
<div className="px-2 py-3 text-sm text-destructive">Failed to load interests.</div>
) : (data?.data ?? []).length === 0 ? (
<div className="px-2 py-3 text-sm text-muted-foreground">No active interests.</div>
) : (
<ul className="divide-y">
{(data?.data ?? []).map((row) => (
<li key={row.interestId} className="px-1 py-1.5">
<Link
href={`/${portSlug}/interests/${row.interestId}`}
className="flex items-center justify-between gap-2 rounded-sm px-1 py-0.5 text-sm hover:bg-muted/60"
>
<span className="flex min-w-0 items-center gap-1 truncate">
{row.isPrimary ? (
<Star className="h-3 w-3 shrink-0 text-amber-500" aria-hidden />
) : null}
<span className="truncate">{row.clientName}</span>
</span>
<span
className={`shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${stageBadgeClass(
row.pipelineStage,
)}`}
>
{stageLabel(row.pipelineStage)}
</span>
</Link>
</li>
))}
</ul>
)}
</PopoverContent>
</Popover>
);
}