Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/recruitmatch/RequisitionDetail.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/recruitmatch/RequisitionDetail.tsx |
| Original SHA-256 | faca11ad85d4e84ecf4b487b99d909c812de2233cd8c4cbedb2087753b3ef9a7 |
| Git blob SHA | b701e569e6d24415c6b23444a0b010b985428b9f |
| Reading-copy SHA-256 | faca11ad85d4e84ecf4b487b99d909c812de2233cd8c4cbedb2087753b3ef9a7 |
Every recorded occurrence
These 2 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / frontend/src/tools/recruitmatch/RequisitionDetail.tsx | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / frontend/src/tools/recruitmatch/RequisitionDetail.tsx | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓317 lines · 12.6 KB
import { useState } from 'react'
import { ArrowLeft, Clock, PauseCircle, Target, UserCheck, UserMinus, UserPlus, Users } from 'lucide-react'
import { Link, useParams } from 'react-router-dom'
import { ToolHeader } from '../../components/ToolHeader'
import { Modal, Panel } from '../../components/ui'
import { useAuth } from '../../lib/useAuth'
import { recruitBrand } from './brand'
import {
useChangeStatus,
useClaimRequisition,
useReleaseRequisition,
useRequisition,
} from './requisitionQueries'
import {
PRIORITY_LABEL,
REQ_TRANSITIONS,
STATUS_ACTION,
STATUS_LABEL,
priorityColor,
rateRange,
statusColor,
targetText,
} from './statusUi'
const inputCls =
'w-full rounded-lg border border-line/70 bg-navy-900 px-3 py-2 text-sm text-ink outline-none focus:border-recruit'
function Stat({ label, value, hint, icon }: { label: string; value: string; hint?: string; icon: React.ReactNode }) {
return (
<div className="rounded-xl border border-line/60 bg-navy-950/40 p-3">
<p className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-faint">
{icon}
{label}
</p>
<p className="mt-1 text-xl font-extrabold tracking-tight">{value}</p>
{hint && <p className="mt-0.5 text-[10px] text-faint">{hint}</p>}
</div>
)
}
export function RequisitionDetail() {
const { reqId } = useParams()
const { principal } = useAuth()
const q = useRequisition(reqId ?? null)
const change = useChangeStatus()
const claim = useClaimRequisition()
const release = useReleaseRequisition()
const [pending, setPending] = useState<string | null>(null)
const [note, setNote] = useState('')
if (q.isLoading) return <Panel className="grid place-items-center py-24 text-muted">Loading job order…</Panel>
if (q.isError || !q.data) {
return (
<Panel className="grid place-items-center py-24 text-warn">
Could not load this job order. {(q.error as Error)?.message}
</Panel>
)
}
const r = q.data
const mine = !!r.owner_user_id && r.owner_user_id === principal?.user_id
const canEdit = mine || !r.owner_user_id
const t = targetText(r)
const moves = REQ_TRANSITIONS[r.status] ?? []
const confirmMove = () => {
if (!pending) return
change.mutate(
{ id: r.id, expected_updated_at: r.updated_at, status: pending, note },
{
onSuccess: () => {
setPending(null)
setNote('')
},
},
)
}
return (
<div className="space-y-4">
<ToolHeader name={recruitBrand.name} tagline="Job order" accent={recruitBrand.accent}>
<Link
to="/recruit/requisitions"
className="flex items-center gap-2 rounded-lg border border-line/70 px-3 py-2 text-sm font-semibold text-muted hover:text-ink"
>
<ArrowLeft size={14} />
All job orders
</Link>
<Link
to={`/recruit/requisitions/${r?.id ?? reqId}/pipeline`}
className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-blue to-navy-700 px-4 py-2.5 text-sm font-bold text-white hover:brightness-110"
>
<Users size={14} />
Pipeline
</Link>
</ToolHeader>
<Panel>
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<h2 className="flex flex-wrap items-center gap-2 text-xl font-extrabold tracking-tight">
{r.title}
<span
className="rounded-full px-2.5 py-0.5 text-[11px] font-bold uppercase tracking-wide"
style={{
color: statusColor(r.status),
background: `color-mix(in srgb, ${statusColor(r.status)} 16%, transparent)`,
}}
>
{STATUS_LABEL[r.status] ?? r.status}
</span>
</h2>
<p className="mt-0.5 text-[11px] text-faint">
{r.client_id ? (
<Link to={`/recruit/clients/${r.client_id}`} className="hover:text-recruit">
{r.client_name}
</Link>
) : (
'No client'
)}
{r.hiring_manager_name ? ` · ${r.hiring_manager_name}` : ''} · {r.openings} opening
{r.openings === 1 ? '' : 's'} · {r.employment_type.replace(/_/g, ' ')}
<span style={{ color: priorityColor(r.priority) }}> · {PRIORITY_LABEL[r.priority]} priority</span>
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
{r.owner_user_id ? (
<>
<span
className="flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-semibold"
style={{
color: mine ? 'var(--color-ok)' : 'var(--color-warn)',
background: `color-mix(in srgb, ${mine ? 'var(--color-ok)' : 'var(--color-warn)'} 14%, transparent)`,
}}
>
<UserCheck size={13} />
{mine ? 'You are working this' : `Worked by ${r.owner_name}`}
</span>
{mine && (
<button
onClick={() => release.mutate(r.id)}
disabled={release.isPending}
className="flex items-center gap-1.5 rounded-lg border border-line/70 px-3 py-1.5 text-xs font-semibold text-muted hover:text-ink disabled:opacity-50"
>
<UserMinus size={13} />
Release
</button>
)}
</>
) : (
<button
onClick={() => claim.mutate(r.id)}
disabled={claim.isPending}
className="flex items-center gap-1.5 rounded-lg border border-recruit/50 px-3 py-1.5 text-xs font-semibold text-recruit hover:bg-recruit/10 disabled:opacity-50"
>
<UserPlus size={13} />
Take this job order
</button>
)}
</div>
</div>
{!canEdit && (
<p className="mt-3 rounded-lg border border-warn/40 bg-navy-950/40 p-3 text-[11px] text-muted">
{r.owner_name} is working this job order. Talk to them before changing it, or ask an admin to
reassign it.
</p>
)}
{/* The three clocks. Both the wall clock and the worked time are shown, because both are
legitimate and the industry does not agree on which to report. Showing only one invites the
question of what it excludes. */}
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Stat
label="Worked"
value={`${r.working_days.toFixed(1)}d`}
hint="excludes time on hold"
icon={<Clock size={10} />}
/>
<Stat label="Open" value={`${r.age_days.toFixed(1)}d`} hint="wall clock since opened" icon={<Clock size={10} />} />
<Stat
label="On hold"
value={`${r.held_days.toFixed(1)}d`}
hint={r.on_hold_since ? 'currently paused' : 'total across all holds'}
icon={<PauseCircle size={10} />}
/>
<Stat
label="Target"
value={t ? t.text : 'None set'}
hint={r.target_fill_date || 'no target fill date'}
icon={<Target size={10} />}
/>
</div>
{(r.bill_rate_min || r.bill_rate_max || r.salary_min || r.salary_max) && (
<p className="mt-3 text-[11px] text-faint">
{rateRange(r.bill_rate_min, r.bill_rate_max) && `Bill ${rateRange(r.bill_rate_min, r.bill_rate_max)}`}
{r.pay_rate_min || r.pay_rate_max
? ` · Pay ${rateRange(r.pay_rate_min, r.pay_rate_max)}`
: ''}
{r.salary_min || r.salary_max ? ` · Salary ${rateRange(r.salary_min, r.salary_max, '')}` : ''}
</p>
)}
{canEdit && moves.length > 0 && (
<div className="mt-4 flex flex-wrap gap-2 border-t border-line/50 pt-4">
<span className="self-center text-[11px] font-bold uppercase tracking-wider text-faint">Move to</span>
{/* Only legal destinations are offered. The server refuses anything else regardless, so this
is a convenience rather than the rule, but offering a button that always fails is worse
than offering fewer buttons. */}
{moves.map((m) => (
<button
key={m}
onClick={() => setPending(m)}
className="rounded-lg border px-3 py-1.5 text-xs font-semibold hover:brightness-125"
style={{ borderColor: `color-mix(in srgb, ${statusColor(m)} 50%, transparent)`, color: statusColor(m) }}
>
{STATUS_ACTION[m] ?? m}
</button>
))}
</div>
)}
{r.description && <p className="mt-3 whitespace-pre-wrap text-sm text-muted">{r.description}</p>}
</Panel>
<Panel title="History" hint={`${r.events.length} event${r.events.length === 1 ? '' : 's'}`}>
{r.events.length === 0 ? (
<p className="py-6 text-center text-sm text-faint">No history yet.</p>
) : (
<ol className="relative space-y-3 border-l border-line/60 pl-5">
{[...r.events].reverse().map((e) => (
<li key={e.id} className="relative">
<span
className="absolute -left-[23px] top-1.5 h-2.5 w-2.5 rounded-full ring-2 ring-navy-950"
style={{ background: statusColor(e.to_status) }}
/>
<p className="text-sm font-semibold">
{e.from_status
? `${STATUS_LABEL[e.from_status] ?? e.from_status} to ${STATUS_LABEL[e.to_status] ?? e.to_status}`
: `Opened as ${STATUS_LABEL[e.to_status] ?? e.to_status}`}
{e.from_status && e.days_in_from > 0 && (
<span className="ml-2 text-[11px] font-normal text-faint">
after {e.days_in_from.toFixed(1)}d
</span>
)}
</p>
{e.note && <p className="text-[11px] text-muted">{e.note}</p>}
<p className="text-[10px] text-faint">
{new Date(e.occurred_at).toLocaleString()}
{e.actor_user_name ? ` · ${e.actor_user_name}` : ''}
</p>
</li>
))}
</ol>
)}
</Panel>
<Modal
open={!!pending}
onClose={() => {
setPending(null)
setNote('')
}}
title={pending ? (STATUS_ACTION[pending] ?? pending) : ''}
>
<div className="space-y-3">
<p className="text-sm text-muted">
This job order moves from <strong>{STATUS_LABEL[r.status] ?? r.status}</strong> to{' '}
<strong>{pending ? (STATUS_LABEL[pending] ?? pending) : ''}</strong>.
</p>
{pending === 'on_hold' && (
<p className="rounded-lg border border-warn/40 bg-navy-950/40 p-3 text-[11px] text-muted">
Time on hold is subtracted from how long this order took to fill, so a client freeze does not
count against the desk.
</p>
)}
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Why (optional)</span>
{/* The note is what makes the history readable a year later. "Client froze the program" and
"candidate accepted elsewhere" are the same transition and different stories. */}
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
rows={2}
placeholder="Client froze the program"
className={inputCls}
/>
</label>
{change.isError && <p className="text-xs text-risk">{(change.error as Error).message}</p>}
<div className="flex justify-end gap-2 pt-1">
<button
onClick={() => {
setPending(null)
setNote('')
}}
className="rounded-lg border border-line/70 px-3 py-2 text-sm font-semibold text-muted hover:text-ink"
>
Cancel
</button>
<button
onClick={confirmMove}
disabled={change.isPending}
className="rounded-lg bg-gradient-to-r from-blue to-navy-700 px-4 py-2 text-sm font-bold text-white hover:brightness-110 disabled:opacity-50"
>
Confirm
</button>
</div>
</div>
</Modal>
</div>
)
}