'use client' /** * Staff Search Admin Page * * Search and browse university staff members and their publications. * Potential customers for BreakPilot services. */ import { useState, useEffect, useCallback } from 'react' import AdminLayout from '@/components/admin/AdminLayout' interface StaffMember { id: string first_name?: string last_name: string full_name?: string title?: string position?: string position_type?: string is_professor: boolean email?: string profile_url?: string photo_url?: string orcid?: string research_interests?: string[] university_name?: string university_short?: string department_name?: string publication_count: number } interface Publication { id: string title: string abstract?: string year?: number pub_type?: string venue?: string doi?: string url?: string citation_count: number } interface StaffStats { total_staff: number total_professors: number total_publications: number total_universities: number by_state?: Record by_uni_type?: Record by_position_type?: Record } interface University { id: string name: string short_name?: string url: string state?: string uni_type?: string } const EDU_SEARCH_API = process.env.NEXT_PUBLIC_EDU_SEARCH_URL || 'http://localhost:8086' export default function StaffSearchPage() { const [searchQuery, setSearchQuery] = useState('') const [staff, setStaff] = useState([]) const [selectedStaff, setSelectedStaff] = useState(null) const [publications, setPublications] = useState([]) const [stats, setStats] = useState(null) const [universities, setUniversities] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) // Filters const [filterState, setFilterState] = useState('') const [filterUniType, setFilterUniType] = useState('') const [filterPositionType, setFilterPositionType] = useState('') const [filterProfessorsOnly, setFilterProfessorsOnly] = useState(false) // Pagination const [total, setTotal] = useState(0) const [offset, setOffset] = useState(0) const limit = 20 // Fetch stats on mount useEffect(() => { fetchStats() fetchUniversities() }, []) const fetchStats = async () => { try { const res = await fetch(`${EDU_SEARCH_API}/api/v1/staff/stats`) if (res.ok) { const data = await res.json() setStats(data) } } catch { // Stats not critical } } const fetchUniversities = async () => { try { const res = await fetch(`${EDU_SEARCH_API}/api/v1/universities`) if (res.ok) { const data = await res.json() setUniversities(data.universities || []) } } catch { // Universities not critical } } const searchStaff = useCallback(async (newOffset = 0) => { setLoading(true) setError(null) try { const params = new URLSearchParams() if (searchQuery) params.append('q', searchQuery) if (filterState) params.append('state', filterState) if (filterUniType) params.append('uni_type', filterUniType) if (filterPositionType) params.append('position_type', filterPositionType) if (filterProfessorsOnly) params.append('is_professor', 'true') params.append('limit', limit.toString()) params.append('offset', newOffset.toString()) const res = await fetch(`${EDU_SEARCH_API}/api/v1/staff/search?${params}`) if (!res.ok) throw new Error('Search failed') const data = await res.json() setStaff(data.staff || []) setTotal(data.total || 0) setOffset(newOffset) } catch (err) { setError(err instanceof Error ? err.message : 'Search failed') setStaff([]) } finally { setLoading(false) } }, [searchQuery, filterState, filterUniType, filterPositionType, filterProfessorsOnly]) // Search on filter change useEffect(() => { const timer = setTimeout(() => { searchStaff(0) }, 300) return () => clearTimeout(timer) }, [searchStaff]) const fetchPublications = async (staffId: string) => { try { const res = await fetch(`${EDU_SEARCH_API}/api/v1/staff/${staffId}/publications`) if (res.ok) { const data = await res.json() setPublications(data.publications || []) } } catch { setPublications([]) } } const handleSelectStaff = (member: StaffMember) => { setSelectedStaff(member) fetchPublications(member.id) } const getPositionBadgeColor = (posType?: string) => { switch (posType) { case 'professor': return 'bg-purple-100 text-purple-800' case 'postdoc': return 'bg-blue-100 text-blue-800' case 'researcher': return 'bg-green-100 text-green-800' case 'phd_student': return 'bg-yellow-100 text-yellow-800' default: return 'bg-gray-100 text-gray-800' } } const getStateBadgeColor = (state?: string) => { const colors: Record = { BW: 'bg-yellow-100 text-yellow-800', BY: 'bg-blue-100 text-blue-800', BE: 'bg-red-100 text-red-800', NW: 'bg-green-100 text-green-800', HE: 'bg-orange-100 text-orange-800', SN: 'bg-purple-100 text-purple-800', } return colors[state || ''] || 'bg-gray-100 text-gray-800' } return ( {/* Stats Overview */} {stats && (
{stats.total_staff.toLocaleString()}
Mitarbeiter gesamt
{stats.total_professors.toLocaleString()}
Professoren
{stats.total_publications.toLocaleString()}
Publikationen
{stats.total_universities}
Universitaten
)}
{/* Left Panel: Search & Results */}
{/* Search Bar */}
setSearchQuery(e.target.value)} className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" />
{/* Filters */}
{/* Error */} {error && (
{error}
)} {/* Results */}
{total > 0 ? `${total.toLocaleString()} Ergebnisse` : 'Keine Ergebnisse'} {total > limit && (
{Math.floor(offset / limit) + 1} / {Math.ceil(total / limit)}
)}
{staff.map((member) => (
handleSelectStaff(member)} className={`p-4 cursor-pointer hover:bg-gray-50 transition-colors ${ selectedStaff?.id === member.id ? 'bg-primary-50' : '' }`} >
{member.photo_url ? ( {member.full_name ) : (
{(member.first_name?.[0] || '') + (member.last_name?.[0] || '')}
)}
{member.title && `${member.title} `} {member.full_name || `${member.first_name || ''} ${member.last_name}`} {member.is_professor && ( Prof )}
{member.position || member.position_type} {member.department_name && ` - ${member.department_name}`}
{member.university_short || member.university_name} {member.publication_count > 0 && ( {member.publication_count} Publikationen )}
{member.position_type && ( {member.position_type} )} {member.email && ( e.stopPropagation()} className="text-xs text-primary-600 hover:underline" > E-Mail )}
{member.research_interests && member.research_interests.length > 0 && (
{member.research_interests.slice(0, 5).map((interest, i) => ( {interest} ))} {member.research_interests.length > 5 && ( +{member.research_interests.length - 5} mehr )}
)}
))} {staff.length === 0 && !loading && (
{searchQuery || filterState || filterUniType || filterPositionType ? 'Keine Ergebnisse fur diese Suche' : 'Geben Sie einen Suchbegriff ein oder wahlen Sie Filter'}
)} {loading && (
Suche lauft...
)}
{/* Right Panel: Detail View */}
{selectedStaff ? (
{/* Header */}
{selectedStaff.photo_url ? ( {selectedStaff.full_name ) : (
{(selectedStaff.first_name?.[0] || '') + (selectedStaff.last_name?.[0] || '')}
)}

{selectedStaff.title && `${selectedStaff.title} `} {selectedStaff.full_name || `${selectedStaff.first_name || ''} ${selectedStaff.last_name}`}

{selectedStaff.position}

{selectedStaff.university_name}

{/* Contact */}
{selectedStaff.email && ( )} {selectedStaff.profile_url && ( )} {selectedStaff.orcid && ( )}
{/* Research Interests */} {selectedStaff.research_interests && selectedStaff.research_interests.length > 0 && (

Forschungsgebiete

{selectedStaff.research_interests.map((interest, i) => ( {interest} ))}
)} {/* Publications */}

Publikationen ({publications.length})

{publications.length > 0 ? (
{publications.map((pub) => (

{pub.title}

{pub.year && {pub.year}} {pub.venue && | {pub.venue}} {pub.citation_count > 0 && ( {pub.citation_count} Zitierungen )}
{pub.doi && ( DOI )}
))}
) : (

Keine Publikationen gefunden

)}
{/* Actions */}
) : (
Wahlen Sie eine Person aus der Liste
)}
) }