706 Zeilen
27 KiB
TypeScript
706 Zeilen
27 KiB
TypeScript
import { useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import type { Employee } from '@skillmate/shared'
|
|
import { employeeApi } from '../services/api'
|
|
import { SKILL_HIERARCHY, LANGUAGE_LEVELS } from '../data/skillCategories'
|
|
import PhotoPreview from '../components/PhotoPreview'
|
|
import OrganizationSelector from '../components/OrganizationSelector'
|
|
|
|
export default function EmployeeForm() {
|
|
const navigate = useNavigate()
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState('')
|
|
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({})
|
|
|
|
const [formData, setFormData] = useState({
|
|
firstName: '',
|
|
lastName: '',
|
|
employeeNumber: '',
|
|
position: '',
|
|
department: '',
|
|
email: '',
|
|
phone: '',
|
|
mobile: '',
|
|
office: '',
|
|
clearance: '',
|
|
availability: 'available' as 'available' | 'parttime' | 'unavailable',
|
|
partTimeHours: '',
|
|
skills: [] as any[],
|
|
languages: [] as string[],
|
|
specializations: [] as string[]
|
|
})
|
|
const [primaryUnitId, setPrimaryUnitId] = useState<string | null>(null)
|
|
const [primaryUnitName, setPrimaryUnitName] = useState<string>('')
|
|
|
|
const [employeePhoto, setEmployeePhoto] = useState<string | null>(null)
|
|
const [photoFile, setPhotoFile] = useState<File | null>(null)
|
|
|
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set())
|
|
const [expandedSubCategories, setExpandedSubCategories] = useState<Set<string>>(new Set())
|
|
const [skillSearchTerm, setSkillSearchTerm] = useState('')
|
|
const [searchResults, setSearchResults] = useState<any[]>([])
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
|
const { name, value } = e.target
|
|
setFormData(prev => ({ ...prev, [name]: value }))
|
|
}
|
|
|
|
const toggleCategory = (categoryId: string) => {
|
|
setExpandedCategories(prev => {
|
|
const newSet = new Set(prev)
|
|
if (newSet.has(categoryId)) {
|
|
newSet.delete(categoryId)
|
|
} else {
|
|
newSet.add(categoryId)
|
|
}
|
|
return newSet
|
|
})
|
|
}
|
|
|
|
const toggleSubCategory = (subCategoryId: string) => {
|
|
setExpandedSubCategories(prev => {
|
|
const newSet = new Set(prev)
|
|
if (newSet.has(subCategoryId)) {
|
|
newSet.delete(subCategoryId)
|
|
} else {
|
|
newSet.add(subCategoryId)
|
|
}
|
|
return newSet
|
|
})
|
|
}
|
|
|
|
const handleSkillToggle = (categoryId: string, subCategoryId: string, skillId: string, skillName: string) => {
|
|
setFormData(prev => {
|
|
const skills = [...prev.skills]
|
|
const existingIndex = skills.findIndex(s =>
|
|
s.categoryId === categoryId &&
|
|
s.subCategoryId === subCategoryId &&
|
|
s.skillId === skillId
|
|
)
|
|
|
|
if (existingIndex > -1) {
|
|
skills.splice(existingIndex, 1)
|
|
} else {
|
|
skills.push({
|
|
categoryId,
|
|
subCategoryId,
|
|
skillId,
|
|
name: skillName,
|
|
level: ''
|
|
})
|
|
}
|
|
return { ...prev, skills }
|
|
})
|
|
}
|
|
|
|
const handleSkillLevelChange = (categoryId: string, subCategoryId: string, skillId: string, level: string) => {
|
|
setFormData(prev => {
|
|
const skills = [...prev.skills]
|
|
const skill = skills.find(s =>
|
|
s.categoryId === categoryId &&
|
|
s.subCategoryId === subCategoryId &&
|
|
s.skillId === skillId
|
|
)
|
|
if (skill) {
|
|
skill.level = level
|
|
}
|
|
return { ...prev, skills }
|
|
})
|
|
}
|
|
|
|
const isSkillSelected = (categoryId: string, subCategoryId: string, skillId: string) => {
|
|
return formData.skills.some(s =>
|
|
s.categoryId === categoryId &&
|
|
s.subCategoryId === subCategoryId &&
|
|
s.skillId === skillId
|
|
)
|
|
}
|
|
|
|
const getSkillLevel = (categoryId: string, subCategoryId: string, skillId: string) => {
|
|
const skill = formData.skills.find(s =>
|
|
s.categoryId === categoryId &&
|
|
s.subCategoryId === subCategoryId &&
|
|
s.skillId === skillId
|
|
)
|
|
return skill?.level || ''
|
|
}
|
|
|
|
// Skill-Suche
|
|
const handleSkillSearch = (searchTerm: string) => {
|
|
setSkillSearchTerm(searchTerm)
|
|
|
|
if (searchTerm.length < 2) {
|
|
setSearchResults([])
|
|
return
|
|
}
|
|
|
|
const results: any[] = []
|
|
const lowerSearch = searchTerm.toLowerCase()
|
|
|
|
SKILL_HIERARCHY.forEach(category => {
|
|
category.subcategories.forEach(subCategory => {
|
|
subCategory.skills.forEach(skill => {
|
|
if (skill.name.toLowerCase().includes(lowerSearch)) {
|
|
results.push({
|
|
categoryId: category.id,
|
|
categoryName: category.name,
|
|
subCategoryId: subCategory.id,
|
|
subCategoryName: subCategory.name,
|
|
skillId: skill.id,
|
|
skillName: skill.name
|
|
})
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
setSearchResults(results)
|
|
}
|
|
|
|
const handleSearchResultClick = (result: any) => {
|
|
// Öffne die entsprechenden Kategorien
|
|
setExpandedCategories(prev => new Set([...prev, result.categoryId]))
|
|
setExpandedSubCategories(prev => new Set([...prev, `${result.categoryId}-${result.subCategoryId}`]))
|
|
|
|
// Wähle den Skill aus
|
|
handleSkillToggle(result.categoryId, result.subCategoryId, result.skillId, result.skillName)
|
|
|
|
// Lösche die Suche
|
|
setSkillSearchTerm('')
|
|
setSearchResults([])
|
|
}
|
|
|
|
const validateForm = () => {
|
|
const errors: Record<string, string> = {}
|
|
|
|
if (!formData.firstName.trim()) errors.firstName = 'Vorname ist erforderlich'
|
|
if (!formData.lastName.trim()) errors.lastName = 'Nachname ist erforderlich'
|
|
if (!formData.employeeNumber.trim()) errors.employeeNumber = 'Personalnummer ist erforderlich'
|
|
if (!formData.position.trim()) errors.position = 'Position ist erforderlich'
|
|
if (!formData.department.trim()) errors.department = 'Abteilung ist erforderlich'
|
|
if (!formData.email.trim()) errors.email = 'E-Mail ist erforderlich'
|
|
else if (!/\S+@\S+\.\S+/.test(formData.email)) errors.email = 'Ungültige E-Mail-Adresse'
|
|
if (!formData.phone.trim()) errors.phone = 'Telefonnummer ist erforderlich'
|
|
if (!primaryUnitId) errors.primaryUnitId = 'Organisatorische Einheit ist erforderlich'
|
|
|
|
setValidationErrors(errors)
|
|
return Object.keys(errors).length === 0
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
setValidationErrors({})
|
|
|
|
if (!validateForm()) {
|
|
setError('Bitte füllen Sie alle Pflichtfelder aus')
|
|
return
|
|
}
|
|
|
|
setLoading(true)
|
|
|
|
try {
|
|
const newEmployee: Partial<Employee> = {
|
|
...formData,
|
|
skills: formData.skills.map((skill, index) => ({
|
|
id: `skill-${index}`,
|
|
name: skill.name,
|
|
category: skill.categoryId,
|
|
level: skill.level || 3
|
|
})),
|
|
languages: formData.skills
|
|
.filter(s => s.subCategoryId === 'languages')
|
|
.map(s => ({
|
|
language: s.name,
|
|
proficiency: s.level || 'B1'
|
|
})),
|
|
clearance: formData.clearance ? {
|
|
level: formData.clearance as 'Ü2' | 'Ü3',
|
|
validUntil: new Date(new Date().setFullYear(new Date().getFullYear() + 5)),
|
|
issuedDate: new Date()
|
|
} : undefined,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
createdBy: 'admin'
|
|
}
|
|
|
|
const result = await employeeApi.create({ ...newEmployee, primaryUnitId })
|
|
const newEmployeeId = result.data.id
|
|
|
|
// Upload photo if we have one
|
|
if (photoFile && newEmployeeId) {
|
|
const formData = new FormData()
|
|
formData.append('photo', photoFile)
|
|
|
|
try {
|
|
await fetch(`http://localhost:3001/api/upload/employee-photo/${newEmployeeId}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
},
|
|
body: formData
|
|
})
|
|
} catch (uploadError) {
|
|
console.error('Failed to upload photo:', uploadError)
|
|
}
|
|
}
|
|
|
|
navigate('/employees')
|
|
} catch (err: any) {
|
|
if (err.response?.status === 401) {
|
|
setError('Ihre Session ist abgelaufen. Bitte melden Sie sich erneut an.')
|
|
} else {
|
|
setError(err.response?.data?.message || 'Fehler beim Erstellen des Profils')
|
|
}
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex justify-between items-center mb-8">
|
|
<h1 className="text-title-lg font-poppins font-bold text-primary">
|
|
Neues Mitarbeitendenprofil
|
|
</h1>
|
|
<button
|
|
onClick={() => navigate('/employees')}
|
|
className="btn-secondary"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-error-bg text-error p-4 rounded-card mb-6">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-8">
|
|
{/* Persönliche Informationen */}
|
|
<div className="form-card">
|
|
<h2 className="text-title-card font-poppins font-semibold text-primary mb-6">
|
|
Persönliche Informationen
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="md:col-span-2 flex justify-center mb-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2 text-center">
|
|
Profilfoto
|
|
</label>
|
|
<PhotoPreview
|
|
currentPhoto={employeePhoto || undefined}
|
|
onPhotoSelect={(file) => {
|
|
setPhotoFile(file)
|
|
// Create preview URL
|
|
const reader = new FileReader()
|
|
reader.onloadend = () => {
|
|
setEmployeePhoto(reader.result as string)
|
|
}
|
|
reader.readAsDataURL(file)
|
|
}}
|
|
onPhotoRemove={() => {
|
|
setPhotoFile(null)
|
|
setEmployeePhoto(null)
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Vorname *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="firstName"
|
|
value={formData.firstName}
|
|
onChange={handleChange}
|
|
className={`input-field ${validationErrors.firstName ? 'border-red-500 ring-red-200' : ''}`}
|
|
required
|
|
/>
|
|
{validationErrors.firstName && (
|
|
<p className="mt-1 text-sm text-red-600">{validationErrors.firstName}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Nachname *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="lastName"
|
|
value={formData.lastName}
|
|
onChange={handleChange}
|
|
className={`input-field ${validationErrors.lastName ? 'border-red-500 ring-red-200' : ''}`}
|
|
required
|
|
/>
|
|
{validationErrors.lastName && (
|
|
<p className="mt-1 text-sm text-red-600">{validationErrors.lastName}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Personalnummer *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="employeeNumber"
|
|
value={formData.employeeNumber}
|
|
onChange={handleChange}
|
|
className={`input-field ${validationErrors.employeeNumber ? 'border-red-500 ring-red-200' : ''}`}
|
|
required
|
|
/>
|
|
{validationErrors.employeeNumber && (
|
|
<p className="mt-1 text-sm text-red-600">{validationErrors.employeeNumber}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
E-Mail *
|
|
</label>
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
className={`input-field ${validationErrors.email ? 'border-red-500 ring-red-200' : ''}`}
|
|
required
|
|
/>
|
|
{validationErrors.email && (
|
|
<p className="mt-1 text-sm text-red-600">{validationErrors.email}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Position *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="position"
|
|
value={formData.position}
|
|
onChange={handleChange}
|
|
className={`input-field ${validationErrors.position ? 'border-red-500 ring-red-200' : ''}`}
|
|
required
|
|
/>
|
|
{validationErrors.position && (
|
|
<p className="mt-1 text-sm text-red-600">{validationErrors.position}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Abteilung *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="department"
|
|
value={formData.department}
|
|
onChange={handleChange}
|
|
className={`input-field ${validationErrors.department ? 'border-red-500 ring-red-200' : ''}`}
|
|
required
|
|
/>
|
|
{validationErrors.department && (
|
|
<p className="mt-1 text-sm text-red-600">{validationErrors.department}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Organisatorische Einheit (Primär) *
|
|
</label>
|
|
<OrganizationSelector
|
|
value={primaryUnitId || undefined}
|
|
onChange={(unitId, unitName) => {
|
|
setPrimaryUnitId(unitId)
|
|
setPrimaryUnitName(unitName)
|
|
}}
|
|
/>
|
|
<p className="text-tertiary text-sm mt-2">{primaryUnitName || 'Bitte auswählen'}</p>
|
|
{validationErrors.primaryUnitId && (
|
|
<p className="mt-1 text-sm text-red-600">{validationErrors.primaryUnitId}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Telefon *
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
name="phone"
|
|
value={formData.phone}
|
|
onChange={handleChange}
|
|
className={`input-field ${validationErrors.phone ? 'border-red-500 ring-red-200' : ''}`}
|
|
required
|
|
/>
|
|
{validationErrors.phone && (
|
|
<p className="mt-1 text-sm text-red-600">{validationErrors.phone}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Mobil
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
name="mobile"
|
|
value={formData.mobile}
|
|
onChange={handleChange}
|
|
className="input-field"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Büro
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="office"
|
|
value={formData.office}
|
|
onChange={handleChange}
|
|
className="input-field"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Sicherheitsfreigabe
|
|
</label>
|
|
<select
|
|
name="clearance"
|
|
value={formData.clearance}
|
|
onChange={handleChange}
|
|
className="input-field"
|
|
>
|
|
<option value="">Keine</option>
|
|
<option value="Ü2">Ü2</option>
|
|
<option value="Ü3">Ü3</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Verfügbarkeit
|
|
</label>
|
|
<select
|
|
name="availability"
|
|
value={formData.availability}
|
|
onChange={handleChange}
|
|
className="input-field"
|
|
>
|
|
<option value="available">Verfügbar</option>
|
|
<option value="parttime">Teilzeit</option>
|
|
<option value="unavailable">Nicht verfügbar</option>
|
|
</select>
|
|
</div>
|
|
|
|
{formData.availability === 'parttime' && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-secondary mb-2">
|
|
Stunden pro Woche
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="partTimeHours"
|
|
value={formData.partTimeHours}
|
|
onChange={handleChange}
|
|
placeholder="z.B. 20 Stunden"
|
|
className="input-field"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Skills */}
|
|
<div className="form-card">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-title-card font-poppins font-semibold text-primary">
|
|
Fähigkeiten und Qualifikationen
|
|
</h2>
|
|
|
|
{/* Skill-Suche */}
|
|
<div className="relative w-64">
|
|
<input
|
|
type="text"
|
|
value={skillSearchTerm}
|
|
onChange={(e) => handleSkillSearch(e.target.value)}
|
|
placeholder="Skills suchen..."
|
|
className="input-field w-full pl-10 pr-3"
|
|
/>
|
|
<svg
|
|
className="absolute left-3 top-4 w-4 h-4 text-text-placeholder pointer-events-none"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
|
|
{/* Suchergebnisse */}
|
|
{searchResults.length > 0 && (
|
|
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-64 overflow-y-auto z-10">
|
|
{searchResults.map((result, index) => (
|
|
<button
|
|
key={index}
|
|
type="button"
|
|
onClick={() => handleSearchResultClick(result)}
|
|
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors border-b border-gray-200 dark:border-gray-600 last:border-b-0 text-gray-900 dark:text-white"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-medium text-gray-900 dark:text-white">{result.skillName}</span>
|
|
{isSkillSelected(result.categoryId, result.subCategoryId, result.skillId) && (
|
|
<svg className="w-4 h-4 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
{result.categoryName} → {result.subCategoryName}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{skillSearchTerm.length >= 2 && searchResults.length === 0 && (
|
|
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg p-4">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">Keine Skills gefunden</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{SKILL_HIERARCHY.map(category => (
|
|
<div key={category.id} className="border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800">
|
|
{/* Oberste Kategorie */}
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleCategory(category.id)}
|
|
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-gray-900 dark:text-white"
|
|
>
|
|
<span className="font-semibold text-gray-900 dark:text-white">{category.name}</span>
|
|
<svg
|
|
className={`w-5 h-5 text-gray-600 dark:text-gray-400 transition-transform ${
|
|
expandedCategories.has(category.id) ? 'rotate-180' : ''
|
|
}`}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Unterkategorien */}
|
|
{expandedCategories.has(category.id) && (
|
|
<div className="border-t border-gray-300 dark:border-gray-600">
|
|
{category.subcategories.map(subCategory => (
|
|
<div key={subCategory.id} className="border-b border-gray-200 dark:border-gray-700 last:border-b-0">
|
|
{/* Mittlere Kategorie */}
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleSubCategory(`${category.id}-${subCategory.id}`)}
|
|
className="w-full px-6 py-2 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-gray-800 dark:text-gray-200"
|
|
>
|
|
<span className="text-gray-700 dark:text-gray-300">{subCategory.name}</span>
|
|
<svg
|
|
className={`w-4 h-4 text-gray-600 dark:text-gray-400 transition-transform ${
|
|
expandedSubCategories.has(`${category.id}-${subCategory.id}`) ? 'rotate-180' : ''
|
|
}`}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Skills */}
|
|
{expandedSubCategories.has(`${category.id}-${subCategory.id}`) && (
|
|
<div className="px-8 py-3 bg-gray-50 dark:bg-gray-900 space-y-2">
|
|
{subCategory.skills.map(skill => (
|
|
<div key={skill.id} className="flex items-center space-x-3">
|
|
<label className="flex items-center space-x-2 flex-1">
|
|
<input
|
|
type="checkbox"
|
|
checked={isSkillSelected(category.id, subCategory.id, skill.id)}
|
|
onChange={() => handleSkillToggle(category.id, subCategory.id, skill.id, skill.name)}
|
|
className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:text-blue-400"
|
|
/>
|
|
<span className="text-sm text-gray-700 dark:text-gray-300">{skill.name}</span>
|
|
</label>
|
|
|
|
{/* Niveauauswahl */}
|
|
{isSkillSelected(category.id, subCategory.id, skill.id) && (
|
|
subCategory.id === 'languages' ? (
|
|
<select
|
|
value={getSkillLevel(category.id, subCategory.id, skill.id)}
|
|
onChange={(e) => handleSkillLevelChange(category.id, subCategory.id, skill.id, e.target.value)}
|
|
className="text-sm px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
>
|
|
<option value="">Niveau wählen</option>
|
|
{LANGUAGE_LEVELS.map(level => (
|
|
<option key={level} value={level}>{level}</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<select
|
|
value={getSkillLevel(category.id, subCategory.id, skill.id)}
|
|
onChange={(e) => handleSkillLevelChange(category.id, subCategory.id, skill.id, e.target.value)}
|
|
className="text-sm px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
|
>
|
|
<option value="">Niveau wählen</option>
|
|
<option value="Beginner">Anfänger</option>
|
|
<option value="Basic">Grundkenntnisse</option>
|
|
<option value="Intermediate">Fortgeschritten</option>
|
|
<option value="Advanced">Sehr fortgeschritten</option>
|
|
<option value="Expert">Experte</option>
|
|
</select>
|
|
)
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Submit */}
|
|
<div className="flex justify-end space-x-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate('/employees')}
|
|
className="btn-secondary"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="btn-primary"
|
|
>
|
|
{loading ? 'Wird gespeichert...' : 'Profil erstellen'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)
|
|
}
|