Tenants could previously see every other tenant's private room inventory including purchase prices. Now matches the app's standard visibility rule: own room + roomId=NULL (shared) items only; landlord/ admin unchanged (see everything).
136 lines
4.8 KiB
TypeScript
136 lines
4.8 KiB
TypeScript
import { Router, Response } from 'express';
|
|
import { PrismaClient, ItemCondition } from '@prisma/client';
|
|
import { AuthedRequest, requireAuth, requireRole } from '../middleware/auth';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
/**
|
|
* Router für die Inventarverwaltung (Möbel & Geräte je Zimmer bzw.
|
|
* Gemeinschaftsfläche). Anlegen/Ändern/Löschen ist Vermieter/Admin
|
|
* vorbehalten. Sichtbarkeit wie überall sonst: Mieter sehen nur Inventar
|
|
* ihres eigenen Zimmers plus echte Gemeinschaftsgegenstände (roomId NULL),
|
|
* nicht das private Zimmerinventar (inkl. Kaufpreis) anderer Mieter.
|
|
* Vermieter/Admin sehen alles.
|
|
*
|
|
* Pro Gegenstand können mehrere Fotos hinterlegt werden (photoUrls) — das
|
|
* Web-Dashboard sendet dafür clientseitig eingelesene Bilder als Data-URLs,
|
|
* es gibt keine separate Objekt-Storage-Anbindung in diesem Demo-Stand.
|
|
*/
|
|
export const inventoryRouter = Router();
|
|
|
|
const CONDITIONS = Object.values(ItemCondition);
|
|
const MAX_PHOTOS = 6;
|
|
|
|
const INVENTORY_SELECT = {
|
|
id: true,
|
|
itemName: true,
|
|
purchaseDate: true,
|
|
purchasePrice: true,
|
|
condition: true,
|
|
manualPdfUrl: true,
|
|
photoUrls: true,
|
|
createdAt: true,
|
|
room: { select: { id: true, roomNumber: true } },
|
|
};
|
|
|
|
function sanitizePhotoUrls(input: unknown): string[] {
|
|
if (!Array.isArray(input)) return [];
|
|
return input
|
|
.filter((u) => typeof u === 'string' && u.trim())
|
|
.slice(0, MAX_PHOTOS)
|
|
.map((u) => (u as string).trim());
|
|
}
|
|
|
|
inventoryRouter.get('/inventory', requireAuth, async (req: AuthedRequest, res: Response) => {
|
|
const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN';
|
|
let where = {};
|
|
if (!isLandlord) {
|
|
const self = await prisma.user.findUnique({ where: { id: req.user!.id }, select: { roomId: true } });
|
|
where = { OR: [{ roomId: null }, { roomId: self?.roomId ?? '__none__' }] };
|
|
}
|
|
|
|
const items = await prisma.inventory.findMany({
|
|
where,
|
|
orderBy: { createdAt: 'desc' },
|
|
select: INVENTORY_SELECT,
|
|
});
|
|
res.status(200).json({ items });
|
|
});
|
|
|
|
inventoryRouter.post(
|
|
'/inventory',
|
|
requireAuth,
|
|
requireRole('LANDLORD', 'ADMIN'),
|
|
async (req: AuthedRequest, res: Response) => {
|
|
const { itemName, roomId, purchaseDate, purchasePrice, condition, manualPdfUrl, photoUrls } = req.body || {};
|
|
|
|
if (!itemName || typeof itemName !== 'string' || !itemName.trim()) {
|
|
return res.status(400).json({ error: 'itemName ist erforderlich' });
|
|
}
|
|
if (condition && !CONDITIONS.includes(condition)) {
|
|
return res.status(400).json({ error: `Ungültiger Zustand. Erlaubt: ${CONDITIONS.join(', ')}` });
|
|
}
|
|
if (roomId) {
|
|
const room = await prisma.room.findUnique({ where: { id: roomId } });
|
|
if (!room) return res.status(400).json({ error: 'Zimmer nicht gefunden' });
|
|
}
|
|
|
|
const item = await prisma.inventory.create({
|
|
data: {
|
|
itemName: itemName.trim().slice(0, 120),
|
|
roomId: roomId || null,
|
|
purchaseDate: purchaseDate && !Number.isNaN(Date.parse(purchaseDate)) ? new Date(purchaseDate) : null,
|
|
purchasePrice: purchasePrice !== undefined && purchasePrice !== null && purchasePrice !== '' ? Number(purchasePrice) : null,
|
|
condition: condition || ItemCondition.GOOD,
|
|
manualPdfUrl: typeof manualPdfUrl === 'string' && manualPdfUrl.trim() ? manualPdfUrl.trim() : null,
|
|
photoUrls: sanitizePhotoUrls(photoUrls),
|
|
},
|
|
select: INVENTORY_SELECT,
|
|
});
|
|
|
|
res.status(201).json({ item });
|
|
},
|
|
);
|
|
|
|
inventoryRouter.patch(
|
|
'/inventory/:id',
|
|
requireAuth,
|
|
requireRole('LANDLORD', 'ADMIN'),
|
|
async (req: AuthedRequest, res: Response) => {
|
|
const { condition, purchasePrice, manualPdfUrl, photoUrls } = req.body || {};
|
|
if (condition && !CONDITIONS.includes(condition)) {
|
|
return res.status(400).json({ error: `Ungültiger Zustand. Erlaubt: ${CONDITIONS.join(', ')}` });
|
|
}
|
|
|
|
try {
|
|
const item = await prisma.inventory.update({
|
|
where: { id: req.params.id },
|
|
data: {
|
|
condition: condition || undefined,
|
|
purchasePrice: purchasePrice !== undefined && purchasePrice !== null && purchasePrice !== '' ? Number(purchasePrice) : undefined,
|
|
manualPdfUrl: typeof manualPdfUrl === 'string' ? manualPdfUrl.trim() || null : undefined,
|
|
photoUrls: Array.isArray(photoUrls) ? sanitizePhotoUrls(photoUrls) : undefined,
|
|
},
|
|
select: INVENTORY_SELECT,
|
|
});
|
|
res.status(200).json({ item });
|
|
} catch {
|
|
res.status(404).json({ error: 'Inventar-Eintrag nicht gefunden' });
|
|
}
|
|
},
|
|
);
|
|
|
|
inventoryRouter.delete(
|
|
'/inventory/:id',
|
|
requireAuth,
|
|
requireRole('LANDLORD', 'ADMIN'),
|
|
async (req: AuthedRequest, res: Response) => {
|
|
try {
|
|
await prisma.inventory.delete({ where: { id: req.params.id } });
|
|
res.status(204).send();
|
|
} catch {
|
|
res.status(404).json({ error: 'Inventar-Eintrag nicht gefunden' });
|
|
}
|
|
},
|
|
);
|