- state-scoped lead worklists (list → row action → form modal) replace per-activity screens - shared Modal / ActivityActions / registry; bespoke bodies + generic ActivityForm - screen RBAC (canSeeScreen + RequireScreen), client-side pagination, lead filters - fixes: document scroll, PickerShell z-index above modal, friendly RBAC message, Lead360 compact INR, segments-card alignment
86 lines
2.9 KiB
TypeScript
86 lines
2.9 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
import { cn } from '../../lib/cn';
|
|
import type { Lead, Tone } from '../../types';
|
|
import { Avatar } from '../core/Avatar';
|
|
import { SegmentBadge } from './SegmentBadge';
|
|
import { ChannelBadge } from './ChannelBadge';
|
|
|
|
export interface LeadRowProps {
|
|
lead: Lead;
|
|
onClick?: () => void;
|
|
className?: string;
|
|
/** When set, replaces the "last action" cell (e.g. a worklist action button).
|
|
* Clicks inside it don't trigger the row's onClick. */
|
|
action?: ReactNode;
|
|
}
|
|
|
|
/** Shared grid template for the leads table — keep header + rows in sync. */
|
|
export const LEAD_GRID = 'grid-cols-[2fr_64px_96px_120px_1.4fr_2fr]';
|
|
|
|
const DOT: Record<Tone, string> = {
|
|
neutral: 'bg-muted',
|
|
success: 'bg-emerald-600',
|
|
warning: 'bg-amber-600',
|
|
accent: 'bg-sunrise-600',
|
|
info: 'bg-sky-600',
|
|
};
|
|
|
|
function scoreColor(score: number): string {
|
|
if (score >= 75) return 'var(--sunrise-600)';
|
|
if (score >= 50) return 'var(--amber-600)';
|
|
return 'var(--slate-500)';
|
|
}
|
|
|
|
/** Leads-list table row — name, score, segment, channel, owner, last AI action
|
|
* (or a custom `action` cell). */
|
|
export function LeadRow({ lead, onClick, className, action }: LeadRowProps) {
|
|
return (
|
|
<div
|
|
onClick={onClick}
|
|
className={cn(
|
|
'grid items-center gap-3 px-[18px] py-3.5 bg-card border-b border-border-subtle font-sans transition-colors duration-150',
|
|
LEAD_GRID,
|
|
onClick && 'cursor-pointer hover:bg-slate-50',
|
|
className,
|
|
)}
|
|
>
|
|
{/* name */}
|
|
<div className="flex items-center gap-2.5 min-w-0">
|
|
<Avatar name={lead.name} size={36} />
|
|
<div className="min-w-0">
|
|
<div className="text-base font-semibold text-strong truncate">{lead.name}</div>
|
|
<div className="text-xs text-faint">{lead.city}</div>
|
|
</div>
|
|
</div>
|
|
{/* score */}
|
|
<div className="font-numeric font-extrabold text-2xl tracking-[-0.02em] nums" style={{ color: scoreColor(lead.score) }}>
|
|
{lead.score}
|
|
</div>
|
|
{/* segment */}
|
|
<div>
|
|
<SegmentBadge segment={lead.segment} size="sm" />
|
|
</div>
|
|
{/* channel */}
|
|
<div>
|
|
<ChannelBadge channel={lead.channel} size="sm" />
|
|
</div>
|
|
{/* owner */}
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<Avatar name={lead.owner} ai={lead.ownerAi} size={24} />
|
|
<span className="text-sm text-body truncate">{lead.owner}</span>
|
|
</div>
|
|
{/* last AI action — or a custom action cell */}
|
|
{action !== undefined ? (
|
|
<div className="flex justify-end" onClick={(e) => e.stopPropagation()}>
|
|
{action}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-[7px] min-w-0">
|
|
<span className={cn('w-1.5 h-1.5 rounded-full shrink-0', DOT[lead.lastTone])} />
|
|
<span className="text-sm text-muted truncate">{lead.lastAction}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|