import { useEffect, useMemo, useState } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { useMutation, useQuery } from '@tanstack/react-query' import Modal from '../ui/Modal' import { DEFAULT_PAGE_SIZE, Pagination, pageAfterSizeChange, pageWindow } from '../ui/DataTable' import PageHeader from '../ui/PageHeader' import { Avatar, Badge, EmptyState, Icon, SkeletonRows } from '../ui/primitives' import { useToast } from '../ui/Toast' import { useAuth } from '../auth/AuthContext' import { qk } from '../lib/queryKeys' import { friendlyAuthError } from '../lib/errors' import * as usersApi from '../api/users' import * as jobsApi from '../api/jobs' import * as inboxApi from '../api/inbox' const PAGE_SIZE_MAX = 500 /** Seeded `hiring_manager` role (backend/role/models.py::EnumRoles). */ const HIRING_MANAGER_ROLE_ID = 4 async function fetchManagers({ top = DEFAULT_PAGE_SIZE, skip = 0 } = {}) { const res = await usersApi.list({ roleId: HIRING_MANAGER_ROLE_ID, top, skip }) const rows = Array.isArray(res?.data) ? res.data : [] return { rows: rows.map(usersApi.toManagerView), total: typeof res?.total === 'number' ? res.total : 0, } } async function fetchJobs() { // Keep within GET /jobs/fetch `top` ceiling (and match Jobs.jsx) so a shared // qk.jobs.list() cache entry is never poisoned by a 422 from top=200. const res = await jobsApi.list({ top: 100 }) const rows = Array.isArray(res?.data) ? res.data : [] return rows.map(jobsApi.toJobView) } export default function Managers() { const { toast } = useToast() const { can } = useAuth() const navigate = useNavigate() const location = useLocation() const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE) const [detail, setDetail] = useState(null) const skip = (page - 1) * pageSize const managersQuery = useQuery({ queryKey: qk.managers.list({ roleId: HIRING_MANAGER_ROLE_ID, top: pageSize, skip }), queryFn: () => fetchManagers({ top: pageSize, skip }), }) const jobsQuery = useQuery({ queryKey: qk.jobs.list(), queryFn: fetchJobs }) const managers = managersQuery.data?.rows ?? [] const total = managersQuery.data?.total ?? 0 const jobs = jobsQuery.data ?? [] const openByManager = useMemo(() => { const map = {} for (const j of jobs) { if (j.hiringManagerId && j.status === 'Open') { const key = String(j.hiringManagerId) map[key] = (map[key] || 0) + 1 } } return map }, [jobs]) const totalReqs = jobs.filter((j) => j.status === 'Open').length const pages = Math.max(1, Math.ceil(total / pageSize)) const currentPage = Math.min(page, pages) useEffect(() => { if (page > pages) setPage(pages) }, [page, pages]) useEffect(() => { const id = location.state?.openManager if (id) setDetail(managers.find((m) => m.id === id) ?? null) }, [location.state, managers]) return (
{managersQuery.isPending && (
)} {managersQuery.isError && ( {friendlyAuthError(managersQuery.error, 'This directory needs rbac_users.view.')} )} {managersQuery.isSuccess && total === 0 && managers.length === 0 && ( No accounts currently hold the hiring-manager role. )} {managersQuery.isSuccess && (managers.length > 0 || total > 0) && ( <>
{managers.map((m) => (
{m.name}
{m.title || m.roleName || 'Hiring manager'}
{openByManager[String(m.id)] ?? m.openReqs ?? 0}Open Reqs
{m.teamSize ?? '—'}Team Size
{m.email || '—'}
))}
{total > 0 && (
{ setPageSize(n); setPage((p) => pageAfterSizeChange(p, total, n)) }} pageSizeMax={PAGE_SIZE_MAX} />
)} )} {detail && ( setDetail(null)} navigate={navigate} toast={toast} /> )}
) } function ManagerDetail({ manager: m, jobs, canSend, onClose, navigate, toast }) { const [messaging, setMessaging] = useState(false) const mineQuery = useQuery({ queryKey: qk.jobs.list({ hiringManagerId: m.id }), queryFn: async () => { const res = await jobsApi.list({ hiringManagerId: m.id, top: 100 }) const rows = Array.isArray(res?.data) ? res.data : [] return rows.map(jobsApi.toJobView) }, }) const mine = mineQuery.data ?? jobs.filter((j) => String(j.hiringManagerId) === String(m.id)) const openMine = mine.filter((j) => j.status === 'Open') const send = useMutation({ mutationFn: (body) => inboxApi.sendEmail({ to: m.email, subject: body.subject, body: body.body, contentType: 'text' }), onError: (err) => toast(friendlyAuthError(err, 'Could not send the message.'), 'error'), onSuccess: () => { setMessaging(false) toast('Message sent', 'success') }, }) const go = (path, state) => { onClose() navigate(path, { state }) } return ( ) : ( <> ) } >
{m.name}
{m.title || m.roleName || 'Hiring manager'}
{m.department && {m.department}} {m.teamSize != null && {m.teamSize} reports}
{messaging ? (