new features
This commit is contained in:
@@ -87,6 +87,7 @@ class AuthController {
|
||||
// Step 2: Get user info from Authentik
|
||||
const userInfo = await authentikService.getUserInfo(tokens.access_token);
|
||||
const groups = userInfo.groups ?? [];
|
||||
const dashboardGroups = groups.filter((g: string) => g.startsWith('dashboard_'));
|
||||
|
||||
// Step 3: Verify ID token if present
|
||||
if (tokens.id_token) {
|
||||
@@ -119,8 +120,8 @@ class AuthController {
|
||||
profile_picture_url: userInfo.picture,
|
||||
});
|
||||
|
||||
await userService.updateGroups(user.id, groups);
|
||||
logger.info('Groups synced for user', { userId: user.id, groupCount: groups.length });
|
||||
await userService.updateGroups(user.id, dashboardGroups);
|
||||
logger.info('Groups synced for user', { userId: user.id, groupCount: dashboardGroups.length });
|
||||
await memberService.ensureProfileExists(user.id);
|
||||
|
||||
// Audit: first-ever login (user record creation)
|
||||
@@ -168,8 +169,8 @@ class AuthController {
|
||||
});
|
||||
|
||||
await userService.updateLastLogin(user.id);
|
||||
await userService.updateGroups(user.id, groups);
|
||||
logger.info('Groups synced for user', { userId: user.id, groupCount: groups.length });
|
||||
await userService.updateGroups(user.id, dashboardGroups);
|
||||
logger.info('Groups synced for user', { userId: user.id, groupCount: dashboardGroups.length });
|
||||
await memberService.ensureProfileExists(user.id);
|
||||
|
||||
const { given_name: updatedGivenName, family_name: updatedFamilyName } = extractNames(userInfo);
|
||||
|
||||
@@ -113,17 +113,29 @@ class BestellungController {
|
||||
}
|
||||
|
||||
async createOrder(req: Request, res: Response): Promise<void> {
|
||||
const { bezeichnung } = req.body;
|
||||
const { bezeichnung, lieferant_id, budget, besteller_id } = req.body;
|
||||
if (!bezeichnung || typeof bezeichnung !== 'string' || bezeichnung.trim().length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Bezeichnung ist erforderlich' });
|
||||
return;
|
||||
}
|
||||
if (lieferant_id != null && (!Number.isInteger(lieferant_id) || lieferant_id <= 0)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Lieferanten-ID' });
|
||||
return;
|
||||
}
|
||||
if (budget != null && (typeof budget !== 'number' || budget < 0)) {
|
||||
res.status(400).json({ success: false, message: 'Budget muss eine positive Zahl sein' });
|
||||
return;
|
||||
}
|
||||
if (besteller_id != null && besteller_id !== '' && (typeof besteller_id !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(besteller_id))) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Besteller-ID' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const order = await bestellungService.createOrder(req.body, req.user!.id);
|
||||
res.status(201).json({ success: true, data: order });
|
||||
} catch (error) {
|
||||
logger.error('BestellungController.createOrder error', { error });
|
||||
res.status(500).json({ success: false, message: 'Bestellung konnte nicht erstellt werden' });
|
||||
res.status(500).json({ success: false, message: error instanceof Error ? error.message : 'Bestellung konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,17 @@ const CreateWartungslogSchema = z.object({
|
||||
(url) => /^https?:\/\//i.test(url),
|
||||
'Nur http/https URLs erlaubt'
|
||||
).optional(),
|
||||
naechste_pruefung_am: isoDate.optional(),
|
||||
});
|
||||
|
||||
const UpdateWartungslogSchema = z.object({
|
||||
datum: isoDate.optional(),
|
||||
art: z.enum(['Prüfung', 'Reparatur', 'Sonstiges']).optional(),
|
||||
beschreibung: z.string().min(1).max(2000).optional(),
|
||||
ergebnis: z.enum(['bestanden', 'bestanden_mit_maengeln', 'nicht_bestanden']).nullable().optional(),
|
||||
kosten: z.number().min(0).nullable().optional(),
|
||||
pruefende_stelle: z.string().max(150).nullable().optional(),
|
||||
naechste_pruefung_am: isoDate.nullable().optional(),
|
||||
});
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────────────────────
|
||||
@@ -403,6 +414,43 @@ class EquipmentController {
|
||||
}
|
||||
}
|
||||
|
||||
async updateWartung(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id, wartungId } = req.params as Record<string, string>;
|
||||
if (!isValidUUID(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' });
|
||||
return;
|
||||
}
|
||||
const wId = parseInt(wartungId, 10);
|
||||
if (isNaN(wId)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Wartungs-ID' });
|
||||
return;
|
||||
}
|
||||
const parsed = UpdateWartungslogSchema.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 entry = await equipmentService.updateWartungslog(id, wId, parsed.data, getUserId(req));
|
||||
res.status(200).json({ success: true, data: entry });
|
||||
} catch (error: any) {
|
||||
if (error?.message === 'Wartungseintrag nicht gefunden') {
|
||||
res.status(404).json({ success: false, message: error.message });
|
||||
return;
|
||||
}
|
||||
logger.error('updateWartung error', { error, id: req.params.id, wartungId: req.params.wartungId });
|
||||
res.status(500).json({ success: false, message: 'Wartungseintrag konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async uploadWartungFile(req: Request, res: Response): Promise<void> {
|
||||
const { wartungId } = req.params as Record<string, string>;
|
||||
const id = parseInt(wartungId, 10);
|
||||
|
||||
176
backend/src/controllers/issue.controller.ts
Normal file
176
backend/src/controllers/issue.controller.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Request, Response } from 'express';
|
||||
import issueService from '../services/issue.service';
|
||||
import { permissionService } from '../services/permission.service';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const param = (req: Request, key: string): string => req.params[key] as string;
|
||||
|
||||
class IssueController {
|
||||
async getIssues(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canViewAll = permissionService.hasPermission(groups, 'issues:view_all');
|
||||
const issues = await issueService.getIssues(userId, canViewAll);
|
||||
res.status(200).json({ success: true, data: issues });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.getIssues error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issues konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getIssue(req: Request, res: Response): Promise<void> {
|
||||
const id = parseInt(param(req, 'id'), 10);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige ID' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const issue = await issueService.getIssueById(id);
|
||||
if (!issue) {
|
||||
res.status(404).json({ success: false, message: 'Issue nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canViewAll = permissionService.hasPermission(groups, 'issues:view_all');
|
||||
if (!canViewAll && issue.erstellt_von !== userId) {
|
||||
res.status(403).json({ success: false, message: 'Kein Zugriff' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: issue });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.getIssue error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issue konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async createIssue(req: Request, res: Response): Promise<void> {
|
||||
const { titel } = req.body;
|
||||
if (!titel || typeof titel !== 'string' || titel.trim().length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Titel ist erforderlich' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const issue = await issueService.createIssue(req.body, req.user!.id);
|
||||
res.status(201).json({ success: true, data: issue });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.createIssue error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issue konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateIssue(req: Request, res: Response): Promise<void> {
|
||||
const id = parseInt(param(req, 'id'), 10);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige ID' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canManage = permissionService.hasPermission(groups, 'issues:manage');
|
||||
if (!canManage) {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
|
||||
return;
|
||||
}
|
||||
const issue = await issueService.updateIssue(id, req.body);
|
||||
if (!issue) {
|
||||
res.status(404).json({ success: false, message: 'Issue nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.status(200).json({ success: true, data: issue });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.updateIssue error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issue konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteIssue(req: Request, res: Response): Promise<void> {
|
||||
const id = parseInt(param(req, 'id'), 10);
|
||||
if (isNaN(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige ID' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const issue = await issueService.getIssueById(id);
|
||||
if (!issue) {
|
||||
res.status(404).json({ success: false, message: 'Issue nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canManage = permissionService.hasPermission(groups, 'issues:manage');
|
||||
if (!canManage && issue.erstellt_von !== userId) {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
|
||||
return;
|
||||
}
|
||||
await issueService.deleteIssue(id);
|
||||
res.status(200).json({ success: true, message: 'Issue gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.deleteIssue error', { error });
|
||||
res.status(500).json({ success: false, message: 'Issue konnte nicht gelöscht werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getComments(req: Request, res: Response): Promise<void> {
|
||||
const issueId = parseInt(param(req, 'id'), 10);
|
||||
if (isNaN(issueId)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige ID' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const issue = await issueService.getIssueById(issueId);
|
||||
if (!issue) {
|
||||
res.status(404).json({ success: false, message: 'Issue nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canViewAll = permissionService.hasPermission(groups, 'issues:view_all');
|
||||
if (!canViewAll && issue.erstellt_von !== userId) {
|
||||
res.status(403).json({ success: false, message: 'Kein Zugriff' });
|
||||
return;
|
||||
}
|
||||
const comments = await issueService.getComments(issueId);
|
||||
res.status(200).json({ success: true, data: comments });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.getComments error', { error });
|
||||
res.status(500).json({ success: false, message: 'Kommentare konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async addComment(req: Request, res: Response): Promise<void> {
|
||||
const issueId = parseInt(param(req, 'id'), 10);
|
||||
if (isNaN(issueId)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige ID' });
|
||||
return;
|
||||
}
|
||||
const { inhalt } = req.body;
|
||||
if (!inhalt || typeof inhalt !== 'string' || inhalt.trim().length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Kommentar darf nicht leer sein' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const issue = await issueService.getIssueById(issueId);
|
||||
if (!issue) {
|
||||
res.status(404).json({ success: false, message: 'Issue nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
const userId = req.user!.id;
|
||||
const groups: string[] = (req.user as any).groups || [];
|
||||
const canViewAll = permissionService.hasPermission(groups, 'issues:view_all');
|
||||
const canManage = permissionService.hasPermission(groups, 'issues:manage');
|
||||
if (!canViewAll && !canManage && issue.erstellt_von !== userId) {
|
||||
res.status(403).json({ success: false, message: 'Kein Zugriff' });
|
||||
return;
|
||||
}
|
||||
const comment = await issueService.addComment(issueId, userId, inhalt.trim());
|
||||
res.status(201).json({ success: true, data: comment });
|
||||
} catch (error) {
|
||||
logger.error('IssueController.addComment error', { error });
|
||||
res.status(500).json({ success: false, message: 'Kommentar konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new IssueController();
|
||||
@@ -188,11 +188,14 @@ class ShopController {
|
||||
|
||||
// Notify requester on status changes
|
||||
if (['genehmigt', 'abgelehnt', 'bestellt', 'erledigt'].includes(status)) {
|
||||
const orderLabel = existing.bestell_jahr && existing.bestell_nummer
|
||||
? `${existing.bestell_jahr}/${String(existing.bestell_nummer).padStart(3, '0')}`
|
||||
: `#${id}`;
|
||||
await notificationService.createNotification({
|
||||
user_id: existing.anfrager_id,
|
||||
typ: 'shop_anfrage',
|
||||
titel: status === 'genehmigt' ? 'Anfrage genehmigt' : status === 'abgelehnt' ? 'Anfrage abgelehnt' : `Anfrage ${status}`,
|
||||
nachricht: `Deine Shop-Anfrage #${id} wurde ${status === 'genehmigt' ? 'genehmigt' : status === 'abgelehnt' ? 'abgelehnt' : status}.`,
|
||||
nachricht: `Deine Shop-Anfrage ${orderLabel} wurde ${status === 'genehmigt' ? 'genehmigt' : status === 'abgelehnt' ? 'abgelehnt' : status}.`,
|
||||
schwere: status === 'abgelehnt' ? 'warnung' : 'info',
|
||||
link: '/shop',
|
||||
quell_id: String(id),
|
||||
@@ -218,6 +221,20 @@ class ShopController {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Overview
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
async getOverview(_req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const overview = await shopService.getOverview();
|
||||
res.status(200).json({ success: true, data: overview });
|
||||
} catch (error) {
|
||||
logger.error('ShopController.getOverview error', { error });
|
||||
res.status(500).json({ success: false, message: 'Übersicht konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Linking
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -86,6 +86,8 @@ const UpdateStatusSchema = z.object({
|
||||
{ message: 'Enddatum muss nach Startdatum liegen', path: ['ausserDienstBis'] }
|
||||
);
|
||||
|
||||
const ErgebnisEnum = z.enum(['bestanden', 'bestanden_mit_maengeln', 'nicht_bestanden']);
|
||||
|
||||
const CreateWartungslogSchema = z.object({
|
||||
datum: isoDate,
|
||||
art: z.enum(['§57a Prüfung', 'Service', 'Sonstiges']).optional(),
|
||||
@@ -94,6 +96,18 @@ const CreateWartungslogSchema = z.object({
|
||||
kraftstoff_liter: z.number().min(0).optional(),
|
||||
kosten: z.number().min(0).optional(),
|
||||
externe_werkstatt: z.string().max(150).optional(),
|
||||
ergebnis: ErgebnisEnum.optional(),
|
||||
naechste_faelligkeit: isoDate.optional(),
|
||||
});
|
||||
|
||||
const UpdateWartungslogSchema = z.object({
|
||||
datum: isoDate,
|
||||
art: z.enum(['§57a Prüfung', 'Service', 'Sonstiges']).optional(),
|
||||
beschreibung: z.string().min(1).max(2000),
|
||||
km_stand: z.number().int().min(0).optional(),
|
||||
externe_werkstatt: z.string().max(150).optional(),
|
||||
ergebnis: ErgebnisEnum.optional(),
|
||||
naechste_faelligkeit: isoDate.optional(),
|
||||
});
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────────────────────
|
||||
@@ -384,6 +398,34 @@ class VehicleController {
|
||||
}
|
||||
}
|
||||
|
||||
async updateWartung(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id, wartungId } = req.params as Record<string, string>;
|
||||
if (!isValidUUID(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Fahrzeug-ID' });
|
||||
return;
|
||||
}
|
||||
const parsed = UpdateWartungslogSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validierungsfehler',
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const entry = await vehicleService.updateWartungslog(wartungId, id, parsed.data, getUserId(req));
|
||||
res.status(200).json({ success: true, data: entry });
|
||||
} catch (error: any) {
|
||||
if (error?.message === 'Wartungseintrag nicht gefunden') {
|
||||
res.status(404).json({ success: false, message: 'Wartungseintrag nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
logger.error('updateWartung error', { error, id: req.params.id, wartungId: req.params.wartungId });
|
||||
res.status(500).json({ success: false, message: 'Wartungseintrag konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async uploadWartungFile(req: Request, res: Response): Promise<void> {
|
||||
const { wartungId } = req.params as Record<string, string>;
|
||||
const id = parseInt(wartungId, 10);
|
||||
|
||||
Reference in New Issue
Block a user