Initial commit
Dieser Commit ist enthalten in:
684
frontend/src/views/EmployeeForm.tsx
Normale Datei
684
frontend/src/views/EmployeeForm.tsx
Normale Datei
@ -0,0 +1,684 @@
|
||||
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'
|
||||
|
||||
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 [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 = 'Mitarbeiternummer 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'
|
||||
|
||||
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)
|
||||
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 Mitarbeiters')
|
||||
}
|
||||
} 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">
|
||||
Neuer Mitarbeiter
|
||||
</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">
|
||||
Mitarbeiterfoto
|
||||
</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>
|
||||
<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...' : 'Mitarbeiter erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren