import { Badge } from '../ui/primitives' import { STAGE_FROM_STATUS } from '../api/pipeline' import { fmtDateTime, toDate, toInstant } from '../lib/format' import { Link } from 'react-router-dom' const SOURCE_LABEL = { inbox: 'Email', manual: 'Manual', form: 'Form', ats: 'ATS', filtered: 'Email', } const STAGE_BADGE = { Shortlist: 'b-indigo', Screening: 'b-teal', Assessment: 'b-purple', Interview: 'b-amber', Offer: 'b-green', Approved: 'b-green', Hired: 'b-green', 'On Hold': 'b-amber', Rejected: 'b-gray', 'Rejected — wrong format': 'b-red', 'No job assigned': 'b-gray', } export function applicationStatusLabel(status, item) { if (item?.rejection_reason === 'wrong_format' || String(status || '').toUpperCase() === 'WRONG_FORMAT') { return 'Rejected — wrong format' } const assigned = Boolean(item?.job_post_id || item?.jobPostId || (item?.source === 'form' && (item.job_title || item.jobTitle))) if (!assigned && (status == null || status === '' || String(status).toUpperCase() === 'CLOSED')) { return 'No job assigned' } if (status == null || status === '') return 'Shortlist' const key = String(status).toUpperCase() if (STAGE_FROM_STATUS[key]) return STAGE_FROM_STATUS[key] const extras = { UNREAD: 'Unread', IMPORTED: 'Imported', PROCESSED: 'Processed', COMPLETED: 'ATS scored', FAILED: 'ATS failed', BANKED: 'CV bank', } if (extras[key]) return extras[key] return key.charAt(0) + key.slice(1).toLowerCase() } function historyItemsOf(row) { if (!row) return [] if (Array.isArray(row.previousApplications)) return row.previousApplications if (Array.isArray(row.previous_applications)) return row.previous_applications return [] } /** Every application for this candidate, including the one currently open. */ export function candidateApplicationsOf(row) { if (!row) return [] const current = currentRowIds(row) const items = [...historyItemsOf(row)] if (current.size && !items.some((item) => isSameApplication(item, current))) { const self = syntheticCurrentApplication(row) if (self) items.push(self) } items.sort((a, b) => (appliedAtMs(b?.applied_at) ?? 0) - (appliedAtMs(a?.applied_at) ?? 0)) return items } /** Applications other than the open row — used by the Reapplied chip. */ export function previousApplicationsOf(row) { if (!row) return [] const current = currentRowIds(row) return candidateApplicationsOf(row).filter((item) => !isSameApplication(item, current)) } function syntheticCurrentApplication(row) { const assigned = row.assignedPost || row.assigned_job_post const position = row.kind !== 'email' && row.position && row.position !== '—' ? row.position : null const jobTitle = assigned?.title || row.job_title || row.jobTitle || row.currentTitle || position || null const kind = row.kind let source = row.source if (kind === 'form') source = 'form' else if (kind === 'email') source = 'inbox' else if (row.manualUploadId || row.manual_upload_candidate_id) source = 'manual' if (source && String(source).includes('@')) source = 'inbox' const id = row.id != null && row.id !== '' ? String(row.id) : null return { source: source || 'inbox', inbox_id: row.inboxId || row.inbox_id || null, message_id: kind === 'email' ? id : (row.message_id || null), form_data_id: kind === 'form' ? id : (row.form_data_id || null), manual_upload_candidate_id: row.manualUploadId || row.manual_upload_candidate_id || null, candidate_id: row.candidate_id || (kind == null && row.jobId ? row.id : null) || null, user_id: row.userId || row.user_id || null, job_post_id: row.assignedId || row.jobId || row.job_post_id || null, job_title: jobTitle, status: row.applicationStatus || row.processingState || row.status || null, applied_at: row.received || row.applied || row.applied_at || row.entry_date || row.when || null, } } function appliedAtMs(value) { if (value == null || value === '') return null if (value instanceof Date) { return Number.isNaN(value.getTime()) ? null : value.getTime() } const instant = toInstant(value) if (instant) return instant.getTime() const wall = toDate(value) return wall ? wall.getTime() : null } function currentRowIds(row) { return new Set( [ row.id, row.inboxId, row.inbox_id, row.message_id, row.messageId, row.form_data_id, row.manualUploadId, row.manual_upload_candidate_id, row.candidate_id, row.upstream_id, ] .filter((v) => v != null && v !== '') .map(String), ) } function isSameApplication(item, currentIds) { if (!item) return false return [ item.inbox_id, item.message_id, item.upstream_id, item.form_data_id, item.manual_upload_candidate_id, item.candidate_id, ].some((id) => id != null && id !== '' && currentIds.has(String(id))) } function hasAssignedJob(item) { if (!item) return false if (item.job_post_id || item.jobPostId) return true return item.source === 'form' && Boolean(item.job_title || item.jobTitle) } export function isReapplicant(row) { if (!row) return false return previousApplicationsOf(row).some(hasAssignedJob) } export function previousApplicationsTip(row) { const items = previousApplicationsOf(row) if (!items.length) return 'Applied before' return items.map((item) => { const job = item.job_title || item.jobTitle || 'No job assigned' return `${job} — ${applicationStatusLabel(item.status, item)}` }).join('\n') } /** Compact chip for tables, kanban cards, and inbox rows. */ export function ReappliedBadge({ row, className = '' }) { if (!isReapplicant(row)) return null const count = previousApplicationsOf(row).filter(hasAssignedJob).length return ( Reapplied{count > 1 ? ` · ${count}` : ''} ) } export function hrefForPreviousApplication(item) { if (!item) return null if (item.source === 'form' && item.form_data_id) { return `/inbox?open=${encodeURIComponent(item.form_data_id)}&kind=form` } if ((item.source === 'inbox' || item.source === 'filtered') && item.message_id) { return `/inbox?open=${encodeURIComponent(item.message_id)}&kind=email` } if (item.source === 'manual') { if (item.user_id) return `/candidate/${encodeURIComponent(item.user_id)}` if (item.manual_upload_candidate_id) { return `/matching?record=${encodeURIComponent(item.manual_upload_candidate_id)}` } } if (item.user_id) return `/candidate/${encodeURIComponent(item.user_id)}` if (item.form_data_id) { return `/inbox?open=${encodeURIComponent(item.form_data_id)}&kind=form` } if (item.message_id) { return `/inbox?open=${encodeURIComponent(item.message_id)}&kind=email` } return null } /** Full application list for profile / inbox / add-candidate. */ export function PreviousApplications({ row, title = 'Total applications' }) { const items = candidateApplicationsOf(row) // One row is the application already on screen — do not show a history card. if (items.length < 2) return null const current = currentRowIds(row) const heading = title === 'Total applications' ? `Total applications (${items.length})` : title return (
{heading}
{items.map((item, idx) => { const stage = applicationStatusLabel(item.status, item) const job = item.job_title || item.jobTitle || 'No job assigned' const href = hrefForPreviousApplication(item) const isCurrent = current.size > 0 && isSameApplication(item, current) const key = [ item.source, item.inbox_id, item.manual_upload_candidate_id, item.form_data_id, item.candidate_id, item.job_post_id, idx, ].filter(Boolean).join(':') const jobLabel = ( <> {job} {isCurrent ? ' (Current)' : ''} ) return (
{href ? ( e.stopPropagation()} > {jobLabel} ) : (
{jobLabel}
)}
{SOURCE_LABEL[item.source] || item.source || 'Application'} {item.applied_at ? ` · ${fmtDateTime(toInstant(item.applied_at) || item.applied_at)}` : ''}
{stage}
) })}
) }