HR-ATS-Portal/frontend/src/screens/CandidatePage.jsx

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'))}
/>
)
}