92 lines
3.5 KiB
JavaScript
92 lines
3.5 KiB
JavaScript
/* Full-page candidate profile — /candidate/:userId.
|
|
|
|
The modal outgrew its box: ten tabs of forms, rating tables and audit trail
|
|
need a real page with a real URL (shareable, refresh-safe). This is a thin
|
|
shell over CandidateProfile in `variant="page"` mode: the identity shell
|
|
carries only the userId and the live detail query fills everything else.
|
|
Opened from Candidates, Talent Pool and the Pipeline board.
|
|
|
|
Previous / Next walk the session queue written when the recruiter opened
|
|
this profile from a list. A direct URL falls back to the first page of
|
|
candidates they can already see, so the arrows still do something. */
|
|
|
|
import { useEffect } from 'react'
|
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
|
|
import { useAuth } from '../auth/AuthContext'
|
|
import { isHiringManager } from '../auth/permissions'
|
|
import { qk } from '../lib/queryKeys'
|
|
import { candidatePath, neighborsOf, readCandidateBrowse, uniqueBrowseEntries } from '../lib/candidateBrowse'
|
|
import * as candidatesApi from '../api/candidates'
|
|
import * as pipelineApi from '../api/pipeline'
|
|
import CandidateProfile from './CandidateProfile'
|
|
|
|
function useCandidateBrowse(userId) {
|
|
const { user } = useAuth()
|
|
const manager = isHiringManager(user)
|
|
const stored = readCandidateBrowse()
|
|
const fallback = useQuery({
|
|
queryKey: qk.candidates.browse({ manager }),
|
|
queryFn: async () => {
|
|
if (manager) {
|
|
const res = await candidatesApi.listForManager({ limit: 200, offset: 0 })
|
|
const rows = Array.isArray(res?.data) ? res.data : []
|
|
return uniqueBrowseEntries(rows.map((row) => ({
|
|
...row,
|
|
stage: pipelineApi.STAGE_FROM_STATUS[String(row.application_status || '').toUpperCase()] || null,
|
|
})))
|
|
}
|
|
const res = await candidatesApi.list({ limit: 100, offset: 0 })
|
|
const rows = Array.isArray(res?.data) ? res.data.map(candidatesApi.toApplicationListView) : []
|
|
return uniqueBrowseEntries(rows)
|
|
},
|
|
enabled: stored.length === 0 && Boolean(userId),
|
|
staleTime: 30_000,
|
|
})
|
|
const entries = stored.length ? stored : (fallback.data ?? [])
|
|
return neighborsOf(entries, userId)
|
|
}
|
|
|
|
export default function CandidatePage() {
|
|
const { userId } = useParams()
|
|
const navigate = useNavigate()
|
|
const [searchParams] = useSearchParams()
|
|
const browse = useCandidateBrowse(userId)
|
|
const tab = searchParams.get('tab') || undefined
|
|
|
|
function goTo(id) {
|
|
if (!id || String(id) === String(userId)) return
|
|
navigate(candidatePath(id, tab))
|
|
}
|
|
|
|
useEffect(() => {
|
|
function onKey(event) {
|
|
if (!event.altKey || event.metaKey || event.ctrlKey) return
|
|
const tag = event.target?.tagName
|
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || event.target?.isContentEditable) return
|
|
if (event.key === 'ArrowLeft' && browse.prev) {
|
|
event.preventDefault()
|
|
goTo(browse.prev.userId)
|
|
}
|
|
if (event.key === 'ArrowRight' && browse.next) {
|
|
event.preventDefault()
|
|
goTo(browse.next.userId)
|
|
}
|
|
}
|
|
window.addEventListener('keydown', onKey)
|
|
return () => window.removeEventListener('keydown', onKey)
|
|
}, [browse.prev, browse.next, userId, tab])
|
|
|
|
return (
|
|
<CandidateProfile
|
|
key={userId}
|
|
variant="page"
|
|
candidate={{ id: userId, userId, name: '' }}
|
|
browse={browse}
|
|
onBrowse={goTo}
|
|
onClose={() => (window.history.length > 1 ? navigate(-1) : navigate('/candidates'))}
|
|
/>
|
|
)
|
|
}
|