Added job review toggle
This commit is contained in:
@@ -885,6 +885,66 @@ async def review_job(job_id: str, body: ReviewRequest):
|
||||
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}")
|
||||
async def delete_job(job_id: str):
|
||||
"""Delete a job record and its stored image."""
|
||||
|
||||
@@ -50,6 +50,7 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} })
|
||||
const [saveResult, setSaveResult] = useState(null)
|
||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [togglingStatus, setTogglingStatus] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
setDeleting(true)
|
||||
try {
|
||||
@@ -148,6 +172,27 @@ function JobDetail({ jobId, onClose, onReviewed, onDeleted, suggestions = {} })
|
||||
{job && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user