/* ============================================================ Recruitment Inbox — six seed-backed tabs plus the Email tab, which is the app's oldest real network call (GET /inbox/fetch, previously the only fetch in the entire prototype). The email body used to be interpolated raw into markup at js/inbox.js:292 — the single widest XSS sink in the repository, and the one that mattered most because inbound mail is attacker-supplied by definition. It renders as text now, which is the structural fix. ============================================================ */ import { useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useQuery, useQueryClient } from '@tanstack/react-query' import Modal from '../ui/Modal' import { Tabs } from '../ui/Tabs' import { Avatar, Badge, EmptyState, Icon, ScoreChip } from '../ui/primitives' import { useToast } from '../ui/Toast' import { seedQuery, useSeedMutation } from '../data/seedQueries' import { qk } from '../lib/queryKeys' import { friendlyAuthError } from '../lib/errors' import * as inboxApi from '../api/inbox' import { atsRecommendationClass, avatarColor, companies, fmtDate, fmtShort, getJob, initials as initialsOf, int, locations, pick, relTime, skillsPool, TODAY, } from '../data/seed' const TABS = ['All Applications', 'Unread', 'Imported', 'Processed', 'Rejected', 'Duplicates', 'Email'] /** The prototype computed "time ago" against a fixed 2026-07-09T20:00. */ const NOW = new Date('2026-07-09T20:00') function resumeText(i) { return `${i.name.toUpperCase()} ${i.email} · ${i.phone} ${'—'.repeat(30)} PROFESSIONAL SUMMARY ${i.experience} years of experience. Applied for ${i.position} via ${i.source}. EXPERIENCE • ${pick(companies)} — Senior role (2021–Present) • ${pick(companies)} — Associate (2018–2021) EDUCATION • Bachelor's Degree, Computer Science SKILLS • ${pick(skillsPool)}, ${pick(skillsPool)}, ${pick(skillsPool)}, ${pick(skillsPool)}` } function SourceChip({ item }) { // The dot carries the partner's brand colour; the label uses theme text — // 11px labels in the partner colour failed AA in both themes. return ( {item.source} ) } export default function Inbox() { const { toast } = useToast() const navigate = useNavigate() const { data: inbox = [] } = useQuery(seedQuery('inbox')) const { data: jobs = [] } = useQuery(seedQuery('jobs')) const { data: recruiters = [] } = useQuery(seedQuery('recruiters')) const updateInbox = useSeedMutation('inbox') const updateCandidates = useSeedMutation('candidates') const [tab, setTab] = useState('All Applications') const [selectedId, setSelectedId] = useState(null) const [q, setQ] = useState('') const [previewing, setPreviewing] = useState(null) const [assigning, setAssigning] = useState(null) const [noting, setNoting] = useState(null) const emailsQuery = useQuery({ queryKey: qk.mailbox.messages(), queryFn: async () => { const res = await inboxApi.listMessages() const rows = Array.isArray(res?.data) ? res.data : [] return rows.map((row) => ({ id: String(row.id), from: row.sender_name || row.fromEmail || 'Unknown', fromEmail: row.fromEmail || '', subject: row.subject || '', body: row.body || '', when: row.when ? new Date(row.when) : new Date(), unread: Boolean(row.unread), attachment: row.attachment_name || 'Resume.pdf', attachmentSize: '—', atsScore: 70, imported: false, })) }, enabled: tab === 'Email', }) const counts = useMemo( () => ({ 'All Applications': inbox.length, Unread: inbox.filter((i) => i.processing === 'Unread').length, Imported: inbox.filter((i) => i.processing === 'Imported').length, Processed: inbox.filter((i) => i.processing === 'Processed').length, Rejected: inbox.filter((i) => i.processing === 'Rejected').length, Duplicates: inbox.filter((i) => i.duplicate).length, Email: (emailsQuery.data ?? []).filter((e) => e.unread).length, }), [inbox, emailsQuery.data], ) const list = useMemo(() => { let l = inbox if (tab === 'Unread') l = l.filter((i) => i.processing === 'Unread') else if (tab === 'Imported') l = l.filter((i) => i.processing === 'Imported') else if (tab === 'Processed') l = l.filter((i) => i.processing === 'Processed') else if (tab === 'Rejected') l = l.filter((i) => i.processing === 'Rejected') else if (tab === 'Duplicates') l = l.filter((i) => i.duplicate) if (q) l = l.filter((i) => (i.name + i.position + i.source).toLowerCase().includes(q.toLowerCase())) return l }, [inbox, tab, q]) const selected = inbox.find((i) => i.id === selectedId) function select(id) { setSelectedId(id) updateInbox((items) => items.map((i) => (i.id === id ? { ...i, unread: false } : i))) } function makeCandidate(item, job, cs) { return { id: `CAN-${5001 + cs.length}`, name: item.name, initials: item.initials, color: item.color, email: item.email, phone: item.phone, jobId: job.id, jobTitle: job.title, department: job.department, experience: item.experience, currentCompany: pick(companies), currentTitle: job.title, location: pick(locations), stage: 'Applied', status: 'Applied', aiScore: item.atsScore, source: item.source, recruiter: item.recruiter, recruiterId: '', applied: new Date(TODAY), education: "Bachelor's Degree", skills: job.skills.slice(0, 4), rating: '4.0', salary: int(90, 180) * 1000, matchedSkills: job.skills.slice(0, 3), missingSkills: job.skills.slice(3), recommendation: item.atsScore >= 82 ? 'Strong Match' : 'Potential Match', subScores: { skills: item.atsScore, experience: item.atsScore, education: 80, keywords: item.atsScore, location: 100, salary: 90 }, noticePeriod: '1 month', availability: '2 weeks', certifications: [], favorite: false, interviewStatus: 'Not Scheduled', } } function importItem(item) { const job = getJob(item.jobId) || jobs[0] updateCandidates((cs) => [makeCandidate(item, job, cs), ...cs]) updateInbox((items) => items.map((i) => (i.id === item.id ? { ...i, processing: 'Imported', unread: false } : i))) toast(`${item.name} imported → Applied stage of ${job.title}`, 'success') } function parseResume(item) { updateInbox((items) => items.map((i) => (i.id === item.id ? { ...i, resumeStatus: 'Parsing' } : i))) toast('Parsing resume with AI…', 'info') setTimeout(() => { updateInbox((items) => items.map((i) => (i.id === item.id ? { ...i, resumeStatus: 'Parsed', atsScore: int(60, 96) } : i)), ) toast('Resume parsed — profile fields extracted', 'success') }, 1100) } function moveToPipeline(item) { if (item.processing !== 'Imported' && item.processing !== 'Processed') importItem(item) updateInbox((items) => items.map((i) => (i.id === item.id ? { ...i, processing: 'Processed' } : i))) toast(`${item.name} moved to pipeline`, 'success') setTimeout(() => navigate('/pipeline'), 700) } function reject(item) { updateInbox((items) => items.map((i) => (i.id === item.id ? { ...i, processing: 'Rejected', unread: false } : i))) toast(`${item.name} rejected`, 'warning') } return (

Recruitment Inbox

Every candidate, every source — one unified queue

Microsoft Graph API · Connected
{ setTab(t); setSelectedId(null) }} tabs={TABS.map((t) => ({ key: t, label: t, count: counts[t] }))} />
{tab === 'Email' ? ( ) : (
setQ(e.target.value)} placeholder="Search applications…" />
{list.length === 0 ? ( No applications in this view. ) : ( list.map((i) => (
select(i.id)} >
{i.name}{' '} {i.duplicate && ( DUP )}
{i.position}
{i.processing}
{relTime(Math.round((NOW - i.received) / 60000))}
)) )}
{!selected ? (
Choose an item from the list to view details and take action.
) : ( setPreviewing(selected)} onImport={() => importItem(selected)} onParse={() => parseResume(selected)} onAssign={() => setAssigning(selected)} onMove={() => moveToPipeline(selected)} onNote={() => setNoting(selected)} onReject={() => reject(selected)} /> )}
)}
{previewing && ( setPreviewing(null)} footer={ <> } >
{resumeText(previewing)}
)} {assigning && ( setAssigning(null)} onSave={(name) => { updateInbox((items) => items.map((i) => (i.id === assigning.id ? { ...i, recruiter: name } : i))) setAssigning(null) toast(`Recruiter assigned to ${assigning.name}`, 'success') }} /> )} {noting && ( setNoting(null)} footer={ <> } >