Files
SkillMate/frontend/src/views/EmployeeDetail.tsx
Claude Project Manager 26f95d2e4a So mit neuen UI Ideen und so
2025-09-22 20:54:57 +02:00

254 Zeilen
10 KiB
TypeScript

import { useParams, useNavigate } from 'react-router-dom'
import { useState, useEffect } from 'react'
import type { Employee } from '@skillmate/shared'
// Dynamische Hierarchie für Darstellung der Nutzer-Skills
import SkillLevelBar from '../components/SkillLevelBar'
import OfficeMapModal from '../components/OfficeMapModal'
import { employeeApi } from '../services/api'
import { useAuthStore } from '../stores/authStore'
export default function EmployeeDetail() {
const { id } = useParams()
const navigate = useNavigate()
const { user } = useAuthStore()
const [employee, setEmployee] = useState<Employee | null>(null)
const [error, setError] = useState('')
const [showOfficeMap, setShowOfficeMap] = useState(false)
const [hierarchy, setHierarchy] = useState<{ id: string; name: string; subcategories: { id: string; name: string; skills: { id: string; name: string }[] }[] }[]>([])
const API_BASE = (import.meta as any).env?.VITE_API_URL || 'http://localhost:3004/api'
const PUBLIC_BASE = (import.meta as any).env?.VITE_API_PUBLIC_URL || API_BASE.replace(/\/api$/, '')
const toPublic = (p?: string | null) => (p && p.startsWith('/uploads/')) ? `${PUBLIC_BASE}${p}` : (p || '')
useEffect(() => {
if (id) {
fetchEmployee(id)
}
}, [id])
useEffect(() => {
const load = async () => {
try {
const res = await fetch(((import.meta as any).env?.VITE_API_URL || 'http://localhost:3004/api') + '/skills/hierarchy', {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
})
const data = await res.json()
if (data?.success) setHierarchy(data.data || [])
} catch {}
}
load()
}, [])
const fetchEmployee = async (employeeId: string) => {
try {
const data = await employeeApi.getById(employeeId)
setEmployee(data)
} catch (error) {
console.error('Failed to fetch employee:', error)
// Fallback to mock data
const mockEmployee: Employee = {
id: '1',
firstName: 'Max',
lastName: 'Mustermann',
employeeNumber: 'EMP001',
position: 'Senior Analyst',
department: 'Cybercrime',
email: 'max.mustermann@behörde.de',
phone: '+49 30 12345-100',
mobile: '+49 170 1234567',
office: 'Raum 3.42',
availability: 'available',
skills: [
{ id: '1', name: 'Python', category: 'it', level: 'expert', verified: true },
{ id: '2', name: 'Netzwerkforensik', category: 'it', level: 'advanced', verified: true },
{ id: '3', name: 'OSINT-Tools', category: 'it', level: 'advanced' },
],
languages: [
{ language: 'Deutsch', proficiency: 'native', isNative: true },
{ language: 'Englisch', proficiency: 'fluent', certified: true, certificateType: 'C1' },
{ language: 'Russisch', proficiency: 'intermediate' },
],
clearance: {
level: 'Ü3',
validUntil: new Date('2025-12-31'),
issuedDate: new Date('2021-01-15'),
},
specializations: ['Digitale Forensik', 'Malware-Analyse', 'Darknet-Ermittlungen'],
createdAt: new Date(),
updatedAt: new Date(),
createdBy: 'admin',
}
setEmployee(mockEmployee)
}
}
if (!employee) {
return (
<div className="flex items-center justify-center h-full">
<p className="text-tertiary">Mitarbeiter wird geladen...</p>
</div>
)
}
// Hinweis: Verfügbarkeits-Badge wird im Mitarbeiter-Detail nicht angezeigt
return (
<div>
<button
onClick={() => navigate('/employees')}
className="text-primary-blue hover:text-primary-blue-hover mb-6 flex items-center space-x-2"
>
<span> Zurück zur Übersicht</span>
</button>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-1">
<div className="card">
<div className="flex flex-col items-center">
<div className="mb-4 w-40 h-40 rounded-full bg-bg-accent dark:bg-dark-primary overflow-hidden flex items-center justify-center">
{employee.photo ? (
<img src={toPublic(employee.photo)} alt="Foto" className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center text-primary-blue dark:text-dark-accent text-3xl font-semibold">
{employee.firstName.charAt(0)}{employee.lastName.charAt(0)}
</div>
)}
</div>
<h1 className="text-title-dialog font-poppins font-bold text-primary text-center">
{employee.firstName} {employee.lastName}
</h1>
{employee.employeeNumber && (
<p className="text-body text-tertiary mb-4">{employee.employeeNumber}</p>
)}
</div>
</div>
<div className="card mt-6">
<h3 className="text-body font-poppins font-semibold text-primary mb-4">
Kontaktdaten
</h3>
<div className="space-y-3 text-body">
<div>
<span className="text-tertiary">E-Mail:</span>
<p className="text-secondary">{employee.email}</p>
</div>
<div>
<span className="text-tertiary">Telefon:</span>
<p className="text-secondary">{employee.phone}</p>
</div>
{employee.mobile && (
<div>
<span className="text-tertiary">Mobil:</span>
<p className="text-secondary">{employee.mobile}</p>
</div>
)}
{employee.office && (
<div>
<span className="text-tertiary">Büro:</span>
<p className="text-secondary">{employee.office}</p>
<button
onClick={() => setShowOfficeMap(!showOfficeMap)}
className="mt-2 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 underline"
>
{showOfficeMap ? 'Karte ausblenden' : 'Büro zeigen'}
</button>
</div>
)}
</div>
</div>
</div>
<div className="lg:col-span-2 space-y-6">
<div className="card">
<h3 className="text-title-card font-poppins font-semibold text-primary mb-4">
Allgemeine Informationen
</h3>
<div className="grid grid-cols-2 gap-4 text-body">
<div>
<span className="text-tertiary">Position:</span>
<p className="text-secondary font-medium">{employee.position}</p>
</div>
<div>
<span className="text-tertiary">Dienststelle:</span>
<p className="text-secondary font-medium">{employee.department}</p>
</div>
{employee.clearance && (
<div>
<span className="text-tertiary">Sicherheitsüberprüfung:</span>
<p className="text-secondary font-medium">
{employee.clearance.level} (gültig bis {new Date(employee.clearance.validUntil).toLocaleDateString('de-DE')})
</p>
</div>
)}
</div>
</div>
<div className="card">
<h3 className="text-title-card font-poppins font-semibold text-primary mb-4">Kompetenzen</h3>
<div className="space-y-4">
{hierarchy.map(cat => {
const subs = cat.subcategories.map(sub => {
const selected = sub.skills.filter(sk => employee.skills.some(es => es.id === sk.id))
return { sub, selected }
}).filter(x => x.selected.length > 0)
if (subs.length === 0) return null
return (
<div key={cat.id}>
<h4 className="text-body font-semibold text-secondary mb-2">{cat.name}</h4>
{subs.map(({ sub, selected }) => (
<div key={`${cat.id}.${sub.id}`} className="mb-3">
<div className="px-2 py-1 rounded-input border border-border-default bg-bg-accent dark:bg-dark-primary text-body font-medium text-primary mb-2">
{sub.name}
</div>
<ul className="space-y-3 text-body">
{selected.map((sk) => {
const info = employee.skills.find(es => es.id === sk.id)
const levelVal = info?.level ? Number(info.level) : ''
return (
<li key={sk.id}>
<div className="flex items-center justify-between mb-1">
<span className="text-secondary">{sk.name}</span>
</div>
<SkillLevelBar value={levelVal as any} onChange={() => {}} disabled showHelp={false} />
</li>
)
})}
</ul>
</div>
))}
</div>
)
})}
</div>
</div>
{employee.specializations.length > 0 && (
<div className="card">
<h3 className="text-title-card font-poppins font-semibold text-primary mb-4">
Spezialisierungen
</h3>
<div className="flex flex-wrap gap-2">
{employee.specializations.map((spec, index) => (
<span key={index} className="badge badge-info">
{spec}
</span>
))}
</div>
</div>
)}
</div>
</div>
{/* Office Map Modal */}
<OfficeMapModal
isOpen={showOfficeMap}
onClose={() => setShowOfficeMap(false)}
targetEmployeeId={employee?.id}
targetRoom={employee?.office || undefined}
currentUserRoom="1-101" // This should come from logged-in user data
employeeName={employee ? `${employee.firstName} ${employee.lastName}` : undefined}
/>
</div>
)
}