dropdwon inserted
parent
fd53730efe
commit
9dadcb4698
|
|
@ -15,6 +15,7 @@ import AiFieldAssist from '../ui/AiFieldAssist'
|
||||||
import DataTable from '../ui/DataTable'
|
import DataTable from '../ui/DataTable'
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
import PageHeader from '../ui/PageHeader'
|
import PageHeader from '../ui/PageHeader'
|
||||||
|
import { Tabs } from '../ui/Tabs'
|
||||||
import { Badge, EmptyState, FieldError, Icon, SkeletonRows } from '../ui/primitives'
|
import { Badge, EmptyState, FieldError, Icon, SkeletonRows } from '../ui/primitives'
|
||||||
import { useToast } from '../ui/Toast'
|
import { useToast } from '../ui/Toast'
|
||||||
import { useAuth } from '../auth/AuthContext'
|
import { useAuth } from '../auth/AuthContext'
|
||||||
|
|
@ -981,17 +982,6 @@ function JobOwnership({ job, canEdit }) {
|
||||||
const managersQuery = useManagerDirectory()
|
const managersQuery = useManagerDirectory()
|
||||||
const recruitersQuery = useRecruiterDirectory()
|
const recruitersQuery = useRecruiterDirectory()
|
||||||
|
|
||||||
const historyQuery = useQuery({
|
|
||||||
queryKey: qk.assignments.job(job.id),
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await assignmentsApi.listJob(job.id, { currentOnly: false })
|
|
||||||
const rows = Array.isArray(res?.data) ? res.data : []
|
|
||||||
return rows.map((r) => assignmentsApi.toAssignmentView(r))
|
|
||||||
},
|
|
||||||
enabled: Boolean(job.id),
|
|
||||||
retry: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const patch = useMutation({
|
const patch = useMutation({
|
||||||
mutationFn: (body) => jobsApi.update(job.id, body),
|
mutationFn: (body) => jobsApi.update(job.id, body),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -1003,8 +993,6 @@ function JobOwnership({ job, canEdit }) {
|
||||||
onError: (err) => toast(friendlyAuthError(err, 'Could not update the assignment.'), 'error'),
|
onError: (err) => toast(friendlyAuthError(err, 'Could not update the assignment.'), 'error'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const history = historyQuery.data ?? []
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
|
|
@ -1054,41 +1042,49 @@ function JobOwnership({ job, canEdit }) {
|
||||||
{canEdit && recruitersQuery.isError && (
|
{canEdit && recruitersQuery.isError && (
|
||||||
<p className="text-muted text-sm">The recruiter list needs the <code>tasks.view</code> permission.</p>
|
<p className="text-muted text-sm">The recruiter list needs the <code>tasks.view</code> permission.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ ...SECTION_LABEL, marginTop: 8 }}>Assignment history</div>
|
|
||||||
{historyQuery.isError ? (
|
|
||||||
<p className="text-muted text-sm">
|
|
||||||
{friendlyAuthError(historyQuery.error, 'History did not load.')}
|
|
||||||
</p>
|
|
||||||
) : historyQuery.isPending ? (
|
|
||||||
<p className="text-muted text-sm">Loading…</p>
|
|
||||||
) : history.length === 0 ? (
|
|
||||||
<p className="text-muted text-sm">No assignment history yet.</p>
|
|
||||||
) : (
|
|
||||||
<div className="list-tight">
|
|
||||||
{history.map((row) => (
|
|
||||||
<div className="list-row" key={row.id}>
|
|
||||||
<div className="lr-main">
|
|
||||||
<div className="lr-title">{row.name || 'Unknown'}</div>
|
|
||||||
<div className="lr-sub">
|
|
||||||
{[
|
|
||||||
ROLE_LABEL[row.role] || row.role?.replace(/_/g, ' '),
|
|
||||||
row.validFrom ? fmtShort(row.validFrom) : null,
|
|
||||||
row.validTo ? `→ ${fmtShort(row.validTo)}` : 'current',
|
|
||||||
row.assignedByName ? `by ${row.assignedByName}` : null,
|
|
||||||
].filter(Boolean).join(' · ')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!row.validTo && <Badge className="b-green">Current</Badge>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AssignmentHistory({ historyQuery }) {
|
||||||
|
const history = historyQuery.data ?? []
|
||||||
|
|
||||||
|
if (historyQuery.isError) {
|
||||||
|
return (
|
||||||
|
<p className="text-muted text-sm">
|
||||||
|
{friendlyAuthError(historyQuery.error, 'History did not load.')}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (historyQuery.isPending) {
|
||||||
|
return <p className="text-muted text-sm">Loading…</p>
|
||||||
|
}
|
||||||
|
if (history.length === 0) {
|
||||||
|
return <p className="text-muted">No assignment history yet.</p>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="list-tight">
|
||||||
|
{history.map((row) => (
|
||||||
|
<div className="list-row" key={row.id}>
|
||||||
|
<div className="lr-main">
|
||||||
|
<div className="lr-title">{row.name || 'Unknown'}</div>
|
||||||
|
<div className="lr-sub">
|
||||||
|
{[
|
||||||
|
ROLE_LABEL[row.role] || row.role?.replace(/_/g, ' '),
|
||||||
|
row.validFrom ? fmtShort(row.validFrom) : null,
|
||||||
|
row.validTo ? `→ ${fmtShort(row.validTo)}` : 'current',
|
||||||
|
row.assignedByName ? `by ${row.assignedByName}` : null,
|
||||||
|
].filter(Boolean).join(' · ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!row.validTo && <Badge className="b-green">Current</Badge>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/* Cover image, when the post has one — fetched with the bearer token into an
|
/* Cover image, when the post has one — fetched with the bearer token into an
|
||||||
object URL, because a bare <img src> cannot carry auth headers. null (404)
|
object URL, because a bare <img src> cannot carry auth headers. null (404)
|
||||||
simply renders nothing. */
|
simply renders nothing. */
|
||||||
|
|
@ -1113,6 +1109,18 @@ function JobCover({ jobId }) {
|
||||||
function JobDetail({
|
function JobDetail({
|
||||||
job: j, canEdit, canDelete, statusBusy, deleteBusy, onClose, onPublish, onEdit, onStatus, onDelete,
|
job: j, canEdit, canDelete, statusBusy, deleteBusy, onClose, onPublish, onEdit, onStatus, onDelete,
|
||||||
}) {
|
}) {
|
||||||
|
const [tab, setTab] = useState('details')
|
||||||
|
const historyQuery = useQuery({
|
||||||
|
queryKey: qk.assignments.job(j.id),
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await assignmentsApi.listJob(j.id, { currentOnly: false })
|
||||||
|
const rows = Array.isArray(res?.data) ? res.data : []
|
||||||
|
return rows.map((r) => assignmentsApi.toAssignmentView(r))
|
||||||
|
},
|
||||||
|
enabled: Boolean(j.id),
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Job Details"
|
title="Job Details"
|
||||||
|
|
@ -1160,37 +1168,52 @@ function JobDetail({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="info-grid mb-18">
|
<Tabs
|
||||||
<div className="info-item"><div className="il">Department</div><div className="iv">{j.department || '—'}</div></div>
|
value={tab}
|
||||||
<div className="info-item"><div className="il">Location</div><div className="iv">{j.location || '—'}</div></div>
|
onChange={setTab}
|
||||||
<div className="info-item"><div className="il">Employment Type</div><div className="iv">{j.type || '—'}</div></div>
|
tabs={[
|
||||||
<div className="info-item"><div className="il">Platform</div><div className="iv">{platformLabel(j.platform) || '—'}</div></div>
|
{ key: 'details', label: 'Details' },
|
||||||
<div className="info-item"><div className="il">Vacancies</div><div className="iv">{j.vacancies ?? '—'}</div></div>
|
{ key: 'history', label: 'History', count: historyQuery.data?.length },
|
||||||
<div className="info-item"><div className="il">Experience</div><div className="iv">{j.experience || '—'}</div></div>
|
]}
|
||||||
<div className="info-item"><div className="il">Created</div><div className="iv">{j.created ? fmtShort(j.created) : '—'}</div></div>
|
/>
|
||||||
<div className="info-item"><div className="il">Created by</div><div className="iv">{j.createdByName || '—'}</div></div>
|
|
||||||
<div className="info-item"><div className="il">Hiring Manager</div><div className="iv">{j.hiringManager || '—'}</div></div>
|
|
||||||
<div className="info-item"><div className="il">Assigned Recruiter</div><div className="iv">{j.recruiter || '—'}</div></div>
|
|
||||||
<div className="info-item"><div className="il">Closed at</div><div className="iv">{j.closedAt ? fmtShort(j.closedAt) : '—'}</div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<JobOwnership job={j} canEdit={canEdit} />
|
{tab === 'details' && (
|
||||||
|
|
||||||
{j.description && (
|
|
||||||
<>
|
<>
|
||||||
<div className="divider" />
|
<div className="info-grid mb-18">
|
||||||
<div className="mb-16">
|
<div className="info-item"><div className="il">Department</div><div className="iv">{j.department || '—'}</div></div>
|
||||||
<div style={SECTION_LABEL}>Description</div>
|
<div className="info-item"><div className="il">Location</div><div className="iv">{j.location || '—'}</div></div>
|
||||||
<p style={{ color: 'var(--text-2)' }}>{j.description}</p>
|
<div className="info-item"><div className="il">Employment Type</div><div className="iv">{j.type || '—'}</div></div>
|
||||||
|
<div className="info-item"><div className="il">Platform</div><div className="iv">{platformLabel(j.platform) || '—'}</div></div>
|
||||||
|
<div className="info-item"><div className="il">Vacancies</div><div className="iv">{j.vacancies ?? '—'}</div></div>
|
||||||
|
<div className="info-item"><div className="il">Experience</div><div className="iv">{j.experience || '—'}</div></div>
|
||||||
|
<div className="info-item"><div className="il">Created</div><div className="iv">{j.created ? fmtShort(j.created) : '—'}</div></div>
|
||||||
|
<div className="info-item"><div className="il">Created by</div><div className="iv">{j.createdByName || '—'}</div></div>
|
||||||
|
<div className="info-item"><div className="il">Hiring Manager</div><div className="iv">{j.hiringManager || '—'}</div></div>
|
||||||
|
<div className="info-item"><div className="il">Assigned Recruiter</div><div className="iv">{j.recruiter || '—'}</div></div>
|
||||||
|
<div className="info-item"><div className="il">Closed at</div><div className="iv">{j.closedAt ? fmtShort(j.closedAt) : '—'}</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<JobOwnership job={j} canEdit={canEdit} />
|
||||||
|
|
||||||
|
{j.description && (
|
||||||
|
<>
|
||||||
|
<div className="divider" />
|
||||||
|
<div className="mb-16">
|
||||||
|
<div style={SECTION_LABEL}>Description</div>
|
||||||
|
<p style={{ color: 'var(--text-2)' }}>{j.description}</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!!(j.skills && j.skills.length) && (
|
||||||
|
<div>
|
||||||
|
<div style={SECTION_LABEL}>Required Skills</div>
|
||||||
|
<div className="k-tags">{j.skills.map((s) => <span className="tag" key={s}>{s}</span>)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!!(j.skills && j.skills.length) && (
|
|
||||||
<div>
|
{tab === 'history' && <AssignmentHistory historyQuery={historyQuery} />}
|
||||||
<div style={SECTION_LABEL}>Required Skills</div>
|
|
||||||
<div className="k-tags">{j.skills.map((s) => <span className="tag" key={s}>{s}</span>)}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue