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>
102 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|