diff --git a/admin-frontend/src/api/pendingIdentifications.ts b/admin-frontend/src/api/pendingIdentifications.ts index 3cf68f7..5099452 100644 --- a/admin-frontend/src/api/pendingIdentifications.ts +++ b/admin-frontend/src/api/pendingIdentifications.ts @@ -61,10 +61,22 @@ export interface ClearDatabaseResponse { } export const pendingIdentificationsApi = { - list: async (includeDenied: boolean = false): Promise => { + list: async ( + includeDenied: boolean = false, + options?: { page?: number; pageSize?: number } + ): Promise => { + const params: Record = { + include_denied: includeDenied, + } + if (options?.page !== undefined) { + params.page = options.page + } + if (options?.pageSize !== undefined) { + params.page_size = options.pageSize + } const res = await apiClient.get( '/api/v1/pending-identifications', - { params: { include_denied: includeDenied } } + { params } ) return res.data }, diff --git a/admin-frontend/src/pages/ApproveIdentified.tsx b/admin-frontend/src/pages/ApproveIdentified.tsx index 9bc161f..aae3c4c 100644 --- a/admin-frontend/src/pages/ApproveIdentified.tsx +++ b/admin-frontend/src/pages/ApproveIdentified.tsx @@ -26,13 +26,21 @@ export default function ApproveIdentified() { const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') const [clearing, setClearing] = useState(false) + const [page, setPage] = useState(1) + const [pageSize] = useState(25) + const [total, setTotal] = useState(0) + const [previewPhotoUrl, setPreviewPhotoUrl] = useState(null) const loadPendingIdentifications = useCallback(async () => { setLoading(true) setError(null) try { - const response = await pendingIdentificationsApi.list(includeDenied) + const response = await pendingIdentificationsApi.list(includeDenied, { + page, + pageSize, + }) setPendingIdentifications(response.items) + setTotal(response.total) } catch (err: any) { let errorMessage = 'Failed to load pending identifications' if (err.response?.data?.detail) { @@ -49,6 +57,10 @@ export default function ApproveIdentified() { } finally { setLoading(false) } + }, [includeDenied, page, pageSize]) + + useEffect(() => { + setPage(1) }, [includeDenied]) useEffect(() => { @@ -284,7 +296,12 @@ export default function ApproveIdentified() { <>
- Total pending identifications: {pendingIdentifications.length} + Total pending identifications: {total} + {total > 0 && ( + + (page {page} of {Math.max(1, Math.ceil(total / pageSize))}) + + )}
)} + {total > pageSize && ( +
+ + + Showing {(page - 1) * pageSize + 1}–{Math.min(page * pageSize, total)} of {total} + + +
+ )} )}
+ {previewPhotoUrl && ( +
setPreviewPhotoUrl(null)} + > + + Full photo preview e.stopPropagation()} + /> +
+ )} + {/* Report Modal */} {showReport && (
diff --git a/backend/api/pending_identifications.py b/backend/api/pending_identifications.py index b9622f9..14a1ad5 100644 --- a/backend/api/pending_identifications.py +++ b/backend/api/pending_identifications.py @@ -152,23 +152,37 @@ def list_pending_identifications( dict, Depends(require_feature_permission("user_identified")) ], include_denied: bool = False, + page: int = Query(1, ge=1, description="Page number (1-based)"), + page_size: int = Query(50, ge=1, le=500, description="Items per page"), db: Session = Depends(get_auth_db), main_db: Session = Depends(get_db), ) -> PendingIdentificationsListResponse: - """List all pending identifications from the auth database. - - This endpoint reads from the separate auth database (DATABASE_URL_AUTH) - and returns all pending identifications from the pending_identifications table. + """List pending identifications from the auth database (paginated). + By default, only shows records with status='pending' for approval. Set include_denied=True to also show denied records. """ try: - # Query pending_identifications from auth database using raw SQL - # Join with users table to get user name/email - # Filter by status='pending' to show only records awaiting approval - # Optionally include denied records if include_denied is True - if include_denied: - result = db.execute(text(""" + where_clause = ( + "WHERE pi.status IN ('pending', 'denied')" + if include_denied + else "WHERE pi.status = 'pending'" + ) + order_clause = ( + "ORDER BY pi.status ASC, pi.last_name ASC, pi.first_name ASC, pi.created_at DESC" + if include_denied + else "ORDER BY pi.last_name ASC, pi.first_name ASC, pi.created_at DESC" + ) + offset = (page - 1) * page_size + + count_row = db.execute( + text(f"SELECT COUNT(*) AS count FROM pending_identifications pi {where_clause}") + ).fetchone() + total = int(count_row.count) if count_row else 0 + + result = db.execute( + text( + f""" SELECT pi.id, pi.face_id, @@ -187,46 +201,29 @@ def list_pending_identifications( pi.updated_at FROM pending_identifications pi LEFT JOIN users u ON pi.user_id = u.id - WHERE pi.status IN ('pending', 'denied') - ORDER BY pi.status ASC, pi.last_name ASC, pi.first_name ASC, pi.created_at DESC - """)) - else: - result = db.execute(text(""" - SELECT - pi.id, - pi.face_id, - pi.user_id, - u.name as user_name, - u.email as user_email, - pi.first_name, - pi.last_name, - pi.middle_name, - pi.maiden_name, - pi.date_of_birth, - pi.email, - pi.phone, - pi.status, - pi.created_at, - pi.updated_at - FROM pending_identifications pi - LEFT JOIN users u ON pi.user_id = u.id - WHERE pi.status = 'pending' - ORDER BY pi.last_name ASC, pi.first_name ASC, pi.created_at DESC - """)) - + {where_clause} + {order_clause} + LIMIT :limit OFFSET :offset + """ + ), + {"limit": page_size, "offset": offset}, + ) + rows = result.fetchall() + face_ids = [row.face_id for row in rows] + photo_by_face: dict[int, int | None] = {} + if face_ids: + for face_id, photo_id in main_db.query(Face.id, Face.photo_id).filter( + Face.id.in_(face_ids) + ): + photo_by_face[face_id] = photo_id + items = [] for row in rows: - # Get photo_id from main database - photo_id = None - face = main_db.query(Face).filter(Face.id == row.face_id).first() - if face: - photo_id = face.photo_id - items.append(PendingIdentificationResponse( id=row.id, face_id=row.face_id, - photo_id=photo_id, + photo_id=photo_by_face.get(row.face_id), user_id=row.user_id, user_name=row.user_name, user_email=row.user_email, @@ -241,8 +238,8 @@ def list_pending_identifications( created_at=str(row.created_at) if row.created_at else '', updated_at=str(row.updated_at) if row.updated_at else '', )) - - return PendingIdentificationsListResponse(items=items, total=len(items)) + + return PendingIdentificationsListResponse(items=items, total=total) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,