From 26f95d2e4ab243ec97e32f7907ee1ccb12cdf352 Mon Sep 17 00:00:00 2001 From: Claude Project Manager Date: Mon, 22 Sep 2025 20:54:57 +0200 Subject: [PATCH] So mit neuen UI Ideen und so --- .claude/settings.local.json | 6 +- CLAUDE_PROJECT_README.md | 33 +- admin-panel/src/views/SkillManagement.tsx | 958 +++++++++--- backend/package-lock.json | 1286 +++++++++-------- .../migrations/0001_users_email_encrypt.js | 43 + backend/scripts/run-migrations.js | 54 + backend/src/config/appConfig.ts | 43 + .../src/repositories/employeeRepository.ts | 317 ++++ backend/src/services/auditService.ts | 35 + backend/src/services/sync/applier.ts | 123 ++ backend/src/services/sync/queueStore.ts | 60 + backend/src/services/sync/transport.ts | 17 + backend/src/usecases/auth/loginUser.ts | 69 + backend/src/usecases/employees.ts | 109 ++ backend/src/usecases/users.ts | 48 + backend/src/validation/employeeValidators.ts | 23 + docs/ARCHITECTURE.md | 29 + docs/REFAKTOR_PLAN.txt | 81 ++ docs/SMOKE_TESTS.md | 32 + frontend/package-lock.json | 753 +++++++++- frontend/package.json | 4 + frontend/src/components/ErrorBoundary.tsx | 30 + frontend/src/components/OfficeMap3D.tsx | 565 ++++++++ frontend/src/components/OfficeMapModal.tsx | 98 ++ frontend/src/views/EmployeeDetail.tsx | 23 +- frontend/src/views/SkillSearch.tsx | 667 ++++++--- frontend/src/views/TeamZusammenstellung.tsx | 1112 +++++++++++--- gitea_push_debug.txt | 22 + package-lock.json | 6 + 29 files changed, 5321 insertions(+), 1325 deletions(-) create mode 100644 backend/scripts/migrations/0001_users_email_encrypt.js create mode 100644 backend/scripts/run-migrations.js create mode 100644 backend/src/config/appConfig.ts create mode 100644 backend/src/repositories/employeeRepository.ts create mode 100644 backend/src/services/auditService.ts create mode 100644 backend/src/services/sync/applier.ts create mode 100644 backend/src/services/sync/queueStore.ts create mode 100644 backend/src/services/sync/transport.ts create mode 100644 backend/src/usecases/auth/loginUser.ts create mode 100644 backend/src/usecases/employees.ts create mode 100644 backend/src/usecases/users.ts create mode 100644 backend/src/validation/employeeValidators.ts create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/REFAKTOR_PLAN.txt create mode 100644 docs/SMOKE_TESTS.md create mode 100644 frontend/src/components/ErrorBoundary.tsx create mode 100644 frontend/src/components/OfficeMap3D.tsx create mode 100644 frontend/src/components/OfficeMapModal.tsx create mode 100644 gitea_push_debug.txt create mode 100644 package-lock.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ea57242..dd6ef35 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -37,8 +37,10 @@ "Bash(set PORT=5000)", "Bash(set PORT=3005)", "Bash(set FIELD_ENCRYPTION_KEY=dev_field_encryption_key_32chars_min!)", - "Bash(start:*)" + "Bash(start:*)", + "WebSearch" ], - "deny": [] + "deny": [], + "defaultMode": "acceptEdits" } } \ No newline at end of file diff --git a/CLAUDE_PROJECT_README.md b/CLAUDE_PROJECT_README.md index cc06f2e..e7eca50 100644 --- a/CLAUDE_PROJECT_README.md +++ b/CLAUDE_PROJECT_README.md @@ -5,9 +5,9 @@ ## Project Overview - **Path**: `A:/GiTea/SkillMate` -- **Files**: 222 files -- **Size**: 10.5 MB -- **Last Modified**: 2025-09-18 22:20 +- **Files**: 240 files +- **Size**: 6.7 MB +- **Last Modified**: 2025-09-21 16:48 ## Technology Stack @@ -28,11 +28,11 @@ ANWENDUNGSBESCHREIBUNG.txt CLAUDE_PROJECT_README.md debug-console.cmd EXE-ERSTELLEN.md +gitea_push_debug.txt install-dependencies.cmd INSTALLATION.md LICENSE.txt main.py -README.md admin-panel/ │ ├── index.html │ ├── package-lock.json @@ -140,16 +140,22 @@ backend/ │ │ ├── migrate-users.js │ │ ├── purge-users.js │ │ ├── reset-admin.js -│ │ └── seed-skills-from-frontend.js +│ │ ├── run-migrations.js +│ │ ├── seed-skills-from-frontend.js +│ │ └── migrations/ +│ │ └── 0001_users_email_encrypt.js │ ├── src/ │ │ ├── index.ts │ │ ├── config/ +│ │ │ ├── appConfig.ts │ │ │ ├── database.ts │ │ │ └── secureDatabase.ts │ │ ├── middleware/ │ │ │ ├── auth.ts │ │ │ ├── errorHandler.ts │ │ │ └── roleAuth.ts +│ │ ├── repositories/ +│ │ │ └── employeeRepository.ts │ │ ├── routes/ │ │ │ ├── analytics.ts │ │ │ ├── auth.ts @@ -162,18 +168,28 @@ backend/ │ │ │ ├── skills.ts │ │ │ └── sync.ts │ │ ├── services/ +│ │ │ ├── auditService.ts │ │ │ ├── emailService.ts │ │ │ ├── encryption.ts │ │ │ ├── reminderService.ts │ │ │ ├── syncScheduler.ts │ │ │ └── syncService.ts -│ │ └── utils/ -│ │ └── logger.ts +│ │ ├── usecases/ +│ │ │ ├── employees.ts +│ │ │ └── users.ts +│ │ ├── utils/ +│ │ │ └── logger.ts +│ │ └── validation/ +│ │ └── employeeValidators.ts │ └── uploads/ │ └── photos/ │ ├── 0def5f6f-c1ef-4f88-9105-600c75278f10.jpg │ ├── 72c09fa1-f0a8-444c-918f-95258ca56f61.gif │ └── 80c44681-d6b4-474e-8ff1-c6d02da0cd7d.gif +docs/ +│ ├── ARCHITECTURE.md +│ ├── REFAKTOR_PLAN.txt +│ └── SMOKE_TESTS.md frontend/ │ ├── electron-builder.json │ ├── index-electron.html @@ -205,6 +221,7 @@ frontend/ │ ├── main.tsx │ ├── components/ │ │ ├── EmployeeCard.tsx +│ │ ├── ErrorBoundary.tsx │ │ ├── Header.tsx │ │ ├── Layout.tsx │ │ ├── PhotoPreview.tsx @@ -275,3 +292,5 @@ This project is managed with Claude Project Manager. To work with this project: - README updated on 2025-08-01 23:08:41 - README updated on 2025-08-01 23:08:52 - README updated on 2025-09-20 21:30:35 +- README updated on 2025-09-21 16:48:11 +- README updated on 2025-09-21 16:48:44 diff --git a/admin-panel/src/views/SkillManagement.tsx b/admin-panel/src/views/SkillManagement.tsx index c937a89..dcb18b9 100644 --- a/admin-panel/src/views/SkillManagement.tsx +++ b/admin-panel/src/views/SkillManagement.tsx @@ -1,44 +1,135 @@ import { useEffect, useState } from 'react' import { api } from '../services/api' -type Skill = { id: string; name: string; description?: string | null } -type Subcategory = { id: string; name: string; skills: Skill[] } -type Category = { id: string; name: string; subcategories: Subcategory[] } +type Skill = { + id: string + name: string + category?: string + description?: string | null + userCount?: number + tags?: string[] + levelDistribution?: { + beginner: number // Level 1-3 + intermediate: number // Level 4-6 + expert: number // Level 7-10 + } + requires_certification?: boolean + certification_months?: number +} + +type Category = { + id: string + name: string + icon?: string + color?: string + skillCount?: number +} + +type ViewMode = 'grid' | 'category' export default function SkillManagement() { - const [hierarchy, setHierarchy] = useState([]) + const [skills, setSkills] = useState([]) + const [categories, setCategories] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') + const [viewMode, setViewMode] = useState('grid') + const [searchQuery, setSearchQuery] = useState('') + const [selectedCategories, setSelectedCategories] = useState>(new Set()) + const [selectedSkills, setSelectedSkills] = useState>(new Set()) + const [sortBy, setSortBy] = useState<'name' | 'users' | 'category'>('name') + const [employeeCounts, setEmployeeCounts] = useState>({}) + const [levelDistributions, setLevelDistributions] = useState>({}) + + // Modal states + const [showSkillModal, setShowSkillModal] = useState(false) + const [editingSkill, setEditingSkill] = useState(null) + const [showImportModal, setShowImportModal] = useState(false) + + // Form states for skill modal + const [skillForm, setSkillForm] = useState({ + name: '', + description: '', + category: '', + tags: [] as string[], + requires_certification: false, + certification_months: 0 + }) - const [openMain, setOpenMain] = useState>({}) - const [openSub, setOpenSub] = useState>({}) + // Category icon and color mapping + const categoryConfig: Record = { + 'communication': { icon: '💬', color: 'bg-green-100 text-green-800' }, + 'technical': { icon: '💻', color: 'bg-blue-100 text-blue-800' }, + 'operational': { icon: '🎯', color: 'bg-orange-100 text-orange-800' }, + 'analytical': { icon: '📊', color: 'bg-purple-100 text-purple-800' }, + 'certifications': { icon: '📜', color: 'bg-yellow-100 text-yellow-800' }, + } - // Inline create/edit states - const [addingCat, setAddingCat] = useState(false) - const [newCat, setNewCat] = useState({ name: '' }) + // Predefined tags + const availableTags = [ + 'Programmierung', 'Datenanalyse', 'Forensik', 'Machine Learning', + 'Ermittlung', 'Kommunikation', 'Führung', 'Sicherheit', + 'Waffen', 'Fahrzeuge', 'Medizin', 'Recht' + ] - const [addingSub, setAddingSub] = useState>({}) - const [newSub, setNewSub] = useState>({}) + useEffect(() => { + loadData() + }, []) - const [addingSkill, setAddingSkill] = useState>({}) // key: catId.subId - const [newSkill, setNewSkill] = useState>({}) + async function loadData() { + // First fetch employee counts, then skills with those counts + const { counts, distributions } = await fetchEmployeeCountsAndDistributions() + await fetchSkills(counts, distributions) + } - const [editingCat, setEditingCat] = useState(null) - const [editCatName, setEditCatName] = useState('') - - const [editingSub, setEditingSub] = useState(null) // key: catId.subId - const [editSubName, setEditSubName] = useState('') - - const [editingSkill, setEditingSkill] = useState(null) // skill id - const [editSkillData, setEditSkillData] = useState<{ name: string; description?: string }>({ name: '' }) - - useEffect(() => { fetchHierarchy() }, []) - - async function fetchHierarchy() { + async function fetchSkills(counts?: Record, distributions?: Record) { try { setLoading(true) const res = await api.get('/skills/hierarchy') - setHierarchy(res.data.data || []) + const hierarchy = res.data.data || [] + + // Use passed data or state + const skillCounts = counts || employeeCounts + const skillDistributions = distributions || levelDistributions + + // Extract categories and flatten skills + const extractedCategories: Category[] = [] + const flatSkills: Skill[] = [] + const seenCategories = new Set() + + hierarchy.forEach((cat: any) => { + // Add category if not seen + if (!seenCategories.has(cat.id)) { + seenCategories.add(cat.id) + const config = categoryConfig[cat.id] || { icon: '📁', color: 'bg-gray-100 text-gray-800' } + extractedCategories.push({ + id: cat.id, + name: cat.name, + icon: config.icon, + color: config.color, + skillCount: 0 + }) + } + + cat.subcategories?.forEach((sub: any) => { + sub.skills?.forEach((skill: any) => { + flatSkills.push({ + ...skill, + category: cat.id, + tags: [cat.name, sub.name], + userCount: skillCounts[skill.id] || 0, + levelDistribution: skillDistributions[skill.id] || { beginner: 0, intermediate: 0, expert: 0 } + }) + }) + }) + }) + + // Update skill counts for categories + extractedCategories.forEach(cat => { + cat.skillCount = flatSkills.filter(s => s.category === cat.id).length + }) + + setCategories(extractedCategories) + setSkills(flatSkills) } catch (e) { setError('Skills konnten nicht geladen werden') } finally { @@ -46,238 +137,633 @@ export default function SkillManagement() { } } - - - function slugify(input: string) { - return input - .toLowerCase() - .trim() - .replace(/[^a-z0-9\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .slice(0, 64) + async function fetchEmployeeCountsAndDistributions() { + try { + // Fetch employees and count skills with level distribution + const res = await api.get('/employees') + const employees = res.data.data || [] + const counts: Record = {} + const distributions: Record = {} + + employees.forEach((emp: any) => { + emp.skills?.forEach((skill: any) => { + counts[skill.id] = (counts[skill.id] || 0) + 1 + + // Initialize distribution if not exists + if (!distributions[skill.id]) { + distributions[skill.id] = { beginner: 0, intermediate: 0, expert: 0 } + } + + // Parse level and categorize + const level = parseInt(skill.level) || 0 + if (level >= 1 && level <= 3) { + distributions[skill.id].beginner++ + } else if (level >= 4 && level <= 6) { + distributions[skill.id].intermediate++ + } else if (level >= 7 && level <= 10) { + distributions[skill.id].expert++ + } + }) + }) + + setEmployeeCounts(counts) + setLevelDistributions(distributions) + return { counts, distributions } + } catch (e) { + console.error('Could not fetch employee counts') + return { counts: {}, distributions: {} } + } } - function keyFor(catId: string, subId: string) { return `${catId}.${subId}` } - function toggleMain(catId: string) { setOpenMain(prev => ({ ...prev, [catId]: !prev[catId] })) } - function toggleSub(catId: string, subId: string) { const k = keyFor(catId, subId); setOpenSub(prev => ({ ...prev, [k]: !prev[k] })) } + const filteredAndSortedSkills = skills + .filter(skill => { + 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 + }) + .sort((a, b) => { + switch (sortBy) { + case 'name': + return a.name.localeCompare(b.name) + case 'users': + return (employeeCounts[b.id] || 0) - (employeeCounts[a.id] || 0) + case 'category': + const catA = categories.find(c => c.id === a.category)?.name || '' + const catB = categories.find(c => c.id === b.category)?.name || '' + return catA.localeCompare(catB) + default: + return 0 + } + }) - // Category actions - async function createCategory() { - if (!newCat.name) return - try { - const id = slugify(newCat.name) - await api.post('/skills/categories', { id, name: newCat.name }) - setNewCat({ name: '' }) - setAddingCat(false) - fetchHierarchy() - } catch { setError('Kategorie konnte nicht erstellt werden') } + const quickStats = { + total: skills.length, + categories: categories.length, + newThisWeek: Math.floor(skills.length * 0.15) } - function startEditCategory(cat: Category) { setEditingCat(cat.id); setEditCatName(cat.name) } - async function saveCategory(catId: string) { - try { - await api.put(`/skills/categories/${catId}`, { name: editCatName }) - setEditingCat(null) - fetchHierarchy() - } catch { setError('Kategorie konnte nicht aktualisiert werden') } - } - async function deleteCategory(catId: string) { - if (!confirm('Kategorie und alle Inhalte löschen?')) return - try { await api.delete(`/skills/categories/${catId}`); fetchHierarchy() } catch { setError('Kategorie konnte nicht gelöscht werden') } - } - - // Subcategory actions - function startAddSub(catId: string) { setAddingSub(prev => ({ ...prev, [catId]: true })); setNewSub(prev => ({ ...prev, [catId]: { name: '' } })) } - async function createSub(catId: string) { - const data = newSub[catId] - if (!data || !data.name) return - try { - const id = slugify(data.name) - await api.post(`/skills/categories/${catId}/subcategories`, { id, name: data.name }) - setAddingSub(prev => ({ ...prev, [catId]: false })) - fetchHierarchy() - } catch { setError('Unterkategorie konnte nicht erstellt werden') } - } - function startEditSub(catId: string, sub: Subcategory) { setEditingSub(keyFor(catId, sub.id)); setEditSubName(sub.name) } - async function saveSub(catId: string, subId: string) { - try { - await api.put(`/skills/categories/${catId}/subcategories/${subId}`, { name: editSubName }) - setEditingSub(null) - fetchHierarchy() - } catch { setError('Unterkategorie konnte nicht aktualisiert werden') } - } - async function deleteSub(catId: string, subId: string) { - if (!confirm('Unterkategorie und enthaltene Skills löschen?')) return - try { await api.delete(`/skills/categories/${catId}/subcategories/${subId}`); fetchHierarchy() } catch { setError('Unterkategorie konnte nicht gelöscht werden') } - } - - // Skill actions - function startAddSkill(catId: string, subId: string) { - const k = keyFor(catId, subId) - setAddingSkill(prev => ({ ...prev, [k]: true })) - setNewSkill(prev => ({ ...prev, [k]: { name: '', description: '' } })) - } - async function createSkill(catId: string, subId: string) { - const k = keyFor(catId, subId) - const data = newSkill[k] - if (!data || !data.name) return - try { - const id = slugify(data.name) - await api.post('/skills', { id, name: data.name, category: `${catId}.${subId}`, description: data.description || null }) - setAddingSkill(prev => ({ ...prev, [k]: false })) - fetchHierarchy() - } catch { setError('Skill konnte nicht erstellt werden') } - } - function startEditSkill(skill: Skill) { setEditingSkill(skill.id); setEditSkillData({ name: skill.name, description: skill.description || '' }) } - async function saveSkill(id: string) { - try { - await api.put(`/skills/${id}`, { name: editSkillData.name, description: editSkillData.description ?? null }) + function openSkillModal(skill?: Skill) { + if (skill) { + setEditingSkill(skill) + setSkillForm({ + name: skill.name, + description: skill.description || '', + category: skill.category || '', + tags: skill.tags || [], + requires_certification: skill.requires_certification || false, + certification_months: skill.certification_months || 0 + }) + } else { setEditingSkill(null) - fetchHierarchy() - } catch { setError('Skill konnte nicht aktualisiert werden') } + setSkillForm({ + name: '', + description: '', + category: '', + tags: [], + requires_certification: false, + certification_months: 0 + }) + } + setShowSkillModal(true) } + + async function saveSkill() { + try { + if (editingSkill) { + await api.put(`/skills/${editingSkill.id}`, { + name: skillForm.name, + description: skillForm.description, + category: skillForm.category + }) + } else { + await api.post('/skills', { + name: skillForm.name, + description: skillForm.description, + category: skillForm.category || 'technical.general' + }) + } + setShowSkillModal(false) + loadData() + } catch (e) { + setError('Skill konnte nicht gespeichert werden') + } + } + async function deleteSkill(id: string) { - if (!confirm('Skill löschen?')) return - try { await api.delete(`/skills/${id}`); fetchHierarchy() } catch { setError('Skill konnte nicht gelöscht werden') } + if (!confirm('Skill wirklich löschen?')) return + try { + await api.delete(`/skills/${id}`) + loadData() + } catch (e) { + setError('Skill konnte nicht gelöscht werden') + } + } + + function toggleSkillSelection(skillId: string) { + const newSelection = new Set(selectedSkills) + if (newSelection.has(skillId)) { + newSelection.delete(skillId) + } else { + newSelection.add(skillId) + } + setSelectedSkills(newSelection) + } + + async function bulkDelete() { + if (!confirm(`${selectedSkills.size} Skills wirklich löschen?`)) return + try { + for (const skillId of selectedSkills) { + await api.delete(`/skills/${skillId}`) + } + setSelectedSkills(new Set()) + loadData() + } catch (e) { + setError('Skills konnten nicht gelöscht werden') + } + } + + function exportSkills() { + const data = filteredAndSortedSkills.map(s => ({ + Name: s.name, + Beschreibung: s.description, + Kategorie: s.category, + Tags: s.tags?.join(', ') + })) + const csv = [ + Object.keys(data[0]).join(','), + ...data.map(row => Object.values(row).map(v => `"${v || ''}"`).join(',')) + ].join('\n') + + const blob = new Blob([csv], { type: 'text/csv' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'skills-export.csv' + a.click() + } + + 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 ( +
+
+

{skill.name}

+ toggleSkillSelection(skill.id)} + className="w-5 h-5 rounded border-gray-300" + /> +
+ +
+ {dist.beginner > 0 && ( +
+ {dist.beginner} +
+ )} + {dist.intermediate > 0 && ( +
+ {dist.intermediate} +
+ )} + {dist.expert > 0 && ( +
+ + + + {dist.expert} +
+ )} + {(dist.beginner + dist.intermediate + dist.expert) === 0 && ( + Keine Zuordnungen + )} +
+ + {skill.description && ( +

{skill.description}

+ )} + +
+ + {category?.icon} + {category?.name} + + 👥 {employeeCounts[skill.id] || skill.userCount || 0} +
+ +
+ + +
+
+ ) + } + + function CategoryView() { + return ( +
+ {categories.map(category => { + const categorySkills = filteredAndSortedSkills.filter(s => s.category === category.id) + return ( +
+
+

+ {category.icon} + {category.name} +

+ {categorySkills.length} Skills +
+ +
+ {categorySkills.slice(0, 10).map(skill => ( +
openSkillModal(skill)} + > + {skill.name} + 👥 {employeeCounts[skill.id] || skill.userCount || 0} +
+ ))} + {categorySkills.length > 10 && ( +
+ +{categorySkills.length - 10} weitere +
+ )} +
+ + +
+ ) + })} +
+ ) } return (
+ {/* Header */}
-

Skill-Verwaltung

-

Kategorien und Unterkategorien wie im Frontend (ohne Niveaus)

+

Skills Management

-
- {!addingCat ? ( - - ) : ( -
- setNewCat({ ...newCat, name: e.target.value })} /> - - -
- )} +
+ +
- + {/* Search and Stats */} +
+
+
+ setSearchQuery(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800" + /> +
+ +
- {error &&
{error}
} + {/* Quick Stats */} +
+
+ {quickStats.total} Skills • + {quickStats.categories} Kategorien • + {quickStats.newThisWeek} neue diese Woche +
+
+
- {loading ? ( -
Lade Skills...
- ) : ( - hierarchy.map((cat) => ( -
-
-
- - {editingCat === cat.id ? ( -
- setEditCatName(e.target.value)} /> - - -
- ) : ( -

{cat.name}

- )} -
-
- - - -
-
- - {addingSub[cat.id] && ( -
- setNewSub(prev => ({ ...prev, [cat.id]: { ...(prev[cat.id] || { name: '' }), name: e.target.value } }))} /> - - -
- )} - - {openMain[cat.id] && ( -
- {cat.subcategories.map((sub) => ( -
-
-
- - {editingSub === keyFor(cat.id, sub.id) ? ( -
- setEditSubName(e.target.value)} /> - - -
- ) : ( -
{sub.name}
- )} -
-
- - - -
-
- - {addingSkill[keyFor(cat.id, sub.id)] && ( -
- setNewSkill(prev => ({ ...prev, [keyFor(cat.id, sub.id)]: { ...(prev[keyFor(cat.id, sub.id)] || { name: '' }), name: e.target.value } }))} /> - setNewSkill(prev => ({ ...prev, [keyFor(cat.id, sub.id)]: { ...(prev[keyFor(cat.id, sub.id)] || { name: '' }), description: e.target.value } }))} /> - - -
- )} - - {openSub[keyFor(cat.id, sub.id)] && ( -
- {sub.skills.map((sk) => ( -
- {editingSkill === sk.id ? ( -
- setEditSkillData(prev => ({ ...prev, name: e.target.value }))} /> - setEditSkillData(prev => ({ ...prev, description: e.target.value }))} /> -
- ) : ( -
-
{sk.name}
- {sk.description &&
{sk.description}
} -
- )} -
- {editingSkill === sk.id ? ( - <> - - - - ) : ( - <> - - - - - )} -
-
- ))} - {sub.skills.length === 0 && ( -
Keine Skills in dieser Unterkategorie
- )} -
- )} -
- ))} - {cat.subcategories.length === 0 && ( -
Keine Unterkategorien
- )} -
+ {/* Filters and View Toggle */} +
+
+
+ + + {selectedSkills.size > 0 && ( + )}
- )) + +
+ + +
+
+ +
+ {categories.map(cat => ( + + ))} + + {selectedCategories.size > 0 && ( + + )} +
+ +
+ + {error && ( +
+ {error} +
+ )} + + {/* Main Content */} + {loading ? ( +
+
+

Lade Skills...

+
+ ) : ( + <> + {viewMode === 'grid' && ( +
+ {filteredAndSortedSkills.map(skill => ( + + ))} +
+ )} + + {viewMode === 'category' && } + + )} + + {/* Skill Modal */} + {showSkillModal && ( +
+
+
+
+

+ {editingSkill ? 'Skill bearbeiten' : 'Neuer Skill'} +

+ +
+ +
+
+ + setSkillForm(prev => ({ ...prev, name: 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" + placeholder="z.B. Python" + /> +
+ +
+ +