From 2689cd2d32c5d480f6dcfae576beed9264d58b24 Mon Sep 17 00:00:00 2001 From: Claude Project Manager Date: Sat, 27 Sep 2025 13:11:39 +0200 Subject: [PATCH] Zwiscshenstand - laufende Version --- CLAUDE_PROJECT_README.md | 97 +--- README.md | 26 +- admin-panel/package-lock.json | 491 ++++++++++++++++++ backend/package-lock.json | 35 ++ backend/package.json | 3 +- backend/scripts/seed-skills-from-shared.js | 134 +++++ backend/src/config/database.ts | 175 ++----- backend/src/config/secureDatabase.ts | 17 +- backend/src/index.ts | 3 + backend/src/routes/employeesSecure.ts | 48 +- backend/src/services/skillSeeder.ts | 112 ++++ debug-console.cmd | 60 --- docs/ARCHITECTURE.md | 5 + frontend/src/components/DeputyManagement.tsx | 2 +- frontend/src/components/OrganizationChart.tsx | 2 +- .../src/components/OrganizationSelector.tsx | 4 +- frontend/src/hooks/usePermissions.ts | 4 +- frontend/src/services/api.ts | 5 +- frontend/src/views/DeskBooking.tsx | 4 +- frontend/src/views/EmployeeForm.tsx | 23 +- frontend/src/views/EmployeeList.tsx | 2 +- frontend/src/views/FloorPlan.tsx | 4 +- frontend/src/views/TeamZusammenstellung.tsx | 2 +- frontend/vite.config.ts | 19 +- install-dependencies.cmd | 32 -- main.py | 67 ++- restart-services.cmd | 16 - start-dev.cmd | 28 - start-skillmate.cmd | 32 -- start-skillmate.sh | 28 - 30 files changed, 1021 insertions(+), 459 deletions(-) create mode 100644 backend/scripts/seed-skills-from-shared.js create mode 100644 backend/src/services/skillSeeder.ts delete mode 100644 debug-console.cmd delete mode 100644 install-dependencies.cmd delete mode 100644 restart-services.cmd delete mode 100644 start-dev.cmd delete mode 100644 start-skillmate.cmd delete mode 100644 start-skillmate.sh diff --git a/CLAUDE_PROJECT_README.md b/CLAUDE_PROJECT_README.md index 38eca95..4f630b1 100644 --- a/CLAUDE_PROJECT_README.md +++ b/CLAUDE_PROJECT_README.md @@ -4,10 +4,10 @@ ## Project Overview -- **Path**: `A:/GiTea/SkillMate` -- **Files**: 277 files -- **Size**: 9.7 MB -- **Last Modified**: 2025-09-23 00:39 +- **Path**: `A:\GiTea\SkillMate` +- **Files**: 191 files +- **Size**: 3.3 MB +- **Last Modified**: 2025-09-26 00:23 ## Technology Stack @@ -42,12 +42,6 @@ admin-panel/ │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts -│ ├── dist/ -│ │ ├── index.html -│ │ └── assets/ -│ │ ├── index-BRjBeEgH.css -│ │ └── index-gij1mIll.js -│ ├── public │ └── src/ │ ├── App.tsx │ ├── index.css @@ -63,9 +57,6 @@ admin-panel/ │ │ ├── SunIcon.tsx │ │ ├── SyncStatus.tsx │ │ └── UsersIcon.tsx -│ ├── data/ -│ │ ├── skillCategories.ts -│ │ └── skills.ts │ ├── services/ │ │ ├── api.ts │ │ └── networkApi.ts @@ -91,65 +82,14 @@ backend/ │ ├── mock-server.js │ ├── package-lock.json │ ├── package.json -│ ├── skillmate.dev.db │ ├── skillmate.dev.encrypted.db -│ ├── dist/ -│ │ ├── index.js -│ │ ├── index.js.map -│ │ ├── config/ -│ │ │ ├── appConfig.js -│ │ │ ├── appConfig.js.map -│ │ │ ├── database.js -│ │ │ ├── database.js.map -│ │ │ ├── secureDatabase.js -│ │ │ └── secureDatabase.js.map -│ │ ├── middleware/ -│ │ │ ├── auth.js -│ │ │ ├── auth.js.map -│ │ │ ├── errorHandler.js -│ │ │ ├── errorHandler.js.map -│ │ │ ├── roleAuth.js -│ │ │ └── roleAuth.js.map -│ │ ├── repositories/ -│ │ │ ├── employeeRepository.js -│ │ │ └── employeeRepository.js.map -│ │ ├── routes/ -│ │ │ ├── analytics.js -│ │ │ ├── analytics.js.map -│ │ │ ├── auth.js -│ │ │ ├── auth.js.map -│ │ │ ├── bookings.js -│ │ │ ├── bookings.js.map -│ │ │ ├── employees.js -│ │ │ ├── employees.js.map -│ │ │ ├── employeesSecure.js -│ │ │ └── employeesSecure.js.map -│ │ ├── services/ -│ │ │ ├── auditService.js -│ │ │ ├── auditService.js.map -│ │ │ ├── emailService.js -│ │ │ ├── emailService.js.map -│ │ │ ├── encryption.js -│ │ │ ├── encryption.js.map -│ │ │ ├── reminderService.js -│ │ │ ├── reminderService.js.map -│ │ │ ├── syncScheduler.js -│ │ │ └── syncScheduler.js.map -│ │ ├── usecases/ -│ │ │ ├── employees.js -│ │ │ ├── employees.js.map -│ │ │ ├── users.js -│ │ │ └── users.js.map -│ │ ├── utils/ -│ │ │ ├── logger.js -│ │ │ └── logger.js.map -│ │ └── validation/ -│ │ ├── employeeValidators.js -│ │ └── employeeValidators.js.map +│ ├── skillmate.dev.encrypted.db-shm +│ ├── skillmate.dev.encrypted.db-wal │ ├── logs/ │ │ ├── combined.log │ │ └── error.log │ ├── scripts/ +│ │ ├── extract-pdf.js │ │ ├── migrate-users.js │ │ ├── purge-users.js │ │ ├── reset-admin.js @@ -175,13 +115,13 @@ backend/ │ │ │ ├── analytics.ts │ │ │ ├── auth.ts │ │ │ ├── bookings.ts.disabled +│ │ │ ├── employeeOrganization.ts │ │ │ ├── employees.ts │ │ │ ├── employeesSecure.ts │ │ │ ├── network.ts │ │ │ ├── organization.ts │ │ │ ├── organizationImport.ts -│ │ │ ├── profiles.ts -│ │ │ └── settings.ts +│ │ │ └── profiles.ts │ │ ├── services/ │ │ │ ├── auditService.ts │ │ │ ├── emailService.ts @@ -197,10 +137,7 @@ backend/ │ │ └── validation/ │ │ └── employeeValidators.ts │ └── uploads/ -│ ├── photos/ -│ │ ├── 0def5f6f-c1ef-4f88-9105-600c75278f10.jpg -│ │ ├── 72c09fa1-f0a8-444c-918f-95258ca56f61.gif -│ │ └── 80c44681-d6b4-474e-8ff1-c6d02da0cd7d.gif +│ ├── photos │ └── temp docs/ │ ├── ARCHITECTURE.md @@ -216,13 +153,7 @@ frontend/ │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json -│ ├── dist/ -│ │ ├── debug.html -│ │ ├── icon.svg -│ │ ├── index.html -│ │ └── assets/ -│ │ ├── index-BUJNM8Sh.css -│ │ └── index-n0FiY1wQ.js +│ ├── vite.config.electron.ts │ ├── electron/ │ │ ├── main.js │ │ ├── preload.js @@ -244,8 +175,8 @@ frontend/ │ │ ├── OfficeMap3D.tsx │ │ ├── OfficeMapModal.tsx │ │ ├── OrganizationChart.tsx -│ │ ├── PhotoPreview.tsx -│ │ └── PhotoUpload.tsx +│ │ ├── OrganizationSelector.tsx +│ │ └── PhotoPreview.tsx │ ├── data/ │ │ └── skillCategories.ts │ ├── hooks/ @@ -312,3 +243,5 @@ This project is managed with Claude Project Manager. To work with this project: - README updated on 2025-09-21 16:48:11 - README updated on 2025-09-21 16:48:44 - README updated on 2025-09-23 19:19:20 +- README updated on 2025-09-25 22:01:37 +- README updated on 2025-09-27 12:01:06 diff --git a/README.md b/README.md index d7cc319..4cd0c45 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ SkillMate ist eine spezialisierte Anwendung zur Verwaltung von Mitarbeiterfähig ### Alle Services starten -**Windows:** `start-dev.bat` -**macOS/Linux:** `./start-dev.sh` +**Windows:** `run-dev.cmd` +**macOS/Linux:** `python3 main.py --mode dev` Dies startet: -- Backend auf http://localhost:3001 +- Backend auf http://localhost:3004 - Frontend auf http://localhost:5173 -- Admin Panel auf http://localhost:3002 +- Admin Panel auf http://localhost:5174 ### Einzelne Services @@ -57,10 +57,9 @@ cd admin-panel && npm run dev ## Production Build -### Kompletten Build erstellen +### Production Start (Windows) -**Windows:** `build-production.bat` -**macOS/Linux:** `./build-production.sh` +`run-prod.cmd` ### Ausgabe @@ -75,12 +74,11 @@ SkillMate/ ├── frontend/ # Electron Desktop App ├── backend/ # Express.js API Server ├── admin-panel/ # React Admin Interface -├── shared/ # Gemeinsame TypeScript-Typen -├── setup.bat # Windows Setup-Script -├── setup.sh # Unix Setup-Script -├── start-dev.bat # Windows Dev-Start -├── start-dev.sh # Unix Dev-Start -└── README.md # Diese Datei +├── shared/ # Gemeinsame TypeScript-Typen +├── main.py # Windows-optimierter Starter (auch für Unix nutzbar) +├── run-dev.cmd # Windows Dev-Start +├── run-prod.cmd # Windows Prod-Start +└── README.md # Diese Datei ``` ## Standard-Login @@ -133,4 +131,4 @@ Bei Fragen oder Problemen erstellen Sie bitte ein Issue im Projekt-Repository. ## Lizenz -Proprietär - Nur für den internen Gebrauch in Sicherheitsbehörden. \ No newline at end of file +Proprietär - Nur für den internen Gebrauch in Sicherheitsbehörden. diff --git a/admin-panel/package-lock.json b/admin-panel/package-lock.json index 81547c8..880b6f6 100644 --- a/admin-panel/package-lock.json +++ b/admin-panel/package-lock.json @@ -16,6 +16,7 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", "react-router-dom": "^6.20.0", + "reactflow": "^11.10.0", "zustand": "^4.4.7" }, "devDependencies": { @@ -849,6 +850,108 @@ "node": ">=14" } }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/@remix-run/router": { "version": "1.23.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", @@ -1194,6 +1297,259 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1201,6 +1557,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1517,6 +1879,12 @@ "node": ">= 6" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1601,6 +1969,111 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -2764,6 +3237,24 @@ "react-dom": ">=16.8" } }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/backend/package-lock.json b/backend/package-lock.json index 8ab1e57..e0711e1 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -26,6 +26,7 @@ "multer": "^2.0.1", "node-cron": "^4.2.1", "nodemailer": "^7.0.6", + "pdf-parse": "^1.1.1", "sqlite3": "^5.1.6", "uuid": "^9.0.1", "winston": "^3.11.0" @@ -3862,6 +3863,12 @@ "node": ">=6.0.0" } }, + "node_modules/node-ensure": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", + "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==", + "license": "MIT" + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -4096,6 +4103,34 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pdf-parse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz", + "integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==", + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "node-ensure": "^0.0.0" + }, + "engines": { + "node": ">=6.8.1" + } + }, + "node_modules/pdf-parse/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/pdf-parse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", diff --git a/backend/package.json b/backend/package.json index a97a372..bb05612 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,7 +9,8 @@ "start": "node dist/index.js", "reset-admin": "node scripts/reset-admin.js", "migrate-users": "node scripts/migrate-users.js", - "seed-skills": "node scripts/seed-skills-from-frontend.js", + "seed-skills": "node scripts/seed-skills-from-shared.js", + "seed-skills-shared": "node scripts/seed-skills-from-shared.js", "purge-users": "node scripts/purge-users.js" }, "dependencies": { diff --git a/backend/scripts/seed-skills-from-shared.js b/backend/scripts/seed-skills-from-shared.js new file mode 100644 index 0000000..1586e3a --- /dev/null +++ b/backend/scripts/seed-skills-from-shared.js @@ -0,0 +1,134 @@ +// Seed controlled vocabulary (categories/subcategories) and skills +// from shared/skills.js (SKILL_HIERARCHY) into the local SQLite DB. +// +// Usage: +// cd backend +// node scripts/seed-skills-from-shared.js +// +// Optionally set DATABASE_PATH to seed a custom DB file. + +const path = require('path') +const fs = require('fs') +const crypto = require('crypto') +const Database = require('better-sqlite3') + +function getDbPath() { + if (process.env.DATABASE_PATH && process.env.DATABASE_PATH.length > 0) { + return process.env.DATABASE_PATH + } + // default dev DB in backend folder + return path.join(__dirname, '..', 'skillmate.dev.encrypted.db') +} + +function ensureTables(db) { + // controlled_vocabulary (canonical definition from secureDatabase) + db.exec(` + CREATE TABLE IF NOT EXISTS controlled_vocabulary ( + id TEXT PRIMARY KEY, + category TEXT NOT NULL, + value TEXT NOT NULL, + description TEXT, + is_active INTEGER DEFAULT 1, + created_at TEXT NOT NULL, + UNIQUE(category, value) + ); + CREATE INDEX IF NOT EXISTS idx_vocab_category ON controlled_vocabulary(category); + CREATE INDEX IF NOT EXISTS idx_vocab_value ON controlled_vocabulary(value); + `) + + // skills (canonical definition from secureDatabase) + db.exec(` + CREATE TABLE IF NOT EXISTS skills ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + category TEXT NOT NULL, + description TEXT, + requires_certification INTEGER DEFAULT 0, + expires_after INTEGER + ) + `) +} + +function loadHierarchy() { + // Load from shared/skills.js + const skillsPath = path.join(__dirname, '..', '..', 'shared', 'skills.js') + if (!fs.existsSync(skillsPath)) { + throw new Error('shared/skills.js not found') + } + const mod = require(skillsPath) + if (!mod || !Array.isArray(mod.SKILL_HIERARCHY)) { + throw new Error('SKILL_HIERARCHY missing or invalid in shared/skills.js') + } + return mod.SKILL_HIERARCHY +} + +function seed(db, hierarchy) { + const now = new Date().toISOString() + + const insertVocab = db.prepare(` + INSERT OR IGNORE INTO controlled_vocabulary (id, category, value, description, is_active, created_at) + VALUES (?, ?, ?, ?, ?, ?) + `) + + const insertSkill = db.prepare(` + INSERT OR IGNORE INTO skills (id, name, category, description, requires_certification, expires_after) + VALUES (?, ?, ?, ?, ?, ?) + `) + + let catCount = 0 + let subCount = 0 + let skillCount = 0 + + for (const cat of hierarchy) { + const catId = String(cat.id) + const catName = String(cat.name || cat.id) + + // category + const catPk = crypto.randomUUID() + const catRes = insertVocab.run(catPk, 'skill_category', catId, catName, 1, now) + if (catRes.changes > 0) catCount++ + + for (const sub of (cat.subcategories || [])) { + const subId = String(sub.id) + const subName = String(sub.name || sub.id) + const key = `${catId}.${subId}` + + // subcategory + const subPk = crypto.randomUUID() + const subRes = insertVocab.run(subPk, 'skill_subcategory', key, subName, 1, now) + if (subRes.changes > 0) subCount++ + + for (const sk of (sub.skills || [])) { + const sId = `${key}.${sk.id}` + const sName = String(sk.name || sk.id) + const requires = (catId === 'certifications' || subId === 'weapons') ? 1 : 0 + const expires = (catId === 'certifications') ? 36 : null + const sRes = insertSkill.run(sId, sName, key, null, requires, expires) + if (sRes.changes > 0) skillCount++ + } + } + } + + return { catCount, subCount, skillCount } +} + +function main() { + const dbPath = getDbPath() + console.log('➡️ Seeding skills into DB:', dbPath) + const db = new Database(dbPath) + try { + ensureTables(db) + const hierarchy = loadHierarchy() + const { catCount, subCount, skillCount } = seed(db, hierarchy) + console.log(`✅ Seeding completed. Inserted: ${catCount} categories, ${subCount} subcategories, ${skillCount} skills.`) + console.log('ℹ️ Re-open the Admin Panel to see Skills in hierarchy view.') + } catch (err) { + console.error('❌ Seeding failed:', err.message) + process.exitCode = 1 + } finally { + try { db.close() } catch {} + } +} + +main() + diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts index d122d5a..d0b1514 100644 --- a/backend/src/config/database.ts +++ b/backend/src/config/database.ts @@ -9,22 +9,30 @@ export { initializeSecureDatabase } from './secureDatabase' // Export the secure database instance export const db = secureDb +// IMPORTANT: secureDatabase.ts owns all security‑sensitive base tables +// (users, employees, skills and their junctions, language_skills, specializations, +// controlled_vocabulary, system_settings, security_audit_log) including +// encrypted fields and indexes. This module defines only extended, non‑sensitive +// schemas (profiles, workspaces/bookings, organizational structure, reminders, analytics). + export function initializeDatabase() { - // Users table - db.exec(` - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - email TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - role TEXT NOT NULL CHECK(role IN ('admin', 'superuser', 'user')), - employee_id TEXT, - last_login TEXT, - is_active INTEGER DEFAULT 1, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ) - `) + // Lightweight schema guard (no mutations): warn if legacy/plain schemas are present + try { + const usersCols: any[] = db.prepare(`PRAGMA table_info(users)`).all() as any + const hasEmailHash = usersCols.some(c => c.name === 'email_hash') + if (!hasEmailHash) { + console.warn('[DB-Guard] users.email_hash missing. Ensure initializeSecureDatabase() runs first and legacy data is migrated.') + } + } catch {} + + try { + const empCols: any[] = db.prepare(`PRAGMA table_info(employees)`).all() as any + const hasEmpEmailHash = empCols.some(c => c.name === 'email_hash') + const hasEmpPhoneHash = empCols.some(c => c.name === 'phone_hash') + if (!hasEmpEmailHash || !hasEmpPhoneHash) { + console.warn('[DB-Guard] employees hash columns missing. Ensure secure schema initialized and data migrated.') + } + } catch {} // Profiles table (erweitert für Yellow Pages) db.exec(` @@ -68,57 +76,7 @@ export function initializeDatabase() { ON profiles(review_due_at); `) - // Employees table (für Kompatibilität beibehalten) - db.exec(` - CREATE TABLE IF NOT EXISTS employees ( - id TEXT PRIMARY KEY, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - employee_number TEXT UNIQUE NOT NULL, - photo TEXT, - position TEXT NOT NULL, - department TEXT NOT NULL, - email TEXT NOT NULL, - phone TEXT NOT NULL, - mobile TEXT, - office TEXT, - availability TEXT NOT NULL, - clearance_level TEXT, - clearance_valid_until TEXT, - clearance_issued_date TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - created_by TEXT NOT NULL, - updated_by TEXT - ) - `) - - // Skills table - db.exec(` - CREATE TABLE IF NOT EXISTS skills ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - category TEXT NOT NULL, - description TEXT, - requires_certification INTEGER DEFAULT 0, - expires_after INTEGER - ) - `) - - // Employee skills junction table - db.exec(` - CREATE TABLE IF NOT EXISTS employee_skills ( - employee_id TEXT NOT NULL, - skill_id TEXT NOT NULL, - level TEXT, - verified INTEGER DEFAULT 0, - verified_by TEXT, - verified_date TEXT, - PRIMARY KEY (employee_id, skill_id), - FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE, - FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE - ) - `) + // Employees/Skills tables are defined in secureDatabase (encrypted & indexed) // Profile Kompetenzen (Arrays als separate Tabellen) db.exec(` @@ -210,62 +168,9 @@ export function initializeDatabase() { ); `) - // Language skills table (für Kompatibilität) - db.exec(` - CREATE TABLE IF NOT EXISTS language_skills ( - id TEXT PRIMARY KEY, - employee_id TEXT NOT NULL, - language TEXT NOT NULL, - proficiency TEXT NOT NULL, - certified INTEGER DEFAULT 0, - certificate_type TEXT, - is_native INTEGER DEFAULT 0, - can_interpret INTEGER DEFAULT 0, - FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE - ) - `) + // Language skills / Specializations are defined in secureDatabase - // Specializations table - db.exec(` - CREATE TABLE IF NOT EXISTS specializations ( - id TEXT PRIMARY KEY, - employee_id TEXT NOT NULL, - name TEXT NOT NULL, - FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE - ) - `) - - // Sync log table - db.exec(` - CREATE TABLE IF NOT EXISTS sync_log ( - id TEXT PRIMARY KEY, - sync_time TEXT NOT NULL, - success INTEGER NOT NULL, - items_synced INTEGER, - error_message TEXT, - duration INTEGER - ) - `) - - // Create default admin user if not exists - const adminExists = db.prepare('SELECT id FROM users WHERE username = ?').get('admin') - if (!adminExists) { - const hashedPassword = bcrypt.hashSync('admin123', 10) - const now = new Date().toISOString() - db.prepare(` - INSERT INTO users (id, username, email, password, role, is_active, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `).run( - uuidv4(), - 'admin', - 'admin@skillmate.local', - hashedPassword, - 'admin', - 1, - now, - now - ) - } + // Sync tables are owned by SyncService initializer (avoid duplicate legacy schema) // Workspace Management Tables @@ -432,9 +337,12 @@ export function initializeDatabase() { FOREIGN KEY (unit_id) REFERENCES organizational_units(id) ON DELETE CASCADE, UNIQUE(employee_id, unit_id, role) ); - + CREATE INDEX IF NOT EXISTS idx_emp_units_employee ON employee_unit_assignments(employee_id); CREATE INDEX IF NOT EXISTS idx_emp_units_unit ON employee_unit_assignments(unit_id); + CREATE UNIQUE INDEX IF NOT EXISTS idx_emp_units_primary_unique + ON employee_unit_assignments(employee_id) + WHERE is_primary = 1; `) // Special Positions (Personalrat, Beauftragte, etc.) @@ -538,29 +446,8 @@ export function initializeDatabase() { CREATE INDEX IF NOT EXISTS idx_reminders_sent ON reminders(sent_at); `) - // Kontrollierte Vokabulare/Tags + // Create indexes for better performance (only for tables defined here) db.exec(` - CREATE TABLE IF NOT EXISTS controlled_vocabulary ( - id TEXT PRIMARY KEY, - category TEXT NOT NULL, - value TEXT NOT NULL, - description TEXT, - is_active INTEGER DEFAULT 1, - created_at TEXT NOT NULL, - UNIQUE(category, value) - ); - - CREATE INDEX IF NOT EXISTS idx_vocab_category ON controlled_vocabulary(category); - `) - - // Create indexes for better performance - db.exec(` - CREATE INDEX IF NOT EXISTS idx_employees_availability ON employees(availability); - CREATE INDEX IF NOT EXISTS idx_employees_department ON employees(department); - CREATE INDEX IF NOT EXISTS idx_employee_skills_employee ON employee_skills(employee_id); - CREATE INDEX IF NOT EXISTS idx_employee_skills_skill ON employee_skills(skill_id); - CREATE INDEX IF NOT EXISTS idx_language_skills_employee ON language_skills(employee_id); - CREATE INDEX IF NOT EXISTS idx_specializations_employee ON specializations(employee_id); CREATE INDEX IF NOT EXISTS idx_bookings_workspace ON bookings(workspace_id); CREATE INDEX IF NOT EXISTS idx_bookings_user ON bookings(user_id); CREATE INDEX IF NOT EXISTS idx_bookings_start_time ON bookings(start_time); diff --git a/backend/src/config/secureDatabase.ts b/backend/src/config/secureDatabase.ts index 6f63b31..d7eb48f 100644 --- a/backend/src/config/secureDatabase.ts +++ b/backend/src/config/secureDatabase.ts @@ -80,17 +80,19 @@ export const encryptedDb = { email_hash: employee.email ? FieldEncryption.hash(employee.email) : null, phone_hash: employee.phone ? FieldEncryption.hash(employee.phone) : null } - + return db.prepare(` INSERT INTO employees ( id, first_name, last_name, employee_number, photo, position, department, email, email_hash, phone, phone_hash, mobile, office, availability, clearance_level, clearance_valid_until, clearance_issued_date, + primary_unit_id, created_at, updated_at, created_by ) VALUES ( @id, @first_name, @last_name, @employee_number, @photo, @position, @department, @email, @email_hash, @phone, @phone_hash, @mobile, @office, @availability, @clearance_level, @clearance_valid_until, @clearance_issued_date, + @primary_unit_id, @created_at, @updated_at, @created_by ) `).run(encrypted) @@ -173,12 +175,24 @@ export function initializeSecureDatabase() { clearance_level TEXT, clearance_valid_until TEXT, clearance_issued_date TEXT, + primary_unit_id TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, created_by TEXT NOT NULL, updated_by TEXT ) `) + + // Add primary_unit_id column if missing (legacy DBs) + try { + const cols: any[] = db.prepare(`PRAGMA table_info(employees)`).all() as any + const hasPrimaryUnitId = cols.some(c => c.name === 'primary_unit_id') + if (!hasPrimaryUnitId) { + db.exec(`ALTER TABLE employees ADD COLUMN primary_unit_id TEXT`) + } + } catch (e) { + // ignore + } // Add indexes for hash fields db.exec(` @@ -309,6 +323,7 @@ export function initializeSecureDatabase() { db.exec(` CREATE INDEX IF NOT EXISTS idx_employees_availability ON employees(availability); CREATE INDEX IF NOT EXISTS idx_employees_department ON employees(department); + CREATE INDEX IF NOT EXISTS idx_employees_primary_unit ON employees(primary_unit_id); CREATE INDEX IF NOT EXISTS idx_employee_skills_employee ON employee_skills(employee_id); CREATE INDEX IF NOT EXISTS idx_employee_skills_skill ON employee_skills(skill_id); CREATE INDEX IF NOT EXISTS idx_language_skills_employee ON language_skills(employee_id); diff --git a/backend/src/index.ts b/backend/src/index.ts index 6a8336a..06714a6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -24,6 +24,7 @@ import employeeOrganizationRoutes from './routes/employeeOrganization' import { errorHandler } from './middleware/errorHandler' import { logger } from './utils/logger' import { syncScheduler } from './services/syncScheduler' +import { ensureSkillsSeeded } from './services/skillSeeder' dotenv.config() @@ -33,6 +34,8 @@ const PORT = process.env.PORT || 3004 // Initialize secure database (core tables) and extended schema (organization, deputies, etc.) initializeSecureDatabase() initializeDatabase() +// Ensure skills/categories exist for Admin Skill Management (idempotent) +ensureSkillsSeeded() // Initialize sync scheduler syncScheduler diff --git a/backend/src/routes/employeesSecure.ts b/backend/src/routes/employeesSecure.ts index f311bbe..d7578ac 100644 --- a/backend/src/routes/employeesSecure.ts +++ b/backend/src/routes/employeesSecure.ts @@ -66,7 +66,32 @@ function logSecurityAudit( // Get all employees router.get('/', authenticate, requirePermission('employees:read'), async (req: AuthRequest, res, next) => { try { - const employees = encryptedDb.getAllEmployees() + const { unitId, includeDescendants } = (req.query || {}) as any + + // Optional filter by unit (including descendants) + let allowedIds: Set | null = null + if (unitId) { + const collectDescendants = (rootId: string): string[] => { + const ids: string[] = [rootId] + const stack: string[] = [rootId] + while (stack.length) { + const curr = stack.pop()! + const children = db.prepare('SELECT id FROM organizational_units WHERE parent_id = ?').all(curr) as any[] + for (const ch of children) { + ids.push(ch.id) + stack.push(ch.id) + } + } + return ids + } + const unitIds = includeDescendants === '1' || includeDescendants === 'true' + ? collectDescendants(String(unitId)) + : [String(unitId)] + const rows = db.prepare(`SELECT id FROM employees WHERE primary_unit_id IN (${unitIds.map(() => '?').join(',')})`).all(...unitIds) as any[] + allowedIds = new Set(rows.map(r => r.id)) + } + + const employees = encryptedDb.getAllEmployees().filter((e: any) => !allowedIds || allowedIds.has(e.id)) const employeesWithDetails = employees.map((emp: any) => { // Get skills @@ -331,7 +356,8 @@ router.post('/', const { firstName, lastName, employeeNumber, photo, position = 'Mitarbeiter', department, email, phone = 'Nicht angegeben', mobile, office, availability = 'available', - clearance, skills = [], languages = [], specializations = [], userRole, createUser + clearance, skills = [], languages = [], specializations = [], userRole, createUser, + primaryUnitId, assignmentRole } = req.body // Generate employee number if not provided @@ -363,11 +389,28 @@ router.post('/', clearance_level: clearance?.level || null, clearance_valid_until: clearance?.validUntil || null, clearance_issued_date: clearance?.issuedDate || null, + primary_unit_id: primaryUnitId || null, created_at: now, updated_at: now, created_by: req.user!.id }) + // Create primary assignment if provided + if (primaryUnitId) { + const unit = db.prepare('SELECT id, type FROM organizational_units WHERE id = ?').get(primaryUnitId) + if (!unit) { + return res.status(400).json({ success: false, error: { message: 'Invalid primary unit' } }) + } + db.prepare(` + INSERT INTO employee_unit_assignments ( + id, employee_id, unit_id, role, start_date, end_date, is_primary, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + uuidv4(), employeeId, primaryUnitId, assignmentRole || 'mitarbeiter', + now, null, 1, now, now + ) + } + // Insert skills (only if they exist in skills table) if (skills && skills.length > 0) { const insertSkill = db.prepare(` @@ -477,6 +520,7 @@ router.post('/', office: office || null, availability, clearance, + primaryUnitId: primaryUnitId || null, skills, languages, specializations, diff --git a/backend/src/services/skillSeeder.ts b/backend/src/services/skillSeeder.ts new file mode 100644 index 0000000..da80138 --- /dev/null +++ b/backend/src/services/skillSeeder.ts @@ -0,0 +1,112 @@ +import path from 'path' +import { db } from '../config/secureDatabase' +import { logger } from '../utils/logger' + +type SkillHierarchy = Array<{ + id: string + name: string + subcategories?: Array<{ + id: string + name: string + skills?: Array<{ id: string; name: string }> + }> +}> + +function loadSharedHierarchy(): SkillHierarchy | null { + try { + const sharedPath = path.join(__dirname, '../../../shared/skills.js') + // eslint-disable-next-line @typescript-eslint/no-var-requires + const mod = require(sharedPath) + if (mod && Array.isArray(mod.SKILL_HIERARCHY)) { + return mod.SKILL_HIERARCHY as SkillHierarchy + } + } catch (err) { + logger.warn('SkillSeeder: Could not load shared/skills.js. Skipping seeding.') + } + return null +} + +export function ensureSkillsSeeded() { + try { + // Always ensure vocabulary tables exist (idempotent) + db.exec(` + CREATE TABLE IF NOT EXISTS controlled_vocabulary ( + id TEXT PRIMARY KEY, + category TEXT NOT NULL, + value TEXT NOT NULL, + description TEXT, + is_active INTEGER DEFAULT 1, + created_at TEXT NOT NULL, + UNIQUE(category, value) + ); + CREATE INDEX IF NOT EXISTS idx_vocab_category ON controlled_vocabulary(category); + CREATE INDEX IF NOT EXISTS idx_vocab_value ON controlled_vocabulary(value); + `) + + db.exec(` + CREATE TABLE IF NOT EXISTS skills ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + category TEXT NOT NULL, + description TEXT, + requires_certification INTEGER DEFAULT 0, + expires_after INTEGER + ) + `) + + const hierarchy = loadSharedHierarchy() + if (!hierarchy) return + + const now = new Date().toISOString() + const insertVocab = db.prepare(` + INSERT OR IGNORE INTO controlled_vocabulary (id, category, value, description, is_active, created_at) + VALUES (?, ?, ?, ?, ?, ?) + `) + const insertSkill = db.prepare(` + INSERT OR IGNORE INTO skills (id, name, category, description, requires_certification, expires_after) + VALUES (?, ?, ?, ?, ?, ?) + `) + + let cats = 0, subs = 0, skills = 0 + + for (const cat of hierarchy) { + const catId = String(cat.id) + const catName = String(cat.name || cat.id) + + // category + insertVocab.run(cryptoRandomUUID(), 'skill_category', catId, catName, 1, now) + cats++ + + for (const sub of (cat.subcategories || [])) { + const subId = String(sub.id) + const subName = String(sub.name || sub.id) + const key = `${catId}.${subId}` + insertVocab.run(cryptoRandomUUID(), 'skill_subcategory', key, subName, 1, now) + subs++ + + for (const sk of (sub.skills || [])) { + const sId = `${key}.${sk.id}` + const sName = String(sk.name || sk.id) + const requires = (catId === 'certifications' || subId === 'weapons') ? 1 : 0 + const expires = (catId === 'certifications') ? 36 : null + insertSkill.run(sId, sName, key, null, requires, expires) + skills++ + } + } + } + + logger.info(`SkillSeeder: ensured ${cats} categories, ${subs} subcategories, ${skills} skills (idempotent).`) + } catch (err) { + logger.warn('SkillSeeder failed (non-fatal): ' + (err as Error).message) + } +} + +function cryptoRandomUUID() { + // Node 18 has crypto.randomUUID, but keep a tiny fallback + try { return (require('crypto').randomUUID() as string) } catch {} + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8) + return v.toString(16) + }) +} + diff --git a/debug-console.cmd b/debug-console.cmd deleted file mode 100644 index e4bc526..0000000 --- a/debug-console.cmd +++ /dev/null @@ -1,60 +0,0 @@ -@echo off -title SkillMate Debug Console - Alle Services -color 0E -echo. -echo ================================================ -echo SkillMate Debug Console - Alle Services -echo Backend mit vollstaendigem Debug-Log -echo + Frontend + Admin Panel -echo ================================================ -echo. -echo Starting all SkillMate services... -echo Zeit: %DATE% %TIME% -echo. - -REM Start Frontend -echo Starting Frontend on port 5173... -start "Frontend" cmd /k "cd frontend && npm run dev" - -REM Wait a bit -timeout /t 2 /nobreak > nul - -REM Start Admin Panel -echo Starting Admin Panel on port 3006... -start "Admin Panel" cmd /k "cd admin-panel && npm run dev" - -REM Wait a bit more -timeout /t 3 /nobreak > nul - -REM Open browsers after services start -echo Opening browsers... -timeout /t 5 /nobreak > nul -start http://localhost:3006 -timeout /t 2 /nobreak > nul -start http://localhost:5173 - -echo. -echo ================================================ -echo Backend Debug Log startet jetzt... -echo Alle API-Requests werden hier angezeigt -echo ================================================ -echo. - -cd backend -set PORT=3005 -set FIELD_ENCRYPTION_KEY=dev_field_key_change_in_production_32chars_min! -set NODE_ENV=development -set DEBUG=* - -echo Backend laeuft auf http://localhost:3005 -echo Frontend laeuft auf http://localhost:5173 -echo Admin Panel laeuft auf http://localhost:3006 -echo. -echo === DEBUG LOG === -echo. - -node full-backend-3005.js - -echo. -echo Backend wurde beendet. Druecken Sie eine Taste zum Schliessen... -pause \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 654d316..74f5ce7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,6 +7,11 @@ Layers - Adapters/HTTP: `backend/src/routes/*` map HTTP <-> use-cases and validate inputs. - Infra/Services: encryption, email, sync components, logger. +Database Ownership +- Canonical core schema lives in `backend/src/config/secureDatabase.ts` (users, employees, skills + junctions, language_skills, specializations, controlled_vocabulary, system_settings, security_audit_log). These include field encryption and hash indexes. +- Extended, non-sensitive schema lives in `backend/src/config/database.ts` (profiles + facets, workspaces/bookings, analytics, organizational structure, deputies, reminders, audit_log). +- Initialization order: `initializeSecureDatabase()` runs before `initializeDatabase()`. Routes import `db` from either module, but both resolve to the same secure DB instance. + Security - JWT required; in production `JWT_SECRET` must be set. - Field-level encryption (AES) for sensitive data + deterministic hashes for lookups. diff --git a/frontend/src/components/DeputyManagement.tsx b/frontend/src/components/DeputyManagement.tsx index 423f18c..4db532f 100644 --- a/frontend/src/components/DeputyManagement.tsx +++ b/frontend/src/components/DeputyManagement.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import api from '../services/api' -import { DeputyAssignment, Employee } from '@skillmate/shared' +import type { DeputyAssignment, Employee } from '@skillmate/shared' import { useAuthStore } from '../stores/authStore' export default function DeputyManagement() { diff --git a/frontend/src/components/OrganizationChart.tsx b/frontend/src/components/OrganizationChart.tsx index bd0c8f7..1a9f99f 100644 --- a/frontend/src/components/OrganizationChart.tsx +++ b/frontend/src/components/OrganizationChart.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, useCallback } from 'react' -import { OrganizationalUnit, EmployeeUnitAssignment } from '@skillmate/shared' +import type { OrganizationalUnit, EmployeeUnitAssignment } from '@skillmate/shared' import api from '../services/api' import { useAuthStore } from '../stores/authStore' diff --git a/frontend/src/components/OrganizationSelector.tsx b/frontend/src/components/OrganizationSelector.tsx index 13ba3ce..ca24cbd 100644 --- a/frontend/src/components/OrganizationSelector.tsx +++ b/frontend/src/components/OrganizationSelector.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { OrganizationalUnit } from '@skillmate/shared' +import type { OrganizationalUnit } from '@skillmate/shared' import api from '../services/api' import { ChevronRight, Building2, Search, X } from 'lucide-react' @@ -277,4 +277,4 @@ export default function OrganizationSelector({ value, onChange, disabled }: Orga )} ) -} \ No newline at end of file +} diff --git a/frontend/src/hooks/usePermissions.ts b/frontend/src/hooks/usePermissions.ts index f99ccc0..51ddd5e 100644 --- a/frontend/src/hooks/usePermissions.ts +++ b/frontend/src/hooks/usePermissions.ts @@ -1,4 +1,4 @@ -import { UserRole } from '@skillmate/shared' +import type { UserRole } from '@skillmate/shared' import { useAuthStore } from '../stores/authStore' // Define role permissions directly to avoid import issues @@ -94,4 +94,4 @@ export function usePermissions() { user, isAuthenticated } -} \ No newline at end of file +} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index dc909f4..27e68fc 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { Employee } from '@skillmate/shared' +import type { Employee } from '@skillmate/shared' const API_BASE_URL = (import.meta as any).env?.VITE_API_URL || 'http://localhost:3004/api' @@ -31,7 +31,8 @@ export const authApi = { export const employeeApi = { getAll: async () => { - const response = await api.get('/employees/public') + // Hole vollständige Liste (berechtigt durch employees:read) + const response = await api.get('/employees') return response.data.data }, getById: async (id: string) => { diff --git a/frontend/src/views/DeskBooking.tsx b/frontend/src/views/DeskBooking.tsx index a6f77f6..e4b9e54 100644 --- a/frontend/src/views/DeskBooking.tsx +++ b/frontend/src/views/DeskBooking.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { api } from '../services/api' -import { Workspace, Booking, WorkspaceFilter } from '@skillmate/shared' +import type { Workspace, Booking, WorkspaceFilter } from '@skillmate/shared' export default function DeskBooking() { const navigate = useNavigate() @@ -257,4 +257,4 @@ export default function DeskBooking() { ) -} \ No newline at end of file +} diff --git a/frontend/src/views/EmployeeForm.tsx b/frontend/src/views/EmployeeForm.tsx index 9068891..abae209 100644 --- a/frontend/src/views/EmployeeForm.tsx +++ b/frontend/src/views/EmployeeForm.tsx @@ -4,6 +4,7 @@ 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() @@ -28,6 +29,8 @@ export default function EmployeeForm() { languages: [] as string[], specializations: [] as string[] }) + const [primaryUnitId, setPrimaryUnitId] = useState(null) + const [primaryUnitName, setPrimaryUnitName] = useState('') const [employeePhoto, setEmployeePhoto] = useState(null) const [photoFile, setPhotoFile] = useState(null) @@ -178,6 +181,7 @@ export default function EmployeeForm() { 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 @@ -220,7 +224,7 @@ export default function EmployeeForm() { createdBy: 'admin' } - const result = await employeeApi.create(newEmployee) + const result = await employeeApi.create({ ...newEmployee, primaryUnitId }) const newEmployeeId = result.data.id // Upload photo if we have one @@ -407,6 +411,23 @@ export default function EmployeeForm() { )} +
+ + { + setPrimaryUnitId(unitId) + setPrimaryUnitName(unitName) + }} + /> +

{primaryUnitName || 'Bitte auswählen'}

+ {validationErrors.primaryUnitId && ( +

{validationErrors.primaryUnitId}

+ )} +
+
) -} \ No newline at end of file +} diff --git a/frontend/src/views/TeamZusammenstellung.tsx b/frontend/src/views/TeamZusammenstellung.tsx index 8959f87..274f4f3 100644 --- a/frontend/src/views/TeamZusammenstellung.tsx +++ b/frontend/src/views/TeamZusammenstellung.tsx @@ -962,4 +962,4 @@ export default function TeamZusammenstellung() { )} ) -} \ No newline at end of file +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4761d47..d63849c 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,16 +11,33 @@ export default defineConfig({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 'process.platform': JSON.stringify('win32') }, + server: { + port: 5173, + strictPort: true, + fs: { + // erlaubt Zugriff auf Monorepo-Nachbarordner (z. B. ../shared) + allow: ['..'] + } + }, resolve: { alias: { '@': path.resolve(__dirname, './src'), }, + // erhöht Kompatibilität mit verlinkten/local deps + preserveSymlinks: true, + }, + optimizeDeps: { + include: ['@skillmate/shared'], }, build: { outDir: 'dist', emptyOutDir: true, + commonjsOptions: { + include: [/node_modules/, /@skillmate\/shared/], + transformMixedEsModules: true, + }, rollupOptions: { external: ['electron'], } }, -}) \ No newline at end of file +}) diff --git a/install-dependencies.cmd b/install-dependencies.cmd deleted file mode 100644 index 35ffad6..0000000 --- a/install-dependencies.cmd +++ /dev/null @@ -1,32 +0,0 @@ -@echo off -echo Installing all dependencies... - -echo. -echo [1/4] Installing root dependencies... -call npm install - -echo. -echo [2/4] Installing backend dependencies... -cd backend -call npm install -cd .. - -echo. -echo [3/4] Installing frontend dependencies... -cd frontend -call npm install -cd .. - -echo. -echo [4/4] Installing admin-panel dependencies... -if exist admin-panel ( - cd admin-panel - call npm install - cd .. -) - -echo. -echo ✅ All dependencies installed! -echo. -echo You can now run: start-dev.cmd -pause \ No newline at end of file diff --git a/main.py b/main.py index 424a7a2..fcd0e15 100644 --- a/main.py +++ b/main.py @@ -29,7 +29,62 @@ class SkillMateStarter: if sys.platform != 'win32': signal.signal(signal.SIGINT, self.signal_handler) signal.signal(signal.SIGTERM, self.signal_handler) - + + def _has_local_binary(self, project_dir: Path, binary: str) -> bool: + """Prüft, ob ein lokales NPM-Binary vorhanden ist (Windows .cmd / Unix ohne Endung).""" + bin_dir = project_dir / 'node_modules' / '.bin' + if sys.platform == 'win32': + return (bin_dir / f'{binary}.cmd').exists() or (bin_dir / f'{binary}.CMD').exists() + else: + return (bin_dir / binary).exists() + + def _ensure_dependencies(self) -> bool: + """Installiert fehlende Node-Abhängigkeiten in backend, frontend, admin-panel. + + Rückgabe: True bei Erfolg, False wenn Installation irreparabel fehlschlägt. + """ + projects = [ + (self.base_dir / 'backend', ['nodemon', 'ts-node']), + (self.base_dir / 'frontend', ['vite']), + (self.base_dir / 'admin-panel', ['vite']) + ] + + any_install = False + for proj_dir, required_bins in projects: + if not proj_dir.exists(): + continue + + needs_install = not (proj_dir / 'node_modules').exists() + if not needs_install: + # Prüfe konkrete Binaries + for b in required_bins: + if not self._has_local_binary(proj_dir, b): + needs_install = True + break + + if needs_install: + any_install = True + print(f"🔧 Installiere Abhängigkeiten in {proj_dir.name}...") + # Bevorzugt npm ci, fallback auf npm install + try: + cmd_ci = ["cmd", "/c", "npm ci"] if sys.platform == 'win32' else ["npm", "ci"] + result = subprocess.run(cmd_ci, cwd=str(proj_dir), check=False) + if result.returncode != 0: + cmd_install = ["cmd", "/c", "npm install"] if sys.platform == 'win32' else ["npm", "install"] + result = subprocess.run(cmd_install, cwd=str(proj_dir), check=False) + if result.returncode != 0: + print(f"❌ Konnte Abhängigkeiten in {proj_dir.name} nicht installieren. Prüfen Sie Internet/Proxy.") + return False + except Exception as e: + print(f"❌ Fehler bei der Installation in {proj_dir.name}: {e}") + return False + + if any_install: + print("✅ Abhängigkeiten installiert. Fahre mit dem Start fort...\n") + else: + print("✅ Abhängigkeiten geprüft – alles vorhanden.\n") + return True + def signal_handler(self, signum, frame): """Handle Strg+C und andere Signale""" print("\n\n🛑 SkillMate wird beendet...") @@ -121,6 +176,12 @@ class SkillMateStarter: for p in [self.backend_port, self.frontend_port, self.admin_port]: self._free_port_windows(p) + # Abhängigkeiten sicherstellen, bevor Services gestartet werden + print(" Prüfe/Installiere Abhängigkeiten...") + if not self._ensure_dependencies(): + print("❌ Abbruch: Abhängigkeiten konnten nicht installiert werden.") + return + # Backend backend_dir = self.base_dir / "backend" if self.mode == 'dev': @@ -136,7 +197,7 @@ class SkillMateStarter: # Frontend frontend_dir = self.base_dir / "frontend" if self.mode == 'dev': - self._popen_new_console(frontend_dir, f"npm run dev -- --port {self.frontend_port}") + self._popen_new_console(frontend_dir, f"npm run dev -- --strictPort --port {self.frontend_port}") else: self._popen_new_console(frontend_dir, f"cmd /c npm run build && npm run preview -- --port {self.frontend_port}") @@ -144,7 +205,7 @@ class SkillMateStarter: admin_dir = self.base_dir / "admin-panel" if admin_dir.exists(): if self.mode == 'dev': - self._popen_new_console(admin_dir, f"npm run dev -- --port {self.admin_port}") + self._popen_new_console(admin_dir, f"npm run dev -- --strictPort --port {self.admin_port}") else: self._popen_new_console(admin_dir, f"cmd /c npm run build && npm run preview -- --port {self.admin_port}") diff --git a/restart-services.cmd b/restart-services.cmd deleted file mode 100644 index 08e6d92..0000000 --- a/restart-services.cmd +++ /dev/null @@ -1,16 +0,0 @@ -@echo off -echo Restarting all services... - -echo. -echo Killing existing Node processes... -taskkill /F /IM node.exe 2>nul - -echo. -echo Rebuilding shared module... -cd shared -call npm run build -cd .. - -echo. -echo Starting services... -call start-dev.cmd \ No newline at end of file diff --git a/start-dev.cmd b/start-dev.cmd deleted file mode 100644 index 36e19bd..0000000 --- a/start-dev.cmd +++ /dev/null @@ -1,28 +0,0 @@ -@echo off -echo Starting SkillMate in development mode... - -REM Start Backend Debug Console (kontinuierliche Log-Ausgabe) -start debug-console.cmd - -REM Wait a bit -timeout /t 3 /nobreak > nul - -REM Start Frontend -start "Frontend" cmd /k "cd frontend && npm run dev" - -REM Start Admin Panel -start "Admin Panel" cmd /k "cd admin-panel && npm run dev" - -echo. -echo SkillMate is starting... -echo. -echo Backend: http://localhost:3005 -echo Frontend: http://localhost:5173 -echo Admin: http://localhost:3006 -echo. -echo Opening Admin Panel and Frontend in browser... -timeout /t 5 /nobreak > nul - -start http://localhost:3006 -timeout /t 2 /nobreak > nul -start http://localhost:5173 \ No newline at end of file diff --git a/start-skillmate.cmd b/start-skillmate.cmd deleted file mode 100644 index 98a6030..0000000 --- a/start-skillmate.cmd +++ /dev/null @@ -1,32 +0,0 @@ -@echo off -REM SkillMate Starter für Windows - -REM Prüfe ob Python installiert ist -python --version >nul 2>&1 -if %errorlevel% neq 0 ( - echo ❌ Python ist nicht installiert! - echo Bitte installieren Sie Python 3.8 oder höher von https://python.org - pause - exit /b 1 -) - -REM Gehe zum Projektverzeichnis -cd /d "%~dp0" - -REM Installiere Python-Abhängigkeiten falls nötig -if not exist "venv" ( - echo 🔧 Erstelle virtuelle Umgebung... - python -m venv venv -) - -REM Aktiviere virtuelle Umgebung -call venv\Scripts\activate.bat - -REM Installiere Abhängigkeiten -pip install -r requirements.txt >nul 2>&1 - -REM Starte SkillMate -echo 🚀 Starte SkillMate... -python main.py - -pause \ No newline at end of file diff --git a/start-skillmate.sh b/start-skillmate.sh deleted file mode 100644 index ff8ae50..0000000 --- a/start-skillmate.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -# SkillMate Starter für Linux/Mac - -# Prüfe ob Python 3 installiert ist -if ! command -v python3 &> /dev/null; then - echo "❌ Python 3 ist nicht installiert!" - echo " Bitte installieren Sie Python 3.8 oder höher" - exit 1 -fi - -# Gehe zum Projektverzeichnis -cd "$(dirname "$0")" - -# Installiere Python-Abhängigkeiten falls nötig -if [ ! -d "venv" ]; then - echo "🔧 Erstelle virtuelle Umgebung..." - python3 -m venv venv -fi - -# Aktiviere virtuelle Umgebung -source venv/bin/activate - -# Installiere Abhängigkeiten -pip install -r requirements.txt 2>/dev/null - -# Starte SkillMate -echo "🚀 Starte SkillMate..." -python3 main.py \ No newline at end of file