website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
175 lines
8.0 KiB
TypeScript
175 lines
8.0 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Staff Search Admin Page
|
|
*
|
|
* Search and browse university staff members and their publications.
|
|
* Potential customers for BreakPilot services.
|
|
*/
|
|
|
|
import AdminLayout from '@/components/admin/AdminLayout'
|
|
import { useStaffSearch } from './_components/useStaffSearch'
|
|
import { StaffListItem } from './_components/StaffListItem'
|
|
import { StaffDetailPanel } from './_components/StaffDetailPanel'
|
|
|
|
export default function StaffSearchPage() {
|
|
const {
|
|
searchQuery, setSearchQuery,
|
|
staff, selectedStaff, publications, stats,
|
|
loading, error,
|
|
filterState, setFilterState,
|
|
filterUniType, setFilterUniType,
|
|
filterPositionType, setFilterPositionType,
|
|
filterProfessorsOnly, setFilterProfessorsOnly,
|
|
total, offset, limit,
|
|
searchStaff, handleSelectStaff,
|
|
} = useStaffSearch()
|
|
|
|
return (
|
|
<AdminLayout
|
|
title="Personensuche"
|
|
description="Universitatsmitarbeiter & Publikationen - Potentielle Kunden"
|
|
>
|
|
{/* Stats Overview */}
|
|
{stats && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
<div className="bg-white p-4 rounded-lg shadow-sm border">
|
|
<div className="text-2xl font-bold text-primary-600">{stats.total_staff.toLocaleString()}</div>
|
|
<div className="text-sm text-slate-500">Mitarbeiter gesamt</div>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-lg shadow-sm border">
|
|
<div className="text-2xl font-bold text-purple-600">{stats.total_professors.toLocaleString()}</div>
|
|
<div className="text-sm text-slate-500">Professoren</div>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-lg shadow-sm border">
|
|
<div className="text-2xl font-bold text-green-600">{stats.total_publications.toLocaleString()}</div>
|
|
<div className="text-sm text-slate-500">Publikationen</div>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-lg shadow-sm border">
|
|
<div className="text-2xl font-bold text-blue-600">{stats.total_universities}</div>
|
|
<div className="text-sm text-slate-500">Universitaten</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-6">
|
|
{/* Left Panel: Search & Results */}
|
|
<div className="flex-1">
|
|
{/* Search Bar */}
|
|
<div className="bg-white p-4 rounded-lg shadow-sm border mb-4">
|
|
<div className="flex gap-4 mb-4">
|
|
<div className="flex-1">
|
|
<input
|
|
type="text"
|
|
placeholder="Suche nach Name, Forschungsgebiet..."
|
|
value={searchQuery}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={() => searchStaff(0)}
|
|
disabled={loading}
|
|
className="px-6 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
|
>
|
|
{loading ? 'Suche...' : 'Suchen'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap gap-4">
|
|
<select value={filterState} onChange={(e) => setFilterState(e.target.value)} className="px-3 py-1.5 border rounded-lg text-sm">
|
|
<option value="">Alle Bundeslander</option>
|
|
<option value="BW">Baden-Wurttemberg</option>
|
|
<option value="BY">Bayern</option>
|
|
<option value="BE">Berlin</option>
|
|
<option value="BB">Brandenburg</option>
|
|
<option value="HB">Bremen</option>
|
|
<option value="HH">Hamburg</option>
|
|
<option value="HE">Hessen</option>
|
|
<option value="MV">Mecklenburg-Vorpommern</option>
|
|
<option value="NI">Niedersachsen</option>
|
|
<option value="NW">Nordrhein-Westfalen</option>
|
|
<option value="RP">Rheinland-Pfalz</option>
|
|
<option value="SL">Saarland</option>
|
|
<option value="SN">Sachsen</option>
|
|
<option value="ST">Sachsen-Anhalt</option>
|
|
<option value="SH">Schleswig-Holstein</option>
|
|
<option value="TH">Thuringen</option>
|
|
</select>
|
|
<select value={filterUniType} onChange={(e) => setFilterUniType(e.target.value)} className="px-3 py-1.5 border rounded-lg text-sm">
|
|
<option value="">Alle Hochschultypen</option>
|
|
<option value="UNI">Universitaten</option>
|
|
<option value="FH">Fachhochschulen</option>
|
|
<option value="PH">Pad. Hochschulen</option>
|
|
<option value="KUNST">Kunsthochschulen</option>
|
|
<option value="PRIVATE">Private</option>
|
|
</select>
|
|
<select value={filterPositionType} onChange={(e) => setFilterPositionType(e.target.value)} className="px-3 py-1.5 border rounded-lg text-sm">
|
|
<option value="">Alle Positionen</option>
|
|
<option value="professor">Professoren</option>
|
|
<option value="postdoc">Postdocs</option>
|
|
<option value="researcher">Wissenschaftler</option>
|
|
<option value="phd_student">Doktoranden</option>
|
|
<option value="staff">Sonstige</option>
|
|
</select>
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input type="checkbox" checked={filterProfessorsOnly} onChange={(e) => setFilterProfessorsOnly(e.target.checked)} className="rounded border-gray-300" />
|
|
Nur Professoren
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="bg-red-50 text-red-700 p-4 rounded-lg mb-4">{error}</div>
|
|
)}
|
|
|
|
{/* Results */}
|
|
<div className="bg-white rounded-lg shadow-sm border">
|
|
<div className="p-4 border-b flex justify-between items-center">
|
|
<span className="font-medium">
|
|
{total > 0 ? `${total.toLocaleString()} Ergebnisse` : 'Keine Ergebnisse'}
|
|
</span>
|
|
{total > limit && (
|
|
<div className="flex gap-2">
|
|
<button onClick={() => searchStaff(Math.max(0, offset - limit))} disabled={offset === 0 || loading} className="px-3 py-1 text-sm border rounded hover:bg-gray-50 disabled:opacity-50">Zuruck</button>
|
|
<span className="px-3 py-1 text-sm text-gray-500">{Math.floor(offset / limit) + 1} / {Math.ceil(total / limit)}</span>
|
|
<button onClick={() => searchStaff(offset + limit)} disabled={offset + limit >= total || loading} className="px-3 py-1 text-sm border rounded hover:bg-gray-50 disabled:opacity-50">Weiter</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="divide-y">
|
|
{staff.map((member) => (
|
|
<StaffListItem
|
|
key={member.id}
|
|
member={member}
|
|
isSelected={selectedStaff?.id === member.id}
|
|
onClick={() => handleSelectStaff(member)}
|
|
/>
|
|
))}
|
|
{staff.length === 0 && !loading && (
|
|
<div className="p-8 text-center text-slate-500">
|
|
{searchQuery || filterState || filterUniType || filterPositionType
|
|
? 'Keine Ergebnisse fur diese Suche'
|
|
: 'Geben Sie einen Suchbegriff ein oder wahlen Sie Filter'}
|
|
</div>
|
|
)}
|
|
{loading && (
|
|
<div className="p-8 text-center text-slate-500">Suche lauft...</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Panel: Detail View */}
|
|
<div className="w-96 shrink-0">
|
|
<div className="bg-white rounded-lg shadow-sm border sticky top-20">
|
|
<StaffDetailPanel selectedStaff={selectedStaff} publications={publications} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
)
|
|
}
|