892 lines
26 KiB
TypeScript
892 lines
26 KiB
TypeScript
// app/api/devices/[id]/route.ts
|
||
import { NextResponse } from 'next/server';
|
||
import { prisma } from '@/lib/prisma';
|
||
import type { Prisma } from '@/generated/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<RouteParams> };
|
||
|
||
export async function GET(_req: Request, ctx: RouteContext) {
|
||
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,
|
||
parentDevice: true,
|
||
accessories: true,
|
||
},
|
||
});
|
||
|
||
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,
|
||
group: device.group?.name ?? null,
|
||
location: device.location?.name ?? null,
|
||
tags: device.tags.map((t) => t.name),
|
||
loanedTo: device.loanedTo,
|
||
loanedFrom: device.loanedFrom
|
||
? device.loanedFrom.toISOString()
|
||
: null,
|
||
loanedUntil: device.loanedUntil
|
||
? device.loanedUntil.toISOString()
|
||
: null,
|
||
loanComment: device.loanComment,
|
||
parentInventoryNumber: device.parentDeviceId,
|
||
parentName: device.parentDevice?.name ?? null,
|
||
accessories: device.accessories.map((a) => ({
|
||
inventoryNumber: a.inventoryNumber,
|
||
name: a.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 POST(req: Request) {
|
||
try {
|
||
const body = await req.json();
|
||
|
||
const {
|
||
inventoryNumber,
|
||
name,
|
||
manufacturer,
|
||
model,
|
||
serialNumber,
|
||
productNumber,
|
||
comment,
|
||
group,
|
||
location,
|
||
ipv4Address,
|
||
ipv6Address,
|
||
macAddress,
|
||
username,
|
||
passwordHash,
|
||
tags,
|
||
parentInventoryNumber,
|
||
loanedTo,
|
||
loanedFrom,
|
||
loanedUntil,
|
||
loanComment,
|
||
} = body;
|
||
|
||
if (!inventoryNumber || !name) {
|
||
return NextResponse.json(
|
||
{ error: 'inventoryNumber und name sind Pflichtfelder.' },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
|
||
const existing = await prisma.device.findUnique({
|
||
where: { inventoryNumber },
|
||
});
|
||
if (existing) {
|
||
return NextResponse.json(
|
||
{ error: 'Gerät mit dieser Inventar-Nr. existiert bereits.' },
|
||
{ status: 409 },
|
||
);
|
||
}
|
||
|
||
const userId = await getCurrentUserId();
|
||
let canConnectUser = false;
|
||
|
||
if (userId) {
|
||
const user = await prisma.user.findUnique({
|
||
where: { nwkennung: userId },
|
||
select: { nwkennung: true },
|
||
});
|
||
if (user) {
|
||
canConnectUser = true;
|
||
} else {
|
||
console.warn(
|
||
`[POST /api/devices] User mit nwkennung=${userId} nicht gefunden – createdBy/changedBy werden nicht gesetzt.`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// Gruppe
|
||
let groupId: string | null = null;
|
||
if (group && typeof group === 'string' && group.trim() !== '') {
|
||
const grp = await prisma.deviceGroup.upsert({
|
||
where: { name: group.trim() },
|
||
update: {},
|
||
create: { name: group.trim() },
|
||
});
|
||
groupId = grp.id;
|
||
}
|
||
|
||
// Standort
|
||
let locationId: string | null = null;
|
||
if (location && typeof location === 'string' && location.trim() !== '') {
|
||
const loc = await prisma.location.upsert({
|
||
where: { name: location.trim() },
|
||
update: {},
|
||
create: { name: location.trim() },
|
||
});
|
||
locationId = loc.id;
|
||
}
|
||
|
||
const tagNames: string[] = Array.isArray(tags)
|
||
? tags
|
||
.map((t: unknown) => String(t).trim())
|
||
.filter((t) => t.length > 0)
|
||
: [];
|
||
|
||
// ⬅️ NEU: Parent/Hauptgerät ermitteln (falls angegeben)
|
||
let parentDeviceConnect: Prisma.DeviceCreateInput['parentDevice'] | undefined;
|
||
|
||
if (typeof parentInventoryNumber === 'string') {
|
||
const trimmedParent = parentInventoryNumber.trim();
|
||
|
||
// nicht auf sich selbst zeigen und nicht leer
|
||
if (trimmedParent && trimmedParent !== inventoryNumber) {
|
||
parentDeviceConnect = {
|
||
connect: { inventoryNumber: trimmedParent },
|
||
};
|
||
}
|
||
}
|
||
|
||
const created = await prisma.device.create({
|
||
data: {
|
||
inventoryNumber,
|
||
name,
|
||
manufacturer,
|
||
model,
|
||
serialNumber: serialNumber ?? null,
|
||
productNumber: productNumber ?? null,
|
||
comment: comment ?? null,
|
||
ipv4Address: ipv4Address ?? null,
|
||
ipv6Address: ipv6Address ?? null,
|
||
macAddress: macAddress ?? null,
|
||
username: username ?? null,
|
||
passwordHash: passwordHash ?? null,
|
||
loanedTo: loanedTo ?? null,
|
||
loanedFrom: loanedFrom ? new Date(loanedFrom) : null,
|
||
loanedUntil: loanedUntil ? new Date(loanedUntil) : null,
|
||
loanComment: loanComment ?? null,
|
||
groupId,
|
||
locationId,
|
||
createdById: canConnectUser && userId ? userId : null,
|
||
...(parentDeviceConnect ? { parentDevice: parentDeviceConnect } : {}),
|
||
|
||
...(tagNames.length
|
||
? {
|
||
tags: {
|
||
connectOrCreate: tagNames.map((name) => ({
|
||
where: { name },
|
||
create: { name },
|
||
})),
|
||
},
|
||
}
|
||
: {}),
|
||
},
|
||
include: {
|
||
group: true,
|
||
location: true,
|
||
tags: true,
|
||
parentDevice: true,
|
||
accessories: true,
|
||
createdBy: true,
|
||
},
|
||
});
|
||
|
||
const snapshot: Prisma.JsonObject = {
|
||
before: null,
|
||
after: {
|
||
inventoryNumber: created.inventoryNumber,
|
||
name: created.name,
|
||
manufacturer: created.manufacturer,
|
||
model: created.model,
|
||
serialNumber: created.serialNumber,
|
||
productNumber: created.productNumber,
|
||
comment: created.comment,
|
||
ipv4Address: created.ipv4Address,
|
||
ipv6Address: created.ipv6Address,
|
||
macAddress: created.macAddress,
|
||
username: created.username,
|
||
passwordHash: created.passwordHash,
|
||
group: created.group?.name ?? null,
|
||
location: created.location?.name ?? null,
|
||
tags: created.tags.map((t) => t.name),
|
||
loanedTo: created.loanedTo,
|
||
loanedFrom: created.loanedFrom
|
||
? created.loanedFrom.toISOString()
|
||
: null,
|
||
loanedUntil: created.loanedUntil
|
||
? created.loanedUntil.toISOString()
|
||
: null,
|
||
loanComment: created.loanComment,
|
||
createdAt: created.createdAt.toISOString(),
|
||
updatedAt: created.updatedAt.toISOString(),
|
||
},
|
||
changes: [],
|
||
};
|
||
|
||
await prisma.deviceHistory.create({
|
||
data: {
|
||
deviceId: created.inventoryNumber,
|
||
changeType: 'CREATED',
|
||
snapshot,
|
||
changedById: canConnectUser && userId ? userId : null,
|
||
},
|
||
});
|
||
|
||
const io = (global as any).devicesIo as IOServer | undefined;
|
||
if (io) {
|
||
io.emit('device:created', {
|
||
inventoryNumber: created.inventoryNumber,
|
||
name: created.name,
|
||
manufacturer: created.manufacturer,
|
||
model: created.model,
|
||
serialNumber: created.serialNumber,
|
||
productNumber: created.productNumber,
|
||
comment: created.comment,
|
||
ipv4Address: created.ipv4Address,
|
||
ipv6Address: created.ipv6Address,
|
||
macAddress: created.macAddress,
|
||
username: created.username,
|
||
group: created.group?.name ?? null,
|
||
location: created.location?.name ?? null,
|
||
tags: created.tags.map((t) => t.name),
|
||
loanedTo: created.loanedTo,
|
||
loanedFrom: created.loanedFrom
|
||
? created.loanedFrom.toISOString()
|
||
: null,
|
||
loanedUntil: created.loanedUntil
|
||
? created.loanedUntil.toISOString()
|
||
: null,
|
||
loanComment: created.loanComment,
|
||
updatedAt: created.updatedAt.toISOString(),
|
||
});
|
||
}
|
||
|
||
return NextResponse.json(
|
||
{
|
||
inventoryNumber: created.inventoryNumber,
|
||
name: created.name,
|
||
manufacturer: created.manufacturer,
|
||
model: created.model,
|
||
serialNumber: created.serialNumber,
|
||
productNumber: created.productNumber,
|
||
comment: created.comment,
|
||
ipv4Address: created.ipv4Address,
|
||
ipv6Address: created.ipv6Address,
|
||
macAddress: created.macAddress,
|
||
username: created.username,
|
||
passwordHash: created.passwordHash,
|
||
group: created.group?.name ?? null,
|
||
location: created.location?.name ?? null,
|
||
tags: created.tags.map((t) => t.name),
|
||
loanedTo: created.loanedTo,
|
||
loanedFrom: created.loanedFrom
|
||
? created.loanedFrom.toISOString()
|
||
: null,
|
||
loanedUntil: created.loanedUntil
|
||
? created.loanedUntil.toISOString()
|
||
: null,
|
||
loanComment: created.loanComment,
|
||
|
||
// ⬅️ NEU: Parent + Accessories wie im GET
|
||
parentInventoryNumber: created.parentDeviceId,
|
||
parentName: created.parentDevice?.name ?? null,
|
||
accessories: created.accessories.map((a) => ({
|
||
inventoryNumber: a.inventoryNumber,
|
||
name: a.name,
|
||
})),
|
||
|
||
createdAt: created.createdAt.toISOString(),
|
||
updatedAt: created.updatedAt.toISOString(),
|
||
},
|
||
{ status: 201 },
|
||
);
|
||
} catch (err) {
|
||
console.error('Error in POST /api/devices/[id]', err);
|
||
return NextResponse.json(
|
||
{ error: 'Interner Serverfehler beim Anlegen des Geräts.' },
|
||
{ status: 500 },
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function PATCH(req: Request, ctx: RouteContext) {
|
||
const { id } = await ctx.params;
|
||
|
||
if (!id) {
|
||
return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 });
|
||
}
|
||
|
||
try {
|
||
const body = await req.json();
|
||
|
||
const userId = await getCurrentUserId();
|
||
let canConnectUpdatedBy = false;
|
||
|
||
if (userId) {
|
||
const user = await prisma.user.findUnique({
|
||
where: { nwkennung: userId }, // ✅ User.nwkennung ist @id
|
||
select: { nwkennung: true },
|
||
});
|
||
|
||
if (user) {
|
||
canConnectUpdatedBy = true;
|
||
} else {
|
||
console.warn(
|
||
`[PATCH /api/devices/${id}] User mit nwkennung=${userId} nicht gefunden – updatedBy wird nicht gesetzt.`,
|
||
);
|
||
}
|
||
}
|
||
|
||
const existing = await prisma.device.findUnique({
|
||
where: { inventoryNumber: id },
|
||
include: {
|
||
group: true,
|
||
location: true,
|
||
tags: true,
|
||
parentDevice: true,
|
||
},
|
||
});
|
||
|
||
if (!existing) {
|
||
return NextResponse.json({ error: 'NOT_FOUND' }, { status: 404 });
|
||
}
|
||
|
||
const data: Prisma.DeviceUpdateInput = {
|
||
name: body.name,
|
||
manufacturer: body.manufacturer,
|
||
model: body.model,
|
||
serialNumber: body.serialNumber ?? null,
|
||
productNumber: body.productNumber ?? null,
|
||
comment: body.comment ?? null,
|
||
ipv4Address: body.ipv4Address ?? null,
|
||
ipv6Address: body.ipv6Address ?? null,
|
||
macAddress: body.macAddress ?? null,
|
||
username: body.username ?? null,
|
||
passwordHash: body.passwordHash ?? null,
|
||
// Verleih
|
||
loanedTo: body.loanedTo ?? null,
|
||
loanedFrom: body.loanedFrom ? new Date(body.loanedFrom) : null,
|
||
loanedUntil: body.loanedUntil ? new Date(body.loanedUntil) : null,
|
||
loanComment: body.loanComment ?? null,
|
||
};
|
||
|
||
// Hauptgerät / Parent
|
||
if ('parentInventoryNumber' in body) {
|
||
const raw = body.parentInventoryNumber;
|
||
|
||
if (typeof raw === 'string') {
|
||
const trimmed = raw.trim();
|
||
|
||
if (trimmed.length > 0 && trimmed !== existing.inventoryNumber) {
|
||
data.parentDevice = {
|
||
connect: { inventoryNumber: trimmed },
|
||
};
|
||
} else {
|
||
// leerer String oder auf sich selbst → trennen
|
||
data.parentDevice = { disconnect: true };
|
||
}
|
||
} else if (raw == null) {
|
||
// null / undefined explizit → trennen
|
||
data.parentDevice = { disconnect: true };
|
||
}
|
||
}
|
||
|
||
|
||
if (canConnectUpdatedBy && userId) {
|
||
data.updatedBy = {
|
||
connect: { nwkennung: userId },
|
||
};
|
||
}
|
||
|
||
// Gruppe
|
||
if (body.group != null && body.group !== '') {
|
||
data.group = {
|
||
connectOrCreate: {
|
||
where: { name: body.group },
|
||
create: { name: body.group },
|
||
},
|
||
};
|
||
} else {
|
||
data.group = { disconnect: true };
|
||
}
|
||
|
||
// Standort
|
||
if (body.location != null && body.location !== '') {
|
||
data.location = {
|
||
connectOrCreate: {
|
||
where: { name: body.location },
|
||
create: { name: body.location },
|
||
},
|
||
};
|
||
} else {
|
||
data.location = { disconnect: true };
|
||
}
|
||
|
||
// Tags
|
||
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 = {
|
||
connectOrCreate: toConnect.map((name) => ({
|
||
where: { name },
|
||
create: { name },
|
||
})),
|
||
disconnect: toDisconnect.map((name) => ({ name })),
|
||
};
|
||
}
|
||
|
||
const updated = await prisma.device.update({
|
||
where: { inventoryNumber: id },
|
||
data,
|
||
include: {
|
||
group: true,
|
||
location: true,
|
||
tags: true,
|
||
parentDevice: 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];
|
||
|
||
const changes: {
|
||
field:
|
||
| TrackedField
|
||
| 'group'
|
||
| 'location'
|
||
| 'tags'
|
||
| 'loanedTo'
|
||
| 'loanedFrom'
|
||
| 'loanedUntil'
|
||
| 'loanComment'
|
||
| 'parentDevice';
|
||
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
|
||
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
|
||
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,
|
||
});
|
||
}
|
||
|
||
// Verleih-Diffs
|
||
const beforeLoanedTo = existing.loanedTo ?? null;
|
||
const afterLoanedTo = updated.loanedTo ?? null;
|
||
if (beforeLoanedTo !== afterLoanedTo) {
|
||
changes.push({
|
||
field: 'loanedTo',
|
||
before: beforeLoanedTo,
|
||
after: afterLoanedTo,
|
||
});
|
||
}
|
||
|
||
const beforeLoanedFrom = existing.loanedFrom
|
||
? existing.loanedFrom.toISOString()
|
||
: null;
|
||
const afterLoanedFrom = updated.loanedFrom
|
||
? updated.loanedFrom.toISOString()
|
||
: null;
|
||
if (beforeLoanedFrom !== afterLoanedFrom) {
|
||
changes.push({
|
||
field: 'loanedFrom',
|
||
before: beforeLoanedFrom,
|
||
after: afterLoanedFrom,
|
||
});
|
||
}
|
||
|
||
const beforeLoanedUntil = existing.loanedUntil
|
||
? existing.loanedUntil.toISOString()
|
||
: null;
|
||
const afterLoanedUntil = updated.loanedUntil
|
||
? updated.loanedUntil.toISOString()
|
||
: null;
|
||
if (beforeLoanedUntil !== afterLoanedUntil) {
|
||
changes.push({
|
||
field: 'loanedUntil',
|
||
before: beforeLoanedUntil,
|
||
after: afterLoanedUntil,
|
||
});
|
||
}
|
||
|
||
const beforeLoanComment = existing.loanComment ?? null;
|
||
const afterLoanComment = updated.loanComment ?? null;
|
||
if (beforeLoanComment !== afterLoanComment) {
|
||
changes.push({
|
||
field: 'loanComment',
|
||
before: beforeLoanComment,
|
||
after: afterLoanComment,
|
||
});
|
||
}
|
||
|
||
// Parent / Hauptgerät-Diff
|
||
const beforeParent =
|
||
existing.parentDeviceId != null
|
||
? `${existing.parentDeviceId}${
|
||
existing.parentDevice?.name ? ' – ' + existing.parentDevice.name : ''
|
||
}`
|
||
: null;
|
||
|
||
const afterParent =
|
||
updated.parentDeviceId != null
|
||
? `${updated.parentDeviceId}${
|
||
updated.parentDevice?.name ? ' – ' + updated.parentDevice.name : ''
|
||
}`
|
||
: null;
|
||
|
||
if (beforeParent !== afterParent) {
|
||
changes.push({
|
||
field: 'parentDevice',
|
||
before: beforeParent,
|
||
after: afterParent,
|
||
});
|
||
}
|
||
|
||
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),
|
||
loanedTo: existing.loanedTo,
|
||
loanedFrom: existing.loanedFrom
|
||
? existing.loanedFrom.toISOString()
|
||
: null,
|
||
loanedUntil: existing.loanedUntil
|
||
? existing.loanedUntil.toISOString()
|
||
: null,
|
||
loanComment: existing.loanComment,
|
||
parentInventoryNumber: existing.parentDeviceId,
|
||
parentName: existing.parentDevice?.name ?? null,
|
||
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),
|
||
loanedTo: updated.loanedTo,
|
||
loanedFrom: updated.loanedFrom
|
||
? updated.loanedFrom.toISOString()
|
||
: null,
|
||
loanedUntil: updated.loanedUntil
|
||
? updated.loanedUntil.toISOString()
|
||
: null,
|
||
loanComment: updated.loanComment,
|
||
parentInventoryNumber: updated.parentDeviceId,
|
||
parentName: updated.parentDevice?.name ?? null,
|
||
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: canConnectUpdatedBy && userId ? userId : null,
|
||
},
|
||
});
|
||
}
|
||
|
||
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,
|
||
tags: updated.tags.map((t) => t.name),
|
||
loanedTo: updated.loanedTo,
|
||
loanedFrom: updated.loanedFrom
|
||
? updated.loanedFrom.toISOString()
|
||
: null,
|
||
loanedUntil: updated.loanedUntil
|
||
? updated.loanedUntil.toISOString()
|
||
: null,
|
||
loanComment: updated.loanComment,
|
||
parentInventoryNumber: updated.parentDeviceId,
|
||
parentName: updated.parentDevice?.name ?? null,
|
||
updatedAt: updated.updatedAt.toISOString(),
|
||
});
|
||
}
|
||
|
||
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,
|
||
tags: updated.tags.map((t) => t.name),
|
||
loanedTo: updated.loanedTo,
|
||
loanedFrom: updated.loanedFrom
|
||
? updated.loanedFrom.toISOString()
|
||
: null,
|
||
loanedUntil: updated.loanedUntil
|
||
? updated.loanedUntil.toISOString()
|
||
: null,
|
||
loanComment: updated.loanComment,
|
||
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 });
|
||
}
|
||
}
|
||
|
||
export async function DELETE(_req: Request, ctx: RouteContext) {
|
||
const { id } = await ctx.params;
|
||
|
||
if (!id) {
|
||
return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 });
|
||
}
|
||
|
||
try {
|
||
const existing = await prisma.device.findUnique({
|
||
where: { inventoryNumber: id },
|
||
include: {
|
||
group: true,
|
||
location: true,
|
||
tags: true,
|
||
accessories: true,
|
||
},
|
||
});
|
||
|
||
if (!existing) {
|
||
return NextResponse.json({ error: 'NOT_FOUND' }, { status: 404 });
|
||
}
|
||
|
||
// ❗ Optional: Hauptgerät mit Zubehör nicht löschen
|
||
if (existing.accessories && existing.accessories.length > 0) {
|
||
return NextResponse.json(
|
||
{
|
||
error: 'HAS_ACCESSORIES',
|
||
message:
|
||
'Das Gerät hat noch Zubehör und kann nicht gelöscht werden. Entferne oder verschiebe zuerst das Zubehör.',
|
||
},
|
||
{ status: 409 },
|
||
);
|
||
}
|
||
|
||
// User für History ermitteln
|
||
const userId = await getCurrentUserId();
|
||
let changedById: string | null = null;
|
||
|
||
if (userId) {
|
||
const user = await prisma.user.findUnique({
|
||
where: { nwkennung: userId },
|
||
select: { nwkennung: true },
|
||
});
|
||
if (user) {
|
||
changedById = userId;
|
||
} else {
|
||
console.warn(
|
||
`[DELETE /api/devices/${id}] User mit nwkennung=${userId} nicht gefunden – changedById wird nicht gesetzt.`,
|
||
);
|
||
}
|
||
}
|
||
|
||
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),
|
||
loanedTo: existing.loanedTo,
|
||
loanedFrom: existing.loanedFrom
|
||
? existing.loanedFrom.toISOString()
|
||
: null,
|
||
loanedUntil: existing.loanedUntil
|
||
? existing.loanedUntil.toISOString()
|
||
: null,
|
||
loanComment: existing.loanComment,
|
||
createdAt: existing.createdAt.toISOString(),
|
||
updatedAt: existing.updatedAt.toISOString(),
|
||
},
|
||
after: null,
|
||
changes: [],
|
||
};
|
||
|
||
// History + Delete in einer Transaktion
|
||
await prisma.$transaction([
|
||
prisma.deviceHistory.create({
|
||
data: {
|
||
deviceId: existing.inventoryNumber,
|
||
changeType: 'DELETED',
|
||
snapshot,
|
||
changedById,
|
||
},
|
||
}),
|
||
prisma.device.delete({
|
||
where: { inventoryNumber: id },
|
||
}),
|
||
]);
|
||
|
||
// Socket.IO Event feuern
|
||
const io = (global as any).devicesIo as IOServer | undefined;
|
||
if (io) {
|
||
io.emit('device:deleted', { inventoryNumber: id });
|
||
}
|
||
|
||
return NextResponse.json({ ok: true });
|
||
} catch (err) {
|
||
console.error('[DELETE /api/devices/[id]]', err);
|
||
return NextResponse.json(
|
||
{ error: 'INTERNAL_ERROR' },
|
||
{ status: 500 },
|
||
);
|
||
}
|
||
}
|