new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 16:58:46 +01:00
parent 948b211f70
commit 55ded22a6f
8 changed files with 452 additions and 43 deletions

View File

@@ -451,6 +451,86 @@ class EquipmentController {
}
}
async createCategory(req: Request, res: Response): Promise<void> {
try {
const { name, kurzname, sortierung, motorisiert } = req.body;
if (!name || typeof name !== 'string' || !name.trim()) {
res.status(400).json({ success: false, message: 'Name ist erforderlich' });
return;
}
if (!kurzname || typeof kurzname !== 'string' || !kurzname.trim()) {
res.status(400).json({ success: false, message: 'Kurzname ist erforderlich' });
return;
}
const category = await equipmentService.createCategory({
name: name.trim(),
kurzname: kurzname.trim(),
sortierung: sortierung != null ? Number(sortierung) : undefined,
motorisiert: motorisiert != null ? Boolean(motorisiert) : undefined,
});
res.status(201).json({ success: true, data: category });
} catch (error) {
logger.error('createCategory error', { error });
res.status(500).json({ success: false, message: 'Kategorie konnte nicht erstellt werden' });
}
}
async updateCategory(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params as Record<string, string>;
if (!isValidUUID(id)) {
res.status(400).json({ success: false, message: 'Ungültige Kategorie-ID' });
return;
}
const { name, kurzname, sortierung, motorisiert } = req.body;
const data: Record<string, unknown> = {};
if (name !== undefined) data.name = String(name).trim();
if (kurzname !== undefined) data.kurzname = String(kurzname).trim();
if (sortierung !== undefined) data.sortierung = Number(sortierung);
if (motorisiert !== undefined) data.motorisiert = Boolean(motorisiert);
if (Object.keys(data).length === 0) {
res.status(400).json({ success: false, message: 'Kein Feld zum Aktualisieren angegeben' });
return;
}
const category = await equipmentService.updateCategory(id, data as any);
if (!category) {
res.status(404).json({ success: false, message: 'Kategorie nicht gefunden' });
return;
}
res.status(200).json({ success: true, data: category });
} catch (error: any) {
if (error?.message === 'No fields to update') {
res.status(400).json({ success: false, message: 'Kein Feld zum Aktualisieren angegeben' });
return;
}
logger.error('updateCategory error', { error, id: req.params.id });
res.status(500).json({ success: false, message: 'Kategorie konnte nicht aktualisiert werden' });
}
}
async deleteCategory(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params as Record<string, string>;
if (!isValidUUID(id)) {
res.status(400).json({ success: false, message: 'Ungültige Kategorie-ID' });
return;
}
const result = await equipmentService.deleteCategory(id);
if (!result.deleted) {
res.status(result.error === 'Kategorie nicht gefunden' ? 404 : 409).json({
success: false,
message: result.error,
});
return;
}
res.status(200).json({ success: true, message: 'Kategorie gelöscht' });
} catch (error) {
logger.error('deleteCategory error', { error, id: req.params.id });
res.status(500).json({ success: false, message: 'Kategorie konnte nicht gelöscht werden' });
}
}
async uploadWartungFile(req: Request, res: Response): Promise<void> {
const { wartungId } = req.params as Record<string, string>;
const id = parseInt(wartungId, 10);

View File

@@ -0,0 +1,4 @@
-- Add manage_categories permission for equipment
INSERT INTO permissions (feature_group, action, beschreibung)
VALUES ('ausruestung', 'manage_categories', 'Kategorien verwalten')
ON CONFLICT DO NOTHING;

View File

@@ -12,6 +12,9 @@ router.get('/', authenticate, requirePermission('ausruestung:
router.get('/stats', authenticate, requirePermission('ausruestung:view'), equipmentController.getStats.bind(equipmentController));
router.get('/alerts', authenticate, requirePermission('ausruestung:view'), equipmentController.getAlerts.bind(equipmentController));
router.get('/categories', authenticate, requirePermission('ausruestung:view'), equipmentController.getCategories.bind(equipmentController));
router.post('/categories', authenticate, requirePermission('ausruestung:manage_categories'), equipmentController.createCategory.bind(equipmentController));
router.patch('/categories/:id', authenticate, requirePermission('ausruestung:manage_categories'), equipmentController.updateCategory.bind(equipmentController));
router.delete('/categories/:id', authenticate, requirePermission('ausruestung:manage_categories'), equipmentController.deleteCategory.bind(equipmentController));
router.get('/vehicle-warnings', authenticate, equipmentController.getVehicleWarnings.bind(equipmentController));
router.get('/vehicle/:fahrzeugId', authenticate, equipmentController.getByVehicle.bind(equipmentController));
router.get('/:id', authenticate, equipmentController.getEquipment.bind(equipmentController));

View File

@@ -132,6 +132,75 @@ class EquipmentService {
}
}
async createCategory(data: { name: string; kurzname: string; sortierung?: number; motorisiert?: boolean }): Promise<AusruestungKategorie> {
try {
const result = await pool.query(
`INSERT INTO ausruestung_kategorien (id, name, kurzname, sortierung, motorisiert)
VALUES (uuid_generate_v4(), $1, $2, COALESCE($3, (SELECT COALESCE(MAX(sortierung),0)+1 FROM ausruestung_kategorien)), COALESCE($4, false))
RETURNING *`,
[data.name, data.kurzname, data.sortierung ?? null, data.motorisiert ?? null]
);
logger.info('Equipment category created', { id: result.rows[0].id, name: data.name });
return result.rows[0] as AusruestungKategorie;
} catch (error) {
logger.error('EquipmentService.createCategory failed', { error });
throw new Error('Failed to create equipment category');
}
}
async updateCategory(id: string, data: { name?: string; kurzname?: string; sortierung?: number; motorisiert?: boolean }): Promise<AusruestungKategorie | null> {
try {
const fields: string[] = [];
const values: unknown[] = [];
let p = 1;
if (data.name !== undefined) { fields.push(`name = $${p++}`); values.push(data.name); }
if (data.kurzname !== undefined) { fields.push(`kurzname = $${p++}`); values.push(data.kurzname); }
if (data.sortierung !== undefined) { fields.push(`sortierung = $${p++}`); values.push(data.sortierung); }
if (data.motorisiert !== undefined) { fields.push(`motorisiert = $${p++}`); values.push(data.motorisiert); }
if (fields.length === 0) throw new Error('No fields to update');
values.push(id);
const result = await pool.query(
`UPDATE ausruestung_kategorien SET ${fields.join(', ')} WHERE id = $${p} RETURNING *`,
values
);
if (result.rows.length === 0) return null;
logger.info('Equipment category updated', { id });
return result.rows[0] as AusruestungKategorie;
} catch (error) {
logger.error('EquipmentService.updateCategory failed', { error, id });
throw error;
}
}
async deleteCategory(id: string): Promise<{ deleted: boolean; error?: string }> {
try {
// Check if any equipment items reference this category
const usage = await pool.query(
`SELECT COUNT(*) AS cnt FROM ausruestung WHERE kategorie_id = $1 AND deleted_at IS NULL`,
[id]
);
const count = parseInt(usage.rows[0].cnt, 10);
if (count > 0) {
return { deleted: false, error: `Kategorie wird von ${count} Ausrüstungsgegenständen verwendet und kann nicht gelöscht werden.` };
}
const result = await pool.query(
`DELETE FROM ausruestung_kategorien WHERE id = $1 RETURNING id`,
[id]
);
if (result.rows.length === 0) {
return { deleted: false, error: 'Kategorie nicht gefunden' };
}
logger.info('Equipment category deleted', { id });
return { deleted: true };
} catch (error) {
logger.error('EquipmentService.deleteCategory failed', { error, id });
throw new Error('Failed to delete equipment category');
}
}
// =========================================================================
// CRUD
// =========================================================================