);
}
function Field({ label, required, optional, missing, full, hint, children }) {
return (
);
}
function SegRadio({ name, value, onChange, options }) {
return (
{options.map(o => (
))}
);
}
// ===== Ping DRI modal — slack-style direct message composer =====
function PingDriModal({ c, job, sender, onClose }) {
// Map job manager (e.g. "Edward Z.") → TEAM member
const driFromJob = TEAM.find(t => {
const last = job?.manager?.split(" ").slice(-1)[0]?.replace(".", "");
return last && t.name.includes(last);
}) || TEAM[0];
const [recipient, setRecipient] = useStateEX(driFromJob);
const [picker, setPicker] = useStateEX(false);
const [message, setMessage] = useStateEX("");
const [includeContext, setIncludeContext] = useStateEX(true);
const [sent, setSent] = useStateEX(false);
// Suggested message templates depend on the candidate's stage
const stageId = c.stage;
const templates = (() => {
if (["sourced","intro"].includes(stageId)) return [
`Quick nudge — ${c.name.split(" ")[0]} is ${c.days}d in ${STAGE_BY_ID[stageId].label.toLowerCase()}. Should I keep them warm or move on?`,
`${c.name.split(" ")[0]} just told me they have a competing offer with a one-week timeline. Worth pushing forward?`,
`Any feedback I can pass along to ${c.name.split(" ")[0]}? They're asking what to expect next.`,
];
if (["tech1","tech2","tech3"].includes(stageId)) return [
`Heads up — ${c.name.split(" ")[0]} has another offer pending and asked about timeline. Can we tighten up the next round?`,
`${c.name.split(" ")[0]} is asking about technical scope of the role. Anything specific I should share?`,
`Just confirming the panel for ${c.name.split(" ")[0]}'s next round — same interviewers as last time?`,
];
if (stageId === "onsite" || stageId === "offer") return [
`${c.name.split(" ")[0]} is asking about comp structure & equity vest. What's the latest I can share?`,
`Status check on ${c.name.split(" ")[0]}'s offer — they've got ${c.days}d on the clock and are asking about next steps.`,
`Are we still on track for an offer this week? ${c.name.split(" ")[0]} has another timeline pressure.`,
];
return [
`Quick check-in on ${c.name.split(" ")[0]} — anything I can do to help move this forward?`,
`${c.name.split(" ")[0]} asked me to follow up. Is there an update I can pass along?`,
];
})();
const drisForJob = [driFromJob, ...TEAM.filter(t => t.id !== driFromJob.id)].slice(0, 6);
const send = () => {
if (!message.trim()) return;
setSent(true);
setTimeout(onClose, 1400);
};
return (
e.stopPropagation()}>
Sends via Slack DM + email fallback
Send reminder to DRI
{sent ? (
✓
Sent to {recipient.name.split(" ")[0]}
They'll see it in Slack as a DM from Nuance ATS. You'll get a Slack ping back when they reply.
) : (
{/* Recipient picker */}
{picker && (
Or send to a different teammate
{drisForJob.map(t => (
))}
)}
{/* About the candidate (auto-attached context card) */}
{c.name}
{c.title} · {c.company} · for {job?.title}
Submitted by you · {c.days}d in current stage
{/* Message templates */}
{templates.map((t, i) => (
))}
{/* Composer */}
)}
{!sent && (
)}
);
}
function ExtPortalKpi({ value, label, hint }) {
return (
{value}
{label}
{hint}
);
}
// Compact status pill for a candidate. Re-uses the stage colors.
function ExtStatusBar({ c }) {
const stage = STAGE_BY_ID[c.stage];
const flowIdx = STAGE_FLOW.indexOf(c.stage);
// Render the flow as 7 dots with the active one filled
return (
{STAGE_FLOW.slice(0, 7).map((s, i) => {
const reached = i <= flowIdx;
const active = i === flowIdx;
const stg = STAGE_BY_ID[s];
return (
);
})}
);
}
function ExtCandidateRow({ c, onPing, onView }) {
const job = JOBS.find(j => j.id === c.jobId);
const isStale = c.days >= 14 && !["hired","rejected","not_interested","declined","backburner"].includes(c.stage);
const blockedOnUs = ["sourced","intro"].includes(c.stage) && c.days >= 5;
const blockedOnAgency = c.lastActivity.includes("Awaiting") && c.stage === "intro";
// Deterministic synthesized timeline from candidate id
const seed = (c.id || "").split("").reduce((s, ch) => s + ch.charCodeAt(0), 0);
const stageIdx = STAGE_FLOW.indexOf(c.stage);
// total days in pipeline: at least c.days, plus an offset proportional to how far along they are
const stagesPassed = Math.max(0, stageIdx);
const submittedDays = c.days + stagesPassed * 5 + (seed % 7);
// days since last activity: less than days-in-current-stage, capped at 14
const lastActivityDays = Math.min(c.days, 1 + (seed % Math.max(1, Math.min(c.days, 9))));
const today = new Date(2025, 3, 24); // Apr 24 2025 — matches the schedule view
const subDate = new Date(today.getTime() - submittedDays * 86400000);
const subLabel = subDate.toLocaleDateString("en-US", { timeZone: "America/Los_Angeles", month: "short", day: "numeric" });
return (
onView && onView(c, job)}>
{c.name}{c.title} · {c.company}
{subLabel}{submittedDays}d ago
= 7 ? " is-stale" : "")}>
{lastActivityDays}d
{c.lastActivity}
{blockedOnUs && (
· with Nuance
)}
{blockedOnAgency && (
· awaiting your input
)}
Track every candidate you've sent in. You see status, notes, and comp on
your submissions only — never anyone else's. Stage updates reflect what
Nuance's hiring team has done in the last 14 days.
You can submit candidates to any open role. {" "}
Nuance's hiring team responds within 3 business days. Submissions outside of
agreed roles will be returned.