feat(persoenliche-ausruestung): show catalog category, remove size/date columns, make zustand admin-configurable

This commit is contained in:
Matthias Hochmeister
2026-04-16 08:19:38 +02:00
parent dac0b79b3b
commit 058ee721e8
14 changed files with 282 additions and 96 deletions

View File

@@ -1,6 +1,7 @@
import { Request, Response } from 'express';
import { z } from 'zod';
import personalEquipmentService from '../services/personalEquipment.service';
import settingsService from '../services/settings.service';
import logger from '../utils/logger';
const uuidString = z.string().regex(
@@ -13,7 +14,7 @@ const isoDate = z.string().regex(
'Erwartet ISO-Datum im Format YYYY-MM-DD',
);
const ZustandEnum = z.enum(['gut', 'beschaedigt', 'abgaengig', 'verloren']);
const ZustandEnum = z.string().min(1).max(50);
const EigenschaftInput = z.object({
eigenschaft_id: z.number().int().positive().nullable().optional(),
@@ -180,6 +181,42 @@ class PersonalEquipmentController {
res.status(500).json({ success: false, message: 'Persönliche Ausrüstung konnte nicht gelöscht werden' });
}
}
async getZustandOptions(_req: Request, res: Response): Promise<void> {
try {
const setting = await settingsService.get('personal_equipment_zustand_options');
const options = Array.isArray(setting?.value) ? setting!.value : [
{ key: 'gut', label: 'Gut', color: 'success' },
{ key: 'beschaedigt', label: 'Beschädigt', color: 'warning' },
{ key: 'abgaengig', label: 'Abgängig', color: 'error' },
{ key: 'verloren', label: 'Verloren', color: 'default' },
];
res.status(200).json({ success: true, data: options });
} catch (error) {
logger.error('personalEquipment.getZustandOptions error', { error });
res.status(500).json({ success: false, message: 'Zustände konnten nicht geladen werden' });
}
}
async updateZustandOptions(req: Request, res: Response): Promise<void> {
try {
const schema = z.array(z.object({
key: z.string().min(1).max(50),
label: z.string().min(1).max(100),
color: z.enum(['success', 'warning', 'error', 'default', 'primary', 'secondary', 'info']),
})).min(1);
const parsed = schema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten() });
return;
}
await settingsService.set('personal_equipment_zustand_options', parsed.data, req.user!.id);
res.status(200).json({ success: true, data: parsed.data });
} catch (error) {
logger.error('personalEquipment.updateZustandOptions error', { error });
res.status(500).json({ success: false, message: 'Zustände konnten nicht gespeichert werden' });
}
}
}
export default new PersonalEquipmentController();

View File

@@ -0,0 +1,17 @@
-- Migration: 091_personal_equipment_configurable_zustand
-- Makes the zustand field on persoenliche_ausruestung admin-configurable
-- by removing the hard-coded CHECK constraint and seeding default options
-- into app_settings.
-- 1. Drop the hard-coded CHECK constraint
ALTER TABLE persoenliche_ausruestung DROP CONSTRAINT IF EXISTS persoenliche_ausruestung_zustand_check;
-- 2. Seed default zustand options into app_settings
INSERT INTO app_settings (key, value, updated_by, updated_at)
VALUES (
'personal_equipment_zustand_options',
'[{"key":"gut","label":"Gut","color":"success"},{"key":"beschaedigt","label":"Beschädigt","color":"warning"},{"key":"abgaengig","label":"Abgängig","color":"error"},{"key":"verloren","label":"Verloren","color":"default"}]',
'system',
NOW()
)
ON CONFLICT (key) DO NOTHING;

View File

@@ -24,6 +24,10 @@ router.get('/user/:userId', authenticate, async (req, res, next) => {
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
}, personalEquipmentController.getByUser.bind(personalEquipmentController));
// Zustand options — GET (all authenticated users), PUT (admin:write)
router.get('/zustand-options', authenticate, personalEquipmentController.getZustandOptions.bind(personalEquipmentController));
router.put('/zustand-options', authenticate, requirePermission('admin:write'), personalEquipmentController.updateZustandOptions.bind(personalEquipmentController));
// Single item
router.get('/:id', authenticate, personalEquipmentController.getById.bind(personalEquipmentController));

View File

@@ -40,10 +40,12 @@ interface UpdatePersonalEquipmentData {
const BASE_SELECT = `
SELECT pa.*,
COALESCE(u.given_name || ' ' || u.family_name, u.name) AS user_display_name,
aa.bezeichnung AS artikel_bezeichnung
aa.bezeichnung AS artikel_bezeichnung,
akk.name AS artikel_kategorie_name
FROM persoenliche_ausruestung pa
LEFT JOIN users u ON u.id = pa.user_id
LEFT JOIN ausruestung_artikel aa ON aa.id = pa.artikel_id
LEFT JOIN ausruestung_kategorien_katalog akk ON akk.id = aa.kategorie_id
WHERE pa.geloescht_am IS NULL
`;