Skip to content

Instantly share code, notes, and snippets.

@thecreazy
Last active March 2, 2026 10:03
Show Gist options
  • Select an option

  • Save thecreazy/8a954076966bc2b61701a460b564c151 to your computer and use it in GitHub Desktop.

Select an option

Save thecreazy/8a954076966bc2b61701a460b564c151 to your computer and use it in GitHub Desktop.
import React, { useEffect, useMemo, useRef, useState } from "react";
type Role = "admin" | "recruiter" | "hiring_manager";
export default function PipelineBoard(props: {
jobId: string;
tenantId: string;
data: any; // stages + applications
role: Role;
onDo?: (x: any) => void;
flag?: boolean;
}) {
const { jobId, tenantId, data, role } = props;
const [d, setD] = useState<any>(data);
const [err, setErr] = useState<string>("");
const [loading, setLoading] = useState(false);
const [drag, setDrag] = useState<any>(null);
const [msg, setMsg] = useState<string>("");
const prev = useRef<any>(null);
const API = "https://api.atlashire.local";
const canMove = role === "admin" || role === "recruiter";
useEffect(() => {
setD(data);
}, [data, tenantId]);
// SSE: live updates
useEffect(() => {
const es: any = new (window as any).EventSource(
`${API}/api/jobs/${jobId}/events?tenant=${tenantId}`
);
es.onmessage = (e: MessageEvent) => {
try {
const ev = JSON.parse(e.data);
if (ev.type === "app_moved") {
// NOTE: simplistic update, no conflict resolution
setD((old: any) => {
const nx = { ...old };
const apps = nx.applications || [];
for (let i = 0; i < apps.length; i++) {
if (apps[i].id === ev.appId) {
apps[i] = { ...apps[i], stageId: ev.toStageId };
break;
}
}
nx.applications = apps;
return nx;
});
}
} catch (e2) {
// ignore
}
};
return () => {
// es.close();
};
}, [jobId]);
// build columns
const cols = useMemo(() => {
const st = (d?.stages || []).slice().sort((a: any, b: any) => a.order - b.order);
const apps = d?.applications || [];
const m: Record<string, any[]> = {};
for (let i = 0; i < st.length; i++) m[st[i].id] = [];
for (let j = 0; j < apps.length; j++) {
const a = apps[j];
if (!m[a.stageId]) m[a.stageId] = [];
m[a.stageId].push(a);
}
Object.keys(m).forEach((k) => m[k].sort((x, y) => (y.score || 0) - (x.score || 0)));
return { st, m };
}, [d]);
const onDrop = async (appId: string, toStageId: string) => {
if (!canMove) {
setMsg("You don't have permission to move candidates.");
return;
}
const apps = d.applications || [];
const idx = apps.findIndex((x: any) => x.id === appId);
if (idx < 0) return;
const fromStageId = apps[idx].stageId;
prev.current = d;
const nx = { ...d, applications: apps.map((a: any) => (a.id === appId ? { ...a, stageId: toStageId } : a)) };
setD(nx);
setLoading(true);
setErr("");
try {
const res = await fetch(
`${API}/api/jobs/${jobId}/applications/${appId}/move?tenant=${tenantId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ toStageId }),
}
);
if (res.status === 409) {
setErr("Conflict: someone else moved this candidate. Rolling back.");
// rollback
if (prev.current) setD(prev.current);
} else if (!res.ok) {
setErr("Move failed. Try again.");
if (prev.current) setD(prev.current);
} else {
const j = await res.json().catch(() => null);
if (j) setD(j);
}
} catch (e) {
setErr("Network error");
if (prev.current) setD(prev.current);
} finally {
setLoading(false);
}
if (fromStageId === toStageId) {
// no-op
}
};
function Card(p: { a: any; onDo: any; flag: any }) {
const a = p.a;
const disabled = !canMove;
return (
<div
draggable={!disabled}
onDragStart={(e) => {
setDrag({ id: a.id, from: a.stageId });
e.dataTransfer.setData("text/plain", a.id);
}}
onDragEnd={() => {
setDrag(null);
}}
tabIndex={0}
aria-disabled={disabled ? "true" : "false"}
title={disabled ? "Hiring Managers cannot move candidates" : ""}
style={{
border: "1px solid #ddd",
borderRadius: 8,
padding: 10,
marginBottom: 8,
background: "white",
boxShadow: "0 1px 2px rgba(0,0,0,.06)",
}}
className="card"
>
<div style={{ display: "flex", justifyContent: "space-between", gap: 8 }}>
<b style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{a.name}
</b>
<span>{a.score ?? 0}</span>
</div>
<div style={{ fontSize: 12, opacity: 0.8 }}>
{a.tags?.slice(0, 3).join(", ") || "—"}
</div>
<div style={{ fontSize: 12, marginTop: 6 }}>
In stage: <i>{a.timeInStageHours ?? 0}h</i>
</div>
</div>
);
}
function List(p: { s: any; items: any[] }) {
const s = p.s;
const items = p.items || [];
return (
<div
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const id = e.dataTransfer.getData("text/plain") || drag?.id;
if (!id) return;
onDrop(id, s.id);
}}
style={{
minWidth: 260,
maxWidth: 320,
background: "#f7f7f7",
padding: 10,
borderRadius: 10,
height: "calc(100vh - 220px)",
overflow: "auto",
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div style={{ fontWeight: 700 }}>{s.name}</div>
<div style={{ fontSize: 12, opacity: 0.7 }}>{items.length}</div>
</div>
<div style={{ marginTop: 10 }}>
{items.map((a: any) => (
<Card key={a.id} a={a} onDo={props.onDo} flag={props.flag} />
))}
</div>
</div>
);
}
return (
<div style={{ padding: 16 }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<h2 style={{ margin: 0 }}>Pipeline</h2>
{loading ? <span style={{ fontSize: 12 }}>Saving…</span> : null}
{msg ? <span style={{ fontSize: 12, opacity: 0.8 }}>{msg}</span> : null}
</div>
{err ? (
<div style={{ marginTop: 10, padding: 10, background: "#fff3f3", border: "1px solid #ffd0d0" }}>
{err}
</div>
) : null}
<div
className="board"
style={{
marginTop: 14,
display: "flex",
gap: 12,
overflowX: "auto",
alignItems: "flex-start",
}}
>
{cols.st.map((s: any) => (
<div key={s.id}>
<List s={s} items={cols.m[s.id] || []} />
</div>
))}
</div>
<div style={{ marginTop: 12, fontSize: 12, opacity: 0.7 }}>
Tenant: {tenantId} • Role: {role}
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment