From 6feafc12cd9597a991bb56c82587e41dbb81d0ff Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 18 Mar 2026 15:22:51 -0700 Subject: [PATCH] feat(interviews): add /api/interviews and /api/jobs/:id/move endpoints Adds GET /api/interviews to fetch all pipeline-stage jobs in one call, and POST /api/jobs/:id/move to transition a job between kanban statuses with automatic timestamp stamping (or rejection_stage capture). --- dev-api.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/dev-api.py b/dev-api.py index c8064ce..67cddca 100644 --- a/dev-api.py +++ b/dev-api.py @@ -274,6 +274,76 @@ def download_pdf(job_id: int): raise HTTPException(501, "reportlab not installed — install it to generate PDFs") +# ── GET /api/interviews ──────────────────────────────────────────────────────── + +PIPELINE_STATUSES = { + "applied", "survey", + "phone_screen", "interviewing", + "offer", "hired", + "interview_rejected", +} + +@app.get("/api/interviews") +def list_interviews(): + db = _get_db() + placeholders = ",".join("?" * len(PIPELINE_STATUSES)) + rows = db.execute( + f"SELECT id, title, company, url, location, is_remote, salary, " + f"match_score, keyword_gaps, status, " + f"interview_date, rejection_stage, " + f"applied_at, phone_screen_at, interviewing_at, offer_at, hired_at, survey_at " + f"FROM jobs WHERE status IN ({placeholders}) " + f"ORDER BY match_score DESC NULLS LAST", + list(PIPELINE_STATUSES), + ).fetchall() + db.close() + # Cast is_remote to bool for consistency with other endpoints + return [{**dict(r), "is_remote": bool(r["is_remote"])} for r in rows] + + +# ── POST /api/jobs/{id}/move ─────────────────────────────────────────────────── + +STATUS_TIMESTAMP_COL = { + "applied": "applied_at", + "survey": "survey_at", + "phone_screen": "phone_screen_at", + "interviewing": "interviewing_at", + "offer": "offer_at", + "hired": "hired_at", + "interview_rejected": None, # uses rejection_stage instead +} + +class MoveBody(BaseModel): + status: str + interview_date: str | None = None + rejection_stage: str | None = None + +@app.post("/api/jobs/{job_id}/move") +def move_job(job_id: int, body: MoveBody): + if body.status not in STATUS_TIMESTAMP_COL: + raise HTTPException(400, f"Invalid pipeline status: {body.status}") + db = _get_db() + ts_col = STATUS_TIMESTAMP_COL[body.status] + if ts_col: + db.execute( + f"UPDATE jobs SET status = ?, {ts_col} = datetime('now') WHERE id = ?", + (body.status, job_id), + ) + else: + db.execute( + "UPDATE jobs SET status = ?, rejection_stage = ? WHERE id = ?", + (body.status, body.rejection_stage, job_id), + ) + if body.interview_date is not None: + db.execute( + "UPDATE jobs SET interview_date = ? WHERE id = ?", + (body.interview_date, job_id), + ) + db.commit() + db.close() + return {"ok": True} + + # ── GET /api/config/user ────────────────────────────────────────────────────── @app.get("/api/config/user")