feat: personal equipment tracking, order assignment, purge fix, widget consolidation
- Migration 084: new persoenliche_ausruestung table with catalog link, user assignment, soft delete; adds zuweisung_typ/ausruestung_id/persoenlich_id columns to ausruestung_anfrage_positionen; seeds feature group + 5 permissions - Fix user data purge: table was shop_anfragen, renamed to ausruestung_anfragen in mig 046 — caused full transaction rollback. Also keep mitglieder_profile row but NULL FDISK-synced fields (dienstgrad, geburtsdatum, etc.) instead of deleting the profile - Personal equipment CRUD: backend service/controller/routes at /api/persoenliche-ausruestung; frontend page with DataTable, user filter, catalog Autocomplete, FAB create dialog; widget in Status group; sidebar entry (Checkroom icon); card in MitgliedDetail Tab 0 - Ausruestungsanfrage item assignment: when a request reaches erledigt, auto-opens ItemAssignmentDialog listing all delivered positions; each item can be assigned as general equipment (vehicle/storage), personal item (user, prefilled with requester), or not tracked; POST /requests/:id/assign backend - StatCard refactored to use WidgetCard as outer shell for consistent header styling across all dashboard widget templates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
177
backend/src/controllers/personalEquipment.controller.ts
Normal file
177
backend/src/controllers/personalEquipment.controller.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import personalEquipmentService from '../services/personalEquipment.service';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const uuidString = z.string().regex(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||
'Ungültige UUID',
|
||||
);
|
||||
|
||||
const isoDate = z.string().regex(
|
||||
/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/,
|
||||
'Erwartet ISO-Datum im Format YYYY-MM-DD',
|
||||
);
|
||||
|
||||
const ZustandEnum = z.enum(['gut', 'beschaedigt', 'abgaengig', 'verloren']);
|
||||
|
||||
const CreateSchema = z.object({
|
||||
bezeichnung: z.string().min(1).max(200),
|
||||
kategorie: z.string().max(100).optional(),
|
||||
artikel_id: z.number().int().positive().optional(),
|
||||
user_id: uuidString.optional(),
|
||||
benutzer_name: z.string().max(200).optional(),
|
||||
groesse: z.string().max(50).optional(),
|
||||
seriennummer: z.string().max(100).optional(),
|
||||
inventarnummer: z.string().max(50).optional(),
|
||||
anschaffung_datum: isoDate.optional(),
|
||||
zustand: ZustandEnum.optional(),
|
||||
notizen: z.string().max(2000).optional(),
|
||||
});
|
||||
|
||||
const UpdateSchema = z.object({
|
||||
bezeichnung: z.string().min(1).max(200).optional(),
|
||||
kategorie: z.string().max(100).nullable().optional(),
|
||||
artikel_id: z.number().int().positive().nullable().optional(),
|
||||
user_id: uuidString.nullable().optional(),
|
||||
benutzer_name: z.string().max(200).nullable().optional(),
|
||||
groesse: z.string().max(50).nullable().optional(),
|
||||
seriennummer: z.string().max(100).nullable().optional(),
|
||||
inventarnummer: z.string().max(50).nullable().optional(),
|
||||
anschaffung_datum: isoDate.nullable().optional(),
|
||||
zustand: ZustandEnum.optional(),
|
||||
notizen: z.string().max(2000).nullable().optional(),
|
||||
});
|
||||
|
||||
function isValidUUID(s: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
||||
}
|
||||
|
||||
class PersonalEquipmentController {
|
||||
async list(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const filters: Record<string, string> = {};
|
||||
if (req.query.user_id) filters.userId = String(req.query.user_id);
|
||||
if (req.query.kategorie) filters.kategorie = String(req.query.kategorie);
|
||||
if (req.query.zustand) filters.zustand = String(req.query.zustand);
|
||||
|
||||
const items = await personalEquipmentService.getAll(filters);
|
||||
res.status(200).json({ success: true, data: items });
|
||||
} catch (error) {
|
||||
logger.error('personalEquipment.list error', { error });
|
||||
res.status(500).json({ success: false, message: 'Persönliche Ausrüstung konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getMy(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const items = await personalEquipmentService.getByUserId(req.user!.id);
|
||||
res.status(200).json({ success: true, data: items });
|
||||
} catch (error) {
|
||||
logger.error('personalEquipment.getMy error', { error });
|
||||
res.status(500).json({ success: false, message: 'Persönliche Ausrüstung konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getByUser(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { userId } = req.params as Record<string, string>;
|
||||
if (!isValidUUID(userId)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige User-ID' });
|
||||
return;
|
||||
}
|
||||
const items = await personalEquipmentService.getByUserId(userId);
|
||||
res.status(200).json({ success: true, data: items });
|
||||
} catch (error) {
|
||||
logger.error('personalEquipment.getByUser error', { error });
|
||||
res.status(500).json({ success: false, message: 'Persönliche Ausrüstung konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getById(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 ID' });
|
||||
return;
|
||||
}
|
||||
const item = await personalEquipmentService.getById(id);
|
||||
if (!item) {
|
||||
res.status(404).json({ success: false, message: 'Nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: item });
|
||||
} catch (error) {
|
||||
logger.error('personalEquipment.getById error', { error });
|
||||
res.status(500).json({ success: false, message: 'Persönliche Ausrüstung konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const parsed = CreateSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors });
|
||||
return;
|
||||
}
|
||||
const item = await personalEquipmentService.create(parsed.data, req.user!.id);
|
||||
res.status(201).json({ success: true, data: item });
|
||||
} catch (error) {
|
||||
logger.error('personalEquipment.create error', { error });
|
||||
res.status(500).json({ success: false, message: 'Persönliche Ausrüstung konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async update(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 ID' });
|
||||
return;
|
||||
}
|
||||
const parsed = UpdateSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors });
|
||||
return;
|
||||
}
|
||||
if (Object.keys(parsed.data).length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Kein Feld zum Aktualisieren angegeben' });
|
||||
return;
|
||||
}
|
||||
const item = await personalEquipmentService.update(id, parsed.data as any);
|
||||
if (!item) {
|
||||
res.status(404).json({ success: false, message: 'Nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: item });
|
||||
} 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('personalEquipment.update error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Persönliche Ausrüstung konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async delete(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 ID' });
|
||||
return;
|
||||
}
|
||||
const deleted = await personalEquipmentService.delete(id);
|
||||
if (!deleted) {
|
||||
res.status(404).json({ success: false, message: 'Nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, message: 'Persönliche Ausrüstung gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('personalEquipment.delete error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Persönliche Ausrüstung konnte nicht gelöscht werden' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new PersonalEquipmentController();
|
||||
Reference in New Issue
Block a user