700 Zeilen
24 KiB
TypeScript
700 Zeilen
24 KiB
TypeScript
import { Router, Response, NextFunction } from 'express'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
import { body, validationResult, query } from 'express-validator'
|
|
import { db } from '../config/database'
|
|
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
|
|
// Profile Interface direkt definiert, da shared module noch nicht aktualisiert
|
|
interface Profile {
|
|
id: string
|
|
name: string
|
|
department?: string
|
|
location?: string
|
|
role?: string
|
|
contacts?: {
|
|
email?: string
|
|
phone?: string
|
|
teams?: string
|
|
}
|
|
domains?: string[]
|
|
tools?: string[]
|
|
methods?: string[]
|
|
industryKnowledge?: string[]
|
|
regulatory?: string[]
|
|
languages?: { code: string; level: string }[]
|
|
projects?: { title: string; role?: string; summary?: string; links?: string[] }[]
|
|
networks?: string[]
|
|
digitalSkills?: string[]
|
|
socialSkills?: string[]
|
|
jobCategory?: string
|
|
jobTitle?: string
|
|
jobDesc?: string
|
|
consentPublicProfile: boolean
|
|
consentSearchable: boolean
|
|
updatedAt: string
|
|
updatedBy: string
|
|
reviewDueAt?: string
|
|
}
|
|
|
|
const router = Router()
|
|
|
|
// Hilfsfunktion f\u00fcr Volltextsuche
|
|
function buildSearchVector(profile: any): string {
|
|
const parts = [
|
|
profile.name,
|
|
profile.role,
|
|
profile.jobTitle,
|
|
profile.jobDesc,
|
|
...(profile.domains || []),
|
|
...(profile.tools || []),
|
|
...(profile.methods || []),
|
|
...(profile.industryKnowledge || []),
|
|
...(profile.digitalSkills || [])
|
|
].filter(Boolean)
|
|
|
|
return parts.join(' ').toLowerCase()
|
|
}
|
|
|
|
// GET /api/profiles - Suche mit Facetten
|
|
router.get('/',
|
|
authenticate,
|
|
[
|
|
query('query').optional().trim(),
|
|
query('dept').optional(),
|
|
query('loc').optional(),
|
|
query('jobCat').optional(),
|
|
query('tools').optional(),
|
|
query('methods').optional(),
|
|
query('lang').optional(),
|
|
query('page').optional().isInt({ min: 1 }).toInt(),
|
|
query('pageSize').optional().isInt({ min: 1, max: 100 }).toInt(),
|
|
query('sort').optional().isIn(['recency', 'relevance'])
|
|
],
|
|
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const errors = validationResult(req)
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: { message: 'Invalid query parameters', details: errors.array() }
|
|
})
|
|
}
|
|
|
|
const {
|
|
query: searchQuery,
|
|
dept,
|
|
loc,
|
|
jobCat,
|
|
tools,
|
|
methods,
|
|
lang,
|
|
page = 1,
|
|
pageSize = 20,
|
|
sort = 'relevance'
|
|
} = req.query
|
|
|
|
// Basis-Query mit Consent-Pr\u00fcfung
|
|
let sql = `
|
|
SELECT DISTINCT p.*
|
|
FROM profiles p
|
|
WHERE (p.consent_searchable = 1 OR p.updated_by = ?)
|
|
`
|
|
const params: any[] = [req.user!.id]
|
|
|
|
// Volltextsuche
|
|
if (searchQuery) {
|
|
sql += ` AND p.search_vector LIKE ?`
|
|
params.push(`%${searchQuery}%`)
|
|
}
|
|
|
|
// Facettenfilter
|
|
if (dept) {
|
|
sql += ` AND p.department = ?`
|
|
params.push(dept)
|
|
}
|
|
|
|
if (loc) {
|
|
sql += ` AND p.location = ?`
|
|
params.push(loc)
|
|
}
|
|
|
|
if (jobCat) {
|
|
sql += ` AND p.job_category = ?`
|
|
params.push(jobCat)
|
|
}
|
|
|
|
// Tool-Filter
|
|
if (tools) {
|
|
const toolList = tools.toString().split(',')
|
|
sql += ` AND EXISTS (
|
|
SELECT 1 FROM profile_tools pt
|
|
WHERE pt.profile_id = p.id
|
|
AND pt.tool IN (${toolList.map(() => '?').join(',')})
|
|
)`
|
|
params.push(...toolList)
|
|
}
|
|
|
|
// Methoden-Filter
|
|
if (methods) {
|
|
const methodList = methods.toString().split(',')
|
|
sql += ` AND EXISTS (
|
|
SELECT 1 FROM profile_methods pm
|
|
WHERE pm.profile_id = p.id
|
|
AND pm.method IN (${methodList.map(() => '?').join(',')})
|
|
)`
|
|
params.push(...methodList)
|
|
}
|
|
|
|
// Sprachen-Filter
|
|
if (lang) {
|
|
const [langCode, level] = lang.toString().split(':')
|
|
sql += ` AND EXISTS (
|
|
SELECT 1 FROM profile_languages pl
|
|
WHERE pl.profile_id = p.id
|
|
AND pl.code = ?
|
|
${level ? 'AND pl.level = ?' : ''}
|
|
)`
|
|
params.push(langCode)
|
|
if (level) params.push(level)
|
|
}
|
|
|
|
// Sortierung
|
|
if (sort === 'recency') {
|
|
sql += ` ORDER BY p.updated_at DESC`
|
|
} else {
|
|
sql += ` ORDER BY p.updated_at DESC` // TODO: Ranking nach Relevanz
|
|
}
|
|
|
|
// Pagination
|
|
const offset = ((page as number) - 1) * (pageSize as number)
|
|
sql += ` LIMIT ? OFFSET ?`
|
|
params.push(pageSize, offset)
|
|
|
|
const profiles = db.prepare(sql).all(...params)
|
|
|
|
// Lade zus\u00e4tzliche Daten f\u00fcr jedes Profil
|
|
const enrichedProfiles = profiles.map((profile: any) => {
|
|
// Lade Arrays
|
|
const domains = db.prepare('SELECT domain FROM profile_domains WHERE profile_id = ?').all(profile.id).map((r: any) => r.domain)
|
|
const tools = db.prepare('SELECT tool FROM profile_tools WHERE profile_id = ?').all(profile.id).map((r: any) => r.tool)
|
|
const methods = db.prepare('SELECT method FROM profile_methods WHERE profile_id = ?').all(profile.id).map((r: any) => r.method)
|
|
const industryKnowledge = db.prepare('SELECT knowledge FROM profile_industry_knowledge WHERE profile_id = ?').all(profile.id).map((r: any) => r.knowledge)
|
|
const regulatory = db.prepare('SELECT regulation FROM profile_regulatory WHERE profile_id = ?').all(profile.id).map((r: any) => r.regulation)
|
|
const networks = db.prepare('SELECT network FROM profile_networks WHERE profile_id = ?').all(profile.id).map((r: any) => r.network)
|
|
const digitalSkills = db.prepare('SELECT skill FROM profile_digital_skills WHERE profile_id = ?').all(profile.id).map((r: any) => r.skill)
|
|
const socialSkills = db.prepare('SELECT skill FROM profile_social_skills WHERE profile_id = ?').all(profile.id).map((r: any) => r.skill)
|
|
|
|
// Lade Sprachen
|
|
const languages = db.prepare('SELECT code, level FROM profile_languages WHERE profile_id = ?').all(profile.id)
|
|
|
|
// Lade Projekte
|
|
const projects = db.prepare('SELECT id, title, role, summary FROM profile_projects WHERE profile_id = ?').all(profile.id)
|
|
for (const project of projects as any[]) {
|
|
project.links = db.prepare('SELECT link FROM project_links WHERE project_id = ?').all(project.id).map((r: any) => r.link)
|
|
delete project.id
|
|
}
|
|
|
|
return {
|
|
...profile,
|
|
contacts: {
|
|
email: profile.email,
|
|
phone: profile.phone,
|
|
teams: profile.teams_link
|
|
},
|
|
domains,
|
|
tools,
|
|
methods,
|
|
industryKnowledge,
|
|
regulatory,
|
|
networks,
|
|
digitalSkills,
|
|
socialSkills,
|
|
languages,
|
|
projects,
|
|
consentPublicProfile: Boolean(profile.consent_public_profile),
|
|
consentSearchable: Boolean(profile.consent_searchable)
|
|
}
|
|
})
|
|
|
|
res.json({
|
|
success: true,
|
|
data: enrichedProfiles,
|
|
meta: {
|
|
page,
|
|
pageSize,
|
|
total: (db.prepare('SELECT COUNT(DISTINCT id) as count FROM profiles WHERE consent_searchable = 1 OR updated_by = ?').get(req.user!.id) as any)?.count || 0
|
|
}
|
|
})
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
// GET /api/profiles/:id - Einzelnes Profil
|
|
router.get('/:id',
|
|
authenticate,
|
|
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const { id } = req.params
|
|
|
|
const profile = db.prepare(`
|
|
SELECT * FROM profiles WHERE id = ?
|
|
`).get(id) as any
|
|
|
|
if (!profile) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: { message: 'Profile not found' }
|
|
})
|
|
}
|
|
|
|
// Pr\u00fcfe Zugriffsrechte
|
|
const isOwner = profile.updated_by === req.user!.id
|
|
const isAdmin = req.user!.role === 'admin'
|
|
|
|
if (!profile.consent_public_profile && !isOwner && !isAdmin) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: { message: 'Access denied' }
|
|
})
|
|
}
|
|
|
|
// Lade zus\u00e4tzliche Daten
|
|
const domains = db.prepare('SELECT domain FROM profile_domains WHERE profile_id = ?').all(id).map((r: any) => r.domain)
|
|
const tools = db.prepare('SELECT tool FROM profile_tools WHERE profile_id = ?').all(id).map((r: any) => r.tool)
|
|
const methods = db.prepare('SELECT method FROM profile_methods WHERE profile_id = ?').all(id).map((r: any) => r.method)
|
|
const industryKnowledge = db.prepare('SELECT knowledge FROM profile_industry_knowledge WHERE profile_id = ?').all(id).map((r: any) => r.knowledge)
|
|
const regulatory = db.prepare('SELECT regulation FROM profile_regulatory WHERE profile_id = ?').all(id).map((r: any) => r.regulation)
|
|
const networks = db.prepare('SELECT network FROM profile_networks WHERE profile_id = ?').all(id).map((r: any) => r.network)
|
|
const digitalSkills = db.prepare('SELECT skill FROM profile_digital_skills WHERE profile_id = ?').all(id).map((r: any) => r.skill)
|
|
const socialSkills = db.prepare('SELECT skill FROM profile_social_skills WHERE profile_id = ?').all(id).map((r: any) => r.skill)
|
|
const languages = db.prepare('SELECT code, level FROM profile_languages WHERE profile_id = ?').all(id)
|
|
|
|
const projects = db.prepare('SELECT id, title, role, summary FROM profile_projects WHERE profile_id = ?').all(id)
|
|
for (const project of projects as any[]) {
|
|
project.links = db.prepare('SELECT link FROM project_links WHERE project_id = ?').all(project.id).map((r: any) => r.link)
|
|
delete project.id
|
|
}
|
|
|
|
const enrichedProfile = {
|
|
...profile,
|
|
contacts: {
|
|
email: profile.email,
|
|
phone: profile.phone,
|
|
teams: profile.teams_link
|
|
},
|
|
domains,
|
|
tools,
|
|
methods,
|
|
industryKnowledge,
|
|
regulatory,
|
|
networks,
|
|
digitalSkills,
|
|
socialSkills,
|
|
languages,
|
|
projects,
|
|
consentPublicProfile: Boolean(profile.consent_public_profile),
|
|
consentSearchable: Boolean(profile.consent_searchable)
|
|
}
|
|
|
|
res.json({ success: true, data: enrichedProfile })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
// POST /api/profiles - Neues Profil erstellen
|
|
router.post('/',
|
|
authenticate,
|
|
[
|
|
body('name').notEmpty().trim(),
|
|
body('consentPublicProfile').isBoolean(),
|
|
body('consentSearchable').isBoolean()
|
|
],
|
|
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const errors = validationResult(req)
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: { message: 'Invalid input', details: errors.array() }
|
|
})
|
|
}
|
|
|
|
const profileId = uuidv4()
|
|
const now = new Date().toISOString()
|
|
const reviewDueAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString()
|
|
|
|
const profileData = {
|
|
...req.body,
|
|
id: profileId,
|
|
created_at: now,
|
|
updated_at: now,
|
|
updated_by: req.user!.id,
|
|
review_due_at: reviewDueAt,
|
|
search_vector: buildSearchVector(req.body)
|
|
}
|
|
|
|
// Transaktion starten
|
|
const transaction = db.transaction(() => {
|
|
// Profil einf\u00fcgen
|
|
db.prepare(`
|
|
INSERT INTO profiles (
|
|
id, name, department, location, role, email, phone, teams_link,
|
|
job_category, job_title, job_desc,
|
|
consent_public_profile, consent_searchable,
|
|
created_at, updated_at, updated_by, review_due_at, search_vector
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
profileId,
|
|
profileData.name,
|
|
profileData.department || null,
|
|
profileData.location || null,
|
|
profileData.role || null,
|
|
profileData.contacts?.email || null,
|
|
profileData.contacts?.phone || null,
|
|
profileData.contacts?.teams || null,
|
|
profileData.jobCategory || null,
|
|
profileData.jobTitle || null,
|
|
profileData.jobDesc || null,
|
|
profileData.consentPublicProfile ? 1 : 0,
|
|
profileData.consentSearchable ? 1 : 0,
|
|
now,
|
|
now,
|
|
req.user!.id,
|
|
reviewDueAt,
|
|
profileData.search_vector
|
|
)
|
|
|
|
// Arrays einf\u00fcgen
|
|
const insertArray = (table: string, field: string, values: string[]) => {
|
|
if (values && values.length > 0) {
|
|
const stmt = db.prepare(`INSERT INTO ${table} (profile_id, ${field}) VALUES (?, ?)`)
|
|
for (const value of values) {
|
|
stmt.run(profileId, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
insertArray('profile_domains', 'domain', profileData.domains)
|
|
insertArray('profile_tools', 'tool', profileData.tools)
|
|
insertArray('profile_methods', 'method', profileData.methods)
|
|
insertArray('profile_industry_knowledge', 'knowledge', profileData.industryKnowledge)
|
|
insertArray('profile_regulatory', 'regulation', profileData.regulatory)
|
|
insertArray('profile_networks', 'network', profileData.networks)
|
|
insertArray('profile_digital_skills', 'skill', profileData.digitalSkills)
|
|
insertArray('profile_social_skills', 'skill', profileData.socialSkills)
|
|
|
|
// Sprachen einf\u00fcgen
|
|
if (profileData.languages && profileData.languages.length > 0) {
|
|
const stmt = db.prepare(`INSERT INTO profile_languages (profile_id, code, level) VALUES (?, ?, ?)`)
|
|
for (const lang of profileData.languages) {
|
|
stmt.run(profileId, lang.code, lang.level)
|
|
}
|
|
}
|
|
|
|
// Projekte einf\u00fcgen
|
|
if (profileData.projects && profileData.projects.length > 0) {
|
|
for (const project of profileData.projects) {
|
|
const projectId = uuidv4()
|
|
db.prepare(`
|
|
INSERT INTO profile_projects (id, profile_id, title, role, summary, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run(projectId, profileId, project.title, project.role || null, project.summary || null, now)
|
|
|
|
if (project.links && project.links.length > 0) {
|
|
const linkStmt = db.prepare(`INSERT INTO project_links (project_id, link) VALUES (?, ?)`)
|
|
for (const link of project.links) {
|
|
linkStmt.run(projectId, link)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Audit-Log
|
|
db.prepare(`
|
|
INSERT INTO audit_log (id, entity_type, entity_id, action, user_id, changes, timestamp)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
uuidv4(),
|
|
'profile',
|
|
profileId,
|
|
'create',
|
|
req.user!.id,
|
|
JSON.stringify(profileData),
|
|
now
|
|
)
|
|
})
|
|
|
|
transaction()
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: { id: profileId },
|
|
message: 'Profile created successfully'
|
|
})
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
// PUT /api/profiles/:id - Profil aktualisieren
|
|
router.put('/:id',
|
|
authenticate,
|
|
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const { id } = req.params
|
|
const now = new Date().toISOString()
|
|
const reviewDueAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString()
|
|
|
|
// Pr\u00fcfe ob Profil existiert und Berechtigungen
|
|
const existing = db.prepare('SELECT * FROM profiles WHERE id = ?').get(id) as any
|
|
if (!existing) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: { message: 'Profile not found' }
|
|
})
|
|
}
|
|
|
|
const isOwner = existing.updated_by === req.user!.id
|
|
const isAdmin = req.user!.role === 'admin'
|
|
|
|
if (!isOwner && !isAdmin) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: { message: 'Access denied' }
|
|
})
|
|
}
|
|
|
|
const profileData = {
|
|
...req.body,
|
|
updated_at: now,
|
|
updated_by: req.user!.id,
|
|
review_due_at: reviewDueAt,
|
|
search_vector: buildSearchVector(req.body)
|
|
}
|
|
|
|
// Transaktion f\u00fcr Update
|
|
const transaction = db.transaction(() => {
|
|
// Profil aktualisieren
|
|
db.prepare(`
|
|
UPDATE profiles SET
|
|
name = ?, department = ?, location = ?, role = ?,
|
|
email = ?, phone = ?, teams_link = ?,
|
|
job_category = ?, job_title = ?, job_desc = ?,
|
|
consent_public_profile = ?, consent_searchable = ?,
|
|
updated_at = ?, updated_by = ?, review_due_at = ?, search_vector = ?
|
|
WHERE id = ?
|
|
`).run(
|
|
profileData.name,
|
|
profileData.department || null,
|
|
profileData.location || null,
|
|
profileData.role || null,
|
|
profileData.contacts?.email || null,
|
|
profileData.contacts?.phone || null,
|
|
profileData.contacts?.teams || null,
|
|
profileData.jobCategory || null,
|
|
profileData.jobTitle || null,
|
|
profileData.jobDesc || null,
|
|
profileData.consentPublicProfile ? 1 : 0,
|
|
profileData.consentSearchable ? 1 : 0,
|
|
now,
|
|
req.user!.id,
|
|
reviewDueAt,
|
|
profileData.search_vector,
|
|
id
|
|
)
|
|
|
|
// Arrays aktualisieren (l\u00f6schen und neu einf\u00fcgen)
|
|
const updateArray = (table: string, field: string, values: string[]) => {
|
|
db.prepare(`DELETE FROM ${table} WHERE profile_id = ?`).run(id)
|
|
if (values && values.length > 0) {
|
|
const stmt = db.prepare(`INSERT INTO ${table} (profile_id, ${field}) VALUES (?, ?)`)
|
|
for (const value of values) {
|
|
stmt.run(id, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
updateArray('profile_domains', 'domain', profileData.domains)
|
|
updateArray('profile_tools', 'tool', profileData.tools)
|
|
updateArray('profile_methods', 'method', profileData.methods)
|
|
updateArray('profile_industry_knowledge', 'knowledge', profileData.industryKnowledge)
|
|
updateArray('profile_regulatory', 'regulation', profileData.regulatory)
|
|
updateArray('profile_networks', 'network', profileData.networks)
|
|
updateArray('profile_digital_skills', 'skill', profileData.digitalSkills)
|
|
updateArray('profile_social_skills', 'skill', profileData.socialSkills)
|
|
|
|
// Sprachen aktualisieren
|
|
db.prepare('DELETE FROM profile_languages WHERE profile_id = ?').run(id)
|
|
if (profileData.languages && profileData.languages.length > 0) {
|
|
const stmt = db.prepare(`INSERT INTO profile_languages (profile_id, code, level) VALUES (?, ?, ?)`)
|
|
for (const lang of profileData.languages) {
|
|
stmt.run(id, lang.code, lang.level)
|
|
}
|
|
}
|
|
|
|
// Projekte aktualisieren
|
|
db.prepare('DELETE FROM profile_projects WHERE profile_id = ?').run(id)
|
|
if (profileData.projects && profileData.projects.length > 0) {
|
|
for (const project of profileData.projects) {
|
|
const projectId = uuidv4()
|
|
db.prepare(`
|
|
INSERT INTO profile_projects (id, profile_id, title, role, summary, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run(projectId, id, project.title, project.role || null, project.summary || null, now)
|
|
|
|
if (project.links && project.links.length > 0) {
|
|
const linkStmt = db.prepare(`INSERT INTO project_links (project_id, link) VALUES (?, ?)`)
|
|
for (const link of project.links) {
|
|
linkStmt.run(projectId, link)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Audit-Log
|
|
db.prepare(`
|
|
INSERT INTO audit_log (id, entity_type, entity_id, action, user_id, changes, timestamp)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
uuidv4(),
|
|
'profile',
|
|
id,
|
|
'update',
|
|
req.user!.id,
|
|
JSON.stringify({ before: existing, after: profileData }),
|
|
now
|
|
)
|
|
})
|
|
|
|
transaction()
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Profile updated successfully'
|
|
})
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
// GET /api/profiles/facets - Facettenwerte f\u00fcr Filter
|
|
router.get('/facets',
|
|
authenticate,
|
|
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const facets = {
|
|
departments: db.prepare('SELECT DISTINCT department FROM profiles WHERE department IS NOT NULL AND consent_searchable = 1 ORDER BY department').all().map((r: any) => r.department),
|
|
locations: db.prepare('SELECT DISTINCT location FROM profiles WHERE location IS NOT NULL AND consent_searchable = 1 ORDER BY location').all().map((r: any) => r.location),
|
|
jobCategories: db.prepare('SELECT DISTINCT job_category FROM profiles WHERE job_category IS NOT NULL AND consent_searchable = 1 ORDER BY job_category').all().map((r: any) => r.job_category),
|
|
tools: db.prepare('SELECT DISTINCT tool FROM profile_tools pt JOIN profiles p ON pt.profile_id = p.id WHERE p.consent_searchable = 1 ORDER BY tool').all().map((r: any) => r.tool),
|
|
methods: db.prepare('SELECT DISTINCT method FROM profile_methods pm JOIN profiles p ON pm.profile_id = p.id WHERE p.consent_searchable = 1 ORDER BY method').all().map((r: any) => r.method),
|
|
languages: db.prepare('SELECT DISTINCT code, level FROM profile_languages pl JOIN profiles p ON pl.profile_id = p.id WHERE p.consent_searchable = 1 ORDER BY code, level').all()
|
|
}
|
|
|
|
res.json({ success: true, data: facets })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
// GET /api/profiles/reminders/overdue - Veraltete Profile (Admin)
|
|
router.get('/reminders/overdue',
|
|
authenticate,
|
|
authorize('admin'),
|
|
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const now = new Date().toISOString()
|
|
|
|
const overdueProfiles = db.prepare(`
|
|
SELECT id, name, department, updated_at, review_due_at
|
|
FROM profiles
|
|
WHERE review_due_at < ?
|
|
ORDER BY review_due_at ASC
|
|
`).all(now)
|
|
|
|
res.json({ success: true, data: overdueProfiles })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
// POST /api/profiles/export - Export-Funktionalit\u00e4t
|
|
router.post('/export',
|
|
authenticate,
|
|
authorize('admin', 'superuser'),
|
|
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const { filter, format = 'json' } = req.body
|
|
|
|
// Baue Query basierend auf Filter
|
|
let sql = `SELECT * FROM profiles WHERE consent_searchable = 1`
|
|
const params: any[] = []
|
|
|
|
if (filter?.department) {
|
|
sql += ` AND department = ?`
|
|
params.push(filter.department)
|
|
}
|
|
|
|
if (filter?.location) {
|
|
sql += ` AND location = ?`
|
|
params.push(filter.location)
|
|
}
|
|
|
|
const profiles = db.prepare(sql).all(...params)
|
|
|
|
if (format === 'csv') {
|
|
// CSV-Export
|
|
const csv = [
|
|
'Name,Department,Location,Role,Email,Phone,Job Category,Job Title',
|
|
...profiles.map((p: any) =>
|
|
`"${p.name}","${p.department || ''}","${p.location || ''}","${p.role || ''}","${p.email || ''}","${p.phone || ''}","${p.job_category || ''}","${p.job_title || ''}"`
|
|
)
|
|
].join('\n')
|
|
|
|
res.setHeader('Content-Type', 'text/csv')
|
|
res.setHeader('Content-Disposition', 'attachment; filename=profiles.csv')
|
|
res.send(csv)
|
|
} else {
|
|
// JSON-Export
|
|
res.json({ success: true, data: profiles })
|
|
}
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
// POST /api/profiles/tags/suggest - Autocomplete f\u00fcr Tags
|
|
router.post('/tags/suggest',
|
|
authenticate,
|
|
[
|
|
body('category').notEmpty(),
|
|
body('query').notEmpty()
|
|
],
|
|
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const { category, query } = req.body
|
|
|
|
const suggestions = db.prepare(`
|
|
SELECT value, description
|
|
FROM controlled_vocabulary
|
|
WHERE category = ? AND value LIKE ? AND is_active = 1
|
|
ORDER BY value
|
|
LIMIT 10
|
|
`).all(category, `${query}%`)
|
|
|
|
res.json({ success: true, data: suggestions })
|
|
} catch (error) {
|
|
next(error)
|
|
}
|
|
}
|
|
)
|
|
|
|
export default router |