Bestellschein-Tab, Favoriten je Benutzer (Scheine + Kabel), Kabelsuche, Loxone Link Kabel Cat.7
Build and Deploy / version (push) Successful in 1s
Build and Deploy / build (push) Successful in 14s
Build and Deploy / deploy (push) Successful in 1s

- Neuer Tab Bestellschein: gleiches Datenmodell/Funktionen wie Materialschein (Projekt.typ)
- Favoriten serverseitig pro Benutzer (Tabelle favoriten, /api/favoriten)
- Kabelauswahl mit Suchfeld + Favoriten (KabelSelect), Suche/Favoriten in der Referenztabelle
- Scheinsuche: mehrere Suchbegriffe, auch Notiz/Datum; Filter Favoriten

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Fidelis Huber
2026-10-09 20:04:52 +02:00
co-authored by Claude Sonnet 5.5
parent 9bd8ebeeb8
commit b6227eb4cf
20 changed files with 409 additions and 93 deletions
+2
View File
@@ -7,6 +7,7 @@ import { requireAuth } from './auth.js';
import { authRouter } from './routes/auth.js';
import { projekteRouter } from './routes/projekte.js';
import { materialRouter } from './routes/material.js';
import { favoritenRouter } from './routes/favoriten.js';
import { prisma } from './prisma.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -22,6 +23,7 @@ app.get('/api/health', (_req, res) => res.json({ ok: true }));
app.use('/api/auth', authRouter);
app.use('/api/projekte', requireAuth, projekteRouter);
app.use('/api/material', requireAuth, materialRouter);
app.use('/api/favoriten', requireAuth, favoritenRouter);
// Statisch gebautes React/Vite-Frontend ausliefern.
app.use(express.static(PUBLIC_DIR));
+46
View File
@@ -0,0 +1,46 @@
import { Router } from 'express';
import { prisma } from '../prisma.js';
import type { AuthedRequest } from '../auth.js';
export const favoritenRouter = Router();
const ARTEN = ['SCHEIN', 'KABEL'];
// Favoriten gehören dem jeweiligen Benutzer: Microsoft-Konto über die oid, lokales
// Konto (keine oid) über den Benutzernamen.
function userKey(req: AuthedRequest): string | null {
const u = req.user;
if (!u) return null;
if (u.oid) return `ms:${u.oid}`;
if (u.email) return `local:${u.email.toLowerCase()}`;
return null;
}
// GET /api/favoriten → [{ art, refId }]
favoritenRouter.get('/', async (req: AuthedRequest, res) => {
const key = userKey(req);
if (!key) { res.status(401).json({ error: 'Benutzer unbekannt.' }); return; }
const items = await prisma.favorit.findMany({ where: { userKey: key }, select: { art: true, refId: true } });
res.json(items);
});
// PUT /api/favoriten { art, refId, favorit: boolean }
favoritenRouter.put('/', async (req: AuthedRequest, res) => {
const key = userKey(req);
if (!key) { res.status(401).json({ error: 'Benutzer unbekannt.' }); return; }
const { art, refId, favorit } = req.body ?? {};
if (!ARTEN.includes(art) || typeof refId !== 'string' || !refId || refId.length > 300 || typeof favorit !== 'boolean') {
res.status(400).json({ error: 'art, refId und favorit sind erforderlich.' });
return;
}
if (favorit) {
await prisma.favorit.upsert({
where: { userKey_art_refId: { userKey: key, art, refId } },
update: {},
create: { userKey: key, art, refId }
});
} else {
await prisma.favorit.deleteMany({ where: { userKey: key, art, refId } });
}
res.status(204).end();
});
+16 -4
View File
@@ -4,13 +4,23 @@ import type { AuthedRequest } from '../auth.js';
export const projekteRouter = Router();
projekteRouter.get('/', async (_req, res) => {
const projekte = await prisma.projekt.findMany({ orderBy: { createdAt: 'desc' } });
const SCHEIN_TYPEN = ['Material', 'Bestell'] as const;
type ScheinTypName = (typeof SCHEIN_TYPEN)[number];
const parseTyp = (v: unknown): ScheinTypName | undefined =>
SCHEIN_TYPEN.find(t => t === v);
// GET /api/projekte?typ=Material|Bestell (ohne typ: alle)
projekteRouter.get('/', async (req, res) => {
const typ = parseTyp(req.query.typ);
const projekte = await prisma.projekt.findMany({
where: typ ? { typ } : undefined,
orderBy: { createdAt: 'desc' }
});
res.json(projekte);
});
projekteRouter.post('/', async (req: AuthedRequest, res) => {
const { titel, auftraggeber, auftragsNr, datum, monteur, notiz } = req.body ?? {};
const { titel, auftraggeber, auftragsNr, datum, monteur, notiz, typ } = req.body ?? {};
if (!titel || typeof titel !== 'string' || !titel.trim()) {
res.status(400).json({ error: 'Titel (Projekt/Baustelle) ist erforderlich.' });
return;
@@ -22,7 +32,8 @@ projekteRouter.post('/', async (req: AuthedRequest, res) => {
auftragsNr: auftragsNr || null,
datum: datum ? new Date(datum) : null,
monteur: monteur || null,
notiz: notiz || null
notiz: notiz || null,
typ: parseTyp(typ) ?? 'Material'
}
});
res.status(201).json(projekt);
@@ -61,5 +72,6 @@ projekteRouter.delete('/:id', async (req, res) => {
return;
}
await prisma.projekt.delete({ where: { id } });
await prisma.favorit.deleteMany({ where: { art: 'SCHEIN', refId: id } });
res.status(204).end();
});