Add Qualifications API routes
This commit is contained in:
@@ -9,6 +9,7 @@ import { modulesRouter } from "./routes/modules.ts";
|
|||||||
import { organizationsRouter } from "./routes/organizations.ts";
|
import { organizationsRouter } from "./routes/organizations.ts";
|
||||||
import { adminRouter } from "./routes/admin.ts";
|
import { adminRouter } from "./routes/admin.ts";
|
||||||
import { partnershipsRouter } from "./routes/partnerships.ts";
|
import { partnershipsRouter } from "./routes/partnerships.ts";
|
||||||
|
import { qualificationsRouter } from "./routes/qualifications.ts";
|
||||||
import { errorHandler } from "./middleware/error.ts";
|
import { errorHandler } from "./middleware/error.ts";
|
||||||
import { requestLogger } from "./middleware/logger.ts";
|
import { requestLogger } from "./middleware/logger.ts";
|
||||||
import { initDB } from "./db/postgres.ts";
|
import { initDB } from "./db/postgres.ts";
|
||||||
@@ -50,6 +51,8 @@ app.use(adminRouter.routes());
|
|||||||
app.use(adminRouter.allowedMethods());
|
app.use(adminRouter.allowedMethods());
|
||||||
app.use(partnershipsRouter.routes());
|
app.use(partnershipsRouter.routes());
|
||||||
app.use(partnershipsRouter.allowedMethods());
|
app.use(partnershipsRouter.allowedMethods());
|
||||||
|
app.use(qualificationsRouter.routes());
|
||||||
|
app.use(qualificationsRouter.allowedMethods());
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.use((ctx) => {
|
app.use((ctx) => {
|
||||||
|
|||||||
491
src/routes/qualifications.ts
Normal file
491
src/routes/qualifications.ts
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
import { Router } from "@oak/oak";
|
||||||
|
import { query, queryOne, execute } from "../db/postgres.ts";
|
||||||
|
import { authMiddleware, requireDisponentOrHigher, requireChef } from "../middleware/auth.ts";
|
||||||
|
import { AppError } from "../middleware/error.ts";
|
||||||
|
|
||||||
|
export const qualificationsRouter = new Router({ prefix: "/api/qualifications" });
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// QUALIFICATION TYPES
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// GET /types - Alle verfügbaren Qualifikationstypen
|
||||||
|
qualificationsRouter.get("/types", authMiddleware, async (ctx) => {
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
|
||||||
|
// System-Typen
|
||||||
|
const systemTypes = await query(`
|
||||||
|
SELECT id, key, name, description, category, has_expiry,
|
||||||
|
validity_months, is_required, icon, sort_order, 'system' as source
|
||||||
|
FROM qualification_types
|
||||||
|
ORDER BY sort_order, name
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Custom-Typen der Organisation
|
||||||
|
const customTypes = await query(`
|
||||||
|
SELECT id, key, name, description, category, has_expiry,
|
||||||
|
validity_months, icon, sort_order, 'custom' as source
|
||||||
|
FROM org_qualification_types
|
||||||
|
WHERE org_id = $1
|
||||||
|
ORDER BY sort_order, name
|
||||||
|
`, [orgId]);
|
||||||
|
|
||||||
|
ctx.response.body = {
|
||||||
|
system: systemTypes,
|
||||||
|
custom: customTypes,
|
||||||
|
categories: [
|
||||||
|
{ key: 'legal', name: 'Gesetzliche Pflicht', icon: '⚖️' },
|
||||||
|
{ key: 'weapons', name: 'Waffen', icon: '🔫' },
|
||||||
|
{ key: 'safety', name: 'Sicherheit & Erste Hilfe', icon: '🏥' },
|
||||||
|
{ key: 'driving', name: 'Führerscheine', icon: '🚗' },
|
||||||
|
{ key: 'special', name: 'Spezial-Ausbildungen', icon: '⭐' },
|
||||||
|
{ key: 'language', name: 'Sprachen', icon: '🌍' },
|
||||||
|
{ key: 'custom', name: 'Eigene', icon: '📌' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /types - Custom-Typ erstellen
|
||||||
|
qualificationsRouter.post("/types", authMiddleware, requireChef, async (ctx) => {
|
||||||
|
const body = await ctx.request.body().value;
|
||||||
|
const { key, name, description, has_expiry, validity_months, icon } = body;
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
|
||||||
|
if (!key || !name) {
|
||||||
|
throw new AppError("key und name sind erforderlich", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await queryOne(`
|
||||||
|
INSERT INTO org_qualification_types
|
||||||
|
(org_id, key, name, description, has_expiry, validity_months, icon)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING *
|
||||||
|
`, [orgId, key, name, description, has_expiry ?? true, validity_months, icon ?? '📜']);
|
||||||
|
|
||||||
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = result;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('unique') || error.message?.includes('duplicate')) {
|
||||||
|
throw new AppError("Ein Typ mit diesem Key existiert bereits", 400);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /types/:id - Custom-Typ löschen
|
||||||
|
qualificationsRouter.delete("/types/:id", authMiddleware, requireChef, async (ctx) => {
|
||||||
|
const typeId = ctx.params.id;
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
|
||||||
|
const result = await execute(`
|
||||||
|
DELETE FROM org_qualification_types
|
||||||
|
WHERE id = $1 AND org_id = $2
|
||||||
|
`, [typeId, orgId]);
|
||||||
|
|
||||||
|
if (result === 0) {
|
||||||
|
throw new AppError("Typ nicht gefunden oder ist System-Typ", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.response.body = { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// EMPLOYEE QUALIFICATIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// GET / - Alle Qualifikationen der Organisation
|
||||||
|
qualificationsRouter.get("/", authMiddleware, async (ctx) => {
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
const userId = ctx.request.url.searchParams.get("user_id");
|
||||||
|
const status = ctx.request.url.searchParams.get("status");
|
||||||
|
const category = ctx.request.url.searchParams.get("category");
|
||||||
|
const expiring = ctx.request.url.searchParams.get("expiring");
|
||||||
|
|
||||||
|
let whereClause = "WHERE eq.org_id = $1";
|
||||||
|
const params: any[] = [orgId];
|
||||||
|
let paramIndex = 2;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
whereClause += ` AND eq.user_id = $${paramIndex++}`;
|
||||||
|
params.push(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
whereClause += ` AND eq.status = $${paramIndex++}`;
|
||||||
|
params.push(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
whereClause += ` AND COALESCE(qt.category, oqt.category) = $${paramIndex++}`;
|
||||||
|
params.push(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expiring) {
|
||||||
|
const days = parseInt(expiring);
|
||||||
|
whereClause += ` AND eq.expiry_date IS NOT NULL AND eq.expiry_date <= CURRENT_DATE + INTERVAL '${days} days' AND eq.expiry_date >= CURRENT_DATE`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qualifications = await query(`
|
||||||
|
SELECT
|
||||||
|
eq.id,
|
||||||
|
eq.user_id,
|
||||||
|
eq.org_id,
|
||||||
|
u.first_name,
|
||||||
|
u.last_name,
|
||||||
|
u.email,
|
||||||
|
COALESCE(qt.key, oqt.key) as qualification_key,
|
||||||
|
COALESCE(qt.name, oqt.name) as qualification_name,
|
||||||
|
COALESCE(qt.category, oqt.category) as category,
|
||||||
|
COALESCE(qt.icon, oqt.icon) as icon,
|
||||||
|
COALESCE(qt.has_expiry, oqt.has_expiry) as has_expiry,
|
||||||
|
eq.issued_date,
|
||||||
|
eq.expiry_date,
|
||||||
|
eq.issuer,
|
||||||
|
eq.certificate_number,
|
||||||
|
eq.level,
|
||||||
|
eq.document_url,
|
||||||
|
eq.document_name,
|
||||||
|
eq.status,
|
||||||
|
eq.verified_by,
|
||||||
|
eq.verified_at,
|
||||||
|
eq.notes,
|
||||||
|
eq.created_at,
|
||||||
|
CASE
|
||||||
|
WHEN eq.expiry_date IS NULL THEN 'no_expiry'
|
||||||
|
WHEN eq.expiry_date < CURRENT_DATE THEN 'expired'
|
||||||
|
WHEN eq.expiry_date < CURRENT_DATE + INTERVAL '30 days' THEN 'expiring_soon'
|
||||||
|
ELSE 'valid'
|
||||||
|
END as expiry_status,
|
||||||
|
CASE
|
||||||
|
WHEN eq.expiry_date IS NOT NULL
|
||||||
|
THEN eq.expiry_date - CURRENT_DATE
|
||||||
|
ELSE NULL
|
||||||
|
END as days_until_expiry
|
||||||
|
FROM employee_qualifications eq
|
||||||
|
JOIN users u ON eq.user_id = u.id
|
||||||
|
LEFT JOIN qualification_types qt ON eq.qualification_type_id = qt.id
|
||||||
|
LEFT JOIN org_qualification_types oqt ON eq.org_qualification_type_id = oqt.id
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY eq.expiry_date ASC NULLS LAST, u.last_name, u.first_name
|
||||||
|
`, params);
|
||||||
|
|
||||||
|
ctx.response.body = qualifications;
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /expiring - Ablaufende Qualifikationen
|
||||||
|
qualificationsRouter.get("/expiring", authMiddleware, requireDisponentOrHigher, async (ctx) => {
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
const days = parseInt(ctx.request.url.searchParams.get("days") || "30");
|
||||||
|
|
||||||
|
const qualifications = await query(`
|
||||||
|
SELECT
|
||||||
|
eq.id,
|
||||||
|
eq.user_id,
|
||||||
|
u.first_name,
|
||||||
|
u.last_name,
|
||||||
|
u.email,
|
||||||
|
COALESCE(qt.name, oqt.name) as qualification_name,
|
||||||
|
COALESCE(qt.icon, oqt.icon) as icon,
|
||||||
|
eq.expiry_date,
|
||||||
|
eq.expiry_date - CURRENT_DATE as days_until_expiry,
|
||||||
|
CASE
|
||||||
|
WHEN eq.expiry_date < CURRENT_DATE THEN 'expired'
|
||||||
|
ELSE 'expiring_soon'
|
||||||
|
END as expiry_status
|
||||||
|
FROM employee_qualifications eq
|
||||||
|
JOIN users u ON eq.user_id = u.id
|
||||||
|
LEFT JOIN qualification_types qt ON eq.qualification_type_id = qt.id
|
||||||
|
LEFT JOIN org_qualification_types oqt ON eq.org_qualification_type_id = oqt.id
|
||||||
|
WHERE eq.org_id = $1
|
||||||
|
AND eq.expiry_date IS NOT NULL
|
||||||
|
AND eq.expiry_date <= CURRENT_DATE + INTERVAL '${days} days'
|
||||||
|
AND eq.status = 'active'
|
||||||
|
ORDER BY eq.expiry_date ASC
|
||||||
|
`, [orgId]);
|
||||||
|
|
||||||
|
const expired = qualifications.filter((r: any) => r.expiry_status === 'expired');
|
||||||
|
const expiringSoon = qualifications.filter((r: any) => r.expiry_status === 'expiring_soon');
|
||||||
|
|
||||||
|
ctx.response.body = {
|
||||||
|
expired,
|
||||||
|
expiring_soon: expiringSoon,
|
||||||
|
total_expired: expired.length,
|
||||||
|
total_expiring: expiringSoon.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /matrix - Qualifikations-Matrix
|
||||||
|
qualificationsRouter.get("/matrix", authMiddleware, requireDisponentOrHigher, async (ctx) => {
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
|
||||||
|
// Alle aktiven Mitarbeiter
|
||||||
|
const users = await query(`
|
||||||
|
SELECT id, first_name, last_name, email
|
||||||
|
FROM users
|
||||||
|
WHERE org_id = $1 AND active = true
|
||||||
|
ORDER BY last_name, first_name
|
||||||
|
`, [orgId]);
|
||||||
|
|
||||||
|
// Alle Qualifikationstypen
|
||||||
|
const systemTypes = await query(`
|
||||||
|
SELECT id, key, name, icon, category, 'system' as source
|
||||||
|
FROM qualification_types
|
||||||
|
ORDER BY sort_order, name
|
||||||
|
`);
|
||||||
|
|
||||||
|
const customTypes = await query(`
|
||||||
|
SELECT id, key, name, icon, category, 'custom' as source
|
||||||
|
FROM org_qualification_types
|
||||||
|
WHERE org_id = $1
|
||||||
|
ORDER BY sort_order, name
|
||||||
|
`, [orgId]);
|
||||||
|
|
||||||
|
const types = [...systemTypes, ...customTypes];
|
||||||
|
|
||||||
|
// Alle Qualifikationen
|
||||||
|
const qualifications = await query(`
|
||||||
|
SELECT
|
||||||
|
eq.user_id,
|
||||||
|
COALESCE(eq.qualification_type_id::text, eq.org_qualification_type_id::text) as type_id,
|
||||||
|
eq.status,
|
||||||
|
eq.expiry_date,
|
||||||
|
CASE
|
||||||
|
WHEN eq.expiry_date IS NULL THEN 'no_expiry'
|
||||||
|
WHEN eq.expiry_date < CURRENT_DATE THEN 'expired'
|
||||||
|
WHEN eq.expiry_date < CURRENT_DATE + INTERVAL '30 days' THEN 'expiring_soon'
|
||||||
|
ELSE 'valid'
|
||||||
|
END as expiry_status
|
||||||
|
FROM employee_qualifications eq
|
||||||
|
WHERE eq.org_id = $1 AND eq.status = 'active'
|
||||||
|
`, [orgId]);
|
||||||
|
|
||||||
|
// Matrix bauen
|
||||||
|
const matrix: Record<string, Record<string, any>> = {};
|
||||||
|
for (const user of users) {
|
||||||
|
matrix[(user as any).id] = {};
|
||||||
|
for (const type of types) {
|
||||||
|
matrix[(user as any).id][(type as any).id] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const qual of qualifications as any[]) {
|
||||||
|
if (matrix[qual.user_id] && qual.type_id) {
|
||||||
|
matrix[qual.user_id][qual.type_id] = {
|
||||||
|
status: qual.status,
|
||||||
|
expiry_date: qual.expiry_date,
|
||||||
|
expiry_status: qual.expiry_status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.response.body = { users, types, matrix };
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /user/:userId - Qualifikationen eines Mitarbeiters
|
||||||
|
qualificationsRouter.get("/user/:userId", authMiddleware, async (ctx) => {
|
||||||
|
const targetUserId = ctx.params.userId;
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
const currentUser = ctx.state.auth.user;
|
||||||
|
|
||||||
|
// Mitarbeiter dürfen nur ihre eigenen sehen
|
||||||
|
if (currentUser.role === 'mitarbeiter' && currentUser.id !== targetUserId) {
|
||||||
|
throw new AppError("Keine Berechtigung", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const qualifications = await query(`
|
||||||
|
SELECT
|
||||||
|
eq.id,
|
||||||
|
COALESCE(qt.key, oqt.key) as qualification_key,
|
||||||
|
COALESCE(qt.name, oqt.name) as qualification_name,
|
||||||
|
COALESCE(qt.category, oqt.category) as category,
|
||||||
|
COALESCE(qt.icon, oqt.icon) as icon,
|
||||||
|
eq.issued_date,
|
||||||
|
eq.expiry_date,
|
||||||
|
eq.issuer,
|
||||||
|
eq.certificate_number,
|
||||||
|
eq.level,
|
||||||
|
eq.document_url,
|
||||||
|
eq.document_name,
|
||||||
|
eq.status,
|
||||||
|
eq.notes,
|
||||||
|
CASE
|
||||||
|
WHEN eq.expiry_date IS NULL THEN 'no_expiry'
|
||||||
|
WHEN eq.expiry_date < CURRENT_DATE THEN 'expired'
|
||||||
|
WHEN eq.expiry_date < CURRENT_DATE + INTERVAL '30 days' THEN 'expiring_soon'
|
||||||
|
ELSE 'valid'
|
||||||
|
END as expiry_status
|
||||||
|
FROM employee_qualifications eq
|
||||||
|
LEFT JOIN qualification_types qt ON eq.qualification_type_id = qt.id
|
||||||
|
LEFT JOIN org_qualification_types oqt ON eq.org_qualification_type_id = oqt.id
|
||||||
|
WHERE eq.user_id = $1 AND eq.org_id = $2
|
||||||
|
ORDER BY category, qualification_name
|
||||||
|
`, [targetUserId, orgId]);
|
||||||
|
|
||||||
|
ctx.response.body = qualifications;
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST / - Neue Qualifikation hinzufügen
|
||||||
|
qualificationsRouter.post("/", authMiddleware, requireDisponentOrHigher, async (ctx) => {
|
||||||
|
const body = await ctx.request.body().value;
|
||||||
|
const {
|
||||||
|
user_id,
|
||||||
|
qualification_type_id,
|
||||||
|
org_qualification_type_id,
|
||||||
|
issued_date,
|
||||||
|
expiry_date,
|
||||||
|
issuer,
|
||||||
|
certificate_number,
|
||||||
|
level,
|
||||||
|
document_url,
|
||||||
|
document_name,
|
||||||
|
notes
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
const verifiedBy = ctx.state.auth.user.id;
|
||||||
|
|
||||||
|
if (!user_id || (!qualification_type_id && !org_qualification_type_id)) {
|
||||||
|
throw new AppError("user_id und qualification_type_id/org_qualification_type_id erforderlich", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob User zur Org gehört
|
||||||
|
const targetUser = await queryOne(
|
||||||
|
`SELECT id FROM users WHERE id = $1 AND org_id = $2`,
|
||||||
|
[user_id, orgId]
|
||||||
|
);
|
||||||
|
if (!targetUser) {
|
||||||
|
throw new AppError("Mitarbeiter nicht gefunden", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await queryOne(`
|
||||||
|
INSERT INTO employee_qualifications (
|
||||||
|
user_id, org_id, qualification_type_id, org_qualification_type_id,
|
||||||
|
issued_date, expiry_date, issuer, certificate_number, level,
|
||||||
|
document_url, document_name, notes, status, verified_by, verified_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'active', $13, NOW())
|
||||||
|
RETURNING *
|
||||||
|
`, [
|
||||||
|
user_id, orgId, qualification_type_id || null, org_qualification_type_id || null,
|
||||||
|
issued_date || null, expiry_date || null, issuer || null, certificate_number || null,
|
||||||
|
level || null, document_url || null, document_name || null, notes || null, verifiedBy
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.response.status = 201;
|
||||||
|
ctx.response.body = result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /:id - Qualifikation aktualisieren
|
||||||
|
qualificationsRouter.put("/:id", authMiddleware, requireDisponentOrHigher, async (ctx) => {
|
||||||
|
const qualId = ctx.params.id;
|
||||||
|
const body = await ctx.request.body().value;
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
|
||||||
|
const result = await queryOne(`
|
||||||
|
UPDATE employee_qualifications SET
|
||||||
|
issued_date = COALESCE($1, issued_date),
|
||||||
|
expiry_date = COALESCE($2, expiry_date),
|
||||||
|
issuer = COALESCE($3, issuer),
|
||||||
|
certificate_number = COALESCE($4, certificate_number),
|
||||||
|
level = COALESCE($5, level),
|
||||||
|
document_url = COALESCE($6, document_url),
|
||||||
|
document_name = COALESCE($7, document_name),
|
||||||
|
status = COALESCE($8, status),
|
||||||
|
notes = COALESCE($9, notes),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $10 AND org_id = $11
|
||||||
|
RETURNING *
|
||||||
|
`, [
|
||||||
|
body.issued_date, body.expiry_date, body.issuer, body.certificate_number,
|
||||||
|
body.level, body.document_url, body.document_name, body.status, body.notes,
|
||||||
|
qualId, orgId
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new AppError("Qualifikation nicht gefunden", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.response.body = result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /:id - Qualifikation löschen
|
||||||
|
qualificationsRouter.delete("/:id", authMiddleware, requireChef, async (ctx) => {
|
||||||
|
const qualId = ctx.params.id;
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
|
||||||
|
const result = await execute(`
|
||||||
|
DELETE FROM employee_qualifications
|
||||||
|
WHERE id = $1 AND org_id = $2
|
||||||
|
`, [qualId, orgId]);
|
||||||
|
|
||||||
|
if (result === 0) {
|
||||||
|
throw new AppError("Qualifikation nicht gefunden", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.response.body = { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /check/:userId - Prüfe ob User für bestimmte Anforderungen qualifiziert ist
|
||||||
|
qualificationsRouter.get("/check/:userId", authMiddleware, async (ctx) => {
|
||||||
|
const userId = ctx.params.userId;
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
const requiredStr = ctx.request.url.searchParams.get("required");
|
||||||
|
|
||||||
|
if (!requiredStr) {
|
||||||
|
ctx.response.body = { qualified: true, missing: [], has: [] };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const required = requiredStr.split(",").map(s => s.trim());
|
||||||
|
|
||||||
|
const qualifications = await query(`
|
||||||
|
SELECT COALESCE(qt.key, oqt.key) as qualification_key
|
||||||
|
FROM employee_qualifications eq
|
||||||
|
LEFT JOIN qualification_types qt ON eq.qualification_type_id = qt.id
|
||||||
|
LEFT JOIN org_qualification_types oqt ON eq.org_qualification_type_id = oqt.id
|
||||||
|
WHERE eq.user_id = $1 AND eq.org_id = $2 AND eq.status = 'active'
|
||||||
|
AND (eq.expiry_date IS NULL OR eq.expiry_date >= CURRENT_DATE)
|
||||||
|
`, [userId, orgId]);
|
||||||
|
|
||||||
|
const userQuals = new Set((qualifications as any[]).map(r => r.qualification_key));
|
||||||
|
const missing = required.filter(r => !userQuals.has(r));
|
||||||
|
const has = required.filter(r => userQuals.has(r));
|
||||||
|
|
||||||
|
ctx.response.body = {
|
||||||
|
qualified: missing.length === 0,
|
||||||
|
missing,
|
||||||
|
has
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /bulk - Mehrere Qualifikationen auf einmal hinzufügen
|
||||||
|
qualificationsRouter.post("/bulk", authMiddleware, requireChef, async (ctx) => {
|
||||||
|
const body = await ctx.request.body().value;
|
||||||
|
const { user_ids, qualification_type_id, issued_date, expiry_date, issuer } = body;
|
||||||
|
|
||||||
|
if (!user_ids || !Array.isArray(user_ids) || !qualification_type_id) {
|
||||||
|
throw new AppError("user_ids Array und qualification_type_id erforderlich", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = ctx.state.auth.user.org_id;
|
||||||
|
const verifiedBy = ctx.state.auth.user.id;
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const userId of user_ids) {
|
||||||
|
try {
|
||||||
|
await execute(`
|
||||||
|
INSERT INTO employee_qualifications (
|
||||||
|
user_id, org_id, qualification_type_id, issued_date, expiry_date,
|
||||||
|
issuer, status, verified_by, verified_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, 'active', $7, NOW())
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`, [userId, orgId, qualification_type_id, issued_date || null, expiry_date || null, issuer || null, verifiedBy]);
|
||||||
|
results.push({ user_id: userId, success: true });
|
||||||
|
} catch {
|
||||||
|
results.push({ user_id: userId, success: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.response.body = { results, total: results.length };
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user