rework internal order system

This commit is contained in:
Matthias Hochmeister
2026-03-24 08:11:32 +01:00
parent 742c37b8de
commit 99f02b8425
11 changed files with 792 additions and 397 deletions

View File

@@ -1,6 +1,7 @@
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 {
@@ -129,9 +130,11 @@ class AusruestungsanfrageController {
async createRequest(req: Request, res: Response): Promise<void> {
try {
const { items, notizen } = req.body as {
const { items, notizen, bezeichnung, fuer_benutzer_id } = req.body as {
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string }[];
notizen?: string;
bezeichnung?: string;
fuer_benutzer_id?: string;
};
if (!items || items.length === 0) {
@@ -150,7 +153,19 @@ class AusruestungsanfrageController {
}
}
const request = await ausruestungsanfrageService.createRequest(req.user!.id, items, notizen);
// 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 });
@@ -158,6 +173,56 @@ class AusruestungsanfrageController {
}
}
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 }[];
};
// 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.anfrager_id === req.user!.id;
if (!canEditAny && !(isOwner && existing.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);

View File

@@ -0,0 +1,26 @@
-- Migration 047: Update Ausrüstungsanfrage (Internal Orders) system
-- - Add bezeichnung column to ausruestung_anfragen
-- - Rename permissions: approve_requests → approve, view_overview → view_all
-- - Add new permission: ausruestungsanfrage:edit
-- 1. Add bezeichnung column to anfragen table
ALTER TABLE ausruestung_anfragen ADD COLUMN IF NOT EXISTS bezeichnung TEXT;
-- 2. Rename permissions
UPDATE permissions SET name = 'ausruestungsanfrage:approve'
WHERE name = 'ausruestungsanfrage:approve_requests';
UPDATE permissions SET name = 'ausruestungsanfrage:view_all'
WHERE name = 'ausruestungsanfrage:view_overview';
-- 3. Add new edit permission
INSERT INTO permissions (name, beschreibung, feature_group)
VALUES ('ausruestungsanfrage:edit', 'Alle Anfragen bearbeiten (unabhängig von Status/Besitzer)', 'ausruestungsanfrage')
ON CONFLICT (name) DO NOTHING;
-- 4. Grant new edit permission to groups that had approve_requests (now approve)
INSERT INTO group_permissions (group_name, permission_name)
SELECT gp.group_name, 'ausruestungsanfrage:edit'
FROM group_permissions gp
WHERE gp.permission_name = 'ausruestungsanfrage:approve'
ON CONFLICT DO NOTHING;

View File

@@ -21,18 +21,19 @@ router.get('/categories', authenticate, requirePermission('ausruestungsanfrage:v
// Overview
// ---------------------------------------------------------------------------
router.get('/overview', authenticate, requirePermission('ausruestungsanfrage:view_overview'), ausruestungsanfrageController.getOverview.bind(ausruestungsanfrageController));
router.get('/overview', authenticate, requirePermission('ausruestungsanfrage:view_all'), ausruestungsanfrageController.getOverview.bind(ausruestungsanfrageController));
// ---------------------------------------------------------------------------
// Requests
// ---------------------------------------------------------------------------
router.get('/requests', authenticate, requirePermission('ausruestungsanfrage:approve_requests'), ausruestungsanfrageController.getRequests.bind(ausruestungsanfrageController));
router.get('/requests', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.getRequests.bind(ausruestungsanfrageController));
router.get('/requests/my', authenticate, ausruestungsanfrageController.getMyRequests.bind(ausruestungsanfrageController));
router.get('/requests/:id', authenticate, ausruestungsanfrageController.getRequestById.bind(ausruestungsanfrageController));
router.post('/requests', authenticate, requirePermission('ausruestungsanfrage:create_request'), ausruestungsanfrageController.createRequest.bind(ausruestungsanfrageController));
router.patch('/requests/:id/status', authenticate, requirePermission('ausruestungsanfrage:approve_requests'), ausruestungsanfrageController.updateRequestStatus.bind(ausruestungsanfrageController));
router.delete('/requests/:id', authenticate, requirePermission('ausruestungsanfrage:approve_requests'), ausruestungsanfrageController.deleteRequest.bind(ausruestungsanfrageController));
router.patch('/requests/:id', authenticate, ausruestungsanfrageController.updateRequest.bind(ausruestungsanfrageController));
router.patch('/requests/:id/status', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.updateRequestStatus.bind(ausruestungsanfrageController));
router.delete('/requests/:id', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.deleteRequest.bind(ausruestungsanfrageController));
// ---------------------------------------------------------------------------
// Linking requests to orders

View File

@@ -189,7 +189,7 @@ async function getRequestById(id: number) {
return {
...reqResult.rows[0],
positionen: positionen.rows,
bestellungen: bestellungen.rows,
linked_bestellungen: bestellungen.rows,
};
}
@@ -197,6 +197,7 @@ async function createRequest(
userId: string,
items: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string }[],
notizen?: string,
bezeichnung?: string,
) {
const client = await pool.connect();
try {
@@ -213,10 +214,10 @@ async function createRequest(
const nextNr = maxResult.rows[0].next_nr;
const anfrageResult = await client.query(
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bestell_nummer, bestell_jahr)
VALUES ($1, $2, $3, $4)
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, bestell_nummer, bestell_jahr)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[userId, notizen || null, nextNr, currentYear],
[userId, notizen || null, bezeichnung || null, nextNr, currentYear],
);
const anfrage = anfrageResult.rows[0];
@@ -252,6 +253,74 @@ async function createRequest(
}
}
async function updateRequest(
id: number,
data: {
bezeichnung?: string;
notizen?: string;
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string }[];
},
) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Update anfrage fields
const fields: string[] = [];
const params: unknown[] = [];
if (data.bezeichnung !== undefined) {
params.push(data.bezeichnung || null);
fields.push(`bezeichnung = $${params.length}`);
}
if (data.notizen !== undefined) {
params.push(data.notizen || null);
fields.push(`notizen = $${params.length}`);
}
if (fields.length > 0) {
params.push(new Date());
fields.push(`aktualisiert_am = $${params.length}`);
params.push(id);
await client.query(
`UPDATE ausruestung_anfragen SET ${fields.join(', ')} WHERE id = $${params.length}`,
params,
);
}
// Replace items if provided
if (data.items) {
await client.query('DELETE FROM ausruestung_anfrage_positionen WHERE anfrage_id = $1', [id]);
for (const item of data.items) {
let bezeichnung = item.bezeichnung;
if (item.artikel_id) {
const artikelResult = await client.query(
'SELECT bezeichnung FROM ausruestung_artikel WHERE id = $1',
[item.artikel_id],
);
if (artikelResult.rows.length > 0) {
bezeichnung = artikelResult.rows[0].bezeichnung;
}
}
await client.query(
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen)
VALUES ($1, $2, $3, $4, $5)`,
[id, item.artikel_id || null, bezeichnung, item.menge, item.notizen || null],
);
}
}
await client.query('COMMIT');
return getRequestById(id);
} catch (error) {
await client.query('ROLLBACK');
logger.error('ausruestungsanfrageService.updateRequest failed', { error });
throw error;
} finally {
client.release();
}
}
async function updateRequestStatus(
id: number,
status: string,
@@ -353,6 +422,7 @@ export default {
getMyRequests,
getRequestById,
createRequest,
updateRequest,
updateRequestStatus,
deleteRequest,
linkToOrder,