// app/api/devices/[id]/route.ts import { NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import type { Prisma } from '@prisma/client'; import { getCurrentUserId } from '@/lib/auth'; import type { Server as IOServer } from 'socket.io'; type RouteParams = { id: string }; // in Next 15+ ist params ein Promise type RouteContext = { params: Promise }; export async function GET( _req: Request, ctx: RouteContext, ) { // params-Promise auflösen const { id } = await ctx.params; if (!id) { return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 }); } try { const device = await prisma.device.findUnique({ where: { inventoryNumber: id }, include: { group: true, location: true, tags: true, // 🔹 NEU }, }); if (!device) { return NextResponse.json({ error: 'NOT_FOUND' }, { status: 404 }); } return NextResponse.json({ inventoryNumber: device.inventoryNumber, name: device.name, manufacturer: device.manufacturer, model: device.model, serialNumber: device.serialNumber, productNumber: device.productNumber, comment: device.comment, ipv4Address: device.ipv4Address, ipv6Address: device.ipv6Address, macAddress: device.macAddress, username: device.username, // passwordHash bewusst weggelassen group: device.group?.name ?? null, location: device.location?.name ?? null, tags: device.tags.map((t) => t.name), // 🔹 createdAt: device.createdAt.toISOString(), updatedAt: device.updatedAt.toISOString(), }); } catch (err) { console.error('[GET /api/devices/[id]]', err); return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 }); } } export async function PATCH( req: Request, ctx: RouteContext, ) { const { id } = await ctx.params; const body = await req.json(); if (!id) { return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 }); } try { // ⬇️ hier jetzt die Request-Header durchreichen const userId = await getCurrentUserId(); // aktuelles Gerät inkl. Relations laden (für "before"-Snapshot) const existing = await prisma.device.findUnique({ where: { inventoryNumber: id }, include: { group: true, location: true, tags: true, // 🔹 NEU }, }); if (!existing) { return NextResponse.json({ error: 'NOT_FOUND' }, { status: 404 }); } // Basis-Update-Daten const data: Prisma.DeviceUpdateInput = { name: body.name, manufacturer: body.manufacturer, model: body.model, serialNumber: body.serialNumber, productNumber: body.productNumber, comment: body.comment, ipv4Address: body.ipv4Address, ipv6Address: body.ipv6Address, macAddress: body.macAddress, username: body.username, passwordHash: body.passwordHash, }; // Tags aus dem Body bereinigen const incomingTags: string[] = Array.isArray(body.tags) ? body.tags.map((t: unknown) => String(t).trim()).filter(Boolean) : []; // existierende Tag-Namen const existingTagNames = existing.tags.map((t) => t.name); // welche sollen entfernt werden? const tagsToRemove = existingTagNames.filter( (name) => !incomingTags.includes(name), ); // welche sind neu hinzuzufügen? const tagsToAdd = incomingTags.filter( (name) => !existingTagNames.includes(name), ); if (!data.tags) { data.tags = {}; } // Tags, die nicht mehr vorkommen → disconnect über name (weil @unique) if (tagsToRemove.length > 0) { (data.tags as any).disconnect = tagsToRemove.map((name) => ({ name })); } // neue Tags → connectOrCreate (Tag wird bei Bedarf angelegt) if (tagsToAdd.length > 0) { (data.tags as any).connectOrCreate = tagsToAdd.map((name) => ({ where: { name }, create: { name }, })); } // updatedBy setzen, wenn User da if (userId) { data.updatedBy = { connect: { id: userId }, }; } // Gruppe (per Name) – DeviceGroup.name ist @unique if (body.group != null && body.group !== '') { data.group = { connectOrCreate: { where: { name: body.group }, create: { name: body.group }, }, }; } else { data.group = { disconnect: true }; } // Standort (per Name) – Location.name ist @unique if (body.location != null && body.location !== '') { data.location = { connectOrCreate: { where: { name: body.location }, create: { name: body.location }, }, }; } else { data.location = { disconnect: true }; } // Tags (Many-to-Many via Tag.name @unique) if (Array.isArray(body.tags)) { const tagNames = (body.tags as string[]) .map((t) => String(t).trim()) .filter(Boolean); const beforeNames = existing.tags.map((t) => t.name.toLowerCase()); const normalized = tagNames.map((n) => n.toLowerCase()); const toConnect = tagNames.filter( (n) => !beforeNames.includes(n.toLowerCase()), ); const toDisconnect = existing.tags .map((t) => t.name) .filter((n) => !normalized.includes(n.toLowerCase())); data.tags = { // neue / fehlende Tags verknüpfen (und ggf. anlegen) connectOrCreate: toConnect.map((name) => ({ where: { name }, create: { name }, })), // nicht mehr vorhandene Tags trennen disconnect: toDisconnect.map((name) => ({ name })), }; } // Update durchführen (für "after"-Snapshot) const updated = await prisma.device.update({ where: { inventoryNumber: id }, data, include: { group: true, location: true, tags: true, }, }); // Felder, die wir tracken wollen const trackedFields = [ 'name', 'manufacturer', 'model', 'serialNumber', 'productNumber', 'comment', 'ipv4Address', 'ipv6Address', 'macAddress', 'username', 'passwordHash', ] as const; type TrackedField = (typeof trackedFields)[number]; // explizit JSON-kompatible Types (string | null) const changes: { field: TrackedField | 'group' | 'location' | 'tags'; before: string | null; after: string | null; }[] = []; // einfache Feld-Diffs for (const field of trackedFields) { const before = (existing as any)[field] as string | null; const after = (updated as any)[field] as string | null; if (before !== after) { changes.push({ field, before, after }); } } // group / location per Name vergleichen const beforeGroup = (existing.group?.name ?? null) as string | null; const afterGroup = (updated.group?.name ?? null) as string | null; if (beforeGroup !== afterGroup) { changes.push({ field: 'group', before: beforeGroup, after: afterGroup, }); } const beforeLocation = (existing.location?.name ?? null) as string | null; const afterLocation = (updated.location?.name ?? null) as string | null; if (beforeLocation !== afterLocation) { changes.push({ field: 'location', before: beforeLocation, after: afterLocation, }); } // Tags vergleichen (als kommagetrennte Liste) const beforeTagsList = existing.tags.map((t) => t.name).sort(); const afterTagsList = updated.tags.map((t) => t.name).sort(); const beforeTags = beforeTagsList.length > 0 ? beforeTagsList.join(', ') : null; const afterTags = afterTagsList.length > 0 ? afterTagsList.join(', ') : null; if (beforeTags !== afterTags) { changes.push({ field: 'tags', before: beforeTags, after: afterTags, }); } // Falls sich *gar nichts* geändert hat, kein History-Eintrag if (changes.length > 0) { const snapshot: Prisma.JsonObject = { before: { inventoryNumber: existing.inventoryNumber, name: existing.name, manufacturer: existing.manufacturer, model: existing.model, serialNumber: existing.serialNumber, productNumber: existing.productNumber, comment: existing.comment, ipv4Address: existing.ipv4Address, ipv6Address: existing.ipv6Address, macAddress: existing.macAddress, username: existing.username, passwordHash: existing.passwordHash, group: existing.group?.name ?? null, location: existing.location?.name ?? null, tags: existing.tags.map((t) => t.name), createdAt: existing.createdAt.toISOString(), updatedAt: existing.updatedAt.toISOString(), }, after: { inventoryNumber: updated.inventoryNumber, name: updated.name, manufacturer: updated.manufacturer, model: updated.model, serialNumber: updated.serialNumber, productNumber: updated.productNumber, comment: updated.comment, ipv4Address: updated.ipv4Address, ipv6Address: updated.ipv6Address, macAddress: updated.macAddress, username: updated.username, passwordHash: updated.passwordHash, group: updated.group?.name ?? null, location: updated.location?.name ?? null, tags: updated.tags.map((t) => t.name), createdAt: updated.createdAt.toISOString(), updatedAt: updated.updatedAt.toISOString(), }, changes: changes.map((c) => ({ field: c.field, before: c.before, after: c.after, })), }; await prisma.deviceHistory.create({ data: { deviceId: updated.inventoryNumber, changeType: 'UPDATED', snapshot, changedById: userId ?? null, }, }); } // 🔊 Socket.IO-Broadcast für echte Live-Updates const io = (global as any).devicesIo as IOServer | undefined; if (io) { io.emit('device:updated', { inventoryNumber: updated.inventoryNumber, name: updated.name, manufacturer: updated.manufacturer, model: updated.model, serialNumber: updated.serialNumber, productNumber: updated.productNumber, comment: updated.comment, ipv4Address: updated.ipv4Address, ipv6Address: updated.ipv6Address, macAddress: updated.macAddress, username: updated.username, group: updated.group?.name ?? null, location: updated.location?.name ?? null, updatedAt: updated.updatedAt.toISOString(), }); } // Antwort an den Client (flattened) return NextResponse.json({ inventoryNumber: updated.inventoryNumber, name: updated.name, manufacturer: updated.manufacturer, model: updated.model, serialNumber: updated.serialNumber, productNumber: updated.productNumber, comment: updated.comment, ipv4Address: updated.ipv4Address, ipv6Address: updated.ipv6Address, macAddress: updated.macAddress, username: updated.username, group: updated.group?.name ?? null, location: updated.location?.name ?? null, createdAt: updated.createdAt.toISOString(), updatedAt: updated.updatedAt.toISOString(), }); } catch (err) { console.error('[PATCH /api/devices/[id]]', err); return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 }); } }