Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/recruitmatch/Assignment.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 |
| Branch | feature/aioe-upgrade |
| Path or archive member | frontend/src/tools/recruitmatch/Assignment.tsx |
| Original SHA-256 | 61fcca97a0c2feaf9f713ed2ed0be5ce58c746c813f1cfea49f6326f83e8001f |
| Git blob SHA | 8d841414068ef59835a89ded190143d95842ee9b |
| Reading-copy SHA-256 | 61fcca97a0c2feaf9f713ed2ed0be5ce58c746c813f1cfea49f6326f83e8001f |
Recovered source transcript
Download complete source text ↓578 lines · 25.6 KB
import { useState } from 'react'
import { ArrowLeft, Briefcase, CalendarClock, CheckCircle2, FileText, ShieldAlert } from 'lucide-react'
import { Link, useParams } from 'react-router-dom'
import { ToolHeader } from '../../components/ToolHeader'
import { Modal, Panel } from '../../components/ui'
import type { ROffer, RPlacement } from '../../lib/types'
import { recruitBrand } from './brand'
import {
useAssignment,
useCreatePlacement,
useEndPlacement,
useExtendOffer,
useRespondToOffer,
useScheduleInterview,
useUpdateInterview,
} from './assignmentQueries'
import { useDispositions } from './pipelineQueries'
import { ActivityFeed } from './ActivityFeed'
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'
const money = (v?: string) => {
if (!v) return ''
const [w, f = ''] = v.split('.')
const t = f.replace(/0+$/, '')
return '$' + w + '.' + (t.length <= 2 ? t.padEnd(2, '0') : t)
}
function statusPill(status: string): string {
switch (status) {
case 'accepted':
case 'active':
return 'var(--color-ok)'
case 'extended':
case 'pending_start':
return 'var(--color-gold)'
case 'declined':
case 'rescinded':
case 'fell_off':
case 'terminated':
return 'var(--color-risk)'
default:
return 'var(--color-muted)'
}
}
function Pill({ text }: { text: string }) {
return (
<span
className="rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide"
style={{
color: statusPill(text),
background: `color-mix(in srgb, ${statusPill(text)} 16%, transparent)`,
}}
>
{text.replace(/_/g, ' ')}
</span>
)
}
export function Assignment() {
const { appId } = useParams()
const q = useAssignment(appId ?? null)
const disp = useDispositions()
const schedule = useScheduleInterview(appId ?? '')
const updateIv = useUpdateInterview(appId ?? '')
const extend = useExtendOffer(appId ?? '')
const respond = useRespondToOffer(appId ?? '')
const place = useCreatePlacement(appId ?? '')
const end = useEndPlacement(appId ?? '')
const [ivOpen, setIvOpen] = useState(false)
const [round, setRound] = useState('1')
const [kind, setKind] = useState('client')
const [when, setWhen] = useState('')
const [who, setWho] = useState('')
const [offerOpen, setOfferOpen] = useState(false)
const [empType, setEmpType] = useState('contract')
const [bill, setBill] = useState('')
const [pay, setPay] = useState('')
const [salary, setSalary] = useState('')
const [feePct, setFeePct] = useState('')
const [placeOpen, setPlaceOpen] = useState(false)
const [startDate, setStartDate] = useState('')
const [guarantee, setGuarantee] = useState('90')
const [ending, setEnding] = useState<RPlacement | null>(null)
const [endStatus, setEndStatus] = useState('completed')
const [endDate, setEndDate] = useState('')
const [endCode, setEndCode] = useState('')
if (q.isLoading) return <Panel className="grid place-items-center py-24 text-muted">Loading assignment…</Panel>
if (q.isError || !q.data) {
return (
<Panel className="grid place-items-center py-24 text-warn">
Could not load this assignment. {(q.error as Error)?.message}
</Panel>
)
}
const a = q.data
const livePlacement = a.placements.find((p) => p.live)
const acceptedOffer = a.offers.find((o) => o.status === 'accepted')
const marginLine = (o: ROffer | RPlacement) => {
if (o.margin.fee) return `Fee ${money(o.margin.fee)}`
if (!o.margin.gross_per_hour) return ''
const pct = o.margin.margin_pct ? ` · ${o.margin.margin_pct}%` : ''
return `Margin ${money(o.margin.gross_per_hour)}/hr${pct}`
}
return (
<div className="space-y-4">
<ToolHeader name={recruitBrand.name} tagline="Assignment" 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} />
Job orders
</Link>
</ToolHeader>
<Panel>
<div className="flex flex-wrap items-baseline justify-between gap-3">
<h2 className="text-lg font-extrabold tracking-tight">{a.candidate_name}</h2>
<Pill text={a.stage} />
</div>
</Panel>
<Panel title="Interviews" hint={`${a.interviews.length} round${a.interviews.length === 1 ? '' : 's'}`}>
<div className="mb-3 flex justify-end">
<button
onClick={() => {
setRound(String(a.interviews.length + 1))
setIvOpen(true)
}}
className="flex items-center gap-2 rounded-lg border border-line/70 px-3 py-1.5 text-xs font-semibold text-muted hover:text-ink"
>
<CalendarClock size={13} />
Schedule a round
</button>
</div>
{a.interviews.length === 0 ? (
<p className="py-4 text-center text-[11px] text-faint">No interviews scheduled.</p>
) : (
<ul className="divide-y divide-line/50">
{a.interviews.map((v) => (
<li key={v.id} className="flex flex-wrap items-center justify-between gap-3 py-2.5">
<div className="min-w-0">
<p className="flex items-center gap-2 text-sm font-semibold">
Round {v.round} · {v.kind.replace(/_/g, ' ')}
<Pill text={v.outcome} />
</p>
<p className="text-[11px] text-faint">
{v.scheduled_at ? new Date(v.scheduled_at).toLocaleString() : 'No time set'}
{v.interviewer ? ` · ${v.interviewer}` : ''}
</p>
</div>
{v.outcome === 'scheduled' && (
<div className="flex gap-1.5">
{/* A concluded outcome needs a time, so completing one stamps the moment it was
recorded rather than asking for a date nobody has to hand. */}
{(['passed', 'failed', 'no_show'] as const).map((o) => (
<button
key={o}
onClick={() =>
updateIv.mutate({
id: v.id,
patch: {
expected_updated_at: v.updated_at,
outcome: o,
completed_at: new Date().toISOString(),
},
})
}
disabled={updateIv.isPending}
className="rounded border border-line/60 px-2 py-0.5 text-[10px] font-semibold text-faint hover:text-ink disabled:opacity-50"
>
{o.replace(/_/g, ' ')}
</button>
))}
</div>
)}
</li>
))}
</ul>
)}
{updateIv.isError && <p className="mt-2 text-xs text-risk">{(updateIv.error as Error).message}</p>}
</Panel>
<Panel title="Offers" hint={`${a.offers.length}`}>
<div className="mb-3 flex justify-end">
{/* Only one offer may be live at a time, so the button says why it is unavailable rather than
vanishing and leaving the reason to be guessed. */}
<button
onClick={() => setOfferOpen(true)}
disabled={a.offers.some((o) => o.status === 'extended')}
className="flex items-center gap-2 rounded-lg border border-line/70 px-3 py-1.5 text-xs font-semibold text-muted hover:text-ink disabled:cursor-not-allowed disabled:opacity-40"
title={
a.offers.some((o) => o.status === 'extended')
? 'An offer is already out to this candidate'
: undefined
}
>
<FileText size={13} />
Extend an offer
</button>
</div>
{a.offers.length === 0 ? (
<p className="py-4 text-center text-[11px] text-faint">No offers extended.</p>
) : (
<ul className="divide-y divide-line/50">
{a.offers.map((o) => (
<li key={o.id} className="flex flex-wrap items-center justify-between gap-3 py-2.5">
<div className="min-w-0">
<p className="flex items-center gap-2 text-sm font-semibold">
{o.employment_type.replace(/_/g, ' ')}
<Pill text={o.status} />
</p>
<p className="text-[11px] text-faint">
{o.bill_rate ? `Bill ${money(o.bill_rate)}/hr · Pay ${money(o.pay_rate)}/hr · ` : ''}
{o.salary ? `Salary ${money(o.salary)} · ` : ''}
{marginLine(o)}
</p>
{o.decline_reason && <p className="text-[11px] text-muted">{o.decline_reason}</p>}
</div>
<div className="flex flex-wrap gap-1.5">
{/* Only the moves the database will accept are offered. */}
{o.next_statuses.map((s) => (
<button
key={s}
onClick={() =>
respond.mutate({ id: o.id, expected_status: o.status, status: s })
}
disabled={respond.isPending}
className="rounded border border-line/60 px-2 py-0.5 text-[10px] font-semibold text-faint hover:text-ink disabled:opacity-50"
>
{s}
</button>
))}
</div>
</li>
))}
</ul>
)}
{(respond.isError || extend.isError) && (
<p className="mt-2 text-xs text-risk">
{((respond.error ?? extend.error) as Error)?.message}
</p>
)}
</Panel>
<Panel title="Placement" hint={a.placements.length ? `${a.placements.length}` : 'none'}>
<div className="mb-3 flex justify-end">
<button
onClick={() => {
setEmpType(acceptedOffer?.employment_type ?? 'contract')
setBill(acceptedOffer?.bill_rate ?? '')
setPay(acceptedOffer?.pay_rate ?? '')
setSalary(acceptedOffer?.salary ?? '')
setPlaceOpen(true)
}}
disabled={!!livePlacement}
className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-blue to-navy-700 px-4 py-2 text-xs font-bold text-white hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-40"
title={livePlacement ? 'This candidate already has a live assignment' : undefined}
>
<Briefcase size={13} />
Record a placement
</button>
</div>
{a.placements.length === 0 ? (
<p className="py-4 text-center text-[11px] text-faint">
No placement yet. Recording one moves this candidate to placed and takes a seat on the job order.
</p>
) : (
<ul className="divide-y divide-line/50">
{a.placements.map((p) => (
<li key={p.id} className="flex flex-wrap items-center justify-between gap-3 py-2.5">
<div className="min-w-0">
<p className="flex items-center gap-2 text-sm font-semibold">
{p.requisition_title || 'Assignment'}
<Pill text={p.status} />
{p.guarantee.breached && (
<span
className="flex items-center gap-1 text-[10px] font-bold uppercase"
style={{ color: 'var(--color-risk)' }}
title="Ended inside the guarantee window, so the fee is at risk"
>
<ShieldAlert size={10} />
guarantee breached
</span>
)}
</p>
<p className="text-[11px] text-faint">
{p.start_date}
{p.end_date ? ` to ${p.end_date}` : ''} · {marginLine(p)}
{p.guarantee.ends_on && !p.end_date
? ` · guarantee to ${p.guarantee.ends_on}${
p.guarantee.days_remaining !== undefined
? p.guarantee.days_remaining >= 0
? ` (${p.guarantee.days_remaining}d left)`
: ' (elapsed)'
: ''
}`
: ''}
</p>
</div>
{p.live && (
<button
onClick={() => {
setEnding(p)
setEndStatus('completed')
setEndDate('')
setEndCode('')
}}
className="rounded-lg border border-line/70 px-3 py-1.5 text-xs font-semibold text-muted hover:text-ink"
>
End assignment
</button>
)}
</li>
))}
</ul>
)}
{(place.isError || end.isError) && (
<p className="mt-2 text-xs text-risk">{((place.error ?? end.error) as Error)?.message}</p>
)}
</Panel>
<ActivityFeed applicationId={a.application_id} title="Activity on this candidate" />
<Modal open={ivOpen} onClose={() => setIvOpen(false)} title="Schedule an interview">
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Round</span>
<input type="number" min="1" max="20" value={round} onChange={(e) => setRound(e.target.value)} className={inputCls} />
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Type</span>
<select value={kind} onChange={(e) => setKind(e.target.value)} className={inputCls}>
{['phone_screen', 'technical', 'client', 'panel', 'final'].map((k) => (
<option key={k} value={k}>
{k.replace(/_/g, ' ')}
</option>
))}
</select>
</label>
</div>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">When</span>
<input type="datetime-local" value={when} onChange={(e) => setWhen(e.target.value)} className={inputCls} />
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Interviewer</span>
<input value={who} onChange={(e) => setWho(e.target.value)} className={inputCls} />
</label>
{schedule.isError && <p className="text-xs text-risk">{(schedule.error as Error).message}</p>}
<div className="flex justify-end gap-2 pt-1">
<button onClick={() => setIvOpen(false)} className="rounded-lg border border-line/70 px-3 py-2 text-sm font-semibold text-muted hover:text-ink">
Cancel
</button>
<button
onClick={() =>
schedule.mutate(
{
round: Number(round) || 1,
kind,
scheduled_at: when ? new Date(when).toISOString() : '',
interviewer: who,
},
{ onSuccess: () => setIvOpen(false) },
)
}
disabled={schedule.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"
>
Schedule
</button>
</div>
</div>
</Modal>
<Modal open={offerOpen} onClose={() => setOfferOpen(false)} title="Extend an offer">
<div className="space-y-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Employment type</span>
<select value={empType} onChange={(e) => setEmpType(e.target.value)} className={inputCls}>
{['contract', 'contract_to_hire', 'direct_hire'].map((k) => (
<option key={k} value={k}>
{k.replace(/_/g, ' ')}
</option>
))}
</select>
</label>
{empType === 'direct_hire' ? (
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Salary</span>
<input value={salary} onChange={(e) => setSalary(e.target.value)} placeholder="150000.00" className={inputCls} />
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Fee</span>
{/* Stored as a fraction. Accepting 20 and storing it would turn a 150000 salary into a
three million dollar fee, so the field says so. */}
<input value={feePct} onChange={(e) => setFeePct(e.target.value)} placeholder="0.20 for 20%" className={inputCls} />
</label>
</div>
) : (
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Bill rate</span>
<input value={bill} onChange={(e) => setBill(e.target.value)} placeholder="165.00" className={inputCls} />
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Pay rate</span>
<input value={pay} onChange={(e) => setPay(e.target.value)} placeholder="110.00" className={inputCls} />
</label>
</div>
)}
{extend.isError && <p className="text-xs text-risk">{(extend.error as Error).message}</p>}
<div className="flex justify-end gap-2 pt-1">
<button onClick={() => setOfferOpen(false)} className="rounded-lg border border-line/70 px-3 py-2 text-sm font-semibold text-muted hover:text-ink">
Cancel
</button>
<button
onClick={() =>
extend.mutate(
{ employment_type: empType, bill_rate: bill, pay_rate: pay, salary, fee_pct: feePct },
{ onSuccess: () => setOfferOpen(false) },
)
}
disabled={extend.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"
>
Extend
</button>
</div>
</div>
</Modal>
<Modal open={placeOpen} onClose={() => setPlaceOpen(false)} title="Record a placement">
<div className="space-y-3">
<p className="rounded-lg border border-line/60 bg-navy-950/40 p-3 text-[11px] text-muted">
This moves {a.candidate_name} to placed and takes a seat on the job order. If the job order is
already full it will be refused.
</p>
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Start date</span>
<input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} className={inputCls} />
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Guarantee days</span>
<input type="number" min="0" value={guarantee} onChange={(e) => setGuarantee(e.target.value)} className={inputCls} />
</label>
</div>
{empType === 'direct_hire' ? (
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Salary</span>
<input value={salary} onChange={(e) => setSalary(e.target.value)} className={inputCls} />
</label>
) : (
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Bill rate</span>
<input value={bill} onChange={(e) => setBill(e.target.value)} className={inputCls} />
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Pay rate</span>
<input value={pay} onChange={(e) => setPay(e.target.value)} className={inputCls} />
</label>
</div>
)}
{place.isError && <p className="text-xs text-risk">{(place.error as Error).message}</p>}
<div className="flex justify-end gap-2 pt-1">
<button onClick={() => setPlaceOpen(false)} className="rounded-lg border border-line/70 px-3 py-2 text-sm font-semibold text-muted hover:text-ink">
Cancel
</button>
<button
onClick={() =>
place.mutate(
{
offer_id: acceptedOffer?.id ?? '',
employment_type: empType,
start_date: startDate,
bill_rate: bill,
pay_rate: pay,
salary,
guarantee_days: Number(guarantee) || 0,
},
{ onSuccess: () => setPlaceOpen(false) },
)
}
disabled={!startDate || place.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"
>
Record
</button>
</div>
</div>
</Modal>
<Modal open={!!ending} onClose={() => setEnding(null)} title="End this assignment">
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
{(['completed', 'terminated', 'fell_off'] as const).map((s) => (
<button
key={s}
onClick={() => setEndStatus(s)}
className={`rounded-full px-3 py-1 text-xs font-semibold ${
endStatus === s
? 'bg-recruit/25 text-recruit shadow-[inset_0_0_0_1px_var(--color-recruit)]'
: 'border border-line/60 bg-navy-900 text-faint hover:text-muted'
}`}
>
{s.replace(/_/g, ' ')}
</button>
))}
</div>
{/* A completed assignment never breaches the guarantee, however short. Ending it early was the
client's call, not a failure to deliver. */}
{endStatus !== 'completed' && ending?.guarantee.ends_on && (
<p className="rounded-lg border border-warn/40 bg-navy-950/40 p-3 text-[11px] text-muted">
The guarantee runs to {ending.guarantee.ends_on}. Ending this way inside that window puts the
fee at risk.
</p>
)}
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">End date</span>
<input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} className={inputCls} />
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">
Move the candidate out of the pipeline (optional)
</span>
<select value={endCode} onChange={(e) => setEndCode(e.target.value)} className={inputCls}>
<option value="">Leave them at placed</option>
{(disp.data?.dispositions ?? []).map((dd) => (
<option key={dd.code} value={dd.code}>
{dd.label}
</option>
))}
</select>
</label>
{end.isError && <p className="text-xs text-risk">{(end.error as Error).message}</p>}
<div className="flex justify-end gap-2 pt-1">
<button onClick={() => setEnding(null)} className="rounded-lg border border-line/70 px-3 py-2 text-sm font-semibold text-muted hover:text-ink">
Cancel
</button>
<button
onClick={() =>
ending &&
end.mutate(
{
id: ending.id,
patch: { status: endStatus, end_date: endDate, disposition_code: endCode },
},
{ onSuccess: () => setEnding(null) },
)
}
disabled={!endDate || end.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"
>
<CheckCircle2 size={13} className="mr-1 inline" />
End
</button>
</div>
</div>
</Modal>
</div>
)
}