new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 13:08:19 +01:00
parent 83b84664ce
commit 5032e1593b
41 changed files with 5157 additions and 40 deletions

View File

@@ -0,0 +1,466 @@
import { Request, Response } from 'express';
import bestellungService from '../services/bestellung.service';
import logger from '../utils/logger';
import fs from 'fs';
// Helper to safely extract a route param as string
const param = (req: Request, key: string): string => req.params[key] as string;
class BestellungController {
// ---------------------------------------------------------------------------
// Vendors
// ---------------------------------------------------------------------------
async listVendors(_req: Request, res: Response): Promise<void> {
try {
const vendors = await bestellungService.getVendors();
res.status(200).json({ success: true, data: vendors });
} catch (error) {
logger.error('BestellungController.listVendors error', { error });
res.status(500).json({ success: false, message: 'Lieferanten konnten nicht geladen werden' });
}
}
async createVendor(req: Request, res: Response): Promise<void> {
const { name } = req.body;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
return;
}
try {
const vendor = await bestellungService.createVendor(req.body, req.user!.id);
res.status(201).json({ success: true, data: vendor });
} catch (error) {
logger.error('BestellungController.createVendor error', { error });
res.status(500).json({ success: false, message: 'Lieferant konnte nicht erstellt werden' });
}
}
async updateVendor(req: Request, res: Response): Promise<void> {
const id = parseInt(param(req, 'id'), 10);
if (isNaN(id)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const vendor = await bestellungService.updateVendor(id, req.body, req.user!.id);
if (!vendor) {
res.status(404).json({ success: false, message: 'Lieferant nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: vendor });
} catch (error) {
logger.error('BestellungController.updateVendor error', { error });
res.status(500).json({ success: false, message: 'Lieferant konnte nicht aktualisiert werden' });
}
}
async deleteVendor(req: Request, res: Response): Promise<void> {
const id = parseInt(param(req, 'id'), 10);
if (isNaN(id)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const deleted = await bestellungService.deleteVendor(id);
if (!deleted) {
res.status(404).json({ success: false, message: 'Lieferant nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Lieferant gelöscht' });
} catch (error) {
logger.error('BestellungController.deleteVendor error', { error });
res.status(500).json({ success: false, message: 'Lieferant konnte nicht gelöscht werden' });
}
}
// ---------------------------------------------------------------------------
// Orders
// ---------------------------------------------------------------------------
async listOrders(req: Request, res: Response): Promise<void> {
try {
const filters: { status?: string; lieferant_id?: number; besteller_id?: string } = {};
if (req.query.status) filters.status = req.query.status as string;
if (req.query.lieferant_id) filters.lieferant_id = parseInt(req.query.lieferant_id as string, 10);
if (req.query.besteller_id) filters.besteller_id = req.query.besteller_id as string;
const orders = await bestellungService.getOrders(filters);
res.status(200).json({ success: true, data: orders });
} catch (error) {
logger.error('BestellungController.listOrders error', { error });
res.status(500).json({ success: false, message: 'Bestellungen konnten nicht geladen werden' });
}
}
async getOrder(req: Request, res: Response): Promise<void> {
const id = parseInt(param(req, 'id'), 10);
if (isNaN(id)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const order = await bestellungService.getOrderById(id);
if (!order) {
res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: order });
} catch (error) {
logger.error('BestellungController.getOrder error', { error });
res.status(500).json({ success: false, message: 'Bestellung konnte nicht geladen werden' });
}
}
async createOrder(req: Request, res: Response): Promise<void> {
const { titel } = req.body;
if (!titel || typeof titel !== 'string' || titel.trim().length === 0) {
res.status(400).json({ success: false, message: 'Titel ist erforderlich' });
return;
}
try {
const order = await bestellungService.createOrder(req.body, req.user!.id);
res.status(201).json({ success: true, data: order });
} catch (error) {
logger.error('BestellungController.createOrder error', { error });
res.status(500).json({ success: false, message: 'Bestellung konnte nicht erstellt werden' });
}
}
async updateOrder(req: Request, res: Response): Promise<void> {
const id = parseInt(param(req, 'id'), 10);
if (isNaN(id)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const order = await bestellungService.updateOrder(id, req.body, req.user!.id);
if (!order) {
res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: order });
} catch (error) {
logger.error('BestellungController.updateOrder error', { error });
res.status(500).json({ success: false, message: 'Bestellung konnte nicht aktualisiert werden' });
}
}
async deleteOrder(req: Request, res: Response): Promise<void> {
const id = parseInt(param(req, 'id'), 10);
if (isNaN(id)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const deleted = await bestellungService.deleteOrder(id, req.user!.id);
if (!deleted) {
res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Bestellung gelöscht' });
} catch (error) {
logger.error('BestellungController.deleteOrder error', { error });
res.status(500).json({ success: false, message: 'Bestellung konnte nicht gelöscht werden' });
}
}
async updateStatus(req: Request, res: Response): Promise<void> {
const id = parseInt(param(req, 'id'), 10);
if (isNaN(id)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
const { status } = req.body;
if (!status || typeof status !== 'string') {
res.status(400).json({ success: false, message: 'Status ist erforderlich' });
return;
}
try {
const order = await bestellungService.updateOrderStatus(id, status, req.user!.id);
if (!order) {
res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: order });
} catch (error: any) {
if (error.message?.includes('Ungültiger Statusübergang')) {
res.status(400).json({ success: false, message: error.message });
return;
}
logger.error('BestellungController.updateStatus error', { error });
res.status(500).json({ success: false, message: 'Status konnte nicht aktualisiert werden' });
}
}
// ---------------------------------------------------------------------------
// Line Items
// ---------------------------------------------------------------------------
async addLineItem(req: Request, res: Response): Promise<void> {
const bestellungId = parseInt(param(req, 'id'), 10);
if (isNaN(bestellungId)) {
res.status(400).json({ success: false, message: 'Ungültige Bestellungs-ID' });
return;
}
const { artikel, menge } = req.body;
if (!artikel || typeof artikel !== 'string' || artikel.trim().length === 0) {
res.status(400).json({ success: false, message: 'Artikel ist erforderlich' });
return;
}
if (menge === undefined || menge === null || menge <= 0) {
res.status(400).json({ success: false, message: 'Menge muss größer als 0 sein' });
return;
}
try {
const item = await bestellungService.addLineItem(bestellungId, req.body, req.user!.id);
res.status(201).json({ success: true, data: item });
} catch (error) {
logger.error('BestellungController.addLineItem error', { error });
res.status(500).json({ success: false, message: 'Position konnte nicht hinzugefügt werden' });
}
}
async updateLineItem(req: Request, res: Response): Promise<void> {
const itemId = parseInt(param(req, 'itemId'), 10);
if (isNaN(itemId)) {
res.status(400).json({ success: false, message: 'Ungültige Position-ID' });
return;
}
try {
const item = await bestellungService.updateLineItem(itemId, req.body, req.user!.id);
if (!item) {
res.status(404).json({ success: false, message: 'Position nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: item });
} catch (error) {
logger.error('BestellungController.updateLineItem error', { error });
res.status(500).json({ success: false, message: 'Position konnte nicht aktualisiert werden' });
}
}
async deleteLineItem(req: Request, res: Response): Promise<void> {
const itemId = parseInt(param(req, 'itemId'), 10);
if (isNaN(itemId)) {
res.status(400).json({ success: false, message: 'Ungültige Position-ID' });
return;
}
try {
const deleted = await bestellungService.deleteLineItem(itemId, req.user!.id);
if (!deleted) {
res.status(404).json({ success: false, message: 'Position nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Position gelöscht' });
} catch (error) {
logger.error('BestellungController.deleteLineItem error', { error });
res.status(500).json({ success: false, message: 'Position konnte nicht gelöscht werden' });
}
}
async updateReceivedQuantity(req: Request, res: Response): Promise<void> {
const itemId = parseInt(param(req, 'itemId'), 10);
if (isNaN(itemId)) {
res.status(400).json({ success: false, message: 'Ungültige Position-ID' });
return;
}
const { menge } = req.body;
if (menge === undefined || menge === null || menge < 0) {
res.status(400).json({ success: false, message: 'Erhaltene Menge muss >= 0 sein' });
return;
}
try {
const item = await bestellungService.updateReceivedQuantity(itemId, menge, req.user!.id);
if (!item) {
res.status(404).json({ success: false, message: 'Position nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: item });
} catch (error) {
logger.error('BestellungController.updateReceivedQuantity error', { error });
res.status(500).json({ success: false, message: 'Liefermenge konnte nicht aktualisiert werden' });
}
}
// ---------------------------------------------------------------------------
// Files
// ---------------------------------------------------------------------------
async uploadFile(req: Request, res: Response): Promise<void> {
const bestellungId = parseInt(param(req, 'id'), 10);
if (isNaN(bestellungId)) {
res.status(400).json({ success: false, message: 'Ungültige Bestellungs-ID' });
return;
}
const file = (req as any).file;
if (!file) {
res.status(400).json({ success: false, message: 'Keine Datei hochgeladen' });
return;
}
try {
const fileRecord = await bestellungService.addFile(bestellungId, {
dateiname: file.originalname,
dateipfad: file.path,
dateityp: file.mimetype,
dateigroesse: file.size,
}, req.user!.id);
res.status(201).json({ success: true, data: fileRecord });
} catch (error) {
logger.error('BestellungController.uploadFile error', { error });
res.status(500).json({ success: false, message: 'Datei konnte nicht hochgeladen werden' });
}
}
async deleteFile(req: Request, res: Response): Promise<void> {
const fileId = parseInt(param(req, 'fileId'), 10);
if (isNaN(fileId)) {
res.status(400).json({ success: false, message: 'Ungültige Datei-ID' });
return;
}
try {
const result = await bestellungService.deleteFile(fileId, req.user!.id);
if (!result) {
res.status(404).json({ success: false, message: 'Datei nicht gefunden' });
return;
}
// Remove from disk
try {
if (result.dateipfad && fs.existsSync(result.dateipfad)) {
fs.unlinkSync(result.dateipfad);
}
} catch (err) {
logger.warn('Failed to delete file from disk', { path: result.dateipfad, error: err });
}
res.status(200).json({ success: true, message: 'Datei gelöscht' });
} catch (error) {
logger.error('BestellungController.deleteFile error', { error });
res.status(500).json({ success: false, message: 'Datei konnte nicht gelöscht werden' });
}
}
async listFiles(req: Request, res: Response): Promise<void> {
const bestellungId = parseInt(param(req, 'id'), 10);
if (isNaN(bestellungId)) {
res.status(400).json({ success: false, message: 'Ungültige Bestellungs-ID' });
return;
}
try {
const files = await bestellungService.getFilesByOrder(bestellungId);
res.status(200).json({ success: true, data: files });
} catch (error) {
logger.error('BestellungController.listFiles error', { error });
res.status(500).json({ success: false, message: 'Dateien konnten nicht geladen werden' });
}
}
// ---------------------------------------------------------------------------
// Reminders
// ---------------------------------------------------------------------------
async addReminder(req: Request, res: Response): Promise<void> {
const bestellungId = parseInt(param(req, 'id'), 10);
if (isNaN(bestellungId)) {
res.status(400).json({ success: false, message: 'Ungültige Bestellungs-ID' });
return;
}
const { titel, faellig_am } = req.body;
if (!titel || typeof titel !== 'string' || titel.trim().length === 0) {
res.status(400).json({ success: false, message: 'Titel ist erforderlich' });
return;
}
if (!faellig_am) {
res.status(400).json({ success: false, message: 'Fälligkeitsdatum ist erforderlich' });
return;
}
try {
const reminder = await bestellungService.addReminder(bestellungId, req.body, req.user!.id);
res.status(201).json({ success: true, data: reminder });
} catch (error) {
logger.error('BestellungController.addReminder error', { error });
res.status(500).json({ success: false, message: 'Erinnerung konnte nicht erstellt werden' });
}
}
async markReminderDone(req: Request, res: Response): Promise<void> {
const remId = parseInt(param(req, 'remId'), 10);
if (isNaN(remId)) {
res.status(400).json({ success: false, message: 'Ungültige Erinnerungs-ID' });
return;
}
try {
const reminder = await bestellungService.markReminderDone(remId, req.user!.id);
if (!reminder) {
res.status(404).json({ success: false, message: 'Erinnerung nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: reminder });
} catch (error) {
logger.error('BestellungController.markReminderDone error', { error });
res.status(500).json({ success: false, message: 'Erinnerung konnte nicht als erledigt markiert werden' });
}
}
async deleteReminder(req: Request, res: Response): Promise<void> {
const remId = parseInt(param(req, 'remId'), 10);
if (isNaN(remId)) {
res.status(400).json({ success: false, message: 'Ungültige Erinnerungs-ID' });
return;
}
try {
const deleted = await bestellungService.deleteReminder(remId);
if (!deleted) {
res.status(404).json({ success: false, message: 'Erinnerung nicht gefunden' });
return;
}
res.status(200).json({ success: true, message: 'Erinnerung gelöscht' });
} catch (error) {
logger.error('BestellungController.deleteReminder error', { error });
res.status(500).json({ success: false, message: 'Erinnerung konnte nicht gelöscht werden' });
}
}
// ---------------------------------------------------------------------------
// History
// ---------------------------------------------------------------------------
async getHistory(req: Request, res: Response): Promise<void> {
const bestellungId = parseInt(param(req, 'id'), 10);
if (isNaN(bestellungId)) {
res.status(400).json({ success: false, message: 'Ungültige Bestellungs-ID' });
return;
}
try {
const history = await bestellungService.getHistory(bestellungId);
res.status(200).json({ success: true, data: history });
} catch (error) {
logger.error('BestellungController.getHistory error', { error });
res.status(500).json({ success: false, message: 'Historie konnte nicht geladen werden' });
}
}
// ---------------------------------------------------------------------------
// Export (placeholder — returns order detail as JSON for now)
// ---------------------------------------------------------------------------
async exportOrder(req: Request, res: Response): Promise<void> {
const id = parseInt(param(req, 'id'), 10);
if (isNaN(id)) {
res.status(400).json({ success: false, message: 'Ungültige ID' });
return;
}
try {
const order = await bestellungService.getOrderById(id);
if (!order) {
res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: order });
} catch (error) {
logger.error('BestellungController.exportOrder error', { error });
res.status(500).json({ success: false, message: 'Export fehlgeschlagen' });
}
}
}
export default new BestellungController();

View File

@@ -0,0 +1,256 @@
import { Request, Response } from 'express';
import shopService from '../services/shop.service';
import notificationService from '../services/notification.service';
import logger from '../utils/logger';
class ShopController {
// -------------------------------------------------------------------------
// Catalog Items
// -------------------------------------------------------------------------
async getItems(req: Request, res: Response): Promise<void> {
try {
const kategorie = req.query.kategorie as string | undefined;
const aktiv = req.query.aktiv !== undefined ? req.query.aktiv === 'true' : undefined;
const items = await shopService.getItems({ kategorie, aktiv });
res.status(200).json({ success: true, data: items });
} catch (error) {
logger.error('ShopController.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 shopService.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('ShopController.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 shopService.createItem(req.body, req.user!.id);
res.status(201).json({ success: true, data: item });
} catch (error) {
logger.error('ShopController.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 shopService.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('ShopController.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 shopService.deleteItem(id);
res.status(200).json({ success: true, message: 'Artikel gelöscht' });
} catch (error) {
logger.error('ShopController.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 shopService.getCategories();
res.status(200).json({ success: true, data: categories });
} catch (error) {
logger.error('ShopController.getCategories error', { error });
res.status(500).json({ success: false, message: 'Kategorien konnten nicht geladen 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 shopService.getRequests({ status, anfrager_id });
res.status(200).json({ success: true, data: requests });
} catch (error) {
logger.error('ShopController.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 shopService.getMyRequests(req.user!.id);
res.status(200).json({ success: true, data: requests });
} catch (error) {
logger.error('ShopController.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 shopService.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('ShopController.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 } = req.body as {
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string }[];
notizen?: 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;
}
}
const request = await shopService.createRequest(req.user!.id, items, notizen);
res.status(201).json({ success: true, data: request });
} catch (error) {
logger.error('ShopController.createRequest error', { error });
res.status(500).json({ success: false, message: 'Anfrage konnte nicht erstellt 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 shopService.getRequestById(id);
if (!existing) {
res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' });
return;
}
const updated = await shopService.updateRequestStatus(id, status, admin_notizen, req.user!.id);
// Notify requester on status changes
if (['genehmigt', 'abgelehnt', 'bestellt', 'erledigt'].includes(status)) {
await notificationService.createNotification({
user_id: existing.anfrager_id,
typ: 'shop_anfrage',
titel: status === 'genehmigt' ? 'Anfrage genehmigt' : status === 'abgelehnt' ? 'Anfrage abgelehnt' : `Anfrage ${status}`,
nachricht: `Deine Shop-Anfrage #${id} wurde ${status === 'genehmigt' ? 'genehmigt' : status === 'abgelehnt' ? 'abgelehnt' : status}.`,
schwere: status === 'abgelehnt' ? 'warnung' : 'info',
link: '/shop',
quell_id: String(id),
quell_typ: 'shop_anfrage',
});
}
res.status(200).json({ success: true, data: updated });
} catch (error) {
logger.error('ShopController.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 shopService.deleteRequest(id);
res.status(200).json({ success: true, message: 'Anfrage gelöscht' });
} catch (error) {
logger.error('ShopController.deleteRequest error', { error });
res.status(500).json({ success: false, message: 'Anfrage konnte nicht gelöscht 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 shopService.linkToOrder(anfrageId, bestellung_id);
res.status(200).json({ success: true, message: 'Verknüpfung erstellt' });
} catch (error) {
logger.error('ShopController.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 shopService.unlinkFromOrder(anfrageId, bestellungId);
res.status(200).json({ success: true, message: 'Verknüpfung entfernt' });
} catch (error) {
logger.error('ShopController.unlinkFromOrder error', { error });
res.status(500).json({ success: false, message: 'Verknüpfung konnte nicht entfernt werden' });
}
}
}
export default new ShopController();