Add Objects API routes

This commit is contained in:
2026-03-12 19:53:35 +00:00
parent 3ebf2dd461
commit 52709fb436
2 changed files with 443 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ import { organizationsRouter } from "./routes/organizations.ts";
import { adminRouter } from "./routes/admin.ts";
import { partnershipsRouter } from "./routes/partnerships.ts";
import { qualificationsRouter } from "./routes/qualifications.ts";
import { objectsRouter } from "./routes/objects.ts";
import { errorHandler } from "./middleware/error.ts";
import { requestLogger } from "./middleware/logger.ts";
import { initDB } from "./db/postgres.ts";
@@ -53,6 +54,8 @@ app.use(partnershipsRouter.routes());
app.use(partnershipsRouter.allowedMethods());
app.use(qualificationsRouter.routes());
app.use(qualificationsRouter.allowedMethods());
app.use(objectsRouter.routes());
app.use(objectsRouter.allowedMethods());
// Health check
app.use((ctx) => {

440
src/routes/objects.ts Normal file
View File

@@ -0,0 +1,440 @@
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 objectsRouter = new Router({ prefix: "/api/objects" });
// ============================================
// OBJECTS (Wachobjekte)
// ============================================
// GET / - Alle Objekte der Organisation
objectsRouter.get("/", authMiddleware, async (ctx) => {
const orgId = ctx.state.auth.user.org_id;
const status = ctx.request.url.searchParams.get("status") || "active";
const type = ctx.request.url.searchParams.get("type");
const search = ctx.request.url.searchParams.get("search");
let whereClause = "WHERE org_id = $1";
const params: any[] = [orgId];
let paramIndex = 2;
if (status !== "all") {
whereClause += ` AND status = $${paramIndex++}`;
params.push(status);
}
if (type) {
whereClause += ` AND object_type = $${paramIndex++}`;
params.push(type);
}
if (search) {
whereClause += ` AND (name ILIKE $${paramIndex} OR city ILIKE $${paramIndex} OR object_number ILIKE $${paramIndex})`;
params.push(`%${search}%`);
paramIndex++;
}
const objects = await query(`
SELECT o.*,
(SELECT COUNT(*) FROM object_contacts WHERE object_id = o.id) as contact_count,
(SELECT COUNT(*) FROM object_checkpoints WHERE object_id = o.id AND active = true) as checkpoint_count
FROM objects o
${whereClause}
ORDER BY o.name
`, params);
ctx.response.body = objects;
});
// GET /types - Objekt-Typen
objectsRouter.get("/types", authMiddleware, (ctx) => {
ctx.response.body = [
{ key: 'building', name: 'Gebäude', icon: '🏢' },
{ key: 'event', name: 'Veranstaltung', icon: '🎪' },
{ key: 'construction', name: 'Baustelle', icon: '🏗️' },
{ key: 'retail', name: 'Einzelhandel', icon: '🛒' },
{ key: 'industrial', name: 'Industrie', icon: '🏭' },
{ key: 'residential', name: 'Wohnanlage', icon: '🏠' },
{ key: 'parking', name: 'Parkhaus/Parkplatz', icon: '🅿️' },
{ key: 'hospital', name: 'Krankenhaus/Klinik', icon: '🏥' },
{ key: 'school', name: 'Schule/Bildung', icon: '🏫' },
{ key: 'other', name: 'Sonstiges', icon: '📍' }
];
});
// GET /:id - Einzelnes Objekt mit Details
objectsRouter.get("/:id", authMiddleware, async (ctx) => {
const objectId = ctx.params.id;
const orgId = ctx.state.auth.user.org_id;
const obj = await queryOne(`
SELECT * FROM objects WHERE id = $1 AND org_id = $2
`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
// Kontakte laden
const contacts = await query(`
SELECT * FROM object_contacts WHERE object_id = $1 ORDER BY is_primary DESC, is_emergency DESC, name
`, [objectId]);
// Dienstanweisungen laden
const instructions = await query(`
SELECT * FROM object_instructions WHERE object_id = $1 ORDER BY sort_order, title
`, [objectId]);
// Dokumente laden
const documents = await query(`
SELECT * FROM object_documents WHERE object_id = $1 ORDER BY uploaded_at DESC
`, [objectId]);
// Checkpoints laden
const checkpoints = await query(`
SELECT * FROM object_checkpoints WHERE object_id = $1 ORDER BY sort_order, name
`, [objectId]);
// Anforderungen laden
const requirements = await query(`
SELECT r.*,
COALESCE(qt.name, oqt.name) as qualification_name,
COALESCE(qt.icon, oqt.icon) as icon
FROM object_requirements r
LEFT JOIN qualification_types qt ON r.qualification_type_id = qt.id
LEFT JOIN org_qualification_types oqt ON r.org_qualification_type_id = oqt.id
WHERE r.object_id = $1
`, [objectId]);
ctx.response.body = {
...obj,
contacts,
instructions,
documents,
checkpoints,
requirements
};
});
// POST / - Neues Objekt erstellen
objectsRouter.post("/", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const body = await ctx.request.body().value;
const orgId = ctx.state.auth.user.org_id;
const userId = ctx.state.auth.user.id;
const {
name, short_name, object_number, object_type,
street, house_number, postal_code, city, country,
latitude, longitude, phone, email,
description, size_sqm, floors,
access_info, parking_info, customer_name, image_url
} = body;
if (!name) {
throw new AppError("Name ist erforderlich", 400);
}
const result = await queryOne(`
INSERT INTO objects (
org_id, name, short_name, object_number, object_type,
street, house_number, postal_code, city, country,
latitude, longitude, phone, email,
description, size_sqm, floors,
access_info, parking_info, customer_name, image_url, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)
RETURNING *
`, [
orgId, name, short_name, object_number, object_type || 'other',
street, house_number, postal_code, city, country || 'Deutschland',
latitude, longitude, phone, email,
description, size_sqm, floors,
access_info, parking_info, customer_name, image_url, userId
]);
ctx.response.status = 201;
ctx.response.body = result;
});
// PUT /:id - Objekt aktualisieren
objectsRouter.put("/:id", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const objectId = ctx.params.id;
const body = await ctx.request.body().value;
const orgId = ctx.state.auth.user.org_id;
const result = await queryOne(`
UPDATE objects SET
name = COALESCE($1, name),
short_name = COALESCE($2, short_name),
object_number = COALESCE($3, object_number),
object_type = COALESCE($4, object_type),
street = COALESCE($5, street),
house_number = COALESCE($6, house_number),
postal_code = COALESCE($7, postal_code),
city = COALESCE($8, city),
country = COALESCE($9, country),
latitude = COALESCE($10, latitude),
longitude = COALESCE($11, longitude),
phone = COALESCE($12, phone),
email = COALESCE($13, email),
description = COALESCE($14, description),
size_sqm = COALESCE($15, size_sqm),
floors = COALESCE($16, floors),
access_info = COALESCE($17, access_info),
parking_info = COALESCE($18, parking_info),
customer_name = COALESCE($19, customer_name),
image_url = COALESCE($20, image_url),
status = COALESCE($21, status),
updated_at = NOW()
WHERE id = $22 AND org_id = $23
RETURNING *
`, [
body.name, body.short_name, body.object_number, body.object_type,
body.street, body.house_number, body.postal_code, body.city, body.country,
body.latitude, body.longitude, body.phone, body.email,
body.description, body.size_sqm, body.floors,
body.access_info, body.parking_info, body.customer_name, body.image_url, body.status,
objectId, orgId
]);
if (!result) {
throw new AppError("Objekt nicht gefunden", 404);
}
ctx.response.body = result;
});
// DELETE /:id - Objekt löschen (oder archivieren)
objectsRouter.delete("/:id", authMiddleware, requireChef, async (ctx) => {
const objectId = ctx.params.id;
const orgId = ctx.state.auth.user.org_id;
const permanent = ctx.request.url.searchParams.get("permanent") === "true";
if (permanent) {
const result = await execute(`
DELETE FROM objects WHERE id = $1 AND org_id = $2
`, [objectId, orgId]);
if (result === 0) {
throw new AppError("Objekt nicht gefunden", 404);
}
} else {
// Soft delete - archivieren
const result = await execute(`
UPDATE objects SET status = 'archived', updated_at = NOW()
WHERE id = $1 AND org_id = $2
`, [objectId, orgId]);
if (result === 0) {
throw new AppError("Objekt nicht gefunden", 404);
}
}
ctx.response.body = { success: true };
});
// ============================================
// CONTACTS
// ============================================
// POST /:id/contacts - Kontakt hinzufügen
objectsRouter.post("/:id/contacts", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const objectId = ctx.params.id;
const body = await ctx.request.body().value;
const orgId = ctx.state.auth.user.org_id;
// Prüfen ob Objekt existiert
const obj = await queryOne(`SELECT id FROM objects WHERE id = $1 AND org_id = $2`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
const result = await queryOne(`
INSERT INTO object_contacts (object_id, name, role, company, phone, mobile, email, availability, is_primary, is_emergency, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`, [
objectId, body.name, body.role, body.company, body.phone, body.mobile,
body.email, body.availability, body.is_primary || false, body.is_emergency || false, body.notes
]);
ctx.response.status = 201;
ctx.response.body = result;
});
// DELETE /:id/contacts/:contactId - Kontakt löschen
objectsRouter.delete("/:id/contacts/:contactId", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const { id: objectId, contactId } = ctx.params;
const orgId = ctx.state.auth.user.org_id;
// Prüfen ob Objekt zur Org gehört
const obj = await queryOne(`SELECT id FROM objects WHERE id = $1 AND org_id = $2`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
await execute(`DELETE FROM object_contacts WHERE id = $1 AND object_id = $2`, [contactId, objectId]);
ctx.response.body = { success: true };
});
// ============================================
// INSTRUCTIONS (Dienstanweisungen)
// ============================================
// POST /:id/instructions - Dienstanweisung hinzufügen
objectsRouter.post("/:id/instructions", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const objectId = ctx.params.id;
const body = await ctx.request.body().value;
const orgId = ctx.state.auth.user.org_id;
const userId = ctx.state.auth.user.id;
const obj = await queryOne(`SELECT id FROM objects WHERE id = $1 AND org_id = $2`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
const result = await queryOne(`
INSERT INTO object_instructions (object_id, title, category, content, sort_order, is_critical, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
`, [
objectId, body.title, body.category || 'general', body.content,
body.sort_order || 0, body.is_critical || false, userId
]);
ctx.response.status = 201;
ctx.response.body = result;
});
// PUT /:id/instructions/:instructionId - Dienstanweisung aktualisieren
objectsRouter.put("/:id/instructions/:instructionId", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const { id: objectId, instructionId } = ctx.params;
const body = await ctx.request.body().value;
const orgId = ctx.state.auth.user.org_id;
const obj = await queryOne(`SELECT id FROM objects WHERE id = $1 AND org_id = $2`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
const result = await queryOne(`
UPDATE object_instructions SET
title = COALESCE($1, title),
category = COALESCE($2, category),
content = COALESCE($3, content),
sort_order = COALESCE($4, sort_order),
is_critical = COALESCE($5, is_critical),
version = version + 1,
updated_at = NOW()
WHERE id = $6 AND object_id = $7
RETURNING *
`, [body.title, body.category, body.content, body.sort_order, body.is_critical, instructionId, objectId]);
if (!result) {
throw new AppError("Dienstanweisung nicht gefunden", 404);
}
ctx.response.body = result;
});
// DELETE /:id/instructions/:instructionId
objectsRouter.delete("/:id/instructions/:instructionId", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const { id: objectId, instructionId } = ctx.params;
const orgId = ctx.state.auth.user.org_id;
const obj = await queryOne(`SELECT id FROM objects WHERE id = $1 AND org_id = $2`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
await execute(`DELETE FROM object_instructions WHERE id = $1 AND object_id = $2`, [instructionId, objectId]);
ctx.response.body = { success: true };
});
// ============================================
// CHECKPOINTS (für Wächterkontrolle)
// ============================================
// POST /:id/checkpoints - Checkpoint hinzufügen
objectsRouter.post("/:id/checkpoints", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const objectId = ctx.params.id;
const body = await ctx.request.body().value;
const orgId = ctx.state.auth.user.org_id;
const obj = await queryOne(`SELECT id FROM objects WHERE id = $1 AND org_id = $2`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
// Code generieren falls nicht angegeben
const code = body.code || `CP-${objectId}-${Date.now().toString(36).toUpperCase()}`;
const result = await queryOne(`
INSERT INTO object_checkpoints (object_id, name, location_description, code, code_type, sort_order, latitude, longitude)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
`, [
objectId, body.name, body.location_description, code,
body.code_type || 'qr', body.sort_order || 0, body.latitude, body.longitude
]);
ctx.response.status = 201;
ctx.response.body = result;
});
// DELETE /:id/checkpoints/:checkpointId
objectsRouter.delete("/:id/checkpoints/:checkpointId", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const { id: objectId, checkpointId } = ctx.params;
const orgId = ctx.state.auth.user.org_id;
const obj = await queryOne(`SELECT id FROM objects WHERE id = $1 AND org_id = $2`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
await execute(`DELETE FROM object_checkpoints WHERE id = $1 AND object_id = $2`, [checkpointId, objectId]);
ctx.response.body = { success: true };
});
// ============================================
// REQUIREMENTS (Qualifikations-Anforderungen)
// ============================================
// POST /:id/requirements - Anforderung hinzufügen
objectsRouter.post("/:id/requirements", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const objectId = ctx.params.id;
const body = await ctx.request.body().value;
const orgId = ctx.state.auth.user.org_id;
const obj = await queryOne(`SELECT id FROM objects WHERE id = $1 AND org_id = $2`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
const result = await queryOne(`
INSERT INTO object_requirements (object_id, qualification_type_id, org_qualification_type_id, is_mandatory, notes)
VALUES ($1, $2, $3, $4, $5)
RETURNING *
`, [
objectId, body.qualification_type_id || null, body.org_qualification_type_id || null,
body.is_mandatory ?? true, body.notes
]);
ctx.response.status = 201;
ctx.response.body = result;
});
// DELETE /:id/requirements/:requirementId
objectsRouter.delete("/:id/requirements/:requirementId", authMiddleware, requireDisponentOrHigher, async (ctx) => {
const { id: objectId, requirementId } = ctx.params;
const orgId = ctx.state.auth.user.org_id;
const obj = await queryOne(`SELECT id FROM objects WHERE id = $1 AND org_id = $2`, [objectId, orgId]);
if (!obj) {
throw new AppError("Objekt nicht gefunden", 404);
}
await execute(`DELETE FROM object_requirements WHERE id = $1 AND object_id = $2`, [requirementId, objectId]);
ctx.response.body = { success: true };
});