Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/recruitmatch/ActivityFeed.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/recruitmatch/ActivityFeed.tsx |
| Original SHA-256 | d2ffcf5efde850e98dc5162166a76363a2c4ed7c7adf6750c3d51c7baafaefed |
| Git blob SHA | 1350e1b1641b9551b36b790ccd4216a6472f743a |
| Reading-copy SHA-256 | d2ffcf5efde850e98dc5162166a76363a2c4ed7c7adf6750c3d51c7baafaefed |
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/ActivityFeed.tsx | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / frontend/src/tools/recruitmatch/ActivityFeed.tsx | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓218 lines · 8.4 KB
import { useState } from 'react'
import { Bot, Mail, MessageSquare, Phone, StickyNote, Users } from 'lucide-react'
import { Modal, Panel } from '../../components/ui'
import type { RActivity } from '../../lib/types'
import { useActivities, useLogActivity } from './activityQueries'
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 KINDS = ['call', 'email', 'inmail', 'text', 'meeting', 'note'] as const
const OUTCOMES = ['logged', 'connected', 'no_answer', 'left_message', 'replied', 'bounced', 'declined'] as const
// Only these two mean a person was actually reached, mirrored from the service. A hundred dials with two
// conversations is a different day from a hundred conversations.
const CONNECTS = new Set(['connected', 'replied'])
function kindIcon(kind: string) {
switch (kind) {
case 'call':
return <Phone size={12} />
case 'email':
case 'inmail':
return <Mail size={12} />
case 'text':
return <MessageSquare size={12} />
case 'meeting':
return <Users size={12} />
case 'submission':
case 'screen':
return <Bot size={12} />
default:
return <StickyNote size={12} />
}
}
export function ActivityFeed({
candidateId,
requisitionId,
applicationId,
title = 'Activity',
}: {
candidateId?: string
requisitionId?: string
applicationId?: string
title?: string
}) {
const [showCaptured, setShowCaptured] = useState(false)
const scope = {
candidate_id: candidateId,
requisition_id: requisitionId,
application_id: applicationId,
include_captured: showCaptured,
}
const q = useActivities(scope)
const log = useLogActivity()
const [open, setOpen] = useState(false)
const [kind, setKind] = useState<string>('call')
const [outcome, setOutcome] = useState<string>('logged')
const [note, setNote] = useState('')
const [when, setWhen] = useState('')
const rows = q.data?.activities ?? []
const submit = () => {
log.mutate(
{
kind,
outcome,
body: note,
candidate_id: candidateId ?? '',
requisition_id: requisitionId ?? '',
application_id: applicationId ?? '',
// Backdating is allowed and forward dating is refused by the server, which is what keeps
// today's numbers honest.
occurred_at: when ? new Date(when).toISOString() : '',
},
{
onSuccess: () => {
setNote('')
setWhen('')
setOutcome('logged')
setOpen(false)
},
},
)
}
return (
<Panel title={title} hint={q.isLoading ? 'loading' : `${q.data?.total ?? 0}`}>
<div className="mb-3 flex flex-wrap items-center justify-end gap-2">
{/* Captured rows are opt in. A recruiter's log means what they did; mixing in everything the
platform recorded on their behalf would drown it. */}
<label className="flex items-center gap-1.5 text-[11px] text-faint">
<input type="checkbox" checked={showCaptured} onChange={(e) => setShowCaptured(e.target.checked)} />
Show what the platform recorded
</label>
<button
onClick={() => setOpen(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"
>
<Phone size={13} />
Log activity
</button>
</div>
{q.isError ? (
<p className="py-4 text-center text-sm text-warn">Could not load activity. {(q.error as Error)?.message}</p>
) : rows.length === 0 ? (
<p className="py-4 text-center text-[11px] text-faint">
Nothing logged yet. Calls, emails and notes recorded here are what the recruiting metrics are
built from.
</p>
) : (
<ul className="space-y-2">
{rows.map((a: RActivity) => (
<li key={a.id} className="flex gap-2.5 rounded-lg border border-line/60 bg-navy-950/40 p-2.5">
<span
className="mt-0.5 shrink-0"
style={{ color: a.auto_captured ? 'var(--color-faint)' : 'var(--color-recruit)' }}
>
{kindIcon(a.kind)}
</span>
<div className="min-w-0 flex-1">
<p className="flex flex-wrap items-center gap-2 text-sm font-semibold">
{a.kind}
<span
className="text-[10px] font-bold uppercase tracking-wide"
style={{
color: CONNECTS.has(a.outcome) ? 'var(--color-ok)' : 'var(--color-faint)',
}}
>
{a.outcome.replace(/_/g, ' ')}
</span>
{a.auto_captured && (
<span
className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-wide"
style={{ color: 'var(--color-muted)', background: 'color-mix(in srgb, var(--color-muted) 14%, transparent)' }}
title="Recorded by the platform, not typed by a person. Counted separately."
>
platform
</span>
)}
</p>
{a.body && <p className="text-[11px] text-muted">{a.body}</p>}
<p className="text-[10px] text-faint">
{new Date(a.occurred_at).toLocaleString()}
{a.actor_name ? ` · ${a.actor_name}` : ''}
{a.candidate_name && !candidateId ? ` · ${a.candidate_name}` : ''}
</p>
</div>
</li>
))}
</ul>
)}
<Modal open={open} onClose={() => setOpen(false)} title="Log an activity">
<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">What</span>
<select value={kind} onChange={(e) => setKind(e.target.value)} className={inputCls}>
{KINDS.map((k) => (
<option key={k} value={k}>
{k}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Outcome</span>
<select value={outcome} onChange={(e) => setOutcome(e.target.value)} className={inputCls}>
{OUTCOMES.map((o) => (
<option key={o} value={o}>
{o.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} />
<span className="text-[10px] text-faint">
Leave blank for now. Yesterday's calls can be entered today; a future date is refused.
</span>
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Note</span>
<textarea value={note} onChange={(e) => setNote(e.target.value)} rows={3} className={inputCls} />
</label>
<p className="text-[10px] text-faint">
Once logged, only the note can be corrected. The type, time and outcome are fixed, because the
recruiting metrics are computed from them.
</p>
{log.isError && <p className="text-xs text-risk">{(log.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={log.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"
>
Log it
</button>
</div>
</div>
</Modal>
</Panel>
)
}