diff --git a/frontend/src/routes/jobs/JobsList.tsx b/frontend/src/routes/jobs/JobsList.tsx index 95b078b..eae5f59 100644 --- a/frontend/src/routes/jobs/JobsList.tsx +++ b/frontend/src/routes/jobs/JobsList.tsx @@ -3,7 +3,7 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { formatDistanceToNow, subDays, isAfter, isPast, format } from 'date-fns'; import { useAuthStore } from '../../lib/auth'; import { JOB_STATUS_LABELS, getJobStatusLabel } from '../../utils/jobStatusMessages'; -import { useJobs, useBulkDeleteJobs, useReprocessJob, useBulkReturnToQC, useCloneJob } from '../../hooks/useJob'; +import { useJobs, useBulkDeleteJobs, useReprocessJob, useBulkReturnToQC, useCloneJob, useDeleteJob, useUpdateJob } from '../../hooks/useJob'; import { StatusBadge } from '../../components/StatusBadge'; import { useToastContext } from '../../contexts/ToastContext'; import { useGlobalWebSocket } from '../../contexts/GlobalWebSocketContext'; @@ -95,6 +95,9 @@ export function JobsList() { const [showDownloadConfirm, setShowDownloadConfirm] = useState(false); const [showReturnToQCConfirm, setShowReturnToQCConfirm] = useState(false); const [returnToQCNotes, setReturnToQCNotes] = useState(''); + const [deleteTargetId, setDeleteTargetId] = useState(null); + const [editTarget, setEditTarget] = useState<{ id: string; title: string } | null>(null); + const [editTitle, setEditTitle] = useState(''); const [downloadProgress, setDownloadProgress] = useState<{ current: number; total: number } | null>(null); // Update filters from URL params @@ -115,6 +118,8 @@ export function JobsList() { }); const bulkDeleteMutation = useBulkDeleteJobs(); + const deleteJobMutation = useDeleteJob(); + const updateJobMutation = useUpdateJob(); const reprocessMutation = useReprocessJob(); const bulkReturnToQCMutation = useBulkReturnToQC(); const cloneJobMutation = useCloneJob(); @@ -260,6 +265,30 @@ export function JobsList() { setBulkAction(''); }; + const handleSingleDelete = async () => { + if (!deleteTargetId) return; + try { + await deleteJobMutation.mutateAsync(deleteTargetId); + toast.toastOnly.success('Job deleted'); + } catch { + toast.toastOnly.error('Failed to delete job'); + } finally { + setDeleteTargetId(null); + } + }; + + const handleEditSave = async () => { + if (!editTarget || !editTitle.trim()) return; + try { + await updateJobMutation.mutateAsync({ id: editTarget.id, data: { title: editTitle.trim() } as never }); + toast.toastOnly.success('Job renamed'); + } catch { + toast.toastOnly.error('Failed to rename job'); + } finally { + setEditTarget(null); + } + }; + const handleDeleteConfirm = async () => { if (selectedJobs.size === 0) return; @@ -878,14 +907,30 @@ export function JobsList() {
{getActionButton(job)} {canManageJobs && ( - + <> + + + + )}
@@ -1230,6 +1275,65 @@ export function JobsList() { ); })()} + {/* Single-job delete confirmation */} + {deleteTargetId && ( +
+
+

Delete job?

+

+ This will permanently delete the job, all generated files, and source video from storage. This cannot be undone. +

+
+ + +
+
+
+ )} + + {/* Rename job modal */} + {editTarget && ( +
+
+

Rename job

+ setEditTitle(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleEditSave(); if (e.key === 'Escape') setEditTarget(null); }} + className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + autoFocus + /> +
+ + +
+
+
+ )} + {/* Download Progress Indicator */} {downloadProgress && (