Sub Kategorie doch erstellt
Dieser Commit ist enthalten in:
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { api } from '../services/api'
|
||||
|
||||
type Skill = {
|
||||
type Skill = {
|
||||
id: string
|
||||
name: string
|
||||
category?: string
|
||||
@ -9,15 +9,18 @@ type Skill = {
|
||||
userCount?: number
|
||||
tags?: string[]
|
||||
levelDistribution?: {
|
||||
beginner: number // Level 1-3
|
||||
intermediate: number // Level 4-6
|
||||
expert: number // Level 7-10
|
||||
beginner: number
|
||||
intermediate: number
|
||||
expert: number
|
||||
}
|
||||
requires_certification?: boolean
|
||||
certification_months?: number
|
||||
typeKey?: string
|
||||
typeName?: string
|
||||
typeIcon?: string
|
||||
}
|
||||
|
||||
type Category = {
|
||||
type Category = {
|
||||
id: string
|
||||
name: string
|
||||
icon?: string
|
||||
@ -25,6 +28,16 @@ type Category = {
|
||||
skillCount?: number
|
||||
}
|
||||
|
||||
type SkillType = {
|
||||
key: string
|
||||
id: string
|
||||
name: string
|
||||
categoryId: string
|
||||
categoryName: string
|
||||
icon: string
|
||||
color: string
|
||||
}
|
||||
|
||||
type ViewMode = 'grid' | 'category'
|
||||
|
||||
export default function SkillManagement() {
|
||||
@ -39,6 +52,11 @@ export default function SkillManagement() {
|
||||
const [sortBy, setSortBy] = useState<'name' | 'users' | 'category'>('name')
|
||||
const [employeeCounts, setEmployeeCounts] = useState<Record<string, number>>({})
|
||||
const [levelDistributions, setLevelDistributions] = useState<Record<string, { beginner: number, intermediate: number, expert: number }>>({})
|
||||
const [skillTypes, setSkillTypes] = useState<SkillType[]>([])
|
||||
const [selectedTypes, setSelectedTypes] = useState<Set<string>>(new Set())
|
||||
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(new Set())
|
||||
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
const lastExpansionRef = useRef<{ key: string; prevTop: number | null } | null>(null)
|
||||
|
||||
// Modal states
|
||||
const [showSkillModal, setShowSkillModal] = useState(false)
|
||||
@ -50,6 +68,7 @@ export default function SkillManagement() {
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
typeKey: '',
|
||||
tags: [] as string[],
|
||||
requires_certification: false,
|
||||
certification_months: 0
|
||||
@ -64,6 +83,17 @@ export default function SkillManagement() {
|
||||
'certifications': { icon: '📜', color: 'bg-yellow-100 text-yellow-800' },
|
||||
}
|
||||
|
||||
const typeIconFallback = '📁'
|
||||
|
||||
const typeIconMap: Record<string, string> = {
|
||||
languages: '🌐',
|
||||
interpersonal: '🤝',
|
||||
presentation: '🗣️',
|
||||
moderation: '🎙️',
|
||||
negotiation: '⚖️',
|
||||
security: '🛡️',
|
||||
}
|
||||
|
||||
// Predefined tags
|
||||
const availableTags = [
|
||||
'Programmierung', 'Datenanalyse', 'Forensik', 'Machine Learning',
|
||||
@ -75,6 +105,12 @@ export default function SkillManagement() {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editingSkill && !skillForm.category && categories.length > 0) {
|
||||
setSkillForm(prev => ({ ...prev, category: categories[0].id }))
|
||||
}
|
||||
}, [categories, editingSkill, skillForm.category])
|
||||
|
||||
async function loadData() {
|
||||
// First fetch employee counts, then skills with those counts
|
||||
const { counts, distributions } = await fetchEmployeeCountsAndDistributions()
|
||||
@ -95,6 +131,8 @@ export default function SkillManagement() {
|
||||
const extractedCategories: Category[] = []
|
||||
const flatSkills: Skill[] = []
|
||||
const seenCategories = new Set<string>()
|
||||
const collectedTypes: SkillType[] = []
|
||||
const seenTypeKeys = new Set<string>()
|
||||
|
||||
hierarchy.forEach((cat: any) => {
|
||||
// Add category if not seen
|
||||
@ -110,11 +148,32 @@ export default function SkillManagement() {
|
||||
})
|
||||
}
|
||||
|
||||
const catName = cat.name
|
||||
|
||||
cat.subcategories?.forEach((sub: any) => {
|
||||
const typeKey = `${cat.id}.${sub.id}`
|
||||
if (!seenTypeKeys.has(typeKey)) {
|
||||
seenTypeKeys.add(typeKey)
|
||||
const typeIcon = typeIconMap[sub.id] || typeIconFallback
|
||||
const typeColor = categoryConfig[cat.id]?.color || 'bg-gray-100 text-gray-800'
|
||||
collectedTypes.push({
|
||||
key: typeKey,
|
||||
id: sub.id,
|
||||
name: sub.name,
|
||||
categoryId: cat.id,
|
||||
categoryName: catName,
|
||||
icon: typeIcon,
|
||||
color: typeColor
|
||||
})
|
||||
}
|
||||
|
||||
sub.skills?.forEach((skill: any) => {
|
||||
flatSkills.push({
|
||||
...skill,
|
||||
category: cat.id,
|
||||
typeKey,
|
||||
typeName: sub.name,
|
||||
typeIcon: typeIconMap[sub.id] || typeIconFallback,
|
||||
tags: [cat.name, sub.name],
|
||||
userCount: skillCounts[skill.id] || 0,
|
||||
levelDistribution: skillDistributions[skill.id] || { beginner: 0, intermediate: 0, expert: 0 }
|
||||
@ -130,6 +189,7 @@ export default function SkillManagement() {
|
||||
|
||||
setCategories(extractedCategories)
|
||||
setSkills(flatSkills)
|
||||
setSkillTypes(collectedTypes.sort((a, b) => a.name.localeCompare(b.name)))
|
||||
} catch (e) {
|
||||
setError('Skills konnten nicht geladen werden')
|
||||
} finally {
|
||||
@ -180,7 +240,8 @@ export default function SkillManagement() {
|
||||
const matchesSearch = skill.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
skill.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesCategory = selectedCategories.size === 0 || selectedCategories.has(skill.category || '')
|
||||
return matchesSearch && matchesCategory
|
||||
const matchesType = selectedTypes.size === 0 || (skill.typeKey && selectedTypes.has(skill.typeKey))
|
||||
return matchesSearch && matchesCategory && matchesType
|
||||
})
|
||||
.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
@ -203,13 +264,150 @@ export default function SkillManagement() {
|
||||
newThisWeek: Math.floor(skills.length * 0.15)
|
||||
}
|
||||
|
||||
const typeMetaMap = new Map(skillTypes.map(type => [type.key, type]))
|
||||
const fallbackTypeMeta: SkillType = {
|
||||
key: '__other',
|
||||
id: 'other',
|
||||
name: 'Weitere Skills',
|
||||
categoryId: 'other',
|
||||
categoryName: 'Weitere',
|
||||
icon: typeIconFallback,
|
||||
color: 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
|
||||
const groupedSkills = filteredAndSortedSkills.reduce<Map<string, Skill[]>>((acc, skill) => {
|
||||
const key = skill.typeKey && typeMetaMap.has(skill.typeKey) ? skill.typeKey : '__other'
|
||||
if (!acc.has(key)) {
|
||||
acc.set(key, [])
|
||||
}
|
||||
acc.get(key)!.push(skill)
|
||||
return acc
|
||||
}, new Map())
|
||||
|
||||
const typeKeysInResults = new Set(groupedSkills.keys())
|
||||
|
||||
const filteredSkillTypes = skillTypes.filter(type => {
|
||||
const categoryMatch = selectedCategories.size === 0 || selectedCategories.has(type.categoryId)
|
||||
return categoryMatch && typeKeysInResults.has(type.key)
|
||||
})
|
||||
|
||||
const displayedTypeChips = filteredSkillTypes.length > 0 ? filteredSkillTypes : skillTypes.filter(type => typeKeysInResults.has(type.key))
|
||||
|
||||
const toggleTypeFilter = (key: string) => {
|
||||
const updated = new Set(selectedTypes)
|
||||
if (updated.has(key)) {
|
||||
updated.delete(key)
|
||||
} else {
|
||||
updated.add(key)
|
||||
}
|
||||
setSelectedTypes(updated)
|
||||
}
|
||||
|
||||
const toggleTypeExpansion = (key: string) => {
|
||||
const section = sectionRefs.current[key]
|
||||
const prevTop = section ? section.getBoundingClientRect().top : null
|
||||
lastExpansionRef.current = { key, prevTop }
|
||||
setExpandedTypes(prev => {
|
||||
const updated = new Set(prev)
|
||||
if (updated.has(key)) {
|
||||
updated.delete(key)
|
||||
} else {
|
||||
updated.add(key)
|
||||
}
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!lastExpansionRef.current) return
|
||||
const { key, prevTop } = lastExpansionRef.current
|
||||
lastExpansionRef.current = null
|
||||
if (prevTop === null) return
|
||||
const section = sectionRefs.current[key]
|
||||
if (!section) return
|
||||
const newTop = section.getBoundingClientRect().top
|
||||
const delta = newTop - prevTop
|
||||
if (delta > 1) {
|
||||
window.scrollBy({ top: delta, behavior: 'auto' })
|
||||
}
|
||||
}, [expandedTypes])
|
||||
|
||||
const getTypeMeta = (key: string): SkillType => {
|
||||
if (key === '__other') return fallbackTypeMeta
|
||||
return typeMetaMap.get(key) || fallbackTypeMeta
|
||||
}
|
||||
|
||||
const sortedTypeKeys = Array.from(groupedSkills.keys()).sort((a, b) => {
|
||||
const metaA = getTypeMeta(a)
|
||||
const metaB = getTypeMeta(b)
|
||||
if (metaA.categoryName !== metaB.categoryName) {
|
||||
return metaA.categoryName.localeCompare(metaB.categoryName)
|
||||
}
|
||||
return metaA.name.localeCompare(metaB.name)
|
||||
})
|
||||
|
||||
function TypeSection({ typeKey }: { typeKey: string }) {
|
||||
const skillsForType = groupedSkills.get(typeKey) || []
|
||||
if (skillsForType.length === 0) return null
|
||||
const typeMeta = getTypeMeta(typeKey)
|
||||
const isExpanded = expandedTypes.has(typeKey)
|
||||
const visibleSkills = isExpanded ? skillsForType : skillsForType.slice(0, 3)
|
||||
const hiddenCount = skillsForType.length - visibleSkills.length
|
||||
|
||||
return (
|
||||
<div
|
||||
key={typeKey}
|
||||
ref={el => {
|
||||
if (el) {
|
||||
sectionRefs.current[typeKey] = el
|
||||
} else {
|
||||
delete sectionRefs.current[typeKey]
|
||||
}
|
||||
}}
|
||||
className="mb-8"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl" aria-hidden>{typeMeta.icon}</span>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-primary">{typeMeta.name}</h3>
|
||||
<p className="text-xs text-secondary">{typeMeta.categoryName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-tertiary">{skillsForType.length} Skills</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{visibleSkills.map(skill => (
|
||||
<SkillCard key={skill.id} skill={skill} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{hiddenCount > 0 && (
|
||||
<div className="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleTypeExpansion(typeKey)}
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{isExpanded ? 'Weniger anzeigen' : `Mehr anzeigen (+${hiddenCount})`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function openSkillModal(skill?: Skill) {
|
||||
if (skill) {
|
||||
setEditingSkill(skill)
|
||||
const categoryId = skill.category || ''
|
||||
const typeKey = skill.typeKey || ''
|
||||
setSkillForm({
|
||||
name: skill.name,
|
||||
description: skill.description || '',
|
||||
category: skill.category || '',
|
||||
category: categoryId,
|
||||
typeKey,
|
||||
tags: skill.tags || [],
|
||||
requires_certification: skill.requires_certification || false,
|
||||
certification_months: skill.certification_months || 0
|
||||
@ -220,6 +418,7 @@ export default function SkillManagement() {
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
typeKey: '',
|
||||
tags: [],
|
||||
requires_certification: false,
|
||||
certification_months: 0
|
||||
@ -230,18 +429,21 @@ export default function SkillManagement() {
|
||||
|
||||
async function saveSkill() {
|
||||
try {
|
||||
const payload = {
|
||||
name: skillForm.name,
|
||||
description: skillForm.description,
|
||||
category: skillForm.typeKey || skillForm.category
|
||||
}
|
||||
|
||||
if (!payload.category || payload.category.indexOf('.') === -1) {
|
||||
setError('Bitte einen Skill-Typ auswählen')
|
||||
return
|
||||
}
|
||||
|
||||
if (editingSkill) {
|
||||
await api.put(`/skills/${editingSkill.id}`, {
|
||||
name: skillForm.name,
|
||||
description: skillForm.description,
|
||||
category: skillForm.category
|
||||
})
|
||||
await api.put(`/skills/${editingSkill.id}`, payload)
|
||||
} else {
|
||||
await api.post('/skills', {
|
||||
name: skillForm.name,
|
||||
description: skillForm.description,
|
||||
category: skillForm.category || 'technical.general'
|
||||
})
|
||||
await api.post('/skills', payload)
|
||||
}
|
||||
setShowSkillModal(false)
|
||||
loadData()
|
||||
@ -287,7 +489,8 @@ export default function SkillManagement() {
|
||||
const data = filteredAndSortedSkills.map(s => ({
|
||||
Name: s.name,
|
||||
Beschreibung: s.description,
|
||||
Kategorie: s.category,
|
||||
Kategorie: categories.find(c => c.id === s.category)?.name || s.category,
|
||||
Typ: s.typeName,
|
||||
Tags: s.tags?.join(', ')
|
||||
}))
|
||||
const csv = [
|
||||
@ -306,7 +509,7 @@ export default function SkillManagement() {
|
||||
function SkillCard({ skill }: { skill: Skill }) {
|
||||
const category = categories.find(c => c.id === skill.category)
|
||||
const dist = skill.levelDistribution || { beginner: 0, intermediate: 0, expert: 0 }
|
||||
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow border border-border-default">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
@ -351,10 +554,18 @@ export default function SkillManagement() {
|
||||
)}
|
||||
|
||||
<div className="flex items-start justify-between gap-2 text-sm">
|
||||
<span className={`inline-flex items-start px-2 py-1 rounded-full text-xs font-medium ${category?.color || 'bg-gray-100'}`}>
|
||||
<span className="flex-shrink-0">{category?.icon}</span>
|
||||
<span className="ml-1">{category?.name}</span>
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${category?.color || 'bg-gray-100'}`}>
|
||||
<span className="flex-shrink-0">{category?.icon}</span>
|
||||
<span className="ml-1">{category?.name}</span>
|
||||
</span>
|
||||
{skill.typeName && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200">
|
||||
<span className="mr-1">{skill.typeIcon || typeIconFallback}</span>
|
||||
{skill.typeName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-tertiary flex-shrink-0">👥 {employeeCounts[skill.id] || skill.userCount || 0}</span>
|
||||
</div>
|
||||
|
||||
@ -376,11 +587,29 @@ export default function SkillManagement() {
|
||||
)
|
||||
}
|
||||
|
||||
const categorySkillsMap = categories.reduce<Map<string, Skill[]>>((acc, category) => {
|
||||
const skillsForCategory = filteredAndSortedSkills.filter(skill => skill.category === category.id)
|
||||
if (skillsForCategory.length > 0) {
|
||||
acc.set(category.id, skillsForCategory)
|
||||
}
|
||||
return acc
|
||||
}, new Map())
|
||||
|
||||
const visibleCategories = categories.filter(category => categorySkillsMap.has(category.id))
|
||||
|
||||
function CategoryView() {
|
||||
if (visibleCategories.length === 0) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-dashed border-gray-300 dark:border-gray-600 p-8 text-center text-secondary">
|
||||
Keine Kategorien mit passenden Skills gefunden. Filter zurücksetzen?
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{categories.map(category => {
|
||||
const categorySkills = filteredAndSortedSkills.filter(s => s.category === category.id)
|
||||
{visibleCategories.map(category => {
|
||||
const categorySkills = categorySkillsMap.get(category.id) || []
|
||||
return (
|
||||
<div key={category.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@ -390,11 +619,11 @@ export default function SkillManagement() {
|
||||
</h3>
|
||||
<span className="text-sm text-gray-500">{categorySkills.length} Skills</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{categorySkills.slice(0, 10).map(skill => (
|
||||
<div
|
||||
key={skill.id}
|
||||
<div
|
||||
key={skill.id}
|
||||
className="flex items-center justify-between p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded cursor-pointer"
|
||||
onClick={() => openSkillModal(skill)}
|
||||
>
|
||||
@ -408,10 +637,10 @@ export default function SkillManagement() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setSkillForm(prev => ({ ...prev, category: category.id }))
|
||||
setSkillForm(prev => ({ ...prev, category: category.id, typeKey: '' }))
|
||||
openSkillModal()
|
||||
}}
|
||||
className="w-full mt-4 text-sm text-blue-600 hover:text-blue-800"
|
||||
@ -553,6 +782,43 @@ export default function SkillManagement() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{displayedTypeChips.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<h3 className="text-sm font-medium text-secondary">Skill-Typen</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{displayedTypeChips.map(type => {
|
||||
const isActive = selectedTypes.has(type.key)
|
||||
const count = groupedSkills.get(type.key)?.length || 0
|
||||
if (count === 0) return null
|
||||
return (
|
||||
<button
|
||||
key={type.key}
|
||||
onClick={() => toggleTypeFilter(type.key)}
|
||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors flex items-center gap-1 ${
|
||||
isActive
|
||||
? 'bg-blue-600 text-white shadow'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span>{type.icon}</span>
|
||||
<span>{type.name}</span>
|
||||
<span className="text-xs opacity-75">({count})</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
{selectedTypes.size > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTypes(new Set())}
|
||||
className="px-3 py-1.5 rounded-full text-sm font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500"
|
||||
>
|
||||
Alle Typen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@ -570,9 +836,9 @@ export default function SkillManagement() {
|
||||
) : (
|
||||
<>
|
||||
{viewMode === 'grid' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredAndSortedSkills.map(skill => (
|
||||
<SkillCard key={skill.id} skill={skill} />
|
||||
<div>
|
||||
{sortedTypeKeys.map(typeKey => (
|
||||
<TypeSection key={typeKey} typeKey={typeKey} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -625,7 +891,14 @@ export default function SkillManagement() {
|
||||
<label className="block text-sm font-medium mb-2">Hauptkategorie</label>
|
||||
<select
|
||||
value={skillForm.category}
|
||||
onChange={(e) => setSkillForm(prev => ({ ...prev, category: e.target.value }))}
|
||||
onChange={(e) => {
|
||||
const newCategory = e.target.value
|
||||
setSkillForm(prev => ({
|
||||
...prev,
|
||||
category: newCategory,
|
||||
typeKey: prev.typeKey.startsWith(`${newCategory}.`) ? prev.typeKey : ''
|
||||
}))
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
>
|
||||
<option value="">Kategorie wählen...</option>
|
||||
@ -635,6 +908,25 @@ export default function SkillManagement() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Skill-Typ</label>
|
||||
<select
|
||||
value={skillForm.typeKey}
|
||||
onChange={(e) => setSkillForm(prev => ({ ...prev, typeKey: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"
|
||||
disabled={!skillForm.category}
|
||||
>
|
||||
<option value="">{skillForm.category ? 'Skill-Typ wählen...' : 'Bitte zuerst eine Kategorie wählen'}</option>
|
||||
{skillTypes
|
||||
.filter(type => type.categoryId === skillForm.category)
|
||||
.map(type => (
|
||||
<option key={type.key} value={type.key}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Tags (Mehrfachzuordnung)</label>
|
||||
<div className="border border-gray-300 rounded-lg p-3 dark:border-gray-600">
|
||||
@ -766,4 +1058,4 @@ export default function SkillManagement() {
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,8 @@ import path from 'path'
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3006,
|
||||
port: Number(process.env.VITE_PORT || 5174),
|
||||
strictPort: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@ -16,4 +17,4 @@ export default defineConfig({
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren