Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/recruitmatch/Requisitions.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/recruitmatch/Requisitions.tsx |
| Original SHA-256 | 74d4df86eeac4769ed937980d3e0fb5b3761368741dd58cf602b5692119cbe5c |
| Git blob SHA | 44d078507dc7f8e9627d5046c8032b399a185a79 |
| Reading-copy SHA-256 | 74d4df86eeac4769ed937980d3e0fb5b3761368741dd58cf602b5692119cbe5c |
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/Requisitions.tsx | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / frontend/src/tools/recruitmatch/Requisitions.tsx | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓336 lines · 14.1 KB
import { useState } from 'react'
import { Briefcase, Clock, PauseCircle, Plus, UserCheck, UserPlus, Users } from 'lucide-react'
import { Link, useSearchParams } 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 { useClients } from './clientQueries'
import { useClaimRequisition, useCreateRequisition, useRequisitions } from './requisitionQueries'
import { PRIORITY_LABEL, STATUS_LABEL, priorityColor, rateRange, statusColor, targetText } from './statusUi'
const chip = (on: boolean) =>
`rounded-full px-3 py-1 text-xs font-semibold transition ${
on
? 'bg-recruit/25 text-recruit shadow-[inset_0_0_0_1px_var(--color-recruit)]'
: 'bg-navy-900 text-faint hover:text-muted border border-line/60'
}`
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'
export function Requisitions() {
const { principal } = useAuth()
// The client screen links here with ?client=<id>. Reading it is what makes that link filter rather
// than quietly showing every order in the organization.
const [params, setParams] = useSearchParams()
const clientFilter = params.get('client') ?? ''
const [status, setStatus] = useState('active')
const [owner, setOwner] = useState('')
const [q, setQ] = useState('')
const [open, setOpen] = useState(false)
const list = useRequisitions({ client_id: clientFilter, status, owner, q })
const clients = useClients({})
const create = useCreateRequisition()
const claim = useClaimRequisition()
const [title, setTitle] = useState('')
const [clientId, setClientId] = useState('')
const [openings, setOpenings] = useState('1')
const [priority, setPriority] = useState('normal')
const [target, setTarget] = useState('')
const [billMin, setBillMin] = useState('')
const [billMax, setBillMax] = useState('')
const submit = () => {
create.mutate(
{
title,
client_id: clientId,
openings: Number(openings) || 1,
priority,
target_fill_date: target,
bill_rate_min: billMin,
bill_rate_max: billMax,
},
{
onSuccess: () => {
setTitle('')
setClientId('')
setOpenings('1')
setPriority('normal')
setTarget('')
setBillMin('')
setBillMax('')
setOpen(false)
},
},
)
}
const rows = list.data?.requisitions ?? []
return (
<div className="space-y-4">
<ToolHeader name={recruitBrand.name} tagline="Job orders" accent={recruitBrand.accent}>
<button
onClick={() => setOpen(true)}
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"
>
<Plus size={14} />
New job order
</button>
</ToolHeader>
<div className="flex flex-wrap items-center gap-2">
{/* Active means open plus on hold. A recruiter asking for their live roles does not mean to
exclude the paused ones, and quietly dropping them would understate the desk. */}
<button className={chip(status === 'active')} onClick={() => setStatus('active')}>
Active
</button>
<button className={chip(status === 'open')} onClick={() => setStatus('open')}>
Open
</button>
<button className={chip(status === 'on_hold')} onClick={() => setStatus('on_hold')}>
On hold
</button>
<button className={chip(status === 'filled')} onClick={() => setStatus('filled')}>
Filled
</button>
<button className={chip(status === '')} onClick={() => setStatus('')}>
All
</button>
<span className="mx-1 h-5 w-px bg-line/60" />
<button className={chip(owner === 'me')} onClick={() => setOwner(owner === 'me' ? '' : 'me')}>
Mine
</button>
<button
className={chip(owner === 'unassigned')}
onClick={() => setOwner(owner === 'unassigned' ? '' : 'unassigned')}
>
Unassigned
</button>
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Search title or labor category"
className={`${inputCls} ml-auto max-w-xs`}
/>
</div>
{clientFilter && (
<div className="flex items-center gap-2 rounded-lg border border-recruit/40 bg-recruit/10 px-3 py-2 text-xs">
<span className="font-semibold text-recruit">
Showing one client only{rows[0]?.client_name ? `: ${rows[0].client_name}` : ''}
</span>
<button
onClick={() => setParams({})}
className="ml-auto rounded border border-line/60 px-2 py-0.5 font-semibold text-muted hover:text-ink"
>
Show all clients
</button>
</div>
)}
<Panel title="Job orders" hint={list.isLoading ? 'loading' : `${list.data?.total ?? 0} total`}>
{list.isError ? (
<p className="py-6 text-center text-sm text-warn">
Could not load job orders. {(list.error as Error)?.message}
</p>
) : list.isLoading ? (
<p className="py-6 text-center text-muted">Loading job orders…</p>
) : rows.length === 0 ? (
<p className="py-8 text-center text-sm text-faint">
{owner === 'me'
? 'You are not working any job orders in this view.'
: status === 'active'
? 'No live job orders. Open one against a client to start filling roles.'
: 'Nothing matches this filter.'}
</p>
) : (
<ul className="divide-y divide-line/50">
{rows.map((r) => {
const mine = !!r.owner_user_id && r.owner_user_id === principal?.user_id
const t = targetText(r)
const rate = rateRange(r.bill_rate_min, r.bill_rate_max)
return (
<li key={r.id} className="flex flex-wrap items-center justify-between gap-3 py-3">
<div className="min-w-0 flex-1">
<p className="flex flex-wrap items-center gap-2">
<Link
to={`/recruit/requisitions/${r.id}`}
className="truncate text-sm font-semibold hover:text-recruit"
>
{r.title}
</Link>
<span
className="rounded-full px-2 py-0.5 text-[10px] 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>
{r.priority !== 'normal' && r.priority !== 'low' && (
<span
className="text-[10px] font-bold uppercase tracking-wide"
style={{ color: priorityColor(r.priority) }}
>
{PRIORITY_LABEL[r.priority]}
</span>
)}
</p>
<p className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-faint">
<span className="truncate">{r.client_name || 'No client'}</span>
<span className="flex items-center gap-1">
<Users size={10} />
{r.openings} opening{r.openings === 1 ? '' : 's'}
</span>
{/* Working days, not wall clock. Time the client had the order frozen is not time
this team took to fill it. */}
<span className="flex items-center gap-1" title="Days worked, excluding time on hold">
<Clock size={10} />
{r.working_days.toFixed(1)}d worked
</span>
{r.held_days > 0 && (
<span className="flex items-center gap-1" title="Time this order spent on hold">
<PauseCircle size={10} />
{r.held_days.toFixed(1)}d held
</span>
)}
{rate && <span>{rate}</span>}
{t && (
<span style={t.urgent ? { color: 'var(--color-warn)', fontWeight: 600 } : undefined}>
{t.text}
</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-2.5 py-1 text-[11px] font-semibold"
style={{
color: mine ? 'var(--color-ok)' : 'var(--color-muted)',
background: `color-mix(in srgb, ${mine ? 'var(--color-ok)' : 'var(--color-muted)'} 14%, transparent)`,
}}
>
<UserCheck size={12} />
{mine ? 'You' : r.owner_name}
</span>
) : (
<button
onClick={() => claim.mutate(r.id)}
disabled={claim.isPending}
className="flex items-center gap-1.5 rounded-full border border-recruit/50 px-2.5 py-1 text-[11px] font-semibold text-recruit hover:bg-recruit/10 disabled:opacity-50"
>
<UserPlus size={12} />
Claim
</button>
)}
</div>
</li>
)
})}
</ul>
)}
</Panel>
<Modal open={open} onClose={() => setOpen(false)} title="New job order">
<div className="space-y-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Title</span>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Cleared Systems Engineer"
className={inputCls}
/>
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Client</span>
<select value={clientId} onChange={(e) => setClientId(e.target.value)} className={inputCls}>
<option value="">No client yet</option>
{(clients.data?.clients ?? []).map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
</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">Openings</span>
<input
type="number"
min="1"
value={openings}
onChange={(e) => setOpenings(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">Priority</span>
<select value={priority} onChange={(e) => setPriority(e.target.value)} className={inputCls}>
{Object.entries(PRIORITY_LABEL).map(([k, v]) => (
<option key={k} value={k}>
{v}
</option>
))}
</select>
</label>
</div>
<div className="grid grid-cols-3 gap-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Target fill</span>
<input type="date" value={target} onChange={(e) => setTarget(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">Bill min</span>
<input
value={billMin}
onChange={(e) => setBillMin(e.target.value)}
placeholder="145.00"
className={inputCls}
/>
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Bill max</span>
<input
value={billMax}
onChange={(e) => setBillMax(e.target.value)}
placeholder="165.00"
className={inputCls}
/>
</label>
</div>
<p className="text-[11px] text-faint">
<Briefcase size={11} className="mr-1 inline" />
You will own this job order. It opens immediately and the clock starts now.
</p>
{create.isError && <p className="text-xs text-risk">{(create.error as Error).message}</p>}
<div className="flex justify-end gap-2 pt-1">
<button
onClick={() => setOpen(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={submit}
disabled={!title.trim() || create.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"
>
Open job order
</button>
</div>
</div>
</Modal>
</div>
)
}