457 lines
20 KiB
TypeScript
457 lines
20 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import ausruestungsanfrageService from '../services/ausruestungsanfrage.service';
|
|
import notificationService from '../services/notification.service';
|
|
import { permissionService } from '../services/permission.service';
|
|
import logger from '../utils/logger';
|
|
|
|
class AusruestungsanfrageController {
|
|
// -------------------------------------------------------------------------
|
|
// Categories (DB-backed)
|
|
// -------------------------------------------------------------------------
|
|
|
|
async getKategorien(_req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const kategorien = await ausruestungsanfrageService.getKategorien();
|
|
res.status(200).json({ success: true, data: kategorien });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.getKategorien error', { error });
|
|
res.status(500).json({ success: false, message: 'Kategorien konnten nicht geladen werden' });
|
|
}
|
|
}
|
|
|
|
async createKategorie(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { name } = req.body;
|
|
if (!name || name.trim().length === 0) {
|
|
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
|
|
return;
|
|
}
|
|
const kategorie = await ausruestungsanfrageService.createKategorie(name.trim());
|
|
res.status(201).json({ success: true, data: kategorie });
|
|
} catch (error: any) {
|
|
if (error?.constraint === 'ausruestung_kategorien_katalog_name_key') {
|
|
res.status(409).json({ success: false, message: 'Kategorie existiert bereits' });
|
|
return;
|
|
}
|
|
logger.error('AusruestungsanfrageController.createKategorie error', { error });
|
|
res.status(500).json({ success: false, message: 'Kategorie konnte nicht erstellt werden' });
|
|
}
|
|
}
|
|
|
|
async updateKategorie(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
const { name } = req.body;
|
|
if (!name || name.trim().length === 0) {
|
|
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
|
|
return;
|
|
}
|
|
const kategorie = await ausruestungsanfrageService.updateKategorie(id, name.trim());
|
|
if (!kategorie) {
|
|
res.status(404).json({ success: false, message: 'Kategorie nicht gefunden' });
|
|
return;
|
|
}
|
|
res.status(200).json({ success: true, data: kategorie });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.updateKategorie error', { error });
|
|
res.status(500).json({ success: false, message: 'Kategorie konnte nicht aktualisiert werden' });
|
|
}
|
|
}
|
|
|
|
async deleteKategorie(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
await ausruestungsanfrageService.deleteKategorie(id);
|
|
res.status(200).json({ success: true, message: 'Kategorie gelöscht' });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.deleteKategorie error', { error });
|
|
res.status(500).json({ success: false, message: 'Kategorie konnte nicht gelöscht werden' });
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Catalog Items
|
|
// -------------------------------------------------------------------------
|
|
|
|
async getItems(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const kategorie = req.query.kategorie as string | undefined;
|
|
const kategorie_id = req.query.kategorie_id ? Number(req.query.kategorie_id) : undefined;
|
|
const aktiv = req.query.aktiv !== undefined ? req.query.aktiv === 'true' : undefined;
|
|
const items = await ausruestungsanfrageService.getItems({ kategorie, kategorie_id, aktiv });
|
|
res.status(200).json({ success: true, data: items });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.getItems error', { error });
|
|
res.status(500).json({ success: false, message: 'Artikel konnten nicht geladen werden' });
|
|
}
|
|
}
|
|
|
|
async getItemById(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
const item = await ausruestungsanfrageService.getItemById(id);
|
|
if (!item) {
|
|
res.status(404).json({ success: false, message: 'Artikel nicht gefunden' });
|
|
return;
|
|
}
|
|
res.status(200).json({ success: true, data: item });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.getItemById error', { error });
|
|
res.status(500).json({ success: false, message: 'Artikel konnte nicht geladen werden' });
|
|
}
|
|
}
|
|
|
|
async createItem(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { bezeichnung } = req.body;
|
|
if (!bezeichnung || bezeichnung.trim().length === 0) {
|
|
res.status(400).json({ success: false, message: 'Bezeichnung ist erforderlich' });
|
|
return;
|
|
}
|
|
const item = await ausruestungsanfrageService.createItem(req.body, req.user!.id);
|
|
res.status(201).json({ success: true, data: item });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.createItem error', { error });
|
|
res.status(500).json({ success: false, message: 'Artikel konnte nicht erstellt werden' });
|
|
}
|
|
}
|
|
|
|
async updateItem(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
const item = await ausruestungsanfrageService.updateItem(id, req.body, req.user!.id);
|
|
if (!item) {
|
|
res.status(404).json({ success: false, message: 'Artikel nicht gefunden' });
|
|
return;
|
|
}
|
|
res.status(200).json({ success: true, data: item });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.updateItem error', { error });
|
|
res.status(500).json({ success: false, message: 'Artikel konnte nicht aktualisiert werden' });
|
|
}
|
|
}
|
|
|
|
async deleteItem(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
await ausruestungsanfrageService.deleteItem(id);
|
|
res.status(200).json({ success: true, message: 'Artikel gelöscht' });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.deleteItem error', { error });
|
|
res.status(500).json({ success: false, message: 'Artikel konnte nicht gelöscht werden' });
|
|
}
|
|
}
|
|
|
|
async getCategories(_req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const categories = await ausruestungsanfrageService.getCategories();
|
|
res.status(200).json({ success: true, data: categories });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.getCategories error', { error });
|
|
res.status(500).json({ success: false, message: 'Kategorien konnten nicht geladen werden' });
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Artikel Eigenschaften (characteristics)
|
|
// -------------------------------------------------------------------------
|
|
|
|
async getArtikelEigenschaften(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const artikelId = Number(req.params.id);
|
|
const eigenschaften = await ausruestungsanfrageService.getArtikelEigenschaften(artikelId);
|
|
res.status(200).json({ success: true, data: eigenschaften });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.getArtikelEigenschaften error', { error });
|
|
res.status(500).json({ success: false, message: 'Eigenschaften konnten nicht geladen werden' });
|
|
}
|
|
}
|
|
|
|
async upsertArtikelEigenschaft(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const artikelId = Number(req.params.id);
|
|
const { name, typ, optionen, pflicht, sort_order, eigenschaft_id } = req.body;
|
|
if (!name || name.trim().length === 0) {
|
|
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
|
|
return;
|
|
}
|
|
if (!typ || !['options', 'freitext'].includes(typ)) {
|
|
res.status(400).json({ success: false, message: 'Typ muss "options" oder "freitext" sein' });
|
|
return;
|
|
}
|
|
const eigenschaft = await ausruestungsanfrageService.upsertArtikelEigenschaft(artikelId, {
|
|
id: eigenschaft_id,
|
|
name: name.trim(),
|
|
typ,
|
|
optionen: optionen || undefined,
|
|
pflicht: pflicht ?? false,
|
|
sort_order: sort_order ?? 0,
|
|
});
|
|
res.status(eigenschaft_id ? 200 : 201).json({ success: true, data: eigenschaft });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.upsertArtikelEigenschaft error', { error });
|
|
res.status(500).json({ success: false, message: 'Eigenschaft konnte nicht gespeichert werden' });
|
|
}
|
|
}
|
|
|
|
async deleteArtikelEigenschaft(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.eigenschaftId);
|
|
await ausruestungsanfrageService.deleteArtikelEigenschaft(id);
|
|
res.status(200).json({ success: true, message: 'Eigenschaft gelöscht' });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.deleteArtikelEigenschaft error', { error });
|
|
res.status(500).json({ success: false, message: 'Eigenschaft konnte nicht gelöscht werden' });
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Requests
|
|
// -------------------------------------------------------------------------
|
|
|
|
async getRequests(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const status = req.query.status as string | undefined;
|
|
const anfrager_id = req.query.anfrager_id as string | undefined;
|
|
const requests = await ausruestungsanfrageService.getRequests({ status, anfrager_id });
|
|
res.status(200).json({ success: true, data: requests });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.getRequests error', { error });
|
|
res.status(500).json({ success: false, message: 'Anfragen konnten nicht geladen werden' });
|
|
}
|
|
}
|
|
|
|
async getMyRequests(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const requests = await ausruestungsanfrageService.getMyRequests(req.user!.id);
|
|
res.status(200).json({ success: true, data: requests });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.getMyRequests error', { error });
|
|
res.status(500).json({ success: false, message: 'Anfragen konnten nicht geladen werden' });
|
|
}
|
|
}
|
|
|
|
async getRequestById(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
const request = await ausruestungsanfrageService.getRequestById(id);
|
|
if (!request) {
|
|
res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' });
|
|
return;
|
|
}
|
|
res.status(200).json({ success: true, data: request });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.getRequestById error', { error });
|
|
res.status(500).json({ success: false, message: 'Anfrage konnte nicht geladen werden' });
|
|
}
|
|
}
|
|
|
|
async createRequest(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { items, notizen, bezeichnung, fuer_benutzer_id } = req.body as {
|
|
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[];
|
|
notizen?: string;
|
|
bezeichnung?: string;
|
|
fuer_benutzer_id?: string;
|
|
};
|
|
|
|
if (!items || items.length === 0) {
|
|
res.status(400).json({ success: false, message: 'Mindestens eine Position ist erforderlich' });
|
|
return;
|
|
}
|
|
|
|
for (const item of items) {
|
|
if (!item.bezeichnung || item.bezeichnung.trim().length === 0) {
|
|
res.status(400).json({ success: false, message: 'Bezeichnung ist für alle Positionen erforderlich' });
|
|
return;
|
|
}
|
|
if (!item.menge || item.menge < 1) {
|
|
res.status(400).json({ success: false, message: 'Menge muss mindestens 1 sein' });
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Determine anfrager: self or on behalf of another user
|
|
let anfragerId = req.user!.id;
|
|
if (fuer_benutzer_id && fuer_benutzer_id !== req.user!.id) {
|
|
const groups = req.user?.groups ?? [];
|
|
const canOrderForUser = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'ausruestungsanfrage:order_for_user');
|
|
if (!canOrderForUser) {
|
|
res.status(403).json({ success: false, message: 'Keine Berechtigung für Bestellung im Auftrag' });
|
|
return;
|
|
}
|
|
anfragerId = fuer_benutzer_id;
|
|
}
|
|
|
|
const request = await ausruestungsanfrageService.createRequest(anfragerId, items, notizen, bezeichnung);
|
|
res.status(201).json({ success: true, data: request });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.createRequest error', { error });
|
|
res.status(500).json({ success: false, message: 'Anfrage konnte nicht erstellt werden' });
|
|
}
|
|
}
|
|
|
|
async updateRequest(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
const { bezeichnung, notizen, items } = req.body as {
|
|
bezeichnung?: string;
|
|
notizen?: string;
|
|
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[];
|
|
};
|
|
|
|
// Validate items if provided
|
|
if (items) {
|
|
if (items.length === 0) {
|
|
res.status(400).json({ success: false, message: 'Mindestens eine Position ist erforderlich' });
|
|
return;
|
|
}
|
|
for (const item of items) {
|
|
if (!item.bezeichnung || item.bezeichnung.trim().length === 0) {
|
|
res.status(400).json({ success: false, message: 'Bezeichnung ist für alle Positionen erforderlich' });
|
|
return;
|
|
}
|
|
if (!item.menge || item.menge < 1) {
|
|
res.status(400).json({ success: false, message: 'Menge muss mindestens 1 sein' });
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const existing = await ausruestungsanfrageService.getRequestById(id);
|
|
if (!existing) {
|
|
res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' });
|
|
return;
|
|
}
|
|
|
|
// Check permission: owner + status=offen, OR ausruestungsanfrage:edit
|
|
const groups = req.user?.groups ?? [];
|
|
const canEditAny = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'ausruestungsanfrage:edit');
|
|
const isOwner = existing.anfrage.anfrager_id === req.user!.id;
|
|
if (!canEditAny && !(isOwner && existing.anfrage.status === 'offen')) {
|
|
res.status(403).json({ success: false, message: 'Keine Berechtigung zum Bearbeiten dieser Anfrage' });
|
|
return;
|
|
}
|
|
|
|
const updated = await ausruestungsanfrageService.updateRequest(id, { bezeichnung, notizen, items });
|
|
res.status(200).json({ success: true, data: updated });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.updateRequest error', { error });
|
|
res.status(500).json({ success: false, message: 'Anfrage konnte nicht aktualisiert werden' });
|
|
}
|
|
}
|
|
|
|
async updateRequestStatus(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
const { status, admin_notizen } = req.body as {
|
|
status?: string;
|
|
admin_notizen?: string;
|
|
};
|
|
|
|
if (!status) {
|
|
res.status(400).json({ success: false, message: 'Status ist erforderlich' });
|
|
return;
|
|
}
|
|
|
|
const validStatuses = ['offen', 'genehmigt', 'abgelehnt', 'bestellt', 'erledigt'];
|
|
if (!validStatuses.includes(status)) {
|
|
res.status(400).json({ success: false, message: `Ungültiger Status. Erlaubt: ${validStatuses.join(', ')}` });
|
|
return;
|
|
}
|
|
|
|
// Fetch request to get anfrager_id for notification
|
|
const existing = await ausruestungsanfrageService.getRequestById(id);
|
|
if (!existing) {
|
|
res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' });
|
|
return;
|
|
}
|
|
|
|
const updated = await ausruestungsanfrageService.updateRequestStatus(id, status, admin_notizen, req.user!.id);
|
|
|
|
// Notify requester on status changes
|
|
if (['genehmigt', 'abgelehnt', 'bestellt', 'erledigt'].includes(status)) {
|
|
const orderLabel = existing.anfrage.bestell_jahr && existing.anfrage.bestell_nummer
|
|
? `${existing.anfrage.bestell_jahr}/${String(existing.anfrage.bestell_nummer).padStart(3, '0')}`
|
|
: `#${id}`;
|
|
await notificationService.createNotification({
|
|
user_id: existing.anfrage.anfrager_id,
|
|
typ: 'ausruestung_anfrage',
|
|
titel: status === 'genehmigt' ? 'Anfrage genehmigt' : status === 'abgelehnt' ? 'Anfrage abgelehnt' : `Anfrage ${status}`,
|
|
nachricht: `Deine Ausrüstungsanfrage ${orderLabel} wurde ${status === 'genehmigt' ? 'genehmigt' : status === 'abgelehnt' ? 'abgelehnt' : status}.`,
|
|
schwere: status === 'abgelehnt' ? 'warnung' : 'info',
|
|
link: '/ausruestungsanfrage',
|
|
quell_id: String(id),
|
|
quell_typ: 'ausruestung_anfrage',
|
|
});
|
|
}
|
|
|
|
res.status(200).json({ success: true, data: updated });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.updateRequestStatus error', { error });
|
|
res.status(500).json({ success: false, message: 'Status konnte nicht aktualisiert werden' });
|
|
}
|
|
}
|
|
|
|
async deleteRequest(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const id = Number(req.params.id);
|
|
await ausruestungsanfrageService.deleteRequest(id);
|
|
res.status(200).json({ success: true, message: 'Anfrage gelöscht' });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.deleteRequest error', { error });
|
|
res.status(500).json({ success: false, message: 'Anfrage konnte nicht gelöscht werden' });
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Overview
|
|
// -------------------------------------------------------------------------
|
|
|
|
async getOverview(_req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const overview = await ausruestungsanfrageService.getOverview();
|
|
res.status(200).json({ success: true, data: overview });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.getOverview error', { error });
|
|
res.status(500).json({ success: false, message: 'Übersicht konnte nicht geladen werden' });
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Linking
|
|
// -------------------------------------------------------------------------
|
|
|
|
async linkToOrder(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const anfrageId = Number(req.params.id);
|
|
const { bestellung_id } = req.body as { bestellung_id?: number };
|
|
|
|
if (!bestellung_id) {
|
|
res.status(400).json({ success: false, message: 'bestellung_id ist erforderlich' });
|
|
return;
|
|
}
|
|
|
|
await ausruestungsanfrageService.linkToOrder(anfrageId, bestellung_id);
|
|
res.status(200).json({ success: true, message: 'Verknüpfung erstellt' });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.linkToOrder error', { error });
|
|
res.status(500).json({ success: false, message: 'Verknüpfung konnte nicht erstellt werden' });
|
|
}
|
|
}
|
|
|
|
async unlinkFromOrder(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const anfrageId = Number(req.params.id);
|
|
const bestellungId = Number(req.params.bestellungId);
|
|
await ausruestungsanfrageService.unlinkFromOrder(anfrageId, bestellungId);
|
|
res.status(200).json({ success: true, message: 'Verknüpfung entfernt' });
|
|
} catch (error) {
|
|
logger.error('AusruestungsanfrageController.unlinkFromOrder error', { error });
|
|
res.status(500).json({ success: false, message: 'Verknüpfung konnte nicht entfernt werden' });
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new AusruestungsanfrageController();
|