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() {