/* ============================================================ Suggested-role picker — shared by Job Matching and Recruitment Inbox. JobCard is the AI/manual radiogroup row. PickRoleModal is the "choose a different role" search popup. Requirement chips light green when the resume text contains them (client-side substring, same as Matching). ============================================================ */ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import Modal from './Modal' import { Badge, EmptyState, Icon, ScoreChip } from './primitives' import { friendlyAuthError } from '../lib/errors' import { qk } from '../lib/queryKeys' import * as jobPostsApi from '../api/jobPosts' /** Requirement chip lights green when the resume text contains it (client-side). */ export function reqInResume(req, resumeText) { if (!req || !resumeText) return false const needle = (typeof req === 'string' ? req : (req?.name || req?.label || '')).trim().toLowerCase() if (!needle) return false return resumeText.toLowerCase().includes(needle) } function reqLabel(req) { if (req == null) return '' if (typeof req === 'string' || typeof req === 'number') return String(req) if (typeof req === 'object') return String(req.name || req.label || req.skill || '') return String(req) } export function JobCard({ post, rank, selected, onSelect, resumeText, manual, badge }) { const unavailable = Boolean(post?.unavailable) || !post?.title const title = post?.title || 'Unavailable' const meta = [ post?.employment_type, post?.location, post?.experience_min != null || post?.experience_max != null ? `${post?.experience_min ?? '?'}–${post?.experience_max ?? '?'} yrs` : null, ].filter(Boolean).join(' · ') const tag = badge ?? (manual ? 'Manual' : `AI #${rank}`) return (
!unavailable && onSelect(post.id)} onKeyDown={(e) => { if (unavailable) return if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() onSelect(post.id) } }} style={{ cursor: unavailable ? 'not-allowed' : 'pointer', opacity: unavailable ? 0.55 : 1, borderColor: selected ? 'var(--primary)' : undefined, boxShadow: selected ? 'var(--ring)' : undefined, marginBottom: 8, alignItems: 'flex-start', }} >
{tag}
{title}
{unavailable ? ( Unavailable ) : ( {post.status || 'draft'} )} {post?.overall_score != null && } {selected && }
{meta &&
{meta}
} {!unavailable && Array.isArray(post.requirements) && post.requirements.length > 0 && (
{post.requirements.slice(0, 8).map((req, i) => { const label = reqLabel(req) if (!label) return null const hit = reqInResume(req, resumeText) return ( {label} ) })}
)}
) } export function PickRoleModal({ onClose, onPick }) { const [q, setQ] = useState('') const { data = [], isPending, isError, error } = useQuery({ queryKey: qk.jobPosts.list({ search: q || undefined, top: 30 }), queryFn: async () => { const res = await jobPostsApi.list({ search: q || undefined, top: 30 }) return Array.isArray(res?.data) ? res.data : [] }, }) return ( Cancel} >
setQ(e.target.value)} placeholder="Search title or location…" autoFocus />
{isPending && Fetching job posts.} {isError && ( {friendlyAuthError(error, 'Request failed')} )} {!isPending && !isError && data.length === 0 && ( Try a different search. )}
{data.map((p) => (
{ onPick(p); onClose() }} >
{p.title}
{[p.employment_type, p.location].filter(Boolean).join(' · ') || '—'}
{p.status}
))}
) }