Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/screens/ManagerView.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/screens/ManagerView.tsx |
| Original SHA-256 | 366e1b6a66d950e17c878d018d070e90e150798787b2031ff680359035bb8843 |
| Git blob SHA | 8d4fa328336103f4ff227bbf089bdee0f1e322a9 |
| Reading-copy SHA-256 | 366e1b6a66d950e17c878d018d070e90e150798787b2031ff680359035bb8843 |
Recovered source transcript
Download complete source text ↓470 lines · 19.9 KB
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { ArrowDown, ArrowRight, ArrowUp, Users2 } from 'lucide-react'
import { Panel } from '../components/ui'
import { Empty, Note, Select, btnGhost, btnPrimary } from '../components/form'
import { useAuth } from '../lib/useAuth'
import { useTeams } from '../lib/settingsQueries'
import {
EXCEPTION_LABELS,
LOAD_BENCHMARK,
LOAD_STRAINED,
MOVABLE_KINDS,
attainment,
bandColor,
betterColor,
severityColor,
useExceptions,
useForecast,
useGoals,
useManagerView,
useMoveDesk,
type Comparison,
type Exception,
type ExceptionKind,
type Goal,
type GoalMetric,
type RecruiterLoad,
} from '../lib/manager'
// THE MANAGER SCREEN.
//
// Everything here answers a question a manager is actually asked. The comparison exists because "fill rate 62
// percent" is not a management fact and "62, down from 71" is. The capacity list exists because the product
// already knew how many open orders each person held and never said it, and load is the single number that
// predicts whether the work is being done properly: above the benchmark, intake meetings get skipped,
// sourcing stops in favour of reviewing inbound, and screening goes shallow.
const PERIODS = [
{ value: '30', label: 'Last 30 days' },
{ value: '60', label: 'Last 60 days' },
{ value: '90', label: 'Last 90 days' },
]
// Figure renders one number against its predecessor. The arrow shows the DIRECTION and the colour shows
// whether that direction is wanted, which are two different facts and are why both cross the wire.
function Figure({
label,
c,
suffix = '',
goal,
}: {
label: string
c: Comparison
suffix?: string
goal?: Goal
}) {
const Icon = c.direction === 'up' ? ArrowUp : c.direction === 'down' ? ArrowDown : ArrowRight
// Attainment is computed from the figure ALREADY ON SCREEN rather than recomputed by the server,
// because the number a manager is judged against has to be the number they are looking at.
const pct = goal ? attainment(c.now, goal.target) : null
return (
<Panel className="px-4 py-3.5">
<div className="text-[11px] font-semibold uppercase tracking-wider text-muted">{label}</div>
<div className="mt-1 flex items-baseline gap-2">
<span className="text-3xl font-extrabold tabular-nums text-ink">
{c.now}
{suffix}
</span>
<span className="flex items-center gap-1 text-[11px] font-semibold" style={{ color: betterColor(c) }}>
<Icon size={12} />
{c.previous}
{suffix}
</span>
</div>
{pct === null ? (
<div className="mt-0.5 text-[10px] text-faint">against the window before</div>
) : (
<div className="mt-0.5 text-[10px]">
<span className="text-faint">against the window before · </span>
<b style={{ color: pct >= 100 ? 'var(--color-ok)' : pct >= 75 ? 'var(--color-warn)' : 'var(--color-risk)' }}>
{pct}% of {goal!.target}
</b>
</div>
)}
</Panel>
)
}
// MoveDeskForm is the action that answers the capacity list.
//
// It opens ON the person who is overloaded rather than as a separate screen somewhere else, because the
// whole failure this phase fixes is a manager seeing a problem and having nowhere to act on it. Making
// them navigate away and remember a name is the same failure with extra steps.
function MoveDeskForm({
from,
candidates,
onDone,
}: {
from: RecruiterLoad
candidates: RecruiterLoad[]
onDone: () => void
}) {
const [to, setTo] = useState('')
// Every kind by default. A manager opening this because somebody has resigned means the whole book, and
// the narrowing case is the rarer one.
const [kinds, setKinds] = useState<string[]>([...MOVABLE_KINDS])
const move = useMoveDesk()
const targets = candidates.filter((c) => c.user_id && c.user_id !== from.user_id)
if (move.data) {
const d = move.data
return (
<div className="mt-2 rounded-md border border-ok/40 bg-ok/10 px-3 py-2.5 text-xs">
{/* The counts are what ACTUALLY changed hands, read back from the server. A manager is never told
a desk moved without being told how much of it did. */}
<div className="font-semibold text-ok">
Moved {d.total === 1 ? '1 record' : `${d.total} records`} from {d.from_name || 'that desk'} to{' '}
{d.to_name || 'the new owner'}
</div>
<div className="mt-1 text-faint">
{d.clients} clients, {d.requisitions} job orders, {d.applications} live applications.
{d.total === 0 && ' There was nothing on that desk to move.'}
</div>
<button type="button" className={`${btnGhost} mt-2`} onClick={onDone}>
Done
</button>
</div>
)
}
return (
<div className="mt-2 space-y-2 rounded-md border border-line/70 bg-surface-2/40 px-3 py-2.5">
<div className="text-xs font-semibold text-ink">Move {from.name}’s desk to</div>
{targets.length === 0 ? (
<Note tone="muted">
There is nobody else on this desk to move the work to. Widen the scope, or add somebody to the team
first.
</Note>
) : (
<>
<Select
value={to}
choices={[
{ value: '', label: 'Choose who takes it' },
...targets.map((c) => ({ value: c.user_id ?? '', label: `${c.name} (${c.open_requisitions} open)` })),
]}
onChange={setTo}
/>
<div className="flex flex-wrap gap-3 text-[11px] text-muted">
{MOVABLE_KINDS.map((k) => (
<label key={k} className="inline-flex items-center gap-1.5">
<input
type="checkbox"
checked={kinds.includes(k)}
onChange={(e) =>
setKinds(e.target.checked ? [...kinds, k] : kinds.filter((x) => x !== k))
}
/>
{k === 'requisitions' ? 'job orders' : k === 'applications' ? 'live applications' : k}
</label>
))}
</div>
<p className="text-[10px] leading-relaxed text-faint">
Placed, rejected and withdrawn applications stay where they are. Moving a decision from six months
ago rewrites history for no benefit.
</p>
{move.isError && <Note tone="risk">{(move.error as Error).message}</Note>}
<div className="flex gap-2">
<button
type="button"
className={btnPrimary}
disabled={!to || kinds.length === 0 || move.isPending}
onClick={() =>
move.mutate({ from_user_id: from.user_id ?? '', to_user_id: to, include: kinds })
}
>
{move.isPending ? 'Moving' : 'Move the desk'}
</button>
<button type="button" className={btnGhost} onClick={onDone}>
Cancel
</button>
</div>
</>
)}
</div>
)
}
// ExceptionRow renders one thing a manager should look at. The sentence comes from the server already
// written, so the interface never has to reconstruct meaning out of a code and a number.
function ExceptionRow({ e }: { e: Exception }) {
return (
<li className="flex items-start justify-between gap-3 border-b border-line/40 py-2 last:border-0">
<div className="min-w-0">
<div className="truncate text-sm text-ink">{e.title || 'Untitled'}</div>
<div className="text-[11px] leading-relaxed text-faint">{e.detail}</div>
</div>
<span
className="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider"
style={{
color: severityColor(e.severity),
background: `color-mix(in srgb, ${severityColor(e.severity)} 14%, transparent)`,
}}
>
{e.severity === 'act' ? 'act' : 'watch'}
</span>
</li>
)
}
export function ManagerView() {
const { principal } = useAuth()
const teams = useTeams()
const [teamID, setTeamID] = useState('')
const [owner, setOwner] = useState('')
const [days, setDays] = useState('30')
// The PERIOD is passed, never a computed instant. See the note on useManagerView: a clock reading in a
// query key is a refetch loop.
const q = useManagerView(teamID, owner, days)
// The queue is scoped by the SAME team and owner as the figures above it, and the server resolves both
// through one resolver, so the two halves of this screen can never describe different people.
const ex = useExceptions(teamID, owner)
// Phase 4. Goals turn the figures into attainment; the forecast weights what is still open.
const goals = useGoals(teamID, owner)
const fc = useForecast(teamID, owner)
const [movingFrom, setMovingFrom] = useState('')
const v = q.data
// The goal that binds a metric for the CURRENT period. A goal for a period that has passed is history
// and must not be shown as though it were live.
const today = new Date().toISOString().slice(0, 10)
const goalFor = (m: GoalMetric): Goal | undefined =>
(goals.data?.goals ?? []).find((g) => {
if (g.metric !== m) return false
const end = new Date(new Date(g.period_start + "T00:00:00Z").getTime() + g.period_days * 86400000)
.toISOString()
.slice(0, 10)
return g.period_start <= today && today < end
})
const activeTeams = (teams.data?.teams ?? []).filter((t) => t.status === 'active')
const members = activeTeams.find((t) => t.id === teamID)?.members ?? []
return (
<div className="space-y-4">
<Panel title="Whose numbers" hint={v ? v.scope_label : ''}>
<div className="flex flex-wrap items-end gap-2">
<Select
value={teamID}
choices={[
{ value: '', label: 'The whole organization' },
...activeTeams.map((t) => ({ value: t.id, label: t.name })),
]}
onChange={(t) => {
setTeamID(t)
setOwner('')
}}
/>
{/* One person, which is the preparation view for a one to one. It only appears once a team is
chosen, because a list of every person in the company is not a control anybody uses. */}
{members.length > 0 && (
<Select
value={owner}
choices={[
{ value: '', label: 'Everyone on the desk' },
...members.map((m) => ({ value: m.user_id, label: m.name })),
]}
onChange={setOwner}
/>
)}
<Select value={days} choices={PERIODS} onChange={setDays} />
<Link to="/admin" className={`${btnGhost} ml-auto`}>
<Users2 size={12} /> Manage desks
</Link>
</div>
{activeTeams.length === 0 && (
<div className="mt-3">
<Note tone="muted">
No desks are set up, so this can only show the whole organization. Teams are created under
accounts and access, and they are what makes this screen about your own people.
</Note>
</div>
)}
</Panel>
{q.isError ? (
<Panel>
<Note tone="risk">{(q.error as Error).message}</Note>
</Panel>
) : !v ? (
<Panel className="py-10 text-center text-muted">Working out the numbers</Panel>
) : (
<>
<div className="grid grid-cols-2 gap-4 xl:grid-cols-4">
<Figure label="Placements" c={v.placements} goal={goalFor("placements")} />
<Figure label="Submissions" c={v.submissions} goal={goalFor("submissions")} />
<Figure label="Fill rate" c={v.fill_rate} suffix="%" goal={goalFor("fill_rate")} />
{/* Time to fill is the one where DOWN is better. The colour already knows. */}
<Figure label="Days to fill" c={v.time_to_fill_days} suffix="d" goal={goalFor("time_to_fill_days")} />
</div>
<Panel
title="Who is carrying what"
hint={v.overloaded > 0 ? `${v.overloaded} over the line` : 'all within range'}
>
<p className="pb-2 text-[11px] leading-relaxed text-faint">
The 2026 benchmark is {LOAD_BENCHMARK} open orders per recruiter. Above about {LOAD_STRAINED} the
work changes shape rather than just getting busier: intake meetings get skipped, sourcing stops
in favour of reviewing whatever came in, and screening goes shallow.
</p>
{v.capacity.length === 0 ? (
<Empty title="No open orders" reason="Nobody on this desk is carrying anything right now." />
) : (
<ul className="space-y-1.5">
{v.capacity.map((c) => (
<li key={c.user_id || c.name}>
<div className="flex items-center justify-between gap-3 text-sm">
<span className="min-w-0 truncate">{c.name}</span>
<span className="flex shrink-0 items-center gap-3 text-[11px] text-faint">
{c.active_applications} in pipeline
<span
className="rounded-full px-2 py-0.5 font-bold tabular-nums"
style={{
color: bandColor(c.band),
background: `color-mix(in srgb, ${bandColor(c.band)} 14%, transparent)`,
}}
>
{c.open_requisitions} open
</span>
{/* The action sits ON the row. Unassigned work has no desk to move, so the button
only appears for a real person. */}
{c.user_id && (
<button
type="button"
className={btnGhost}
onClick={() => setMovingFrom(movingFrom === c.user_id ? '' : (c.user_id ?? ''))}
>
Move desk
</button>
)}
</span>
</div>
{movingFrom && movingFrom === c.user_id && (
<MoveDeskForm from={c} candidates={v.capacity} onDone={() => setMovingFrom('')} />
)}
</li>
))}
</ul>
)}
{v.overloaded > 0 && (
<div className="mt-3">
<Note tone="warn">
{v.overloaded === 1 ? '1 person is' : `${v.overloaded} people are`} at or past the point
where the work changes shape. Move a whole desk from the row above, or a single job order
from the order itself.
</Note>
</div>
)}
</Panel>
{/* THE FORECAST. What the open pipeline is worth, weighted by this firm's OWN conversion
history. Deliberately not a constant: a plausible number from nowhere is what this product
refuses everywhere else, so a stage without enough history contributes nothing and is named. */}
<Panel
title="What the pipeline is worth"
hint={fc.data ? `${fc.data.open_applications} open` : ''}
>
{fc.isError ? (
<Note tone="risk">{(fc.error as Error).message}</Note>
) : !fc.data ? (
<div className="py-6 text-center text-xs text-muted">Weighting the pipeline</div>
) : (
<div className="space-y-3">
<div className="flex items-baseline gap-3">
<span className="text-3xl font-extrabold tabular-nums text-ink">
{fc.data.expected_placements}
</span>
<span className="text-[11px] text-muted">
placements expected from {fc.data.open_applications} open applications
</span>
</div>
<ul className="space-y-1">
{fc.data.conversions.map((c) => (
<li key={c.stage} className="flex items-center justify-between gap-3 text-[11px]">
<span className="capitalize text-muted">{c.stage}</span>
<span className="text-faint">
{c.measured ? (
<>
<b className="text-ink">{Math.round(Number(c.rate) * 100)}%</b> reach a placement,
from {c.reached} that got this far
</>
) : (
<>only {c.reached} have reached this stage, too few to measure</>
)}
</span>
</li>
))}
</ul>
{/* A floor, stated as one. The unmeasured stages hold real applications that contribute
nothing to the number above, and saying so is what keeps it from reading as a ceiling. */}
{fc.data.unmeasured_stages && fc.data.unmeasured_stages.length > 0 && (
<Note tone="muted">
This is a floor, not a prediction. {fc.data.unmeasured_stages.join(', ')} have fewer than{' '}
{fc.data.sample_floor} applications of history each, so anything sitting there counts
toward the open total and adds nothing to the expected figure. The number grows as the
history does.
</Note>
)}
</div>
)}
</Panel>
{/* THE EXCEPTION QUEUE. Deliberately below capacity: capacity says who is under pressure, this
says what is actually going wrong, and a manager reads them in that order. */}
<Panel
title="What needs you"
hint={ex.data ? (ex.data.total === 0 ? 'nothing outstanding' : `${ex.data.total} to look at`) : ''}
>
{ex.isError ? (
<Note tone="risk">{(ex.error as Error).message}</Note>
) : !ex.data ? (
<div className="py-6 text-center text-xs text-muted">Checking the desk</div>
) : ex.data.exceptions.length === 0 ? (
<Empty
title="Nothing is being neglected"
reason="No open order has gone untouched, no desk has gone quiet, and every redeployment has been picked up."
/>
) : (
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
{(Object.keys(EXCEPTION_LABELS) as ExceptionKind[]).map((k) =>
ex.data.counts[k] ? (
<span
key={k}
className="rounded-full border border-line/70 px-2.5 py-1 text-[10px] font-semibold text-muted"
>
{EXCEPTION_LABELS[k]}: {ex.data.counts[k]}
</span>
) : null,
)}
</div>
<ul>
{ex.data.exceptions.map((e) => (
<ExceptionRow key={`${e.kind}:${e.subject_id}`} e={e} />
))}
</ul>
{/* Never let a capped list read as the real number. */}
{ex.data.truncated && (
<Note tone="muted">
Showing the worst {ex.data.exceptions.length}. There are more, so narrow to one desk to
work through them.
</Note>
)}
</div>
)}
</Panel>
<p className="px-1 text-[11px] leading-relaxed text-faint">
Every figure covers the last {v.period_days} days and is compared with the {v.period_days} days
before it, so the two windows are the same length and a quarter is never compared with a month.
{principal?.role === 'manager' &&
' You see the desks you are on; an owner or an administrator sees every desk.'}
</p>
</>
)}
</div>
)
}