diff --git a/frontend/src/routes/jobs/JobsList.tsx b/frontend/src/routes/jobs/JobsList.tsx index cf05775..b8db22f 100644 --- a/frontend/src/routes/jobs/JobsList.tsx +++ b/frontend/src/routes/jobs/JobsList.tsx @@ -96,6 +96,8 @@ export function JobsList() { const [userFilter, setUserFilter] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [dateRangeFilter, setDateRangeFilter] = useState(''); + const [page, setPage] = useState(1); + const PAGE_SIZE = 50; // Sort state const [sortColumn, setSortColumn] = useState('created_at'); @@ -116,14 +118,16 @@ export function JobsList() { const status = searchParams.get('status'); if (status) { setStatusFilter(status); + setPage(1); } }, [searchParams]); - // Fetch ALL jobs for full client-side filtering + // Fetch jobs with server-side status filter and pagination const { data: jobsData, isLoading, error } = useJobs({ mine: user?.role === 'client', - page: 1, - size: 10000, // Fetch all jobs + page, + size: PAGE_SIZE, + status: statusFilter || undefined, }); const bulkDeleteMutation = useBulkDeleteJobs(); @@ -147,15 +151,8 @@ export function JobsList() { .sort((a, b) => a.label.localeCompare(b.label)); }, [jobsData?.jobs]); - // Extract unique statuses from current jobs for filter dropdown - const uniqueStatuses = useMemo(() => { - if (!jobsData?.jobs) return []; - const statuses = new Set(); - jobsData.jobs.forEach((job: Job) => { - if (job.status) statuses.add(job.status); - }); - return Array.from(statuses).sort(); - }, [jobsData?.jobs]); + // All possible statuses for the dropdown (static, server-side filtered) + const uniqueStatuses = Object.keys(STATUS_LABELS); // Handle column sort const handleSort = (column: string) => { @@ -188,11 +185,6 @@ export function JobsList() { jobs = jobs.filter((job: Job) => job.client_id === userFilter); } - // Filter by status - if (statusFilter) { - jobs = jobs.filter((job: Job) => job.status === statusFilter); - } - // Filter by date range if (dateRangeFilter) { const now = new Date(); @@ -230,7 +222,7 @@ export function JobsList() { }); return jobs; - }, [jobsData?.jobs, searchTerm, userFilter, statusFilter, dateRangeFilter, sortColumn, sortDirection]); + }, [jobsData?.jobs, searchTerm, userFilter, dateRangeFilter, sortColumn, sortDirection]); // Clear all filters const clearFilters = () => { @@ -238,6 +230,20 @@ export function JobsList() { setUserFilter(''); setStatusFilter(''); setDateRangeFilter(''); + setPage(1); + }; + + // Quick preset filters for PM productivity (PM-9) + const PM_PRESETS = [ + { label: 'Pending Final Review', status: 'pending_final_review' }, + { label: 'In QC', status: 'pending_qc' }, + { label: 'TTS Failed', status: 'tts_failed' }, + { label: 'Render Failed', status: 'render_failed' }, + ]; + + const applyPreset = (status: string) => { + setStatusFilter(prev => prev === status ? '' : status); + setPage(1); }; const hasActiveFilters = searchTerm || userFilter || statusFilter || dateRangeFilter; @@ -427,12 +433,13 @@ export function JobsList() { const isAdmin = user?.role === 'admin'; const isProduction = user?.role === 'production'; - const canManageJobs = isAdmin || isProduction; + const isPM = user?.role === 'project_manager'; + const canManageJobs = isAdmin || isProduction || isPM; const canBulkDelete = canManageJobs && selectedJobs.size > 0; const canBulkReprocess = canManageJobs && selectedJobs.size > 0; const getActionButton = (job: Job) => { - const isReviewer = ['reviewer', 'production', 'admin'].includes(user?.role || ''); + const isReviewer = ['reviewer', 'production', 'admin', 'project_manager'].includes(user?.role || ''); switch (job.status) { case 'pending_qc': @@ -505,7 +512,7 @@ export function JobsList() {

- {['client', 'production', 'admin'].includes(user?.role || '') && ( + {['client', 'production', 'admin', 'project_manager'].includes(user?.role || '') && ( + {/* Quick filter presets — shown for PM and admin */} + {(isPM || isAdmin) && ( +
+ {PM_PRESETS.map(preset => ( + + ))} +
+ )} + {/* Filters and Search */}
@@ -694,7 +720,9 @@ export function JobsList() { {/* Results Summary */}

- Showing {filteredAndSortedJobs.length} of {jobsData?.total || 0} jobs + {jobsData?.total + ? `Showing ${(page - 1) * PAGE_SIZE + 1}–${Math.min(page * PAGE_SIZE, jobsData.total)} of ${jobsData.total} jobs` + : 'No jobs found'}

{hasActiveFilters && (
+ {/* Pagination */} + {jobsData && jobsData.total > PAGE_SIZE && ( +
+ + {Array.from({ length: Math.ceil(jobsData.total / PAGE_SIZE) }, (_, i) => i + 1) + .filter(p => p === 1 || p === Math.ceil(jobsData.total / PAGE_SIZE) || Math.abs(p - page) <= 2) + .reduce<(number | 'ellipsis')[]>((acc, p, idx, arr) => { + if (idx > 0 && (arr[idx - 1] as number) + 1 < p) acc.push('ellipsis'); + acc.push(p); + return acc; + }, []) + .map((item, idx) => + item === 'ellipsis' ? ( + + ) : ( + + ) + )} + +
+ )} + {/* Delete Confirmation Modal */} {showDeleteConfirm && (
diff --git a/frontend/src/routes/jobs/NewJob.tsx b/frontend/src/routes/jobs/NewJob.tsx index 355fc2e..b3d99ef 100644 --- a/frontend/src/routes/jobs/NewJob.tsx +++ b/frontend/src/routes/jobs/NewJob.tsx @@ -791,12 +791,20 @@ export function NewJob() {