Added job review toggle

This commit is contained in:
Aaron Roberts
2026-06-23 10:43:44 +01:00
parent 91c134faa7
commit 48f958de6c
2 changed files with 105 additions and 0 deletions

View File

@@ -885,6 +885,66 @@ async def review_job(job_id: str, body: ReviewRequest):
return JSONResponse(_job_row_to_dict(row)) return JSONResponse(_job_row_to_dict(row))
class StatusRequest(BaseModel):
status: str
reviewer_name: Optional[str] = None
@app.put("/api/jobs/{job_id}/status")
async def set_job_status(job_id: str, body: StatusRequest):
"""Toggle a job's reviewed status without touching its text or metadata.
Marking 'reviewed' requires a reviewer_name and stamps reviewed_at.
Marking 'unreviewed' clears reviewed_at while preserving reviewed_text.
"""
try:
uuid.UUID(job_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid job ID.")
if body.status not in ("reviewed", "unreviewed"):
raise HTTPException(status_code=400, detail="status must be 'reviewed' or 'unreviewed'.")
if body.status == "reviewed" and not (body.reviewer_name or "").strip():
raise HTTPException(status_code=400, detail="Reviewer name is required to mark reviewed.")
try:
with get_db() as conn:
with conn.cursor() as cur:
if body.status == "reviewed":
cur.execute(
"""
UPDATE ocr_jobs
SET status = 'reviewed',
reviewer_name = %s,
reviewed_at = NOW()
WHERE id = %s
RETURNING *
""",
(body.reviewer_name.strip(), job_id),
)
else:
cur.execute(
"""
UPDATE ocr_jobs
SET status = 'unreviewed',
reviewed_at = NULL
WHERE id = %s
RETURNING *
""",
(job_id,),
)
row = cur.fetchone()
except Exception as exc:
print(f"set_job_status DB error: {exc}")
raise HTTPException(status_code=500, detail="Database error.")
if not row:
raise HTTPException(status_code=404, detail="Job not found.")
return JSONResponse(_job_row_to_dict(row))
@app.delete("/api/jobs/{job_id}") @app.delete("/api/jobs/{job_id}")
async def delete_job(job_id: str): async def delete_job(job_id: str):
"""Delete a job record and its stored image.""" """Delete a job record and its stored image."""

View File

@@ -50,6 +50,7 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} })
const [saveResult, setSaveResult] = useState(null) const [saveResult, setSaveResult] = useState(null)
const [confirmDelete, setConfirmDelete] = useState(false) const [confirmDelete, setConfirmDelete] = useState(false)
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const [togglingStatus, setTogglingStatus] = useState(false)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@@ -112,6 +113,29 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} })
} }
} }
const handleToggleStatus = async () => {
const next = isReviewed ? 'unreviewed' : 'reviewed'
if (next === 'reviewed' && !reviewerName.trim()) {
setSaveResult({ success: false, error: 'Reviewer name is required to mark reviewed.' })
return
}
setTogglingStatus(true)
setSaveResult(null)
try {
const res = await axios.put(`${API_BASE}/jobs/${jobId}/status`, {
status: next,
reviewer_name: reviewerName.trim() || null,
})
setJob(res.data)
setReviewerName(res.data.reviewer_name || '')
onReviewed(res.data)
} catch (err) {
setSaveResult({ success: false, error: err.response?.data?.detail || err.message })
} finally {
setTogglingStatus(false)
}
}
const handleDelete = async () => { const handleDelete = async () => {
setDeleting(true) setDeleting(true)
try { try {
@@ -148,6 +172,27 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} })
{job && ( {job && (
<> <>
<StatusBadge status={job.status} /> <StatusBadge status={job.status} />
<motion.button
onClick={handleToggleStatus}
disabled={togglingStatus}
title={isReviewed ? 'Revert to unreviewed' : 'Mark as reviewed'}
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors disabled:opacity-50 ${
isReviewed
? 'glass glass-hover text-amber-400 hover:bg-amber-500/10'
: 'glass glass-hover text-green-400 hover:bg-green-500/10'
}`}
whileHover={!togglingStatus ? { scale: 1.02 } : {}}
whileTap={!togglingStatus ? { scale: 0.98 } : {}}
>
{togglingStatus ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : isReviewed ? (
<Clock className="w-3.5 h-3.5" />
) : (
<CheckCircle2 className="w-3.5 h-3.5" />
)}
{isReviewed ? 'Mark Unreviewed' : 'Mark Reviewed'}
</motion.button>
<span className="text-xs text-gray-500 font-mono hidden sm:block">{job.id}</span> <span className="text-xs text-gray-500 font-mono hidden sm:block">{job.id}</span>
</> </>
)} )}