rework internal order system

This commit is contained in:
Matthias Hochmeister
2026-03-24 14:02:16 +01:00
parent 90944ca5f6
commit abb337c683
9 changed files with 261 additions and 75 deletions

View File

@@ -208,6 +208,20 @@ class AusruestungsanfrageController {
}
}
// -------------------------------------------------------------------------
// Users (for "order on behalf of" autocomplete)
// -------------------------------------------------------------------------
async getAllUsers(_req: Request, res: Response): Promise<void> {
try {
const users = await ausruestungsanfrageService.getAllUsers();
res.status(200).json({ success: true, data: users });
} catch (error) {
logger.error('AusruestungsanfrageController.getAllUsers error', { error });
res.status(500).json({ success: false, message: 'Benutzer konnten nicht geladen werden' });
}
}
// -------------------------------------------------------------------------
// Requests
// -------------------------------------------------------------------------
@@ -251,11 +265,12 @@ class AusruestungsanfrageController {
async createRequest(req: Request, res: Response): Promise<void> {
try {
const { items, notizen, bezeichnung, fuer_benutzer_id } = req.body as {
const { items, notizen, bezeichnung, fuer_benutzer_id, fuer_benutzer_name } = 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;
fuer_benutzer_name?: string;
};
if (!items || items.length === 0) {
@@ -276,6 +291,7 @@ class AusruestungsanfrageController {
// Determine anfrager: self or on behalf of another user
let anfragerId = req.user!.id;
let storedFuerBenutzerName: string | undefined;
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');
@@ -284,9 +300,18 @@ class AusruestungsanfrageController {
return;
}
anfragerId = fuer_benutzer_id;
} else if (fuer_benutzer_name && !fuer_benutzer_id) {
// Custom name for user not in system — keep anfrager_id as current user
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;
}
storedFuerBenutzerName = fuer_benutzer_name;
}
const request = await ausruestungsanfrageService.createRequest(anfragerId, items, notizen, bezeichnung);
const request = await ausruestungsanfrageService.createRequest(anfragerId, items, notizen, bezeichnung, storedFuerBenutzerName);
res.status(201).json({ success: true, data: request });
} catch (error) {
logger.error('AusruestungsanfrageController.createRequest error', { error });

View File

@@ -0,0 +1,2 @@
-- Add fuer_benutzer_name column for custom names (users not in system)
ALTER TABLE ausruestung_anfragen ADD COLUMN IF NOT EXISTS fuer_benutzer_name TEXT;

View File

@@ -32,6 +32,12 @@ router.delete('/eigenschaften/:eigenschaftId', authenticate, requirePermission('
// Legacy text-based categories
router.get('/categories', authenticate, requirePermission('ausruestungsanfrage:view'), ausruestungsanfrageController.getCategories.bind(ausruestungsanfrageController));
// ---------------------------------------------------------------------------
// Users (for "order on behalf of" autocomplete)
// ---------------------------------------------------------------------------
router.get('/users', authenticate, requirePermission('ausruestungsanfrage:order_for_user'), ausruestungsanfrageController.getAllUsers.bind(ausruestungsanfrageController));
// ---------------------------------------------------------------------------
// Overview
// ---------------------------------------------------------------------------

View File

@@ -266,6 +266,20 @@ async function deleteArtikelEigenschaft(id: number) {
await pool.query('DELETE FROM ausruestung_artikel_eigenschaften WHERE id = $1', [id]);
}
// ---------------------------------------------------------------------------
// Users (for "order on behalf of" autocomplete)
// ---------------------------------------------------------------------------
async function getAllUsers() {
const result = await pool.query(
`SELECT id, COALESCE(given_name || ' ' || family_name, name) AS name
FROM users
WHERE is_active = true
ORDER BY name`,
);
return result.rows;
}
// ---------------------------------------------------------------------------
// Requests (ausruestung_anfragen)
// ---------------------------------------------------------------------------
@@ -288,6 +302,7 @@ async function getRequests(filters?: { status?: string; anfrager_id?: string })
`SELECT a.*,
COALESCE(u.given_name || ' ' || u.family_name, u.name) AS anfrager_name,
COALESCE(u2.given_name || ' ' || u2.family_name, u2.name) AS bearbeitet_von_name,
a.fuer_benutzer_name,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count
FROM ausruestung_anfragen a
@@ -316,7 +331,8 @@ async function getRequestById(id: number) {
const reqResult = await pool.query(
`SELECT a.*,
COALESCE(u.given_name || ' ' || u.family_name, u.name) AS anfrager_name,
COALESCE(u2.given_name || ' ' || u2.family_name, u2.name) AS bearbeitet_von_name
COALESCE(u2.given_name || ' ' || u2.family_name, u2.name) AS bearbeitet_von_name,
a.fuer_benutzer_name
FROM ausruestung_anfragen a
LEFT JOIN users u ON u.id = a.anfrager_id
LEFT JOIN users u2 ON u2.id = a.bearbeitet_von
@@ -388,6 +404,7 @@ async function createRequest(
items: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[],
notizen?: string,
bezeichnung?: string,
fuerBenutzerName?: string,
) {
const client = await pool.connect();
try {
@@ -407,20 +424,20 @@ async function createRequest(
);
const nextNr = maxResult.rows[0].next_nr;
const anfrageResult = await client.query(
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, bestell_nummer, bestell_jahr)
VALUES ($1, $2, $3, $4, $5)
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, bestell_nummer, bestell_jahr, fuer_benutzer_name)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[userId, notizen || null, bezeichnung || null, nextNr, currentYear],
[userId, notizen || null, bezeichnung || null, nextNr, currentYear, fuerBenutzerName || null],
);
await client.query('RELEASE SAVEPOINT sp_bestell_nr');
anfrage = anfrageResult.rows[0];
} catch {
await client.query('ROLLBACK TO SAVEPOINT sp_bestell_nr');
const anfrageResult = await client.query(
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung)
VALUES ($1, $2, $3)
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, fuer_benutzer_name)
VALUES ($1, $2, $3, $4)
RETURNING *`,
[userId, notizen || null, bezeichnung || null],
[userId, notizen || null, bezeichnung || null, fuerBenutzerName || null],
);
anfrage = anfrageResult.rows[0];
}
@@ -752,6 +769,7 @@ async function getWidgetOverview() {
}
export default {
getAllUsers,
getKategorien,
createKategorie,
updateKategorie,